diff --git a/.travis.yml b/.travis.yml index e43d54a0..c8b04cba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ cache: - ~/.npm - .nyc_output node_js: - - "12.13.0" + - "12.14.1" notifications: email: false stages: @@ -29,7 +29,7 @@ jobs: before_script: - npm i -g coveralls script: - - npx nyc check-coverage --lines 85 --per-file + - npx nyc check-coverage --lines 100 --per-file after_success: - npx nyc report > lcov.info - coveralls < lcov.info diff --git a/README.md b/README.md index 0666e219..9f8ce91e 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,12 @@ [![Maintainability](https://api.codeclimate.com/v1/badges/f84f0bcb39c9a5c5fb99/maintainability)](https://codeclimate.com/github/eclass/semantic-release-ssh-commands/maintainability) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) -> [semantic-release](https://github.com/semantic-release/semantic-release) plugin to deploy app +> [semantic-release](https://github.com/semantic-release/semantic-release) plugin to deploy app with ssh commands | Step | Description | |--------------------|---------------------------------------------------------------------------------------------| -| `verifyConditions` | Verify the presence of the `CUSTOM_ENV` environment variable. | -| `publish` | Deploy app. | +| `verifyConditions` | Verify the presence of the `SSH_USER`, `SSH_HOST` environment variables. | +| `publish` | Deploy app over ssh. | ## Install @@ -33,7 +33,13 @@ The plugin can be configured in the [**semantic-release** configuration file](ht "@semantic-release/npm", "@semantic-release/git", "@semantic-release/gitlab", - "@eclass/semantic-release-ssh-commands" + [ + "@eclass/semantic-release-ssh-commands", + { + "verifyConditionsCmd": "sh /usr/local/verifyConditionsCmd.sh", + "publishCmd": "sh /usr/local/publishCmd.sh", + } + ] ] } ``` @@ -44,7 +50,17 @@ The plugin can be configured in the [**semantic-release** configuration file](ht | Variable | Description | | -------------------- | ----------------------------------------------------------------- | -| `CUSTOM_ENV` | A custom env var | +| `SSH_USER` | A ssh user | +| `SSH_HOST` | A ssh host | +| `SSH_PRIVATE_KEY` | Content of private ssh key (Optional) | + +### Options + +| Variable | Description | +| --------- | ----------------------------------------------------------------- | +| `verifyConditionsCmd` | A command to verificate. Required. Ex: "sh /usr/local/verifyConditionsCmd.sh" | +| `publishCmd` | A command to publish new release. This step inject VERSIOn environment variable to use in you command. Required. Ex: "sh /usr/local/publishCmd.sh" | + ### Examples @@ -55,7 +71,13 @@ The plugin can be configured in the [**semantic-release** configuration file](ht "@semantic-release/npm", "@semantic-release/git", "@semantic-release/gitlab", - "@eclass/semantic-release-ssh-commands" + [ + "@eclass/semantic-release-ssh-commands", + { + "verifyConditionsCmd": "sh /usr/local/verifyConditionsCmd.sh", + "publishCmd": "sh /usr/local/publishCmd.sh", + } + ] ] } ``` @@ -65,6 +87,14 @@ The plugin can be configured in the [**semantic-release** configuration file](ht release: image: node:alpine stage: release + before_script: + - apk add --no-cache git openssh-client + - mkdir -p /root/.ssh + - chmod 0700 /root/.ssh + - ssh-keyscan $SSH_HOST > /root/.ssh/known_hosts + - echo "$SSH_PRIVATE_KEY" > /root/.ssh/id_rsa + - chmod 600 /root/.ssh/id_rsa + - echo " IdentityFile /root/.ssh/id_rsa" >> /etc/ssh/ssh_config script: - npx semantic-release only: @@ -88,6 +118,13 @@ jobs: - stage: test script: npm t - stage: deploy + addons: + ssh_known_hosts: $SSH_HOST + before_deploy: + - eval "$(ssh-agent -s)" + - echo "$SSH_PRIVATE_KEY" > /tmp/deploy_rsa + - chmod 600 /tmp/deploy_rsa + - ssh-add /tmp/deploy_rsa script: npx semantic-release ``` diff --git a/package-lock.json b/package-lock.json index 6a4b461e..64a8f08a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1591,6 +1591,15 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true }, + "@types/semantic-release": { + "version": "15.13.1", + "resolved": "https://registry.npmjs.org/@types/semantic-release/-/semantic-release-15.13.1.tgz", + "integrity": "sha512-oWWJ8f3ChRxmw9G0aXCkq9BEE8dA97XbftWZXeg1x4NN6SckE7zXqbv5urO+Ekenb6VwF9X8aAh06EE6qPUkFA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/unist": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", @@ -1971,6 +1980,14 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -2719,8 +2736,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cosmiconfig": { "version": "5.2.1", @@ -3044,6 +3060,17 @@ "readable-stream": "^2.0.2" } }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, "editorconfig": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", @@ -3078,7 +3105,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -4981,8 +5007,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", @@ -5447,8 +5472,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -6721,6 +6745,33 @@ } } }, + "mock-require": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz", + "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==", + "dev": true, + "requires": { + "get-caller-file": "^1.0.2", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, "modify-values": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", @@ -10872,7 +10923,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -11707,8 +11757,7 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "process-on-spawn": { "version": "1.0.0", @@ -11847,7 +11896,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -11973,6 +12021,12 @@ "xtend": "^4.0.1" } }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, "repeat-element": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", @@ -12097,8 +12151,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "1.1.0", @@ -12112,8 +12165,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semantic-release": { "version": "17.0.1", @@ -12953,6 +13005,81 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "ssh-exec": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ssh-exec/-/ssh-exec-2.0.0.tgz", + "integrity": "sha1-H6xUoqNI80fmiSiYIVQnJP1XNkY=", + "requires": { + "duplexify": "^3.2.0", + "once": "^1.3.3", + "ssh2": "^0.4.8" + } + }, + "ssh2": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.4.15.tgz", + "integrity": "sha1-B8b0EG2fe26m5N9jbGxT8fmBf/g=", + "requires": { + "readable-stream": "~1.0.0", + "ssh2-streams": "~0.0.22" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "ssh2-streams": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.0.23.tgz", + "integrity": "sha1-ru8wgxu1/Er2qj9tCiYaQTUxYSs=", + "requires": { + "asn1": "~0.2.0", + "readable-stream": "~1.0.0", + "streamsearch": "~0.1.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "state-toggle": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.2.tgz", @@ -12996,6 +13123,16 @@ "readable-stream": "^2.0.2" } }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", @@ -13036,7 +13173,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -13385,6 +13521,12 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", + "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", + "dev": true + }, "uglify-js": { "version": "3.7.6", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.6.tgz", @@ -13606,8 +13748,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { "version": "3.4.0", @@ -13813,8 +13954,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "1.0.3", diff --git a/package.json b/package.json index 5f540324..bcfbf177 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "semantic-release plugin to deploy app", "main": "src/index.js", "scripts": { - "lint": "eslint . --fix", - "format": "prettier-standard '{src,test}/**/*.js'", + "lint": "prettier-standard --lint '{src,test}/**/*.js'", + "ts-compile-check": "tsc -p tsconfig.json --noEmit", "test": "nyc mocha test" }, "engines": { @@ -26,7 +26,8 @@ }, "homepage": "https://github.com/eclass/semantic-release-ssh-commands#readme", "dependencies": { - "@semantic-release/error": "^2.1.0" + "@semantic-release/error": "^2.1.0", + "ssh-exec": "2.0.0" }, "devDependencies": { "@commitlint/cli": "8.3.5", @@ -38,6 +39,7 @@ "@types/chai": "4.2.7", "@types/mocha": "5.2.7", "@types/node": "13.5.1", + "@types/semantic-release": "15.13.1", "chai": "4.2.0", "eslint": "6.8.0", "eslint-plugin-array-func": "3.1.3", @@ -52,13 +54,15 @@ "husky": "4.2.1", "lint-staged": "10.0.4", "mocha": "7.0.1", + "mock-require": "3.0.3", "nyc": "15.0.0", "nyc-config-common": "1.0.1", "prettier-standard": "16.1.0", "semantic-release": "17.0.1", "sinon": "8.1.1", "stream-buffers": "3.0.2", - "tempy": "0.3.0" + "tempy": "0.3.0", + "typescript": "3.7.5" }, "peerDependencies": { "semantic-release": ">=11.0.0 <16.0.0" @@ -87,9 +91,8 @@ ] }, "renovate": { - "automerge": "minor", "extends": [ - "config:js-lib" + "@eclass:js-lib" ] }, "release": { diff --git a/src/errors.js b/src/errors.js index d4354fb2..c7df95c6 100644 --- a/src/errors.js +++ b/src/errors.js @@ -1,5 +1,18 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +// @ts-ignore +const pkg = require('../package.json') + +const [homepage] = pkg.homepage.split('#') +/** + * @param {string} file - + * @returns {string} - + * @example + * const link = linkify(href) + */ +const linkify = file => `${homepage}/blob/master/${file}` + /** - * @typedef {import('semantic-release').Context} Context + * @typedef {import('./types').Context} Context */ /** * @typedef {Object} SemanticReleaseError @@ -9,14 +22,41 @@ module.exports = new Map([ [ - 'CUSTOMERROR', + 'ENOSSHUSER', + /** + * @param {Context} ctx - + * @returns {SemanticReleaseError} - + */ + ctx => ({ + message: 'No ssh user specified.', + details: `An [ssh user](${linkify( + 'README.md#environment-variables' + )}) must be created and set in the \`SSH_USER\` environment variable on your CI environment.` + }) + ], + [ + 'ENOSSHHOST', + /** + * @param {Context} ctx - + * @returns {SemanticReleaseError} - + */ + ctx => ({ + message: 'No ssh host specified.', + details: `An [ssh host](${linkify( + 'README.md#environment-variables' + )}) must be created and set in the \`SSH_HOST\` environment variable on your CI environment.` + }) + ], + [ + 'ESSHCOMMAND', /** * @param {Context} ctx - * @returns {SemanticReleaseError} - */ - (ctx) => ({ - message: 'A custom message.', - details: 'A custom description.' + ctx => ({ + message: 'Error executing ssh command.', + details: ctx.message }) ] ]) +/* eslint-enable sonarjs/no-duplicate-string */ diff --git a/src/exec.js b/src/exec.js new file mode 100644 index 00000000..67e178e0 --- /dev/null +++ b/src/exec.js @@ -0,0 +1,18 @@ +const exec = require('ssh-exec') + +/** @typedef {import('./types').ExecOptions} ExecOptions */ +/** + * @param {string} command - Command to send over ssh. + * @param {ExecOptions} options - SSH client options. + * @returns {Promise} - + * @example + * await verify(data, token, org) + */ +module.exports = (command, options) => + new Promise((resolve, reject) => { + exec(command, options, (err, stdout, stderr) => { + if (err) return reject(err) + if (stderr) return reject(new Error(stderr)) + resolve(stdout) + }) + }) diff --git a/src/get-error.js b/src/get-error.js index 56f5c260..8ce01d38 100644 --- a/src/get-error.js +++ b/src/get-error.js @@ -9,7 +9,7 @@ const ERROR_DEFINITIONS = require('./errors') * @example * const throw getError('CUSTOMERROR') */ -module.exports = (code, ctx = {}) => { +module.exports = (code, ctx) => { const { message, details } = ERROR_DEFINITIONS.get(code)(ctx) return new SemanticReleaseError(message, code, details) } diff --git a/src/publish.js b/src/publish.js index 80f11b1c..0d3a720e 100644 --- a/src/publish.js +++ b/src/publish.js @@ -1,14 +1,29 @@ +const getError = require('./get-error') +const exec = require('./exec') + /** - * @typedef {import('semantic-release').Context} Context - * @typedef {import('semantic-release').Config} Config + * @typedef {import('./types').Context} Context + * @typedef {import('./types').Config} Config + * @typedef {import('./types').ExecOptions} ExecOptions */ /** * @param {Config} pluginConfig - * @param {Context} ctx - - * @returns {*} - + * @returns {Promise} - * @example * publish(pluginConfig, ctx) */ -module.exports = (pluginConfig, ctx) => { - ctx.logger.log('Deploy') +module.exports = async (pluginConfig, ctx) => { + try { + /** @type {ExecOptions} */ + const options = { user: ctx.env.SSH_USER, host: ctx.env.SSH_HOST } + if (ctx.env.SSH_PRIVATE_KEY) { + options.key = ctx.env.SSH_PRIVATE_KEY + } + const command = `export VERSION=${ctx.nextRelease.version};\n${pluginConfig.publishCmd}` + return await exec(command, options) + } catch (err) { + ctx.message = err.message + throw getError('ESSHCOMMAND', ctx) + } } diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 00000000..ce95ec89 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,21 @@ +import { Context as SemanticReleaseContext } from 'semantic-release' +import { Config as SemanticReleaseConfig } from 'semantic-release' + +export interface Context extends SemanticReleaseContext { + commits?: SemanticRelease.Commit[] + message?: string +} + +export interface Config extends SemanticReleaseConfig { + verifyConditionsCmd?: string + publishCmd?: string +} + +export interface ExecOptions { + host: string + user: string + port?: number + key?: string|Buffer + fingerprint?: string + password?: string +} diff --git a/src/verify.js b/src/verify.js index 0ddb06bf..68cb8621 100644 --- a/src/verify.js +++ b/src/verify.js @@ -1,18 +1,39 @@ +const AggregateError = require('aggregate-error') const getError = require('./get-error') +const exec = require('./exec') /** - * @typedef {import('semantic-release').Context} Context - * @typedef {import('semantic-release').Config} Config + * @typedef {import('./types').Context} Context + * @typedef {import('./types').Config} Config */ /** * @param {Config} pluginConfig - * @param {Context} ctx - - * @returns {*} - + * @returns {Promise<*>} - * @example * verifyConditions(pluginConfig, ctx) */ -module.exports = (pluginConfig, ctx) => { - if (!ctx.env.CUSTOM_ENV) { - throw getError('CUSTOMERROR') +module.exports = async (pluginConfig, ctx) => { + const errors = [] + if (!ctx.env.SSH_USER) { + errors.push(getError('ENOSSHUSER', ctx)) + } + if (!ctx.env.SSH_HOST) { + errors.push(getError('ENOSSHHOST', ctx)) + } + if (errors.length > 0) { + throw new AggregateError(errors) + } + if (pluginConfig.verifyConditionsCmd) { + try { + const options = { user: ctx.env.SSH_USER, host: ctx.env.SSH_HOST } + if (ctx.env.SSH_PRIVATE_KEY) { + options.key = ctx.env.SSH_PRIVATE_KEY + } + return await exec(pluginConfig.verifyConditionsCmd, options) + } catch (err) { + ctx.message = err.message + throw new AggregateError([getError('ESSHCOMMAND', ctx)]) + } } } diff --git a/test/publish.test.js b/test/publish.test.js index a581454e..f0977bba 100644 --- a/test/publish.test.js +++ b/test/publish.test.js @@ -1,30 +1,55 @@ -const tempy = require('tempy') -const { stub } = require('sinon') -const { describe, it, before, beforeEach } = require('mocha') +const { describe, it, before, after } = require('mocha') const { expect } = require('chai') -const { WritableStreamBuffer } = require('stream-buffers') +const mock = require('mock-require') describe('Publish', () => { - let stdout - let stderr - let cwd - let logger + const ctx = { + env: { + SSH_USER: 'root', + SSH_HOST: 'localhost' + }, + nextRelease: { version: '1.0.0', gitTag: 'v1.0.0' } + } let publish before(() => { - logger = { log: stub() } - cwd = tempy.directory() + mock('ssh-exec', (command, options, cb) => { + if (/error/.test(command)) { + return cb(new Error(command), '', '') + } + if (/stderr/.test(command)) { + return cb(null, '', command) + } + return cb(null, '', '') + }) + publish = require('../src/publish') }) - beforeEach(() => { - stdout = new WritableStreamBuffer() - stderr = new WritableStreamBuffer() + it('Return SemanticReleaseError if a get a error from ssh command', async () => { + try { + // @ts-ignore + await publish({ publishCmd: 'stderr' }, ctx) + } catch (err) { + expect(err.name).to.equal('SemanticReleaseError') + expect(err.code).to.equal('ESSHCOMMAND') + } }) - it('Deploy app', async () => { - expect(await publish({}, { cwd, stdout, stderr, logger })).to.be.a( - 'undefined' + it('Deploy app with a ssh command', async () => { + // @ts-ignore + expect(await publish({ publishCmd: 'sh /root/update.sh' }, ctx)).to.equal( + '' ) }) + + it('Deploy app with a ssh command and custom ssh key', async () => { + ctx.env.SSH_PRIVATE_KEY = 'myPrivateKey' + // @ts-ignore + expect(await publish({ publishCmd: 'sh /root/update.sh' }, ctx)).to.equal( + '' + ) + }) + + after(() => mock.stopAll()) }) diff --git a/test/verify.test.js b/test/verify.test.js index 56f97334..05df93f5 100644 --- a/test/verify.test.js +++ b/test/verify.test.js @@ -1,26 +1,75 @@ -const { describe, it, before } = require('mocha') +const { describe, it, before, after } = require('mocha') const { expect } = require('chai') -const tempy = require('tempy') -const verify = require('../src/verify') +const mock = require('mock-require') describe('Verify', () => { - let cwd + const env = {} + let verify before(() => { - cwd = tempy.directory() + mock('ssh-exec', (command, options, cb) => { + if (/error/.test(command)) { + return cb(new Error(command), '', '') + } + if (/stderr/.test(command)) { + return cb(null, '', command) + } + return cb(null, '', '') + }) + + verify = require('../src/verify') + }) + + it('Return SemanticReleaseError if a SSH_USER environment variable is not defined', async () => { + try { + // @ts-ignore + await verify({}, { env }) + } catch (errs) { + const err = errs._errors[0] + expect(err.name).to.equal('SemanticReleaseError') + expect(err.code).to.equal('ENOSSHUSER') + } + }) + + it('Return SemanticReleaseError if a SSH_HOST environment variable is not defined', async () => { + try { + env.SSH_USER = 'root' + // @ts-ignore + await verify({}, { env }) + } catch (errs) { + const err = errs._errors[0] + expect(err.name).to.equal('SemanticReleaseError') + expect(err.code).to.equal('ENOSSHHOST') + } }) - it('Return SemanticReleaseError if a custom environment variable is not defined', async () => { + it('Return SemanticReleaseError if a ssh command fail', async () => { try { - await verify({}, { cwd, env: {} }) - } catch (err) { + env.SSH_HOST = 'localhost' + // @ts-ignore + await verify({ verifyConditionsCmd: 'error' }, { env }) + } catch (errs) { + const err = errs._errors[0] expect(err.name).to.equal('SemanticReleaseError') - expect(err.code).to.equal('CUSTOMERROR') + expect(err.code).to.equal('ESSHCOMMAND') } }) - it('Verify alias from a custom environmen variable', async () => { - const env = { CUSTOM_ENV: 'custom' } - expect(await verify({}, { cwd, env })).to.be.a('undefined') + it('Verify with a ssh command', async () => { + // @ts-ignore + expect(await verify({ verifyConditionsCmd: 'exit' }, { env })).to.equal('') + }) + + it('Verify with a ssh command and custom private key', async () => { + env.SSH_PRIVATE_KEY = 'myPrivateKey' + // @ts-ignore + expect(await verify({ verifyConditionsCmd: 'exit' }, { env })).to.equal('') + }) + + it('Verify without ssh command', async () => { + // @ts-ignore + expect(await verify({}, { env })).to.be.a('undefined') }) + + after(() => mock.stopAll()) }) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..e9cdee36 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "lib": [ "es2017", "es7" ], + "allowJs": true, + "checkJs": true, + "downlevelIteration": true, + "outDir": "build", + "skipLibCheck": true + }, + "exclude": [ + "node_modules", + "coverage" + ] +}