├── bin ├── run.cmd └── run ├── .prettierrc ├── .gitignore ├── .editorconfig ├── tsconfig.json ├── src ├── controller.ts └── index.ts ├── LICENSE ├── package.json └── README.md /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "semi": false, 5 | "jsxBracketSameLine": true 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /lib 6 | /tmp 7 | /yarn.lock 8 | node_modules 9 | /tsconfig.tsbuildinfo 10 | /app.template.yaml 11 | /app.yaml 12 | /.idea 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const project = path.join(__dirname, '../tsconfig.json') 6 | const dev = fs.existsSync(project) 7 | 8 | if (dev) { 9 | require('ts-node').register({project}) 10 | } 11 | 12 | require(`../${dev ? 'src' : 'lib'}`).run() 13 | .catch(require('@oclif/errors/handle')) 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "module": "commonjs", 6 | "outDir": "lib", 7 | "rootDir": "src", 8 | "strict": true, 9 | "target": "es2017", 10 | "composite": true, 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true 13 | }, 14 | "include": [ 15 | "src/**/*" 16 | ] 17 | } -------------------------------------------------------------------------------- /src/controller.ts: -------------------------------------------------------------------------------- 1 | import YAML from 'yaml' 2 | import fs from 'fs' 3 | import path from 'path' 4 | 5 | type TParams = { 6 | appYamlTemplatePath: string 7 | envPrefix: string 8 | } 9 | 10 | export function generate({appYamlTemplatePath, envPrefix}: TParams): string { 11 | const KEY_PREFIX = 'APP_' 12 | 13 | const parsed = YAML.parse(fs.readFileSync(path.resolve(appYamlTemplatePath), 'utf8')) || {} 14 | 15 | const newEnv: Record = {} 16 | 17 | Object.keys(process.env).filter(key => key.startsWith(KEY_PREFIX)).map(key => newEnv[key.replace(KEY_PREFIX, '')] = process.env[key] as string) 18 | 19 | if (!parsed['env_variables']) { 20 | parsed['env_variables'] = {} 21 | } 22 | 23 | Object.assign(parsed['env_variables'], newEnv) 24 | 25 | const file = YAML.stringify(parsed) 26 | 27 | fs.writeFileSync('app.yaml', file) 28 | 29 | return file 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Robert Lancer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { generate } from './controller' 3 | 4 | class AppyamlGenerator extends Command { 5 | static description = 'describe the command here' 6 | 7 | static flags = { 8 | // add --version flag to show CLI version 9 | version: flags.version({ char: 'v' }), 10 | help: flags.help({ char: 'h' }), 11 | // flag with no value (-f, --force) 12 | prefix: flags.string({ char: 'p', description: 'Enviorment varible prefix, defaults to ENV_' }), 13 | force: flags.boolean({ char: 'f' }), 14 | 'no-output': flags.boolean(), 15 | } 16 | 17 | static args = [{ name: 'file' }] 18 | 19 | async run() { 20 | const { args, flags } = this.parse(AppyamlGenerator) 21 | 22 | const fileName = args.file || 'app.template.yaml' 23 | const prefix = flags.prefix || 'ENV_' 24 | const noOutput = flags['no-output'] || false 25 | 26 | try { 27 | const newFile = generate({ appYamlTemplatePath: fileName, envPrefix: prefix }) 28 | if (noOutput) { 29 | console.log('Generated new app.yaml') 30 | } else { 31 | this.log(`Generated new app.yaml 32 | ${newFile}`) 33 | } 34 | } catch (err) { 35 | this.error(err) 36 | } 37 | } 38 | } 39 | 40 | export = AppyamlGenerator 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gae-ayaml-env", 3 | "description": "Generates an app.yaml file from a template and environment variables", 4 | "version": "0.0.20", 5 | "author": "Robert Lancer", 6 | "bin": { 7 | "gae-ayaml-env": "./bin/run" 8 | }, 9 | "bugs": "https://github.com/rlancer/gae-ayaml-env-generator/issues", 10 | "dependencies": { 11 | "@oclif/command": "^1.5.12", 12 | "@oclif/config": "^1.12.12", 13 | "@oclif/plugin-help": "^2.1.6", 14 | "@types/yaml": "^1.0.2", 15 | "tslib": "^1.9.3", 16 | "yaml": "^1.5.0" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^10.14.4", 20 | "ts-node": "^8.0.3", 21 | "typescript": "^3.4.2" 22 | }, 23 | "engines": { 24 | "node": ">=8.0.0" 25 | }, 26 | "files": [ 27 | "/bin", 28 | "/lib" 29 | ], 30 | "homepage": "https://github.com/rlancer/gae-ayaml-env-generator", 31 | "keywords": [ 32 | "oclif", 33 | "typescript", 34 | "google app engine", 35 | "gae", 36 | "nodejs", 37 | "gitlab", 38 | "ci / cd" 39 | ], 40 | "license": "MIT", 41 | "main": "lib/index.js", 42 | "oclif": { 43 | "bin": "gae-ayaml-env" 44 | }, 45 | "repository": "https://github.com/rlancer/gae-ayaml-env-generator", 46 | "scripts": { 47 | "prepack": "rm -f tsconfig.tsbuildinfo && rm -rf lib && tsc", 48 | "test": "echo NO TESTS" 49 | }, 50 | "types": "lib/index.d.ts" 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google App Engine - app.yaml environment variable generator for CI / CD systems 2 | 3 | Generates an app.yaml file from a template and environment variables, designed for use with GitLab's CI / CD system. 4 | 5 | [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io) 6 | [![Version](https://img.shields.io/npm/v/gae-ayaml-env.svg)](https://npmjs.org/package/gae-ayaml-env) 7 | [![Downloads/week](https://img.shields.io/npm/dw/gae-ayaml-env.svg)](https://npmjs.org/package/gae-ayaml-env) 8 | [![License](https://img.shields.io/npm/l/gae-ayaml-env.svg)](https://github.com/code/gae-ayaml-env/blob/master/package.json) 9 | 10 | 11 | # Usage 12 | 13 | Set your environment variables in GitLab (or other system), prefix variables you'd like to persist in app.yaml with "APP\_", for example: 14 | 15 | ![Environment variables in GitLab](https://gitlab.com/collaborizm-community/gae-appyaml-env-generate/uploads/a490e948a1f26f08d6cf77e180b826c6/image.png) 16 | 17 | Create an app.template.yaml file include everything sans environment variables 18 | 19 | ```yaml 20 | runtime: nodejs10 21 | env: standard 22 | 23 | automatic_scaling: 24 | min_instances: 0 25 | max_instances: 2 26 | 27 | service: default 28 | 29 | env_variables: 30 | NODE_ENV: 'production' 31 | ``` 32 | 33 | In your CI / CD process run `gae-ayaml-env` to emit a populated app.yaml file, make sure you do not commit an actual app.yaml file as it will be overwritten. 34 | 35 | Example for GitLab 36 | 37 | ```yaml 38 | deploy: 39 | image: 'rlancer/gcloud-node:LTS-229' 40 | script: 41 | - npm i 42 | - npm run build 43 | - npx gae-ayaml-env 44 | - echo $GCLOUD_SERVICE > /tmp/$CI_PIPELINE_ID.json 45 | - gcloud auth activate-service-account --key-file /tmp/$CI_PIPELINE_ID.json 46 | - gcloud --quiet --project $GCLOUD_PROJECT_ID app deploy app.yaml 47 | only: 48 | - prod 49 | ``` 50 | 51 | The system will write an app.yaml file fully populated with all the variables prefixed with "APP\_". 52 | 53 | ```yaml 54 | runtime: nodejs10 55 | env: standard 56 | 57 | env_variables: 58 | APIMARKET_FROM: '******************' 59 | DB_DATABASE: '******************' 60 | DB_HOST: '******************' 61 | DB_PASSWORD: '******************' 62 | DB_USER: '******************' 63 | NODE_ENV: production 64 | SLACK_APP_ID: '******************' 65 | SLACK_BOT_TOKEN: '******************' 66 | SLACK_CLIENT_ID: '******************' 67 | SLACK_CLIENT_SECRET: '******************' 68 | SLACK_OAUTH_REDIR: '******************' 69 | SLACK_SIGNING_SECRET: '******************' 70 | SLACK_TOKEN: '******************' 71 | SLACK_VERIFICATION_TOKEN: '******************' 72 | automatic_scaling: 73 | max_instances: 2 74 | ``` 75 | 76 | ### Hide Output in Console 77 | to hide the output of the generated file in the console you can use the flag: *no-output* 78 | 79 | Example using npx: 80 | ``` 81 | npx gae-ayaml-env --no-output 82 | ``` 83 | 84 | ### Special thanks 85 | 86 | > to [@dannyzen](https://github.com/dannyzen) from Google for helping Collaborizm move to GCP. 87 | --------------------------------------------------------------------------------