diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index db5184c..7a2274f 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -97,6 +97,41 @@ jobs: expected: failure actual: ${{ steps.sad_path_error.outcome }} + - name: happy-path (continue_on_error) + id: happy_path_continue_on_error + uses: ./ + with: + command: node -e "process.exit(0)" + timeout_minutes: 1 + continue_on_error: true + - name: sad-path (continue_on_error) + id: sad_path_continue_on_error + uses: ./ + with: + command: node -e "process.exit(33)" + timeout_minutes: 1 + continue_on_error: true + - name: Verify continue_on_error returns correct exit code on success + uses: nick-invision/assert-action@v1 + with: + expected: 0 + actual: ${{ steps.happy_path_continue_on_error.outputs.exit_code }} + - name: Verify continue_on_error exits with correct outcome on success + uses: nick-invision/assert-action@v1 + with: + expected: success + actual: ${{ steps.happy_path_continue_on_error.outcome }} + - name: Verify continue_on_error returns correct exit code on error + uses: nick-invision/assert-action@v1 + with: + expected: 33 + actual: ${{ steps.sad_path_continue_on_error.outputs.exit_code }} + - name: Verify continue_on_error exits with successful outcome when an error occurs + uses: nick-invision/assert-action@v1 + with: + expected: success + actual: ${{ steps.sad_path_continue_on_error.outcome }} + - name: retry_on (timeout) fails early if error encountered id: retry_on_timeout_fail uses: ./ diff --git a/README.md b/README.md index 37280da..0486a81 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ Retries an Action step on failure or timeout. This is currently intended to repl **Optional** Command to run before a retry (such as a cleanup script). Any error thrown from retry command is caught and surfaced as a warning. +### `continue_on_error` + +**Optional** Exit successfully even if an error occurs. Same as native continue-on-error behavior, but for use in composite actions. Defaults to `false` + ## Outputs ### `total_attempts` @@ -113,7 +117,29 @@ with: command: npm run some-typically-fast-script ``` -### Retry but allow failure and do something with output +### Retry using continue_on_error input (in composite action) but allow failure and do something with output + +```yaml +- uses: nick-invision/retry@v2 + id: retry + with: + timeout_seconds: 15 + max_attempts: 3 + continue-on-error: true + command: node -e 'process.exit(99);' +- name: Assert that step succeeded (despite failing command) + uses: nick-invision/assert-action@v1 + with: + expected: success + actual: ${{ steps.retry.outcome }} +- name: Assert that action exited with expected exit code + uses: nick-invision/assert-action@v1 + with: + expected: 99 + actual: ${{ steps.retry.outputs.exit_code }} +``` + +### Retry using continue-on-error built-in command (in workflow action) but allow failure and do something with output ```yaml - uses: nick-invision/retry@v2 diff --git a/action.yml b/action.yml index cc82eea..ada5748 100644 --- a/action.yml +++ b/action.yml @@ -33,6 +33,9 @@ inputs: on_retry_command: description: Command to run before a retry (such as a cleanup script). Any error thrown from retry command is caught and surfaced as a warning. required: false + continue_on_error: + description: Exits successfully even if an error occurs. Same as native continue-on-error behavior, but for use in composite actions. Default is false + default: false outputs: total_attempts: description: The final number of attempts made @@ -42,4 +45,4 @@ outputs: description: The final error returned by the command runs: using: 'node12' - main: 'dist/index.js' \ No newline at end of file + main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index 265f844..bc92e95 100644 --- a/dist/index.js +++ b/dist/index.js @@ -51,7 +51,7 @@ module.exports = // We use any as a valid input type /* eslint-disable @typescript-eslint/no-explicit-any */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.toCommandValue = void 0; +exports.toCommandProperties = exports.toCommandValue = void 0; /** * Sanitizes an input into a string so it can be passed into issueCommand safely * @param input input to sanitize into a string @@ -66,6 +66,25 @@ function toCommandValue(input) { return JSON.stringify(input); } exports.toCommandValue = toCommandValue; +/** + * + * @param annotationProperties + * @returns The command properties to send with the actual annotation command + * See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646 + */ +function toCommandProperties(annotationProperties) { + if (!Object.keys(annotationProperties).length) { + return {}; + } + return { + title: annotationProperties.title, + line: annotationProperties.startLine, + endLine: annotationProperties.endLine, + col: annotationProperties.startColumn, + endColumn: annotationProperties.endColumn + }; +} +exports.toCommandProperties = toCommandProperties; //# sourceMappingURL=utils.js.map /***/ }), @@ -268,6 +287,7 @@ var POLLING_INTERVAL_SECONDS = getInputNumber('polling_interval_seconds', false) var RETRY_ON = core_1.getInput('retry_on') || 'any'; var WARNING_ON_RETRY = core_1.getInput('warning_on_retry').toLowerCase() === 'true'; var ON_RETRY_COMMAND = core_1.getInput('on_retry_command'); +var CONTINUE_ON_ERROR = getInputBoolean('continue_on_error'); var OS = process.platform; var OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts'; var OUTPUT_EXIT_CODE_KEY = 'exit_code'; @@ -286,6 +306,13 @@ function getInputNumber(id, required) { } return num; } +function getInputBoolean(id) { + var input = core_1.getInput(id); + if (!['true', 'false'].includes(input.toLowerCase())) { + throw "Input " + id + " only accepts boolean values. Received " + input; + } + return input.toLowerCase() === 'true'; +} function retryWait() { return __awaiter(this, void 0, void 0, function () { var waitStart; @@ -497,12 +524,20 @@ runAction() process.exit(0); // success }) .catch(function (err) { - core_1.error(err.message); + // exact error code if available, otherwise just 1 + var exitCode = exit > 0 ? exit : 1; + if (CONTINUE_ON_ERROR) { + core_1.warning(err.message); + } + else { + core_1.error(err.message); + } // these can be helpful to know if continue-on-error is true core_1.setOutput(OUTPUT_EXIT_ERROR_KEY, err.message); - core_1.setOutput(OUTPUT_EXIT_CODE_KEY, exit > 0 ? exit : 1); - // exit with exact error code if available, otherwise just exit with 1 - process.exit(exit > 0 ? exit : 1); + core_1.setOutput(OUTPUT_EXIT_CODE_KEY, exitCode); + // if continue_on_error, exit with exact error code else exit gracefully + // mimics native continue-on-error that is not supported in composite actions + process.exit(CONTINUE_ON_ERROR ? 0 : exitCode); }); @@ -641,7 +676,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0; +exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.notice = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0; const command_1 = __webpack_require__(431); const file_command_1 = __webpack_require__(102); const utils_1 = __webpack_require__(82); @@ -819,19 +854,30 @@ exports.debug = debug; /** * Adds an error issue * @param message error issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. */ -function error(message) { - command_1.issue('error', message instanceof Error ? message.toString() : message); +function error(message, properties = {}) { + command_1.issueCommand('error', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message); } exports.error = error; /** - * Adds an warning issue + * Adds a warning issue * @param message warning issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. */ -function warning(message) { - command_1.issue('warning', message instanceof Error ? message.toString() : message); +function warning(message, properties = {}) { + command_1.issueCommand('warning', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message); } exports.warning = warning; +/** + * Adds a notice issue + * @param message notice issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function notice(message, properties = {}) { + command_1.issueCommand('notice', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message); +} +exports.notice = notice; /** * Writes info to log with console.log. * @param message info message diff --git a/package-lock.json b/package-lock.json index b541483..d4d6fe1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@actions/core": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.4.0.tgz", - "integrity": "sha512-CGx2ilGq5i7zSLgiiGUtBCxhRRxibJYU6Fim0Q1Wg2aQL2LTnF27zbqZOrxfvFQ55eSBW0L8uVStgtKMpa0Qlg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.5.0.tgz", + "integrity": "sha512-eDOLH1Nq9zh+PJlYLqEMkS/jLQxhksPNmUGNBHfa4G+tQmnIhzpctxmchETtVGyBOvXgOVVpYuE40+eS4cUnwQ==" }, "@babel/code-frame": { "version": "7.8.3", diff --git a/package.json b/package.json index 37a3e5d..149234b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "homepage": "https://github.com/nick-invision/retry#readme", "dependencies": { - "@actions/core": "^1.4.0", + "@actions/core": "^1.5.0", "milliseconds": "^1.0.3", "tree-kill": "^1.2.2" }, diff --git a/src/index.ts b/src/index.ts index b9ac3d7..947ad0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ const POLLING_INTERVAL_SECONDS = getInputNumber('polling_interval_seconds', fals const RETRY_ON = getInput('retry_on') || 'any'; const WARNING_ON_RETRY = getInput('warning_on_retry').toLowerCase() === 'true'; const ON_RETRY_COMMAND = getInput('on_retry_command'); +const CONTINUE_ON_ERROR = getInputBoolean('continue_on_error'); const OS = process.platform; const OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts'; @@ -41,6 +42,15 @@ function getInputNumber(id: string, required: boolean): number | undefined { return num; } +function getInputBoolean(id: string): Boolean { + const input = getInput(id); + + if (!['true','false'].includes(input.toLowerCase())) { + throw `Input ${id} only accepts boolean values. Received ${input}`; + } + return input.toLowerCase() === 'true' +} + async function retryWait() { const waitStart = Date.now(); await wait(ms.seconds(RETRY_WAIT_SECONDS)); @@ -195,12 +205,20 @@ runAction() process.exit(0); // success }) .catch((err) => { - error(err.message); + // exact error code if available, otherwise just 1 + const exitCode = exit > 0 ? exit : 1; + + if (CONTINUE_ON_ERROR) { + warning(err.message); + } else { + error(err.message); + } // these can be helpful to know if continue-on-error is true setOutput(OUTPUT_EXIT_ERROR_KEY, err.message); - setOutput(OUTPUT_EXIT_CODE_KEY, exit > 0 ? exit : 1); + setOutput(OUTPUT_EXIT_CODE_KEY, exitCode); - // exit with exact error code if available, otherwise just exit with 1 - process.exit(exit > 0 ? exit : 1); + // if continue_on_error, exit with exact error code else exit gracefully + // mimics native continue-on-error that is not supported in composite actions + process.exit(CONTINUE_ON_ERROR ? 0 : exitCode); });