├── .eslintrc.json ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── action.yml ├── bin └── add-private-key ├── dist └── index.js ├── index.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 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | # Editors 4 | .vscode 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://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 | # Other Dependency directories 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.6.2 4 | 5 | - Handle boolean string again as to not always use GIGALIXIR_CLEAN header [#41](https://github.com/mhanberg/gigalixir-action/pull/41) by [Ian Young](https://github.com/iangreenleaf) 6 | 7 | 8 | ## v0.6.1 9 | 10 | - Coerce stringified boolean to an actual bool [#38](https://github.com/mhanberg/gigalixir-action/pull/38) by [Mitch Hanberg](https://github.com/mhanberg) 11 | 12 | ## v0.6.0 13 | 14 | - Add option to clean build cache -[#35](https://github.com/mhanberg/gigalixir-action/pull/35) by [Raul Pereira](https://github.com/raulpe7eira) 15 | - Only require SSH key if you have migratinos configured -[#35](https://github.com/mhanberg/gigalixir-action/pull/35) by [Raul Pereira](https://github.com/raulpe7eira) 16 | 17 | ## v0.5.0 18 | 19 | - Add subfolder support - [#34](https://github.com/mhanberg/gigalixir-action/pull/34) by [Christian Tovar](https://github.com/ChristianTovar) 20 | 21 | ## v0.4.3 22 | 23 | - Fix broken build 24 | 25 | ## v0.4.2 26 | 27 | - De-sudo call to `pip` 28 | 29 | ## v0.4.1 30 | 31 | - Deployment works if the action is making the very first deployment. 32 | 33 | ## v0.4.0 34 | 35 | - Fixed an issue where the action would get stuck at 'Getting current replicas' for apps requesting more than one replica 36 | - Does a health check every 10 seconds instead of increasing the wait time exponentially. Times out now after 10 minutes. 37 | 38 | ## v0.3.0 39 | 40 | - Only add private key and wait for deploy if we are migrating [(#9)](https://github.com/mhanberg/gigalixir-action/pull/9) 41 | 42 | ## v0.2.1 43 | 44 | - Update NPM packages 45 | 46 | ## v0.2.0 47 | 48 | - Config option to run without migrations 49 | 50 | ## v0.1.0 51 | 52 | - Initial Release 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 GitHub Actions 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 | # Gigalixir Action 2 | 3 | This action will deploy your elixir application to Gigalixir and will run your migrations automatically. 4 | 5 | Note: This action has only been tested in one repo and has no unit tests. 6 | 7 | ## Usage 8 | 9 | ```yaml 10 | test: 11 | # A job to run your tests, linters, etc 12 | 13 | deploy: 14 | needs: test # Will only run if the test job succeeds 15 | if: github.ref == 'refs/heads/main' # Only run this job if it is on the main branch 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | with: 22 | ref: main # Check out main instead of the latest commit 23 | fetch-depth: 0 # Checkout the whole branch 24 | 25 | - uses: actions/setup-python@v2 26 | with: 27 | python-version: 3.8.1 28 | 29 | - uses: mhanberg/gigalixir-action@ 30 | with: 31 | APP_SUBFOLDER: my-app-subfolder # Add only if you want to deploy an app that is not at the root of your repository 32 | GIGALIXIR_APP: my-gigalixir-app # Feel free to also put this in your secrets 33 | GIGALIXIR_CLEAN: true # defaults to false 34 | GIGALIXIR_USERNAME: ${{ secrets.GIGALIXIR_USERNAME }} 35 | GIGALIXIR_PASSWORD: ${{ secrets.GIGALIXIR_PASSWORD }} 36 | MIGRATIONS: false # defaults to true 37 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 38 | ``` 39 | 40 | ## Migrations 41 | 42 | Currently running migrations is only supported when your app is deployed as a mix release. 43 | 44 | The migrations are run with the `gigalixir ps:migrate` command, which requires having a public key uploaded to your app's container and a private key locally to connect via an `ssh` connection. 45 | 46 | Please see the docs for [How to Run Migrations](https://gigalixir.readthedocs.io/en/latest/main.html#migrations) for more information. 47 | 48 | If your migrations fail, the action will rollback the app to the last version. 49 | 50 | ## Contributing 51 | 52 | Remember to 53 | 54 | - `npm install` 55 | - `npm run package` 56 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Gigalixir Action' 2 | description: 'Deploy to Gigalixir' 3 | inputs: 4 | APP_SUBFOLDER: 5 | description: 'Subfolder of your app' 6 | required: false 7 | GIGALIXIR_APP: 8 | description: 'Your gigalixir app name' 9 | required: true 10 | GIGALIXIR_CLEAN: 11 | description: 'Extra flag you can pass to clean your cache before building' 12 | required: false 13 | default: false 14 | GIGALIXIR_USERNAME: 15 | description: 'Your Gigalixir username' 16 | required: true 17 | GIGALIXIR_PASSWORD: 18 | description: 'Your Gigalixir password' 19 | required: true 20 | MIGRATIONS: 21 | description: 'Configuration for migrations' 22 | required: true 23 | default: true 24 | SSH_PRIVATE_KEY: 25 | description: 'Your ssh private key that is paired with a public key that is uploaded to Gigalixir' 26 | required: ${MIGRATIONS} 27 | 28 | runs: 29 | using: 'node12' 30 | main: 'dist/index.js' 31 | -------------------------------------------------------------------------------- /bin/add-private-key: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -ev 4 | 5 | mkdir ~/.ssh 6 | 7 | printf "Host *\n StrictHostKeyChecking no" > ~/.ssh/config 8 | 9 | echo "$1" > ~/.ssh/id_rsa 10 | 11 | chmod 400 ~/.ssh/id_rsa 12 | -------------------------------------------------------------------------------- /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 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 23 | /******/ 24 | /******/ // Flag the module as loaded 25 | /******/ module.l = true; 26 | /******/ 27 | /******/ // Return the exports of the module 28 | /******/ return module.exports; 29 | /******/ } 30 | /******/ 31 | /******/ 32 | /******/ __webpack_require__.ab = __dirname + "/"; 33 | /******/ 34 | /******/ // the startup function 35 | /******/ function startup() { 36 | /******/ // Load entry module and return exports 37 | /******/ return __webpack_require__(104); 38 | /******/ }; 39 | /******/ 40 | /******/ // run startup 41 | /******/ return startup(); 42 | /******/ }) 43 | /************************************************************************/ 44 | /******/ ({ 45 | 46 | /***/ 1: 47 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 48 | 49 | "use strict"; 50 | 51 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 52 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 53 | return new (P || (P = Promise))(function (resolve, reject) { 54 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 55 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 56 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 57 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 58 | }); 59 | }; 60 | Object.defineProperty(exports, "__esModule", { value: true }); 61 | const childProcess = __webpack_require__(129); 62 | const path = __webpack_require__(622); 63 | const util_1 = __webpack_require__(669); 64 | const ioUtil = __webpack_require__(672); 65 | const exec = util_1.promisify(childProcess.exec); 66 | /** 67 | * Copies a file or folder. 68 | * Based off of shelljs - https://github.com/shelljs/shelljs/blob/9237f66c52e5daa40458f94f9565e18e8132f5a6/src/cp.js 69 | * 70 | * @param source source path 71 | * @param dest destination path 72 | * @param options optional. See CopyOptions. 73 | */ 74 | function cp(source, dest, options = {}) { 75 | return __awaiter(this, void 0, void 0, function* () { 76 | const { force, recursive } = readCopyOptions(options); 77 | const destStat = (yield ioUtil.exists(dest)) ? yield ioUtil.stat(dest) : null; 78 | // Dest is an existing file, but not forcing 79 | if (destStat && destStat.isFile() && !force) { 80 | return; 81 | } 82 | // If dest is an existing directory, should copy inside. 83 | const newDest = destStat && destStat.isDirectory() 84 | ? path.join(dest, path.basename(source)) 85 | : dest; 86 | if (!(yield ioUtil.exists(source))) { 87 | throw new Error(`no such file or directory: ${source}`); 88 | } 89 | const sourceStat = yield ioUtil.stat(source); 90 | if (sourceStat.isDirectory()) { 91 | if (!recursive) { 92 | throw new Error(`Failed to copy. ${source} is a directory, but tried to copy without recursive flag.`); 93 | } 94 | else { 95 | yield cpDirRecursive(source, newDest, 0, force); 96 | } 97 | } 98 | else { 99 | if (path.relative(source, newDest) === '') { 100 | // a file cannot be copied to itself 101 | throw new Error(`'${newDest}' and '${source}' are the same file`); 102 | } 103 | yield copyFile(source, newDest, force); 104 | } 105 | }); 106 | } 107 | exports.cp = cp; 108 | /** 109 | * Moves a path. 110 | * 111 | * @param source source path 112 | * @param dest destination path 113 | * @param options optional. See MoveOptions. 114 | */ 115 | function mv(source, dest, options = {}) { 116 | return __awaiter(this, void 0, void 0, function* () { 117 | if (yield ioUtil.exists(dest)) { 118 | let destExists = true; 119 | if (yield ioUtil.isDirectory(dest)) { 120 | // If dest is directory copy src into dest 121 | dest = path.join(dest, path.basename(source)); 122 | destExists = yield ioUtil.exists(dest); 123 | } 124 | if (destExists) { 125 | if (options.force == null || options.force) { 126 | yield rmRF(dest); 127 | } 128 | else { 129 | throw new Error('Destination already exists'); 130 | } 131 | } 132 | } 133 | yield mkdirP(path.dirname(dest)); 134 | yield ioUtil.rename(source, dest); 135 | }); 136 | } 137 | exports.mv = mv; 138 | /** 139 | * Remove a path recursively with force 140 | * 141 | * @param inputPath path to remove 142 | */ 143 | function rmRF(inputPath) { 144 | return __awaiter(this, void 0, void 0, function* () { 145 | if (ioUtil.IS_WINDOWS) { 146 | // Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another 147 | // program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del. 148 | try { 149 | if (yield ioUtil.isDirectory(inputPath, true)) { 150 | yield exec(`rd /s /q "${inputPath}"`); 151 | } 152 | else { 153 | yield exec(`del /f /a "${inputPath}"`); 154 | } 155 | } 156 | catch (err) { 157 | // if you try to delete a file that doesn't exist, desired result is achieved 158 | // other errors are valid 159 | if (err.code !== 'ENOENT') 160 | throw err; 161 | } 162 | // Shelling out fails to remove a symlink folder with missing source, this unlink catches that 163 | try { 164 | yield ioUtil.unlink(inputPath); 165 | } 166 | catch (err) { 167 | // if you try to delete a file that doesn't exist, desired result is achieved 168 | // other errors are valid 169 | if (err.code !== 'ENOENT') 170 | throw err; 171 | } 172 | } 173 | else { 174 | let isDir = false; 175 | try { 176 | isDir = yield ioUtil.isDirectory(inputPath); 177 | } 178 | catch (err) { 179 | // if you try to delete a file that doesn't exist, desired result is achieved 180 | // other errors are valid 181 | if (err.code !== 'ENOENT') 182 | throw err; 183 | return; 184 | } 185 | if (isDir) { 186 | yield exec(`rm -rf "${inputPath}"`); 187 | } 188 | else { 189 | yield ioUtil.unlink(inputPath); 190 | } 191 | } 192 | }); 193 | } 194 | exports.rmRF = rmRF; 195 | /** 196 | * Make a directory. Creates the full path with folders in between 197 | * Will throw if it fails 198 | * 199 | * @param fsPath path to create 200 | * @returns Promise 201 | */ 202 | function mkdirP(fsPath) { 203 | return __awaiter(this, void 0, void 0, function* () { 204 | yield ioUtil.mkdirP(fsPath); 205 | }); 206 | } 207 | exports.mkdirP = mkdirP; 208 | /** 209 | * Returns path of a tool had the tool actually been invoked. Resolves via paths. 210 | * If you check and the tool does not exist, it will throw. 211 | * 212 | * @param tool name of the tool 213 | * @param check whether to check if tool exists 214 | * @returns Promise path to tool 215 | */ 216 | function which(tool, check) { 217 | return __awaiter(this, void 0, void 0, function* () { 218 | if (!tool) { 219 | throw new Error("parameter 'tool' is required"); 220 | } 221 | // recursive when check=true 222 | if (check) { 223 | const result = yield which(tool, false); 224 | if (!result) { 225 | if (ioUtil.IS_WINDOWS) { 226 | throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.`); 227 | } 228 | else { 229 | throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.`); 230 | } 231 | } 232 | } 233 | try { 234 | // build the list of extensions to try 235 | const extensions = []; 236 | if (ioUtil.IS_WINDOWS && process.env.PATHEXT) { 237 | for (const extension of process.env.PATHEXT.split(path.delimiter)) { 238 | if (extension) { 239 | extensions.push(extension); 240 | } 241 | } 242 | } 243 | // if it's rooted, return it if exists. otherwise return empty. 244 | if (ioUtil.isRooted(tool)) { 245 | const filePath = yield ioUtil.tryGetExecutablePath(tool, extensions); 246 | if (filePath) { 247 | return filePath; 248 | } 249 | return ''; 250 | } 251 | // if any path separators, return empty 252 | if (tool.includes('/') || (ioUtil.IS_WINDOWS && tool.includes('\\'))) { 253 | return ''; 254 | } 255 | // build the list of directories 256 | // 257 | // Note, technically "where" checks the current directory on Windows. From a toolkit perspective, 258 | // it feels like we should not do this. Checking the current directory seems like more of a use 259 | // case of a shell, and the which() function exposed by the toolkit should strive for consistency 260 | // across platforms. 261 | const directories = []; 262 | if (process.env.PATH) { 263 | for (const p of process.env.PATH.split(path.delimiter)) { 264 | if (p) { 265 | directories.push(p); 266 | } 267 | } 268 | } 269 | // return the first match 270 | for (const directory of directories) { 271 | const filePath = yield ioUtil.tryGetExecutablePath(directory + path.sep + tool, extensions); 272 | if (filePath) { 273 | return filePath; 274 | } 275 | } 276 | return ''; 277 | } 278 | catch (err) { 279 | throw new Error(`which failed with message ${err.message}`); 280 | } 281 | }); 282 | } 283 | exports.which = which; 284 | function readCopyOptions(options) { 285 | const force = options.force == null ? true : options.force; 286 | const recursive = Boolean(options.recursive); 287 | return { force, recursive }; 288 | } 289 | function cpDirRecursive(sourceDir, destDir, currentDepth, force) { 290 | return __awaiter(this, void 0, void 0, function* () { 291 | // Ensure there is not a run away recursive copy 292 | if (currentDepth >= 255) 293 | return; 294 | currentDepth++; 295 | yield mkdirP(destDir); 296 | const files = yield ioUtil.readdir(sourceDir); 297 | for (const fileName of files) { 298 | const srcFile = `${sourceDir}/${fileName}`; 299 | const destFile = `${destDir}/${fileName}`; 300 | const srcFileStat = yield ioUtil.lstat(srcFile); 301 | if (srcFileStat.isDirectory()) { 302 | // Recurse 303 | yield cpDirRecursive(srcFile, destFile, currentDepth, force); 304 | } 305 | else { 306 | yield copyFile(srcFile, destFile, force); 307 | } 308 | } 309 | // Change the mode for the newly created directory 310 | yield ioUtil.chmod(destDir, (yield ioUtil.stat(sourceDir)).mode); 311 | }); 312 | } 313 | // Buffered file copy 314 | function copyFile(srcFile, destFile, force) { 315 | return __awaiter(this, void 0, void 0, function* () { 316 | if ((yield ioUtil.lstat(srcFile)).isSymbolicLink()) { 317 | // unlink/re-link it 318 | try { 319 | yield ioUtil.lstat(destFile); 320 | yield ioUtil.unlink(destFile); 321 | } 322 | catch (e) { 323 | // Try to override file permission 324 | if (e.code === 'EPERM') { 325 | yield ioUtil.chmod(destFile, '0666'); 326 | yield ioUtil.unlink(destFile); 327 | } 328 | // other errors = it doesn't exist, no work to do 329 | } 330 | // Copy over symlink 331 | const symlinkFull = yield ioUtil.readlink(srcFile); 332 | yield ioUtil.symlink(symlinkFull, destFile, ioUtil.IS_WINDOWS ? 'junction' : null); 333 | } 334 | else if (!(yield ioUtil.exists(destFile)) || force) { 335 | yield ioUtil.copyFile(srcFile, destFile); 336 | } 337 | }); 338 | } 339 | //# sourceMappingURL=io.js.map 340 | 341 | /***/ }), 342 | 343 | /***/ 9: 344 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 345 | 346 | "use strict"; 347 | 348 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 349 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 350 | return new (P || (P = Promise))(function (resolve, reject) { 351 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 352 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 353 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 354 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 355 | }); 356 | }; 357 | Object.defineProperty(exports, "__esModule", { value: true }); 358 | const os = __webpack_require__(87); 359 | const events = __webpack_require__(614); 360 | const child = __webpack_require__(129); 361 | const path = __webpack_require__(622); 362 | const io = __webpack_require__(1); 363 | const ioUtil = __webpack_require__(672); 364 | /* eslint-disable @typescript-eslint/unbound-method */ 365 | const IS_WINDOWS = process.platform === 'win32'; 366 | /* 367 | * Class for running command line tools. Handles quoting and arg parsing in a platform agnostic way. 368 | */ 369 | class ToolRunner extends events.EventEmitter { 370 | constructor(toolPath, args, options) { 371 | super(); 372 | if (!toolPath) { 373 | throw new Error("Parameter 'toolPath' cannot be null or empty."); 374 | } 375 | this.toolPath = toolPath; 376 | this.args = args || []; 377 | this.options = options || {}; 378 | } 379 | _debug(message) { 380 | if (this.options.listeners && this.options.listeners.debug) { 381 | this.options.listeners.debug(message); 382 | } 383 | } 384 | _getCommandString(options, noPrefix) { 385 | const toolPath = this._getSpawnFileName(); 386 | const args = this._getSpawnArgs(options); 387 | let cmd = noPrefix ? '' : '[command]'; // omit prefix when piped to a second tool 388 | if (IS_WINDOWS) { 389 | // Windows + cmd file 390 | if (this._isCmdFile()) { 391 | cmd += toolPath; 392 | for (const a of args) { 393 | cmd += ` ${a}`; 394 | } 395 | } 396 | // Windows + verbatim 397 | else if (options.windowsVerbatimArguments) { 398 | cmd += `"${toolPath}"`; 399 | for (const a of args) { 400 | cmd += ` ${a}`; 401 | } 402 | } 403 | // Windows (regular) 404 | else { 405 | cmd += this._windowsQuoteCmdArg(toolPath); 406 | for (const a of args) { 407 | cmd += ` ${this._windowsQuoteCmdArg(a)}`; 408 | } 409 | } 410 | } 411 | else { 412 | // OSX/Linux - this can likely be improved with some form of quoting. 413 | // creating processes on Unix is fundamentally different than Windows. 414 | // on Unix, execvp() takes an arg array. 415 | cmd += toolPath; 416 | for (const a of args) { 417 | cmd += ` ${a}`; 418 | } 419 | } 420 | return cmd; 421 | } 422 | _processLineBuffer(data, strBuffer, onLine) { 423 | try { 424 | let s = strBuffer + data.toString(); 425 | let n = s.indexOf(os.EOL); 426 | while (n > -1) { 427 | const line = s.substring(0, n); 428 | onLine(line); 429 | // the rest of the string ... 430 | s = s.substring(n + os.EOL.length); 431 | n = s.indexOf(os.EOL); 432 | } 433 | strBuffer = s; 434 | } 435 | catch (err) { 436 | // streaming lines to console is best effort. Don't fail a build. 437 | this._debug(`error processing line. Failed with error ${err}`); 438 | } 439 | } 440 | _getSpawnFileName() { 441 | if (IS_WINDOWS) { 442 | if (this._isCmdFile()) { 443 | return process.env['COMSPEC'] || 'cmd.exe'; 444 | } 445 | } 446 | return this.toolPath; 447 | } 448 | _getSpawnArgs(options) { 449 | if (IS_WINDOWS) { 450 | if (this._isCmdFile()) { 451 | let argline = `/D /S /C "${this._windowsQuoteCmdArg(this.toolPath)}`; 452 | for (const a of this.args) { 453 | argline += ' '; 454 | argline += options.windowsVerbatimArguments 455 | ? a 456 | : this._windowsQuoteCmdArg(a); 457 | } 458 | argline += '"'; 459 | return [argline]; 460 | } 461 | } 462 | return this.args; 463 | } 464 | _endsWith(str, end) { 465 | return str.endsWith(end); 466 | } 467 | _isCmdFile() { 468 | const upperToolPath = this.toolPath.toUpperCase(); 469 | return (this._endsWith(upperToolPath, '.CMD') || 470 | this._endsWith(upperToolPath, '.BAT')); 471 | } 472 | _windowsQuoteCmdArg(arg) { 473 | // for .exe, apply the normal quoting rules that libuv applies 474 | if (!this._isCmdFile()) { 475 | return this._uvQuoteCmdArg(arg); 476 | } 477 | // otherwise apply quoting rules specific to the cmd.exe command line parser. 478 | // the libuv rules are generic and are not designed specifically for cmd.exe 479 | // command line parser. 480 | // 481 | // for a detailed description of the cmd.exe command line parser, refer to 482 | // http://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/7970912#7970912 483 | // need quotes for empty arg 484 | if (!arg) { 485 | return '""'; 486 | } 487 | // determine whether the arg needs to be quoted 488 | const cmdSpecialChars = [ 489 | ' ', 490 | '\t', 491 | '&', 492 | '(', 493 | ')', 494 | '[', 495 | ']', 496 | '{', 497 | '}', 498 | '^', 499 | '=', 500 | ';', 501 | '!', 502 | "'", 503 | '+', 504 | ',', 505 | '`', 506 | '~', 507 | '|', 508 | '<', 509 | '>', 510 | '"' 511 | ]; 512 | let needsQuotes = false; 513 | for (const char of arg) { 514 | if (cmdSpecialChars.some(x => x === char)) { 515 | needsQuotes = true; 516 | break; 517 | } 518 | } 519 | // short-circuit if quotes not needed 520 | if (!needsQuotes) { 521 | return arg; 522 | } 523 | // the following quoting rules are very similar to the rules that by libuv applies. 524 | // 525 | // 1) wrap the string in quotes 526 | // 527 | // 2) double-up quotes - i.e. " => "" 528 | // 529 | // this is different from the libuv quoting rules. libuv replaces " with \", which unfortunately 530 | // doesn't work well with a cmd.exe command line. 531 | // 532 | // note, replacing " with "" also works well if the arg is passed to a downstream .NET console app. 533 | // for example, the command line: 534 | // foo.exe "myarg:""my val""" 535 | // is parsed by a .NET console app into an arg array: 536 | // [ "myarg:\"my val\"" ] 537 | // which is the same end result when applying libuv quoting rules. although the actual 538 | // command line from libuv quoting rules would look like: 539 | // foo.exe "myarg:\"my val\"" 540 | // 541 | // 3) double-up slashes that precede a quote, 542 | // e.g. hello \world => "hello \world" 543 | // hello\"world => "hello\\""world" 544 | // hello\\"world => "hello\\\\""world" 545 | // hello world\ => "hello world\\" 546 | // 547 | // technically this is not required for a cmd.exe command line, or the batch argument parser. 548 | // the reasons for including this as a .cmd quoting rule are: 549 | // 550 | // a) this is optimized for the scenario where the argument is passed from the .cmd file to an 551 | // external program. many programs (e.g. .NET console apps) rely on the slash-doubling rule. 552 | // 553 | // b) it's what we've been doing previously (by deferring to node default behavior) and we 554 | // haven't heard any complaints about that aspect. 555 | // 556 | // note, a weakness of the quoting rules chosen here, is that % is not escaped. in fact, % cannot be 557 | // escaped when used on the command line directly - even though within a .cmd file % can be escaped 558 | // by using %%. 559 | // 560 | // the saving grace is, on the command line, %var% is left as-is if var is not defined. this contrasts 561 | // the line parsing rules within a .cmd file, where if var is not defined it is replaced with nothing. 562 | // 563 | // one option that was explored was replacing % with ^% - i.e. %var% => ^%var^%. this hack would 564 | // often work, since it is unlikely that var^ would exist, and the ^ character is removed when the 565 | // variable is used. the problem, however, is that ^ is not removed when %* is used to pass the args 566 | // to an external program. 567 | // 568 | // an unexplored potential solution for the % escaping problem, is to create a wrapper .cmd file. 569 | // % can be escaped within a .cmd file. 570 | let reverse = '"'; 571 | let quoteHit = true; 572 | for (let i = arg.length; i > 0; i--) { 573 | // walk the string in reverse 574 | reverse += arg[i - 1]; 575 | if (quoteHit && arg[i - 1] === '\\') { 576 | reverse += '\\'; // double the slash 577 | } 578 | else if (arg[i - 1] === '"') { 579 | quoteHit = true; 580 | reverse += '"'; // double the quote 581 | } 582 | else { 583 | quoteHit = false; 584 | } 585 | } 586 | reverse += '"'; 587 | return reverse 588 | .split('') 589 | .reverse() 590 | .join(''); 591 | } 592 | _uvQuoteCmdArg(arg) { 593 | // Tool runner wraps child_process.spawn() and needs to apply the same quoting as 594 | // Node in certain cases where the undocumented spawn option windowsVerbatimArguments 595 | // is used. 596 | // 597 | // Since this function is a port of quote_cmd_arg from Node 4.x (technically, lib UV, 598 | // see https://github.com/nodejs/node/blob/v4.x/deps/uv/src/win/process.c for details), 599 | // pasting copyright notice from Node within this function: 600 | // 601 | // Copyright Joyent, Inc. and other Node contributors. All rights reserved. 602 | // 603 | // Permission is hereby granted, free of charge, to any person obtaining a copy 604 | // of this software and associated documentation files (the "Software"), to 605 | // deal in the Software without restriction, including without limitation the 606 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 607 | // sell copies of the Software, and to permit persons to whom the Software is 608 | // furnished to do so, subject to the following conditions: 609 | // 610 | // The above copyright notice and this permission notice shall be included in 611 | // all copies or substantial portions of the Software. 612 | // 613 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 614 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 615 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 616 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 617 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 618 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 619 | // IN THE SOFTWARE. 620 | if (!arg) { 621 | // Need double quotation for empty argument 622 | return '""'; 623 | } 624 | if (!arg.includes(' ') && !arg.includes('\t') && !arg.includes('"')) { 625 | // No quotation needed 626 | return arg; 627 | } 628 | if (!arg.includes('"') && !arg.includes('\\')) { 629 | // No embedded double quotes or backslashes, so I can just wrap 630 | // quote marks around the whole thing. 631 | return `"${arg}"`; 632 | } 633 | // Expected input/output: 634 | // input : hello"world 635 | // output: "hello\"world" 636 | // input : hello""world 637 | // output: "hello\"\"world" 638 | // input : hello\world 639 | // output: hello\world 640 | // input : hello\\world 641 | // output: hello\\world 642 | // input : hello\"world 643 | // output: "hello\\\"world" 644 | // input : hello\\"world 645 | // output: "hello\\\\\"world" 646 | // input : hello world\ 647 | // output: "hello world\\" - note the comment in libuv actually reads "hello world\" 648 | // but it appears the comment is wrong, it should be "hello world\\" 649 | let reverse = '"'; 650 | let quoteHit = true; 651 | for (let i = arg.length; i > 0; i--) { 652 | // walk the string in reverse 653 | reverse += arg[i - 1]; 654 | if (quoteHit && arg[i - 1] === '\\') { 655 | reverse += '\\'; 656 | } 657 | else if (arg[i - 1] === '"') { 658 | quoteHit = true; 659 | reverse += '\\'; 660 | } 661 | else { 662 | quoteHit = false; 663 | } 664 | } 665 | reverse += '"'; 666 | return reverse 667 | .split('') 668 | .reverse() 669 | .join(''); 670 | } 671 | _cloneExecOptions(options) { 672 | options = options || {}; 673 | const result = { 674 | cwd: options.cwd || process.cwd(), 675 | env: options.env || process.env, 676 | silent: options.silent || false, 677 | windowsVerbatimArguments: options.windowsVerbatimArguments || false, 678 | failOnStdErr: options.failOnStdErr || false, 679 | ignoreReturnCode: options.ignoreReturnCode || false, 680 | delay: options.delay || 10000 681 | }; 682 | result.outStream = options.outStream || process.stdout; 683 | result.errStream = options.errStream || process.stderr; 684 | return result; 685 | } 686 | _getSpawnOptions(options, toolPath) { 687 | options = options || {}; 688 | const result = {}; 689 | result.cwd = options.cwd; 690 | result.env = options.env; 691 | result['windowsVerbatimArguments'] = 692 | options.windowsVerbatimArguments || this._isCmdFile(); 693 | if (options.windowsVerbatimArguments) { 694 | result.argv0 = `"${toolPath}"`; 695 | } 696 | return result; 697 | } 698 | /** 699 | * Exec a tool. 700 | * Output will be streamed to the live console. 701 | * Returns promise with return code 702 | * 703 | * @param tool path to tool to exec 704 | * @param options optional exec options. See ExecOptions 705 | * @returns number 706 | */ 707 | exec() { 708 | return __awaiter(this, void 0, void 0, function* () { 709 | // root the tool path if it is unrooted and contains relative pathing 710 | if (!ioUtil.isRooted(this.toolPath) && 711 | (this.toolPath.includes('/') || 712 | (IS_WINDOWS && this.toolPath.includes('\\')))) { 713 | // prefer options.cwd if it is specified, however options.cwd may also need to be rooted 714 | this.toolPath = path.resolve(process.cwd(), this.options.cwd || process.cwd(), this.toolPath); 715 | } 716 | // if the tool is only a file name, then resolve it from the PATH 717 | // otherwise verify it exists (add extension on Windows if necessary) 718 | this.toolPath = yield io.which(this.toolPath, true); 719 | return new Promise((resolve, reject) => { 720 | this._debug(`exec tool: ${this.toolPath}`); 721 | this._debug('arguments:'); 722 | for (const arg of this.args) { 723 | this._debug(` ${arg}`); 724 | } 725 | const optionsNonNull = this._cloneExecOptions(this.options); 726 | if (!optionsNonNull.silent && optionsNonNull.outStream) { 727 | optionsNonNull.outStream.write(this._getCommandString(optionsNonNull) + os.EOL); 728 | } 729 | const state = new ExecState(optionsNonNull, this.toolPath); 730 | state.on('debug', (message) => { 731 | this._debug(message); 732 | }); 733 | const fileName = this._getSpawnFileName(); 734 | const cp = child.spawn(fileName, this._getSpawnArgs(optionsNonNull), this._getSpawnOptions(this.options, fileName)); 735 | const stdbuffer = ''; 736 | if (cp.stdout) { 737 | cp.stdout.on('data', (data) => { 738 | if (this.options.listeners && this.options.listeners.stdout) { 739 | this.options.listeners.stdout(data); 740 | } 741 | if (!optionsNonNull.silent && optionsNonNull.outStream) { 742 | optionsNonNull.outStream.write(data); 743 | } 744 | this._processLineBuffer(data, stdbuffer, (line) => { 745 | if (this.options.listeners && this.options.listeners.stdline) { 746 | this.options.listeners.stdline(line); 747 | } 748 | }); 749 | }); 750 | } 751 | const errbuffer = ''; 752 | if (cp.stderr) { 753 | cp.stderr.on('data', (data) => { 754 | state.processStderr = true; 755 | if (this.options.listeners && this.options.listeners.stderr) { 756 | this.options.listeners.stderr(data); 757 | } 758 | if (!optionsNonNull.silent && 759 | optionsNonNull.errStream && 760 | optionsNonNull.outStream) { 761 | const s = optionsNonNull.failOnStdErr 762 | ? optionsNonNull.errStream 763 | : optionsNonNull.outStream; 764 | s.write(data); 765 | } 766 | this._processLineBuffer(data, errbuffer, (line) => { 767 | if (this.options.listeners && this.options.listeners.errline) { 768 | this.options.listeners.errline(line); 769 | } 770 | }); 771 | }); 772 | } 773 | cp.on('error', (err) => { 774 | state.processError = err.message; 775 | state.processExited = true; 776 | state.processClosed = true; 777 | state.CheckComplete(); 778 | }); 779 | cp.on('exit', (code) => { 780 | state.processExitCode = code; 781 | state.processExited = true; 782 | this._debug(`Exit code ${code} received from tool '${this.toolPath}'`); 783 | state.CheckComplete(); 784 | }); 785 | cp.on('close', (code) => { 786 | state.processExitCode = code; 787 | state.processExited = true; 788 | state.processClosed = true; 789 | this._debug(`STDIO streams have closed for tool '${this.toolPath}'`); 790 | state.CheckComplete(); 791 | }); 792 | state.on('done', (error, exitCode) => { 793 | if (stdbuffer.length > 0) { 794 | this.emit('stdline', stdbuffer); 795 | } 796 | if (errbuffer.length > 0) { 797 | this.emit('errline', errbuffer); 798 | } 799 | cp.removeAllListeners(); 800 | if (error) { 801 | reject(error); 802 | } 803 | else { 804 | resolve(exitCode); 805 | } 806 | }); 807 | }); 808 | }); 809 | } 810 | } 811 | exports.ToolRunner = ToolRunner; 812 | /** 813 | * Convert an arg string to an array of args. Handles escaping 814 | * 815 | * @param argString string of arguments 816 | * @returns string[] array of arguments 817 | */ 818 | function argStringToArray(argString) { 819 | const args = []; 820 | let inQuotes = false; 821 | let escaped = false; 822 | let arg = ''; 823 | function append(c) { 824 | // we only escape double quotes. 825 | if (escaped && c !== '"') { 826 | arg += '\\'; 827 | } 828 | arg += c; 829 | escaped = false; 830 | } 831 | for (let i = 0; i < argString.length; i++) { 832 | const c = argString.charAt(i); 833 | if (c === '"') { 834 | if (!escaped) { 835 | inQuotes = !inQuotes; 836 | } 837 | else { 838 | append(c); 839 | } 840 | continue; 841 | } 842 | if (c === '\\' && escaped) { 843 | append(c); 844 | continue; 845 | } 846 | if (c === '\\' && inQuotes) { 847 | escaped = true; 848 | continue; 849 | } 850 | if (c === ' ' && !inQuotes) { 851 | if (arg.length > 0) { 852 | args.push(arg); 853 | arg = ''; 854 | } 855 | continue; 856 | } 857 | append(c); 858 | } 859 | if (arg.length > 0) { 860 | args.push(arg.trim()); 861 | } 862 | return args; 863 | } 864 | exports.argStringToArray = argStringToArray; 865 | class ExecState extends events.EventEmitter { 866 | constructor(options, toolPath) { 867 | super(); 868 | this.processClosed = false; // tracks whether the process has exited and stdio is closed 869 | this.processError = ''; 870 | this.processExitCode = 0; 871 | this.processExited = false; // tracks whether the process has exited 872 | this.processStderr = false; // tracks whether stderr was written to 873 | this.delay = 10000; // 10 seconds 874 | this.done = false; 875 | this.timeout = null; 876 | if (!toolPath) { 877 | throw new Error('toolPath must not be empty'); 878 | } 879 | this.options = options; 880 | this.toolPath = toolPath; 881 | if (options.delay) { 882 | this.delay = options.delay; 883 | } 884 | } 885 | CheckComplete() { 886 | if (this.done) { 887 | return; 888 | } 889 | if (this.processClosed) { 890 | this._setResult(); 891 | } 892 | else if (this.processExited) { 893 | this.timeout = setTimeout(ExecState.HandleTimeout, this.delay, this); 894 | } 895 | } 896 | _debug(message) { 897 | this.emit('debug', message); 898 | } 899 | _setResult() { 900 | // determine whether there is an error 901 | let error; 902 | if (this.processExited) { 903 | if (this.processError) { 904 | error = new Error(`There was an error when attempting to execute the process '${this.toolPath}'. This may indicate the process failed to start. Error: ${this.processError}`); 905 | } 906 | else if (this.processExitCode !== 0 && !this.options.ignoreReturnCode) { 907 | error = new Error(`The process '${this.toolPath}' failed with exit code ${this.processExitCode}`); 908 | } 909 | else if (this.processStderr && this.options.failOnStdErr) { 910 | error = new Error(`The process '${this.toolPath}' failed because one or more lines were written to the STDERR stream`); 911 | } 912 | } 913 | // clear the timeout 914 | if (this.timeout) { 915 | clearTimeout(this.timeout); 916 | this.timeout = null; 917 | } 918 | this.done = true; 919 | this.emit('done', error, this.processExitCode); 920 | } 921 | static HandleTimeout(state) { 922 | if (state.done) { 923 | return; 924 | } 925 | if (!state.processClosed && state.processExited) { 926 | const message = `The STDIO streams did not close within ${state.delay / 927 | 1000} seconds of the exit event from process '${state.toolPath}'. This may indicate a child process inherited the STDIO streams and has not yet exited.`; 928 | state._debug(message); 929 | } 930 | state._setResult(); 931 | } 932 | } 933 | //# sourceMappingURL=toolrunner.js.map 934 | 935 | /***/ }), 936 | 937 | /***/ 82: 938 | /***/ (function(__unusedmodule, exports) { 939 | 940 | "use strict"; 941 | 942 | // We use any as a valid input type 943 | /* eslint-disable @typescript-eslint/no-explicit-any */ 944 | Object.defineProperty(exports, "__esModule", { value: true }); 945 | /** 946 | * Sanitizes an input into a string so it can be passed into issueCommand safely 947 | * @param input input to sanitize into a string 948 | */ 949 | function toCommandValue(input) { 950 | if (input === null || input === undefined) { 951 | return ''; 952 | } 953 | else if (typeof input === 'string' || input instanceof String) { 954 | return input; 955 | } 956 | return JSON.stringify(input); 957 | } 958 | exports.toCommandValue = toCommandValue; 959 | //# sourceMappingURL=utils.js.map 960 | 961 | /***/ }), 962 | 963 | /***/ 87: 964 | /***/ (function(module) { 965 | 966 | module.exports = require("os"); 967 | 968 | /***/ }), 969 | 970 | /***/ 102: 971 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 972 | 973 | "use strict"; 974 | 975 | // For internal use, subject to change. 976 | var __importStar = (this && this.__importStar) || function (mod) { 977 | if (mod && mod.__esModule) return mod; 978 | var result = {}; 979 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 980 | result["default"] = mod; 981 | return result; 982 | }; 983 | Object.defineProperty(exports, "__esModule", { value: true }); 984 | // We use any as a valid input type 985 | /* eslint-disable @typescript-eslint/no-explicit-any */ 986 | const fs = __importStar(__webpack_require__(747)); 987 | const os = __importStar(__webpack_require__(87)); 988 | const utils_1 = __webpack_require__(82); 989 | function issueCommand(command, message) { 990 | const filePath = process.env[`GITHUB_${command}`]; 991 | if (!filePath) { 992 | throw new Error(`Unable to find environment variable for file command ${command}`); 993 | } 994 | if (!fs.existsSync(filePath)) { 995 | throw new Error(`Missing file at path: ${filePath}`); 996 | } 997 | fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, { 998 | encoding: 'utf8' 999 | }); 1000 | } 1001 | exports.issueCommand = issueCommand; 1002 | //# sourceMappingURL=file-command.js.map 1003 | 1004 | /***/ }), 1005 | 1006 | /***/ 104: 1007 | /***/ (function(__unusedmodule, __unusedexports, __webpack_require__) { 1008 | 1009 | const core = __webpack_require__(470); 1010 | const exec = __webpack_require__(986); 1011 | const path = __webpack_require__(622); 1012 | 1013 | function wait(seconds) { 1014 | return new Promise(resolve => { 1015 | if ((typeof seconds) !== 'number') { 1016 | throw new Error('seconds not a number'); 1017 | } 1018 | 1019 | core.info(`Waiting ${seconds} seconds...`); 1020 | 1021 | setTimeout(() => resolve("done!"), seconds * 1000) 1022 | }); 1023 | } 1024 | 1025 | async function isNextReleaseHealthy(release, app) { 1026 | let releasesOutput = ''; 1027 | 1028 | const options = { 1029 | listeners: { 1030 | stdout: data => { 1031 | releasesOutput += data.toString(); 1032 | } 1033 | } 1034 | }; 1035 | 1036 | await core.group("Getting current replicas", async () => { 1037 | await exec.exec(`gigalixir ps -a ${app}`, [], options); 1038 | }); 1039 | 1040 | const releases = JSON.parse(releasesOutput); 1041 | return releases.pods.filter((pod) => (Number(pod.version) === release && pod.status === "Healthy")).length >= releases.replicas_desired; 1042 | } 1043 | 1044 | async function waitForNewRelease(oldRelease, app, attempts) { 1045 | const maxAttempts = 60; 1046 | 1047 | if (await isNextReleaseHealthy(oldRelease + 1, app)) { 1048 | return await Promise.resolve(true); 1049 | } else { 1050 | if (attempts <= maxAttempts) { 1051 | await wait(10); 1052 | await waitForNewRelease(oldRelease, app, attempts + 1); 1053 | } else { 1054 | throw "Taking too long for new release to deploy"; 1055 | } 1056 | } 1057 | } 1058 | 1059 | async function getCurrentRelease(app) { 1060 | let releasesOutput = ''; 1061 | 1062 | const options = { 1063 | listeners: { 1064 | stdout: data => { 1065 | releasesOutput += data.toString(); 1066 | } 1067 | } 1068 | }; 1069 | 1070 | await core.group("Getting current release", async () => { 1071 | await exec.exec(`gigalixir releases -a ${app}`, [], options); 1072 | }); 1073 | 1074 | const releases = JSON.parse(releasesOutput); 1075 | const currentRelease = releases.length ? Number(releases[0].version) : 0; 1076 | 1077 | return currentRelease; 1078 | } 1079 | 1080 | function formatReleaseMessage(releaseNumber) { 1081 | return releaseNumber ? 1082 | `The current release is ${releaseNumber}` : 1083 | "This is the first release"; 1084 | } 1085 | 1086 | function addExtraFlagCleanCache(gigalixirClean) { 1087 | return (gigalixirClean === "true") ? ` -c http.extraheader="GIGALIXIR-CLEAN: true" ` : "" 1088 | } 1089 | 1090 | async function run() { 1091 | try { 1092 | const appSubfolder = core.getInput('APP_SUBFOLDER', {required: false}); 1093 | const gigalixirApp = core.getInput('GIGALIXIR_APP', {required: true}); 1094 | const gigalixirClean = core.getInput('GIGALIXIR_CLEAN', {required: false}); 1095 | const gigalixirUsername = core.getInput('GIGALIXIR_USERNAME', {required: true}); 1096 | const gigalixirPassword = core.getInput('GIGALIXIR_PASSWORD', {required: true}); 1097 | const migrations = core.getInput('MIGRATIONS', {required: true}); 1098 | const sshPrivateKey = core.getInput('SSH_PRIVATE_KEY', {required: JSON.parse(migrations)}); 1099 | 1100 | await core.group("Installing gigalixir", async () => { 1101 | await exec.exec('pip3 install gigalixir') 1102 | }); 1103 | 1104 | await core.group("Logging in to gigalixir", async () => { 1105 | await exec.exec(`gigalixir login -e "${gigalixirUsername}" -y -p "${gigalixirPassword}"`) 1106 | }); 1107 | 1108 | await core.group("Setting git remote for gigalixir", async () => { 1109 | await exec.exec(`gigalixir git:remote ${gigalixirApp}`); 1110 | }); 1111 | 1112 | const currentRelease = await core.group("Getting current release", async () => { 1113 | return await getCurrentRelease(gigalixirApp); 1114 | }); 1115 | 1116 | core.info(formatReleaseMessage(currentRelease)); 1117 | 1118 | await core.group("Deploying to gigalixir", async () => { 1119 | if (appSubfolder) { 1120 | await exec.exec(`git ${addExtraFlagCleanCache(gigalixirClean)} subtree push --prefix ${appSubfolder} gigalixir master`); 1121 | } else { 1122 | await exec.exec(`git ${addExtraFlagCleanCache(gigalixirClean)} push -f gigalixir HEAD:refs/heads/master`); 1123 | } 1124 | }); 1125 | 1126 | if (migrations === "true") { 1127 | await core.group("Adding private key to gigalixir", async () => { 1128 | await exec.exec(path.join(__dirname, "../bin/add-private-key"), [sshPrivateKey]); 1129 | }); 1130 | 1131 | await core.group("Waiting for new release to deploy", async () => { 1132 | await waitForNewRelease(currentRelease, gigalixirApp, 1); 1133 | }); 1134 | 1135 | try { 1136 | await core.group("Running migrations", async () => { 1137 | await exec.exec(`gigalixir ps:migrate -a ${gigalixirApp}`) 1138 | }); 1139 | } catch (error) { 1140 | if (currentRelease === 0) { 1141 | core.warning("Migration failed"); 1142 | } else { 1143 | core.warning(`Migration failed, rolling back to the previous release: ${currentRelease}`); 1144 | await core.group("Rolling back", async () => { 1145 | await exec.exec(`gigalixir releases:rollback -a ${gigalixirApp}`) 1146 | }); 1147 | } 1148 | 1149 | core.setFailed(error.message); 1150 | } 1151 | } 1152 | } catch (error) { 1153 | core.setFailed(error.message); 1154 | } 1155 | } 1156 | 1157 | run(); 1158 | 1159 | 1160 | /***/ }), 1161 | 1162 | /***/ 129: 1163 | /***/ (function(module) { 1164 | 1165 | module.exports = require("child_process"); 1166 | 1167 | /***/ }), 1168 | 1169 | /***/ 357: 1170 | /***/ (function(module) { 1171 | 1172 | module.exports = require("assert"); 1173 | 1174 | /***/ }), 1175 | 1176 | /***/ 431: 1177 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 1178 | 1179 | "use strict"; 1180 | 1181 | var __importStar = (this && this.__importStar) || function (mod) { 1182 | if (mod && mod.__esModule) return mod; 1183 | var result = {}; 1184 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 1185 | result["default"] = mod; 1186 | return result; 1187 | }; 1188 | Object.defineProperty(exports, "__esModule", { value: true }); 1189 | const os = __importStar(__webpack_require__(87)); 1190 | const utils_1 = __webpack_require__(82); 1191 | /** 1192 | * Commands 1193 | * 1194 | * Command Format: 1195 | * ::name key=value,key=value::message 1196 | * 1197 | * Examples: 1198 | * ::warning::This is the message 1199 | * ::set-env name=MY_VAR::some value 1200 | */ 1201 | function issueCommand(command, properties, message) { 1202 | const cmd = new Command(command, properties, message); 1203 | process.stdout.write(cmd.toString() + os.EOL); 1204 | } 1205 | exports.issueCommand = issueCommand; 1206 | function issue(name, message = '') { 1207 | issueCommand(name, {}, message); 1208 | } 1209 | exports.issue = issue; 1210 | const CMD_STRING = '::'; 1211 | class Command { 1212 | constructor(command, properties, message) { 1213 | if (!command) { 1214 | command = 'missing.command'; 1215 | } 1216 | this.command = command; 1217 | this.properties = properties; 1218 | this.message = message; 1219 | } 1220 | toString() { 1221 | let cmdStr = CMD_STRING + this.command; 1222 | if (this.properties && Object.keys(this.properties).length > 0) { 1223 | cmdStr += ' '; 1224 | let first = true; 1225 | for (const key in this.properties) { 1226 | if (this.properties.hasOwnProperty(key)) { 1227 | const val = this.properties[key]; 1228 | if (val) { 1229 | if (first) { 1230 | first = false; 1231 | } 1232 | else { 1233 | cmdStr += ','; 1234 | } 1235 | cmdStr += `${key}=${escapeProperty(val)}`; 1236 | } 1237 | } 1238 | } 1239 | } 1240 | cmdStr += `${CMD_STRING}${escapeData(this.message)}`; 1241 | return cmdStr; 1242 | } 1243 | } 1244 | function escapeData(s) { 1245 | return utils_1.toCommandValue(s) 1246 | .replace(/%/g, '%25') 1247 | .replace(/\r/g, '%0D') 1248 | .replace(/\n/g, '%0A'); 1249 | } 1250 | function escapeProperty(s) { 1251 | return utils_1.toCommandValue(s) 1252 | .replace(/%/g, '%25') 1253 | .replace(/\r/g, '%0D') 1254 | .replace(/\n/g, '%0A') 1255 | .replace(/:/g, '%3A') 1256 | .replace(/,/g, '%2C'); 1257 | } 1258 | //# sourceMappingURL=command.js.map 1259 | 1260 | /***/ }), 1261 | 1262 | /***/ 470: 1263 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 1264 | 1265 | "use strict"; 1266 | 1267 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 1268 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 1269 | return new (P || (P = Promise))(function (resolve, reject) { 1270 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 1271 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 1272 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 1273 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 1274 | }); 1275 | }; 1276 | var __importStar = (this && this.__importStar) || function (mod) { 1277 | if (mod && mod.__esModule) return mod; 1278 | var result = {}; 1279 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 1280 | result["default"] = mod; 1281 | return result; 1282 | }; 1283 | Object.defineProperty(exports, "__esModule", { value: true }); 1284 | const command_1 = __webpack_require__(431); 1285 | const file_command_1 = __webpack_require__(102); 1286 | const utils_1 = __webpack_require__(82); 1287 | const os = __importStar(__webpack_require__(87)); 1288 | const path = __importStar(__webpack_require__(622)); 1289 | /** 1290 | * The code to exit an action 1291 | */ 1292 | var ExitCode; 1293 | (function (ExitCode) { 1294 | /** 1295 | * A code indicating that the action was successful 1296 | */ 1297 | ExitCode[ExitCode["Success"] = 0] = "Success"; 1298 | /** 1299 | * A code indicating that the action was a failure 1300 | */ 1301 | ExitCode[ExitCode["Failure"] = 1] = "Failure"; 1302 | })(ExitCode = exports.ExitCode || (exports.ExitCode = {})); 1303 | //----------------------------------------------------------------------- 1304 | // Variables 1305 | //----------------------------------------------------------------------- 1306 | /** 1307 | * Sets env variable for this action and future actions in the job 1308 | * @param name the name of the variable to set 1309 | * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify 1310 | */ 1311 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 1312 | function exportVariable(name, val) { 1313 | const convertedVal = utils_1.toCommandValue(val); 1314 | process.env[name] = convertedVal; 1315 | const filePath = process.env['GITHUB_ENV'] || ''; 1316 | if (filePath) { 1317 | const delimiter = '_GitHubActionsFileCommandDelimeter_'; 1318 | const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`; 1319 | file_command_1.issueCommand('ENV', commandValue); 1320 | } 1321 | else { 1322 | command_1.issueCommand('set-env', { name }, convertedVal); 1323 | } 1324 | } 1325 | exports.exportVariable = exportVariable; 1326 | /** 1327 | * Registers a secret which will get masked from logs 1328 | * @param secret value of the secret 1329 | */ 1330 | function setSecret(secret) { 1331 | command_1.issueCommand('add-mask', {}, secret); 1332 | } 1333 | exports.setSecret = setSecret; 1334 | /** 1335 | * Prepends inputPath to the PATH (for this action and future actions) 1336 | * @param inputPath 1337 | */ 1338 | function addPath(inputPath) { 1339 | const filePath = process.env['GITHUB_PATH'] || ''; 1340 | if (filePath) { 1341 | file_command_1.issueCommand('PATH', inputPath); 1342 | } 1343 | else { 1344 | command_1.issueCommand('add-path', {}, inputPath); 1345 | } 1346 | process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; 1347 | } 1348 | exports.addPath = addPath; 1349 | /** 1350 | * Gets the value of an input. The value is also trimmed. 1351 | * 1352 | * @param name name of the input to get 1353 | * @param options optional. See InputOptions. 1354 | * @returns string 1355 | */ 1356 | function getInput(name, options) { 1357 | const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; 1358 | if (options && options.required && !val) { 1359 | throw new Error(`Input required and not supplied: ${name}`); 1360 | } 1361 | return val.trim(); 1362 | } 1363 | exports.getInput = getInput; 1364 | /** 1365 | * Sets the value of an output. 1366 | * 1367 | * @param name name of the output to set 1368 | * @param value value to store. Non-string values will be converted to a string via JSON.stringify 1369 | */ 1370 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 1371 | function setOutput(name, value) { 1372 | command_1.issueCommand('set-output', { name }, value); 1373 | } 1374 | exports.setOutput = setOutput; 1375 | /** 1376 | * Enables or disables the echoing of commands into stdout for the rest of the step. 1377 | * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. 1378 | * 1379 | */ 1380 | function setCommandEcho(enabled) { 1381 | command_1.issue('echo', enabled ? 'on' : 'off'); 1382 | } 1383 | exports.setCommandEcho = setCommandEcho; 1384 | //----------------------------------------------------------------------- 1385 | // Results 1386 | //----------------------------------------------------------------------- 1387 | /** 1388 | * Sets the action status to failed. 1389 | * When the action exits it will be with an exit code of 1 1390 | * @param message add error issue message 1391 | */ 1392 | function setFailed(message) { 1393 | process.exitCode = ExitCode.Failure; 1394 | error(message); 1395 | } 1396 | exports.setFailed = setFailed; 1397 | //----------------------------------------------------------------------- 1398 | // Logging Commands 1399 | //----------------------------------------------------------------------- 1400 | /** 1401 | * Gets whether Actions Step Debug is on or not 1402 | */ 1403 | function isDebug() { 1404 | return process.env['RUNNER_DEBUG'] === '1'; 1405 | } 1406 | exports.isDebug = isDebug; 1407 | /** 1408 | * Writes debug message to user log 1409 | * @param message debug message 1410 | */ 1411 | function debug(message) { 1412 | command_1.issueCommand('debug', {}, message); 1413 | } 1414 | exports.debug = debug; 1415 | /** 1416 | * Adds an error issue 1417 | * @param message error issue message. Errors will be converted to string via toString() 1418 | */ 1419 | function error(message) { 1420 | command_1.issue('error', message instanceof Error ? message.toString() : message); 1421 | } 1422 | exports.error = error; 1423 | /** 1424 | * Adds an warning issue 1425 | * @param message warning issue message. Errors will be converted to string via toString() 1426 | */ 1427 | function warning(message) { 1428 | command_1.issue('warning', message instanceof Error ? message.toString() : message); 1429 | } 1430 | exports.warning = warning; 1431 | /** 1432 | * Writes info to log with console.log. 1433 | * @param message info message 1434 | */ 1435 | function info(message) { 1436 | process.stdout.write(message + os.EOL); 1437 | } 1438 | exports.info = info; 1439 | /** 1440 | * Begin an output group. 1441 | * 1442 | * Output until the next `groupEnd` will be foldable in this group 1443 | * 1444 | * @param name The name of the output group 1445 | */ 1446 | function startGroup(name) { 1447 | command_1.issue('group', name); 1448 | } 1449 | exports.startGroup = startGroup; 1450 | /** 1451 | * End an output group. 1452 | */ 1453 | function endGroup() { 1454 | command_1.issue('endgroup'); 1455 | } 1456 | exports.endGroup = endGroup; 1457 | /** 1458 | * Wrap an asynchronous function call in a group. 1459 | * 1460 | * Returns the same type as the function itself. 1461 | * 1462 | * @param name The name of the group 1463 | * @param fn The function to wrap in the group 1464 | */ 1465 | function group(name, fn) { 1466 | return __awaiter(this, void 0, void 0, function* () { 1467 | startGroup(name); 1468 | let result; 1469 | try { 1470 | result = yield fn(); 1471 | } 1472 | finally { 1473 | endGroup(); 1474 | } 1475 | return result; 1476 | }); 1477 | } 1478 | exports.group = group; 1479 | //----------------------------------------------------------------------- 1480 | // Wrapper action state 1481 | //----------------------------------------------------------------------- 1482 | /** 1483 | * Saves state for current action, the state can only be retrieved by this action's post job execution. 1484 | * 1485 | * @param name name of the state to store 1486 | * @param value value to store. Non-string values will be converted to a string via JSON.stringify 1487 | */ 1488 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 1489 | function saveState(name, value) { 1490 | command_1.issueCommand('save-state', { name }, value); 1491 | } 1492 | exports.saveState = saveState; 1493 | /** 1494 | * Gets the value of an state set by this action's main execution. 1495 | * 1496 | * @param name name of the state to get 1497 | * @returns string 1498 | */ 1499 | function getState(name) { 1500 | return process.env[`STATE_${name}`] || ''; 1501 | } 1502 | exports.getState = getState; 1503 | //# sourceMappingURL=core.js.map 1504 | 1505 | /***/ }), 1506 | 1507 | /***/ 614: 1508 | /***/ (function(module) { 1509 | 1510 | module.exports = require("events"); 1511 | 1512 | /***/ }), 1513 | 1514 | /***/ 622: 1515 | /***/ (function(module) { 1516 | 1517 | module.exports = require("path"); 1518 | 1519 | /***/ }), 1520 | 1521 | /***/ 669: 1522 | /***/ (function(module) { 1523 | 1524 | module.exports = require("util"); 1525 | 1526 | /***/ }), 1527 | 1528 | /***/ 672: 1529 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 1530 | 1531 | "use strict"; 1532 | 1533 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 1534 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 1535 | return new (P || (P = Promise))(function (resolve, reject) { 1536 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 1537 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 1538 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 1539 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 1540 | }); 1541 | }; 1542 | var _a; 1543 | Object.defineProperty(exports, "__esModule", { value: true }); 1544 | const assert_1 = __webpack_require__(357); 1545 | const fs = __webpack_require__(747); 1546 | const path = __webpack_require__(622); 1547 | _a = fs.promises, exports.chmod = _a.chmod, exports.copyFile = _a.copyFile, exports.lstat = _a.lstat, exports.mkdir = _a.mkdir, exports.readdir = _a.readdir, exports.readlink = _a.readlink, exports.rename = _a.rename, exports.rmdir = _a.rmdir, exports.stat = _a.stat, exports.symlink = _a.symlink, exports.unlink = _a.unlink; 1548 | exports.IS_WINDOWS = process.platform === 'win32'; 1549 | function exists(fsPath) { 1550 | return __awaiter(this, void 0, void 0, function* () { 1551 | try { 1552 | yield exports.stat(fsPath); 1553 | } 1554 | catch (err) { 1555 | if (err.code === 'ENOENT') { 1556 | return false; 1557 | } 1558 | throw err; 1559 | } 1560 | return true; 1561 | }); 1562 | } 1563 | exports.exists = exists; 1564 | function isDirectory(fsPath, useStat = false) { 1565 | return __awaiter(this, void 0, void 0, function* () { 1566 | const stats = useStat ? yield exports.stat(fsPath) : yield exports.lstat(fsPath); 1567 | return stats.isDirectory(); 1568 | }); 1569 | } 1570 | exports.isDirectory = isDirectory; 1571 | /** 1572 | * On OSX/Linux, true if path starts with '/'. On Windows, true for paths like: 1573 | * \, \hello, \\hello\share, C:, and C:\hello (and corresponding alternate separator cases). 1574 | */ 1575 | function isRooted(p) { 1576 | p = normalizeSeparators(p); 1577 | if (!p) { 1578 | throw new Error('isRooted() parameter "p" cannot be empty'); 1579 | } 1580 | if (exports.IS_WINDOWS) { 1581 | return (p.startsWith('\\') || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello 1582 | ); // e.g. C: or C:\hello 1583 | } 1584 | return p.startsWith('/'); 1585 | } 1586 | exports.isRooted = isRooted; 1587 | /** 1588 | * Recursively create a directory at `fsPath`. 1589 | * 1590 | * This implementation is optimistic, meaning it attempts to create the full 1591 | * path first, and backs up the path stack from there. 1592 | * 1593 | * @param fsPath The path to create 1594 | * @param maxDepth The maximum recursion depth 1595 | * @param depth The current recursion depth 1596 | */ 1597 | function mkdirP(fsPath, maxDepth = 1000, depth = 1) { 1598 | return __awaiter(this, void 0, void 0, function* () { 1599 | assert_1.ok(fsPath, 'a path argument must be provided'); 1600 | fsPath = path.resolve(fsPath); 1601 | if (depth >= maxDepth) 1602 | return exports.mkdir(fsPath); 1603 | try { 1604 | yield exports.mkdir(fsPath); 1605 | return; 1606 | } 1607 | catch (err) { 1608 | switch (err.code) { 1609 | case 'ENOENT': { 1610 | yield mkdirP(path.dirname(fsPath), maxDepth, depth + 1); 1611 | yield exports.mkdir(fsPath); 1612 | return; 1613 | } 1614 | default: { 1615 | let stats; 1616 | try { 1617 | stats = yield exports.stat(fsPath); 1618 | } 1619 | catch (err2) { 1620 | throw err; 1621 | } 1622 | if (!stats.isDirectory()) 1623 | throw err; 1624 | } 1625 | } 1626 | } 1627 | }); 1628 | } 1629 | exports.mkdirP = mkdirP; 1630 | /** 1631 | * Best effort attempt to determine whether a file exists and is executable. 1632 | * @param filePath file path to check 1633 | * @param extensions additional file extensions to try 1634 | * @return if file exists and is executable, returns the file path. otherwise empty string. 1635 | */ 1636 | function tryGetExecutablePath(filePath, extensions) { 1637 | return __awaiter(this, void 0, void 0, function* () { 1638 | let stats = undefined; 1639 | try { 1640 | // test file exists 1641 | stats = yield exports.stat(filePath); 1642 | } 1643 | catch (err) { 1644 | if (err.code !== 'ENOENT') { 1645 | // eslint-disable-next-line no-console 1646 | console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); 1647 | } 1648 | } 1649 | if (stats && stats.isFile()) { 1650 | if (exports.IS_WINDOWS) { 1651 | // on Windows, test for valid extension 1652 | const upperExt = path.extname(filePath).toUpperCase(); 1653 | if (extensions.some(validExt => validExt.toUpperCase() === upperExt)) { 1654 | return filePath; 1655 | } 1656 | } 1657 | else { 1658 | if (isUnixExecutable(stats)) { 1659 | return filePath; 1660 | } 1661 | } 1662 | } 1663 | // try each extension 1664 | const originalFilePath = filePath; 1665 | for (const extension of extensions) { 1666 | filePath = originalFilePath + extension; 1667 | stats = undefined; 1668 | try { 1669 | stats = yield exports.stat(filePath); 1670 | } 1671 | catch (err) { 1672 | if (err.code !== 'ENOENT') { 1673 | // eslint-disable-next-line no-console 1674 | console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); 1675 | } 1676 | } 1677 | if (stats && stats.isFile()) { 1678 | if (exports.IS_WINDOWS) { 1679 | // preserve the case of the actual file (since an extension was appended) 1680 | try { 1681 | const directory = path.dirname(filePath); 1682 | const upperName = path.basename(filePath).toUpperCase(); 1683 | for (const actualName of yield exports.readdir(directory)) { 1684 | if (upperName === actualName.toUpperCase()) { 1685 | filePath = path.join(directory, actualName); 1686 | break; 1687 | } 1688 | } 1689 | } 1690 | catch (err) { 1691 | // eslint-disable-next-line no-console 1692 | console.log(`Unexpected error attempting to determine the actual case of the file '${filePath}': ${err}`); 1693 | } 1694 | return filePath; 1695 | } 1696 | else { 1697 | if (isUnixExecutable(stats)) { 1698 | return filePath; 1699 | } 1700 | } 1701 | } 1702 | } 1703 | return ''; 1704 | }); 1705 | } 1706 | exports.tryGetExecutablePath = tryGetExecutablePath; 1707 | function normalizeSeparators(p) { 1708 | p = p || ''; 1709 | if (exports.IS_WINDOWS) { 1710 | // convert slashes on Windows 1711 | p = p.replace(/\//g, '\\'); 1712 | // remove redundant slashes 1713 | return p.replace(/\\\\+/g, '\\'); 1714 | } 1715 | // remove redundant slashes 1716 | return p.replace(/\/\/+/g, '/'); 1717 | } 1718 | // on Mac/Linux, test the execute bit 1719 | // R W X R W X R W X 1720 | // 256 128 64 32 16 8 4 2 1 1721 | function isUnixExecutable(stats) { 1722 | return ((stats.mode & 1) > 0 || 1723 | ((stats.mode & 8) > 0 && stats.gid === process.getgid()) || 1724 | ((stats.mode & 64) > 0 && stats.uid === process.getuid())); 1725 | } 1726 | //# sourceMappingURL=io-util.js.map 1727 | 1728 | /***/ }), 1729 | 1730 | /***/ 747: 1731 | /***/ (function(module) { 1732 | 1733 | module.exports = require("fs"); 1734 | 1735 | /***/ }), 1736 | 1737 | /***/ 986: 1738 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 1739 | 1740 | "use strict"; 1741 | 1742 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 1743 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 1744 | return new (P || (P = Promise))(function (resolve, reject) { 1745 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 1746 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 1747 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 1748 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 1749 | }); 1750 | }; 1751 | Object.defineProperty(exports, "__esModule", { value: true }); 1752 | const tr = __webpack_require__(9); 1753 | /** 1754 | * Exec a command. 1755 | * Output will be streamed to the live console. 1756 | * Returns promise with return code 1757 | * 1758 | * @param commandLine command to execute (can include additional args). Must be correctly escaped. 1759 | * @param args optional arguments for tool. Escaping is handled by the lib. 1760 | * @param options optional exec options. See ExecOptions 1761 | * @returns Promise exit code 1762 | */ 1763 | function exec(commandLine, args, options) { 1764 | return __awaiter(this, void 0, void 0, function* () { 1765 | const commandArgs = tr.argStringToArray(commandLine); 1766 | if (commandArgs.length === 0) { 1767 | throw new Error(`Parameter 'commandLine' cannot be null or empty.`); 1768 | } 1769 | // Path to tool to execute should be first arg 1770 | const toolPath = commandArgs[0]; 1771 | args = commandArgs.slice(1).concat(args || []); 1772 | const runner = new tr.ToolRunner(toolPath, args, options); 1773 | return runner.exec(); 1774 | }); 1775 | } 1776 | exports.exec = exec; 1777 | //# sourceMappingURL=exec.js.map 1778 | 1779 | /***/ }) 1780 | 1781 | /******/ }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core'); 2 | const exec = require('@actions/exec'); 3 | const path = require('path'); 4 | 5 | function wait(seconds) { 6 | return new Promise(resolve => { 7 | if ((typeof seconds) !== 'number') { 8 | throw new Error('seconds not a number'); 9 | } 10 | 11 | core.info(`Waiting ${seconds} seconds...`); 12 | 13 | setTimeout(() => resolve("done!"), seconds * 1000) 14 | }); 15 | } 16 | 17 | async function isNextReleaseHealthy(release, app) { 18 | let releasesOutput = ''; 19 | 20 | const options = { 21 | listeners: { 22 | stdout: data => { 23 | releasesOutput += data.toString(); 24 | } 25 | } 26 | }; 27 | 28 | await core.group("Getting current replicas", async () => { 29 | await exec.exec(`gigalixir ps -a ${app}`, [], options); 30 | }); 31 | 32 | const releases = JSON.parse(releasesOutput); 33 | return releases.pods.filter((pod) => (Number(pod.version) === release && pod.status === "Healthy")).length >= releases.replicas_desired; 34 | } 35 | 36 | async function waitForNewRelease(oldRelease, app, attempts) { 37 | const maxAttempts = 60; 38 | 39 | if (await isNextReleaseHealthy(oldRelease + 1, app)) { 40 | return await Promise.resolve(true); 41 | } else { 42 | if (attempts <= maxAttempts) { 43 | await wait(10); 44 | await waitForNewRelease(oldRelease, app, attempts + 1); 45 | } else { 46 | throw "Taking too long for new release to deploy"; 47 | } 48 | } 49 | } 50 | 51 | async function getCurrentRelease(app) { 52 | let releasesOutput = ''; 53 | 54 | const options = { 55 | listeners: { 56 | stdout: data => { 57 | releasesOutput += data.toString(); 58 | } 59 | } 60 | }; 61 | 62 | await core.group("Getting current release", async () => { 63 | await exec.exec(`gigalixir releases -a ${app}`, [], options); 64 | }); 65 | 66 | const releases = JSON.parse(releasesOutput); 67 | const currentRelease = releases.length ? Number(releases[0].version) : 0; 68 | 69 | return currentRelease; 70 | } 71 | 72 | function formatReleaseMessage(releaseNumber) { 73 | return releaseNumber ? 74 | `The current release is ${releaseNumber}` : 75 | "This is the first release"; 76 | } 77 | 78 | function addExtraFlagCleanCache(gigalixirClean) { 79 | return (gigalixirClean === "true") ? ` -c http.extraheader="GIGALIXIR-CLEAN: true" ` : "" 80 | } 81 | 82 | async function run() { 83 | try { 84 | const appSubfolder = core.getInput('APP_SUBFOLDER', {required: false}); 85 | const gigalixirApp = core.getInput('GIGALIXIR_APP', {required: true}); 86 | const gigalixirClean = core.getInput('GIGALIXIR_CLEAN', {required: false}); 87 | const gigalixirUsername = core.getInput('GIGALIXIR_USERNAME', {required: true}); 88 | const gigalixirPassword = core.getInput('GIGALIXIR_PASSWORD', {required: true}); 89 | const migrations = core.getInput('MIGRATIONS', {required: true}); 90 | const sshPrivateKey = core.getInput('SSH_PRIVATE_KEY', {required: JSON.parse(migrations)}); 91 | 92 | await core.group("Installing gigalixir", async () => { 93 | await exec.exec('pip3 install gigalixir') 94 | }); 95 | 96 | await core.group("Logging in to gigalixir", async () => { 97 | await exec.exec(`gigalixir login -e "${gigalixirUsername}" -y -p "${gigalixirPassword}"`) 98 | }); 99 | 100 | await core.group("Setting git remote for gigalixir", async () => { 101 | await exec.exec(`gigalixir git:remote ${gigalixirApp}`); 102 | }); 103 | 104 | const currentRelease = await core.group("Getting current release", async () => { 105 | return await getCurrentRelease(gigalixirApp); 106 | }); 107 | 108 | core.info(formatReleaseMessage(currentRelease)); 109 | 110 | await core.group("Deploying to gigalixir", async () => { 111 | if (appSubfolder) { 112 | await exec.exec(`git ${addExtraFlagCleanCache(gigalixirClean)} subtree push --prefix ${appSubfolder} gigalixir master`); 113 | } else { 114 | await exec.exec(`git ${addExtraFlagCleanCache(gigalixirClean)} push -f gigalixir HEAD:refs/heads/master`); 115 | } 116 | }); 117 | 118 | if (migrations === "true") { 119 | await core.group("Adding private key to gigalixir", async () => { 120 | await exec.exec(path.join(__dirname, "../bin/add-private-key"), [sshPrivateKey]); 121 | }); 122 | 123 | await core.group("Waiting for new release to deploy", async () => { 124 | await waitForNewRelease(currentRelease, gigalixirApp, 1); 125 | }); 126 | 127 | try { 128 | await core.group("Running migrations", async () => { 129 | await exec.exec(`gigalixir ps:migrate -a ${gigalixirApp}`) 130 | }); 131 | } catch (error) { 132 | if (currentRelease === 0) { 133 | core.warning("Migration failed"); 134 | } else { 135 | core.warning(`Migration failed, rolling back to the previous release: ${currentRelease}`); 136 | await core.group("Rolling back", async () => { 137 | await exec.exec(`gigalixir releases:rollback -a ${gigalixirApp}`) 138 | }); 139 | } 140 | 141 | core.setFailed(error.message); 142 | } 143 | } 144 | } catch (error) { 145 | core.setFailed(error.message); 146 | } 147 | } 148 | 149 | run(); 150 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gigalixir-action", 3 | "version": "0.6.2", 4 | "description": "Action to deploy to gigalixir", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint index.js", 8 | "package": "ncc build index.js -o dist", 9 | "test": "eslint index.js && jest" 10 | }, 11 | "keywords": [ 12 | "GitHub", 13 | "Actions", 14 | "JavaScript", 15 | "Gigalixir" 16 | ], 17 | "author": "Mitchell Hanberg", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@actions/core": "^1.2.6", 21 | "@actions/exec": "^1.0.3", 22 | "@actions/io": "^1.0.2" 23 | }, 24 | "devDependencies": { 25 | "@zeit/ncc": "^0.20.5", 26 | "eslint": "^6.3.0", 27 | "jest": "^24.9.0" 28 | } 29 | } 30 | --------------------------------------------------------------------------------