├── .eslintrc.json ├── .github └── workflows │ └── unittest.yml ├── .gitignore ├── LICENSE ├── README.md ├── action.yml ├── app.js ├── dist └── index.js ├── index.js ├── index.test.js ├── package-lock.json └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2018 14 | }, 15 | "rules": { 16 | } 17 | } -------------------------------------------------------------------------------- /.github/workflows/unittest.yml: -------------------------------------------------------------------------------- 1 | name: "units-test" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | - 'releases/*' 8 | 9 | jobs: 10 | # unit tests 11 | units: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - run: npm ci 16 | - run: npm test 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Package Lock 45 | # package-lock.json 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nithyanantham Subramaniam (nithyananthamn@gmail.com) 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![units-test](https://github.com/snithyanantham/docker-compose-multiple-apps-heroku-deploy/workflows/units-test/badge.svg) 3 | 4 | # docker-compose-multiple-apps-heroku-deploy - GitHub Action 5 | 6 | A simple action to build multiple docker images using docker-compose, push and deploy your applications to Heroku Apps 7 | 8 | 9 | ## How to use it 10 | 11 | ```yml 12 | name: '' #set whatevername you want to your github job 13 | on: {} # set the events you would like to trigger this job 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | - name: Build, Push and Deploy to Heroku #set the whatever name you want to this step 21 | id: heroku 22 | uses: snithyanantham/docker-compose-multiple-apps-heroku-deploy@v1.0 # use the latest version of the action 23 | with: 24 | email: ${{ secrets.HEROKU_EMAIL }} # your heroku email 25 | api_key: ${{ secrets.HEROKU_API_KEY }} # your heroku api key 26 | docker_compose_file: './src/docker-compose.heroku.yml' # set the path to the folder where the docker-compose file is located 27 | heroku_apps: '[{"imagename":"app1","appname":"app1","apptype":"web"},{"imagename":"app2","appname":"app2","apptype":"web"},{"imagename":"app3","appname":"app2","apptype":"worker"}]' # List of Docker Image name, Heroku app and Heroku app type 28 | ``` 29 | 30 | | Variables | Required | 31 | | ------------- |:-------------:| 32 | | email | ✅| 33 | | api_key | ✅| 34 | | docker_compose_file | ✅| 35 | | heroku_apps | ✅| 36 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Deploy Multiple Docker Images to Heroku Apps' 2 | author: 'Nithyanantham Subramaniam' 3 | description: 'A simple action to build multiple docker images using docker-compose, push and deploy your applications to Heroku Apps.' 4 | inputs: 5 | email: 6 | description: 'Email linked to your Heroku account' 7 | required: true 8 | api_key: 9 | description: 'Your Heroku API Key' 10 | required: true 11 | app_name: 12 | description: 'Your Heroku App Name' 13 | required: true 14 | docker_compose_file: 15 | description: 'docker-compose File Path' 16 | required: true 17 | heroku_apps: 18 | description: 'List of Docker Images generated from docker-compose and Heroku app names with type' 19 | required: true 20 | branding: 21 | icon: package 22 | color: blue 23 | runs: 24 | using: 'node12' 25 | main: 'dist/index.js' 26 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core'); 2 | const { promisify } = require('util'); 3 | 4 | const exec = promisify(require('child_process').exec); 5 | 6 | const asyncForEach = async (array, callback) => { 7 | for (let index = 0; index < array.length; index++) { 8 | await callback(array[index], index, array) 9 | } 10 | } 11 | 12 | let loginToHeroku = async function loginToHeroku(login, password) { 13 | try { 14 | 15 | await exec(`cat >~/.netrc < 0) { 55 | await asyncForEach(imageList, async (item) => { 56 | console.log('Processing image -' + item.imagename); 57 | await exec(`docker tag ${item.imagename} registry.heroku.com/${item.appname}/${item.apptype}`); 58 | console.log('Container tagged for image - ' + item.imagename); 59 | await exec(`docker push registry.heroku.com/${item.appname}/web`); 60 | console.log('Container pushed for image - ' + item.imagename); 61 | await exec(`heroku container:release ${item.apptype} --app ${item.appname}`); 62 | console.log('Container deployed for image - ' + item.imagename); 63 | }); 64 | console.log('App Deployed successfully ✅'); 65 | } else { 66 | core.setFailed(`No image given to process.`); 67 | } 68 | } 69 | catch (error) { 70 | core.setFailed(`Somthing went wrong while pushing and deploying your image. Error: ${error.message}`); 71 | } 72 | } 73 | 74 | let buildAndDeploy = async function buildAndDeploy(login, password, dockerComposeFilePath, imageListString) 75 | { 76 | await loginToHeroku(login, password); 77 | await buildDockerCompose(dockerComposeFilePath); 78 | const imageList = await getImageAppNameList(imageListString); 79 | await pushAndDeployAllImages(imageList); 80 | } 81 | 82 | module.exports.loginToHeroku = loginToHeroku; 83 | module.exports.getImageAppNameList = getImageAppNameList; 84 | module.exports.buildDockerCompose = buildDockerCompose; 85 | module.exports.pushAndDeployAllImages = pushAndDeployAllImages; 86 | module.exports.buildAndDeploy = buildAndDeploy; 87 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | /******/ (function(modules, runtime) { // webpackBootstrap 3 | /******/ "use strict"; 4 | /******/ // The module cache 5 | /******/ var installedModules = {}; 6 | /******/ 7 | /******/ // The require function 8 | /******/ function __webpack_require__(moduleId) { 9 | /******/ 10 | /******/ // Check if module is in cache 11 | /******/ if(installedModules[moduleId]) { 12 | /******/ return installedModules[moduleId].exports; 13 | /******/ } 14 | /******/ // Create a new module (and put it into the cache) 15 | /******/ var module = installedModules[moduleId] = { 16 | /******/ i: moduleId, 17 | /******/ l: false, 18 | /******/ exports: {} 19 | /******/ }; 20 | /******/ 21 | /******/ // Execute the module function 22 | /******/ var threw = true; 23 | /******/ try { 24 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 25 | /******/ threw = false; 26 | /******/ } finally { 27 | /******/ if(threw) delete installedModules[moduleId]; 28 | /******/ } 29 | /******/ 30 | /******/ // Flag the module as loaded 31 | /******/ module.l = true; 32 | /******/ 33 | /******/ // Return the exports of the module 34 | /******/ return module.exports; 35 | /******/ } 36 | /******/ 37 | /******/ 38 | /******/ __webpack_require__.ab = __dirname + "/"; 39 | /******/ 40 | /******/ // the startup function 41 | /******/ function startup() { 42 | /******/ // Load entry module and return exports 43 | /******/ return __webpack_require__(104); 44 | /******/ }; 45 | /******/ 46 | /******/ // run startup 47 | /******/ return startup(); 48 | /******/ }) 49 | /************************************************************************/ 50 | /******/ ({ 51 | 52 | /***/ 87: 53 | /***/ (function(module) { 54 | 55 | module.exports = require("os"); 56 | 57 | /***/ }), 58 | 59 | /***/ 104: 60 | /***/ (function(__unusedmodule, __unusedexports, __webpack_require__) { 61 | 62 | const core = __webpack_require__(470); 63 | const app = __webpack_require__(964); 64 | 65 | async function run() { 66 | try { 67 | const login = core.getInput('email'); 68 | const password = core.getInput('api_key'); 69 | const imageListString = core.getInput('heroku_apps'); 70 | const dockerComposeFilePath = core.getInput('docker_compose_file'); 71 | 72 | await app.buildAndDeploy(login, password, dockerComposeFilePath, imageListString); 73 | } 74 | catch (error) { 75 | console.log({ message: error.message }); 76 | core.setFailed(error.message); 77 | } 78 | } 79 | 80 | run() 81 | 82 | /***/ }), 83 | 84 | /***/ 129: 85 | /***/ (function(module) { 86 | 87 | module.exports = require("child_process"); 88 | 89 | /***/ }), 90 | 91 | /***/ 431: 92 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 93 | 94 | "use strict"; 95 | 96 | var __importStar = (this && this.__importStar) || function (mod) { 97 | if (mod && mod.__esModule) return mod; 98 | var result = {}; 99 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 100 | result["default"] = mod; 101 | return result; 102 | }; 103 | Object.defineProperty(exports, "__esModule", { value: true }); 104 | const os = __importStar(__webpack_require__(87)); 105 | /** 106 | * Commands 107 | * 108 | * Command Format: 109 | * ::name key=value,key=value::message 110 | * 111 | * Examples: 112 | * ::warning::This is the message 113 | * ::set-env name=MY_VAR::some value 114 | */ 115 | function issueCommand(command, properties, message) { 116 | const cmd = new Command(command, properties, message); 117 | process.stdout.write(cmd.toString() + os.EOL); 118 | } 119 | exports.issueCommand = issueCommand; 120 | function issue(name, message = '') { 121 | issueCommand(name, {}, message); 122 | } 123 | exports.issue = issue; 124 | const CMD_STRING = '::'; 125 | class Command { 126 | constructor(command, properties, message) { 127 | if (!command) { 128 | command = 'missing.command'; 129 | } 130 | this.command = command; 131 | this.properties = properties; 132 | this.message = message; 133 | } 134 | toString() { 135 | let cmdStr = CMD_STRING + this.command; 136 | if (this.properties && Object.keys(this.properties).length > 0) { 137 | cmdStr += ' '; 138 | let first = true; 139 | for (const key in this.properties) { 140 | if (this.properties.hasOwnProperty(key)) { 141 | const val = this.properties[key]; 142 | if (val) { 143 | if (first) { 144 | first = false; 145 | } 146 | else { 147 | cmdStr += ','; 148 | } 149 | cmdStr += `${key}=${escapeProperty(val)}`; 150 | } 151 | } 152 | } 153 | } 154 | cmdStr += `${CMD_STRING}${escapeData(this.message)}`; 155 | return cmdStr; 156 | } 157 | } 158 | /** 159 | * Sanitizes an input into a string so it can be passed into issueCommand safely 160 | * @param input input to sanitize into a string 161 | */ 162 | function toCommandValue(input) { 163 | if (input === null || input === undefined) { 164 | return ''; 165 | } 166 | else if (typeof input === 'string' || input instanceof String) { 167 | return input; 168 | } 169 | return JSON.stringify(input); 170 | } 171 | exports.toCommandValue = toCommandValue; 172 | function escapeData(s) { 173 | return toCommandValue(s) 174 | .replace(/%/g, '%25') 175 | .replace(/\r/g, '%0D') 176 | .replace(/\n/g, '%0A'); 177 | } 178 | function escapeProperty(s) { 179 | return toCommandValue(s) 180 | .replace(/%/g, '%25') 181 | .replace(/\r/g, '%0D') 182 | .replace(/\n/g, '%0A') 183 | .replace(/:/g, '%3A') 184 | .replace(/,/g, '%2C'); 185 | } 186 | //# sourceMappingURL=command.js.map 187 | 188 | /***/ }), 189 | 190 | /***/ 470: 191 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 192 | 193 | "use strict"; 194 | 195 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 196 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 197 | return new (P || (P = Promise))(function (resolve, reject) { 198 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 199 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 200 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 201 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 202 | }); 203 | }; 204 | var __importStar = (this && this.__importStar) || function (mod) { 205 | if (mod && mod.__esModule) return mod; 206 | var result = {}; 207 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 208 | result["default"] = mod; 209 | return result; 210 | }; 211 | Object.defineProperty(exports, "__esModule", { value: true }); 212 | const command_1 = __webpack_require__(431); 213 | const os = __importStar(__webpack_require__(87)); 214 | const path = __importStar(__webpack_require__(622)); 215 | /** 216 | * The code to exit an action 217 | */ 218 | var ExitCode; 219 | (function (ExitCode) { 220 | /** 221 | * A code indicating that the action was successful 222 | */ 223 | ExitCode[ExitCode["Success"] = 0] = "Success"; 224 | /** 225 | * A code indicating that the action was a failure 226 | */ 227 | ExitCode[ExitCode["Failure"] = 1] = "Failure"; 228 | })(ExitCode = exports.ExitCode || (exports.ExitCode = {})); 229 | //----------------------------------------------------------------------- 230 | // Variables 231 | //----------------------------------------------------------------------- 232 | /** 233 | * Sets env variable for this action and future actions in the job 234 | * @param name the name of the variable to set 235 | * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify 236 | */ 237 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 238 | function exportVariable(name, val) { 239 | const convertedVal = command_1.toCommandValue(val); 240 | process.env[name] = convertedVal; 241 | command_1.issueCommand('set-env', { name }, convertedVal); 242 | } 243 | exports.exportVariable = exportVariable; 244 | /** 245 | * Registers a secret which will get masked from logs 246 | * @param secret value of the secret 247 | */ 248 | function setSecret(secret) { 249 | command_1.issueCommand('add-mask', {}, secret); 250 | } 251 | exports.setSecret = setSecret; 252 | /** 253 | * Prepends inputPath to the PATH (for this action and future actions) 254 | * @param inputPath 255 | */ 256 | function addPath(inputPath) { 257 | command_1.issueCommand('add-path', {}, inputPath); 258 | process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; 259 | } 260 | exports.addPath = addPath; 261 | /** 262 | * Gets the value of an input. The value is also trimmed. 263 | * 264 | * @param name name of the input to get 265 | * @param options optional. See InputOptions. 266 | * @returns string 267 | */ 268 | function getInput(name, options) { 269 | const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; 270 | if (options && options.required && !val) { 271 | throw new Error(`Input required and not supplied: ${name}`); 272 | } 273 | return val.trim(); 274 | } 275 | exports.getInput = getInput; 276 | /** 277 | * Sets the value of an output. 278 | * 279 | * @param name name of the output to set 280 | * @param value value to store. Non-string values will be converted to a string via JSON.stringify 281 | */ 282 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 283 | function setOutput(name, value) { 284 | command_1.issueCommand('set-output', { name }, value); 285 | } 286 | exports.setOutput = setOutput; 287 | /** 288 | * Enables or disables the echoing of commands into stdout for the rest of the step. 289 | * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. 290 | * 291 | */ 292 | function setCommandEcho(enabled) { 293 | command_1.issue('echo', enabled ? 'on' : 'off'); 294 | } 295 | exports.setCommandEcho = setCommandEcho; 296 | //----------------------------------------------------------------------- 297 | // Results 298 | //----------------------------------------------------------------------- 299 | /** 300 | * Sets the action status to failed. 301 | * When the action exits it will be with an exit code of 1 302 | * @param message add error issue message 303 | */ 304 | function setFailed(message) { 305 | process.exitCode = ExitCode.Failure; 306 | error(message); 307 | } 308 | exports.setFailed = setFailed; 309 | //----------------------------------------------------------------------- 310 | // Logging Commands 311 | //----------------------------------------------------------------------- 312 | /** 313 | * Gets whether Actions Step Debug is on or not 314 | */ 315 | function isDebug() { 316 | return process.env['RUNNER_DEBUG'] === '1'; 317 | } 318 | exports.isDebug = isDebug; 319 | /** 320 | * Writes debug message to user log 321 | * @param message debug message 322 | */ 323 | function debug(message) { 324 | command_1.issueCommand('debug', {}, message); 325 | } 326 | exports.debug = debug; 327 | /** 328 | * Adds an error issue 329 | * @param message error issue message. Errors will be converted to string via toString() 330 | */ 331 | function error(message) { 332 | command_1.issue('error', message instanceof Error ? message.toString() : message); 333 | } 334 | exports.error = error; 335 | /** 336 | * Adds an warning issue 337 | * @param message warning issue message. Errors will be converted to string via toString() 338 | */ 339 | function warning(message) { 340 | command_1.issue('warning', message instanceof Error ? message.toString() : message); 341 | } 342 | exports.warning = warning; 343 | /** 344 | * Writes info to log with console.log. 345 | * @param message info message 346 | */ 347 | function info(message) { 348 | process.stdout.write(message + os.EOL); 349 | } 350 | exports.info = info; 351 | /** 352 | * Begin an output group. 353 | * 354 | * Output until the next `groupEnd` will be foldable in this group 355 | * 356 | * @param name The name of the output group 357 | */ 358 | function startGroup(name) { 359 | command_1.issue('group', name); 360 | } 361 | exports.startGroup = startGroup; 362 | /** 363 | * End an output group. 364 | */ 365 | function endGroup() { 366 | command_1.issue('endgroup'); 367 | } 368 | exports.endGroup = endGroup; 369 | /** 370 | * Wrap an asynchronous function call in a group. 371 | * 372 | * Returns the same type as the function itself. 373 | * 374 | * @param name The name of the group 375 | * @param fn The function to wrap in the group 376 | */ 377 | function group(name, fn) { 378 | return __awaiter(this, void 0, void 0, function* () { 379 | startGroup(name); 380 | let result; 381 | try { 382 | result = yield fn(); 383 | } 384 | finally { 385 | endGroup(); 386 | } 387 | return result; 388 | }); 389 | } 390 | exports.group = group; 391 | //----------------------------------------------------------------------- 392 | // Wrapper action state 393 | //----------------------------------------------------------------------- 394 | /** 395 | * Saves state for current action, the state can only be retrieved by this action's post job execution. 396 | * 397 | * @param name name of the state to store 398 | * @param value value to store. Non-string values will be converted to a string via JSON.stringify 399 | */ 400 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 401 | function saveState(name, value) { 402 | command_1.issueCommand('save-state', { name }, value); 403 | } 404 | exports.saveState = saveState; 405 | /** 406 | * Gets the value of an state set by this action's main execution. 407 | * 408 | * @param name name of the state to get 409 | * @returns string 410 | */ 411 | function getState(name) { 412 | return process.env[`STATE_${name}`] || ''; 413 | } 414 | exports.getState = getState; 415 | //# sourceMappingURL=core.js.map 416 | 417 | /***/ }), 418 | 419 | /***/ 622: 420 | /***/ (function(module) { 421 | 422 | module.exports = require("path"); 423 | 424 | /***/ }), 425 | 426 | /***/ 669: 427 | /***/ (function(module) { 428 | 429 | module.exports = require("util"); 430 | 431 | /***/ }), 432 | 433 | /***/ 964: 434 | /***/ (function(module, __unusedexports, __webpack_require__) { 435 | 436 | const core = __webpack_require__(470); 437 | const { promisify } = __webpack_require__(669); 438 | 439 | const exec = promisify(__webpack_require__(129).exec); 440 | 441 | const asyncForEach = async (array, callback) => { 442 | for (let index = 0; index < array.length; index++) { 443 | await callback(array[index], index, array) 444 | } 445 | } 446 | 447 | let loginToHeroku = async function loginToHeroku(login, password) { 448 | try { 449 | 450 | await exec(`cat >~/.netrc < 0) { 490 | await asyncForEach(imageList, async (item) => { 491 | console.log('Processing image -' + item.imagename); 492 | await exec(`docker tag ${item.imagename} registry.heroku.com/${item.appname}/${item.apptype}`); 493 | console.log('Container tagged for image - ' + item.imagename); 494 | await exec(`docker push registry.heroku.com/${item.appname}/web`); 495 | console.log('Container pushed for image - ' + item.imagename); 496 | await exec(`heroku container:release ${item.apptype} --app ${item.appname}`); 497 | console.log('Container deployed for image - ' + item.imagename); 498 | }); 499 | console.log('App Deployed successfully ✅'); 500 | } else { 501 | core.setFailed(`No image given to process.`); 502 | } 503 | } 504 | catch (error) { 505 | core.setFailed(`Somthing went wrong while pushing and deploying your image. Error: ${error.message}`); 506 | } 507 | } 508 | 509 | let buildAndDeploy = async function buildAndDeploy(login, password, dockerComposeFilePath, imageListString) 510 | { 511 | await loginToHeroku(login, password); 512 | await buildDockerCompose(dockerComposeFilePath); 513 | const imageList = await getImageAppNameList(imageListString); 514 | await pushAndDeployAllImages(imageList); 515 | } 516 | 517 | module.exports.loginToHeroku = loginToHeroku; 518 | module.exports.getImageAppNameList = getImageAppNameList; 519 | module.exports.buildDockerCompose = buildDockerCompose; 520 | module.exports.pushAndDeployAllImages = pushAndDeployAllImages; 521 | module.exports.buildAndDeploy = buildAndDeploy; 522 | 523 | 524 | /***/ }) 525 | 526 | /******/ }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core'); 2 | const app = require('./app'); 3 | 4 | async function run() { 5 | try { 6 | const login = core.getInput('email'); 7 | const password = core.getInput('api_key'); 8 | const imageListString = core.getInput('heroku_apps'); 9 | const dockerComposeFilePath = core.getInput('docker_compose_file'); 10 | 11 | await app.buildAndDeploy(login, password, dockerComposeFilePath, imageListString); 12 | } 13 | catch (error) { 14 | console.log({ message: error.message }); 15 | core.setFailed(error.message); 16 | } 17 | } 18 | 19 | run() -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | 3 | let inputs = { 4 | 'email': 'test@test.com', 5 | 'api_key': 'test', 6 | 'heroku_apps': '[{"imagename":"app1","appname":"app3","apptype":"web"},{"imagename":"app2","appname":"app2","apptype":"web"},{"imagename":"app3","appname":"app2","apptype":"worker"}]', 7 | 'docker_compose_file': './src/docker-compose-heroku.yml' 8 | }; 9 | 10 | jest.mock('util', () => ({ 11 | promisify: jest.fn(() => { 12 | return jest.fn(); 13 | }) 14 | })); 15 | 16 | test('build and deploy with 3 images', async () => { 17 | await app.buildAndDeploy(inputs.email, inputs.api_key, inputs.docker_compose_file, inputs.heroku_apps); 18 | }); 19 | 20 | test('build and deploy with single image', async () => { 21 | await app.buildAndDeploy(inputs.email, inputs.api_key, inputs.docker_compose_file, '[{"imagename":"app1","appname":"app3","apptype":"web"}]'); 22 | }); 23 | 24 | test('get image string to json object', async () => { 25 | 26 | const imageString = '[{"imagename":"app1","appname":"app3","apptype":"web"},{"imagename":"app2","appname":"app2","apptype":"web"},{"imagename":"app3","appname":"app2","apptype":"worker"}]'; 27 | var images = await app.getImageAppNameList(imageString); 28 | 29 | expect(images.length).toBeGreaterThan(0); 30 | expect(images[0].appname).toBe('app3'); 31 | expect(images.length).toBe(3); 32 | 33 | }); 34 | 35 | test('get image string to json object for single element', async () => { 36 | 37 | const imageString = '[{"imagename":"app1","appname":"app3","apptype":"web"}]'; 38 | var images = await app.getImageAppNameList(imageString); 39 | 40 | expect(images.length).toBeGreaterThan(0); 41 | expect(images[0].appname).toBe('app3'); 42 | expect(images.length).toBe(1); 43 | 44 | }); 45 | 46 | test('check execution times', async () => { 47 | 48 | const images = JSON.parse('[{"imagename":"app1","appname":"app3","apptype":"web"}]'); 49 | await app.pushAndDeployAllImages(images); 50 | }); 51 | 52 | test('check build docker compose', async () => { 53 | const path = './src/docker-compose.yml'; 54 | await app.buildDockerCompose(path); 55 | }); 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-compose-multiple-apps-heroku-deploy", 3 | "version": "1.0.0", 4 | "description": "A simple action to build multiple docker images using docker-compose, push and deploy your apps to Heroku Applications", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint index.js", 8 | "build": "npx @zeit/ncc build index.js", 9 | "test": "eslint index.js && jest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/snithyanantham/docker-compose-multiple-apps-heroku-deploy.git" 14 | }, 15 | "keywords": [ 16 | "github", 17 | "actions", 18 | "heroku", 19 | "docker-compose", 20 | "container", 21 | "docker", 22 | "deploy" 23 | ], 24 | "author": "Nithyanantham Subramaniam", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/snithyanantham/docker-compose-multiple-apps-heroku-deploy/issues" 28 | }, 29 | "homepage": "https://github.com/snithyanantham/docker-compose-multiple-apps-heroku-deploy#readme", 30 | "dependencies": { 31 | "@actions/core": "^1.2.4" 32 | }, 33 | "devDependencies": { 34 | "@zeit/ncc": "^0.22.3", 35 | "eslint": "^7.1.0", 36 | "jest": "^26.0.1" 37 | } 38 | } --------------------------------------------------------------------------------