mirror of
https://github.com/nick-fields/retry.git
synced 2026-02-10 07:05:29 +00:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f986c438b | ||
|
|
3dad7de805 | ||
|
|
14b6b46d04 | ||
|
|
f2eb0f4f8a | ||
|
|
2762157955 | ||
|
|
ce44dab6c9 | ||
|
|
40cf3886b8 | ||
|
|
02a3f09f15 | ||
|
|
6b1204d918 | ||
|
|
8629cc7c0b | ||
|
|
e88a9994b0 | ||
|
|
e4acf08f18 | ||
|
|
51e448da7c | ||
|
|
5f63400863 | ||
|
|
c0687a0dcd | ||
|
|
102f21a736 | ||
|
|
752366eac8 | ||
|
|
a3da592761 | ||
|
|
7c5cca7536 | ||
|
|
f227091f2e | ||
|
|
6183d5c3dd | ||
|
|
71062288b7 | ||
|
|
afe1ef9058 | ||
|
|
e53cf64f16 | ||
|
|
7f8f3d9f0f | ||
|
|
bf1736e338 | ||
|
|
f7cf641580 | ||
|
|
002ef572db | ||
|
|
e80198a9da | ||
|
|
c77dc43532 | ||
|
|
0019811846 | ||
|
|
a63662d5a7 | ||
|
|
67e1bdfd8d | ||
|
|
45ba062d35 | ||
|
|
0470ad1628 | ||
|
|
2750220347 | ||
|
|
b00fd808da | ||
|
|
bebba89192 | ||
|
|
52b3fdbcaa | ||
|
|
f3f0bb1a3c | ||
|
|
7c68161adf | ||
|
|
025c480d85 | ||
|
|
3073a9f1e1 | ||
|
|
9e6dab8302 | ||
|
|
850bd83fba | ||
|
|
88ed4273a8 | ||
|
|
bee86ddb77 | ||
|
|
f865f2ade8 | ||
|
|
8310ca5ae8 | ||
|
|
e48877fb9c | ||
|
|
4af9664183 | ||
|
|
d0aac3501c | ||
|
|
877a0ac37e | ||
|
|
7463808b4e | ||
|
|
0aeb89504c | ||
|
|
b2ee390b23 | ||
|
|
fb3bca3fb5 | ||
|
|
51e29ff1ae | ||
|
|
292d515fa9 | ||
|
|
5ee366655c | ||
|
|
0bbc6bd3b0 | ||
|
|
409054c003 | ||
|
|
02159d7095 | ||
|
|
c36bd33fae | ||
|
|
ad6c447324 | ||
|
|
36c6f604ab | ||
|
|
31e0097983 | ||
|
|
7a4513731b | ||
|
|
0a47821646 | ||
|
|
193acc1924 | ||
|
|
8965a748e1 | ||
|
|
dea74f0715 |
1
.config/.eslintignore
Normal file
1
.config/.eslintignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.eslintrc.js
|
||||||
7
.config/.eslintrc.js
Normal file
7
.config/.eslintrc.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||||
|
ignorePatterns: ['**/*.js', 'dist/'],
|
||||||
|
};
|
||||||
2
.config/.prettierignore
Normal file
2
.config/.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
5
.config/.prettierrc.yml
Normal file
5
.config/.prettierrc.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
trailingComma: 'es5'
|
||||||
|
tabWidth: 2
|
||||||
|
semi: true
|
||||||
|
singleQuote: true
|
||||||
|
printWidth: 100
|
||||||
19
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: nick-invision
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is, **including the snippet from your workflow `yaml` showing your configuration and command being executed.**
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Logs**
|
||||||
|
Enable [debug logging](https://docs.github.com/en/free-pro-team@latest/actions/managing-workflow-runs/enabling-debug-logging#enabling-step-debug-logging) then attach the [raw logs](https://docs.github.com/en/free-pro-team@latest/actions/managing-workflow-runs/using-workflow-run-logs#downloading-logs) (specifically the raw output of this action).
|
||||||
5
.github/scripts/log-examples.js
vendored
Normal file
5
.github/scripts/log-examples.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
console.log('console.log test');
|
||||||
|
console.warn('console.warn test');
|
||||||
|
console.error('console.error test');
|
||||||
|
process.stdout.write('stdout test');
|
||||||
|
process.stderr.write('stderr test');
|
||||||
184
.github/workflows/ci_cd.yml
vendored
184
.github/workflows/ci_cd.yml
vendored
@@ -1,8 +1,7 @@
|
|||||||
name: CI/CD
|
name: CI/CD
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- '**'
|
|
||||||
jobs:
|
jobs:
|
||||||
# runs on branch pushes only
|
# runs on branch pushes only
|
||||||
ci:
|
ci:
|
||||||
@@ -15,7 +14,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12
|
node-version: 16
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -31,6 +30,12 @@ jobs:
|
|||||||
expected: true
|
expected: true
|
||||||
actual: ${{ steps.happy_path.outputs.total_attempts == '1' && steps.happy_path.outputs.exit_code == '0' }}
|
actual: ${{ steps.happy_path.outputs.total_attempts == '1' && steps.happy_path.outputs.exit_code == '0' }}
|
||||||
|
|
||||||
|
- name: log examples
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
command: node ./.github/scripts/log-examples.js
|
||||||
|
timeout_minutes: 1
|
||||||
|
|
||||||
- name: sad-path (retry_wait_seconds)
|
- name: sad-path (retry_wait_seconds)
|
||||||
id: sad_path_wait_sec
|
id: sad_path_wait_sec
|
||||||
uses: ./
|
uses: ./
|
||||||
@@ -54,6 +59,71 @@ jobs:
|
|||||||
actual: ${{ steps.sad_path_wait_sec.outputs.exit_error }}
|
actual: ${{ steps.sad_path_wait_sec.outputs.exit_error }}
|
||||||
comparison: contains
|
comparison: contains
|
||||||
|
|
||||||
|
- name: new-command-on-retry
|
||||||
|
id: new-command-on-retry
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 3
|
||||||
|
command: node -e "process.exit(1)"
|
||||||
|
new_command_on_retry: node -e "console.log('this is the new command on retry')"
|
||||||
|
|
||||||
|
- name: on-retry-cmd
|
||||||
|
id: on-retry-cmd
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 3
|
||||||
|
command: node -e "process.exit(1)"
|
||||||
|
on_retry_command: node -e "console.log('this is a retry command')"
|
||||||
|
|
||||||
|
- name: retry_on_exit_code (with expected error code)
|
||||||
|
id: retry_on_exit_code_expected
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
retry_on_exit_code: 2
|
||||||
|
max_attempts: 3
|
||||||
|
command: node -e "process.exit(2)"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.retry_on_exit_code_expected.outcome }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 3
|
||||||
|
actual: ${{ steps.retry_on_exit_code_expected.outputs.total_attempts }}
|
||||||
|
|
||||||
|
- name: retry_on_exit_code (with unexpected error code)
|
||||||
|
id: retry_on_exit_code_unexpected
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
retry_on_exit_code: 2
|
||||||
|
max_attempts: 3
|
||||||
|
command: node -e "process.exit(1)"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.retry_on_exit_code_unexpected.outcome }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 1
|
||||||
|
actual: ${{ steps.retry_on_exit_code_unexpected.outputs.total_attempts }}
|
||||||
|
|
||||||
|
- name: on-retry-cmd (on-retry fails)
|
||||||
|
id: on-retry-cmd-fails
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 3
|
||||||
|
command: node -e "process.exit(1)"
|
||||||
|
on_retry_command: node -e "throw new Error('This is an on-retry command error')"
|
||||||
|
|
||||||
- name: sad-path (error)
|
- name: sad-path (error)
|
||||||
id: sad_path_error
|
id: sad_path_error
|
||||||
uses: ./
|
uses: ./
|
||||||
@@ -71,6 +141,41 @@ jobs:
|
|||||||
expected: failure
|
expected: failure
|
||||||
actual: ${{ steps.sad_path_error.outcome }}
|
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
|
- name: retry_on (timeout) fails early if error encountered
|
||||||
id: retry_on_timeout_fail
|
id: retry_on_timeout_fail
|
||||||
uses: ./
|
uses: ./
|
||||||
@@ -115,7 +220,6 @@ jobs:
|
|||||||
expected: 2
|
expected: 2
|
||||||
actual: ${{ steps.retry_on_error.outputs.exit_code }}
|
actual: ${{ steps.retry_on_error.outputs.exit_code }}
|
||||||
|
|
||||||
|
|
||||||
# timeout tests (takes longer to run so run last)
|
# timeout tests (takes longer to run so run last)
|
||||||
- name: sad-path (timeout)
|
- name: sad-path (timeout)
|
||||||
id: sad_path_timeout
|
id: sad_path_timeout
|
||||||
@@ -191,6 +295,76 @@ jobs:
|
|||||||
expected: failure
|
expected: failure
|
||||||
actual: ${{ steps.sad_path_timeout.outcome }}
|
actual: ${{ steps.sad_path_timeout.outcome }}
|
||||||
|
|
||||||
|
- name: sad-path (wrong shell for OS)
|
||||||
|
id: wrong_shell
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 2
|
||||||
|
shell: cmd
|
||||||
|
command: 'dir'
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.wrong_shell.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.wrong_shell.outcome }}
|
||||||
|
|
||||||
|
ci_windows:
|
||||||
|
name: Run Windows Tests
|
||||||
|
if: startsWith(github.ref, 'refs/heads')
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Powershell test
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 2
|
||||||
|
shell: powershell
|
||||||
|
command: Get-ComputerInfo
|
||||||
|
- name: CMD.exe test
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 2
|
||||||
|
shell: cmd
|
||||||
|
command: echo %PATH%
|
||||||
|
- name: Python test
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 2
|
||||||
|
shell: python
|
||||||
|
command: print('1', '2', '3')
|
||||||
|
- name: Multi-line multi-command Test
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 2
|
||||||
|
command: |
|
||||||
|
Get-ComputerInfo
|
||||||
|
Get-Date
|
||||||
|
- name: Multi-line single-command Test
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 2
|
||||||
|
shell: cmd
|
||||||
|
command: >-
|
||||||
|
echo "this is
|
||||||
|
a test"
|
||||||
|
|
||||||
# runs on push to master only
|
# runs on push to master only
|
||||||
cd:
|
cd:
|
||||||
name: Publish Action
|
name: Publish Action
|
||||||
@@ -203,7 +377,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12
|
node-version: 16
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Release
|
- name: Release
|
||||||
|
|||||||
5
.husky/commit-msg
Executable file
5
.husky/commit-msg
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
# lint commit message
|
||||||
|
npx --no -- commitlint --config ./.config/.commitlintrc.js --edit $1
|
||||||
8
.husky/pre-commit
Executable file
8
.husky/pre-commit
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
# run lint/styling on staged changes
|
||||||
|
npx lint-staged
|
||||||
|
|
||||||
|
# regenerate dist
|
||||||
|
npm run prepare && git add .
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
tabWidth: 2,
|
|
||||||
printWidth: 100,
|
|
||||||
semi: true,
|
|
||||||
singleQuote: true,
|
|
||||||
trailingComma: 'es5',
|
|
||||||
};
|
|
||||||
@@ -8,6 +8,7 @@ module.exports = {
|
|||||||
{ type: 'minor', release: 'minor' },
|
{ type: 'minor', release: 'minor' },
|
||||||
{ type: 'major', release: 'major' },
|
{ type: 'major', release: 'major' },
|
||||||
{ type: 'patch', release: 'patch' },
|
{ type: 'patch', release: 'patch' },
|
||||||
|
{ type: 'test', release: false },
|
||||||
{ scope: 'no-release', release: false },
|
{ scope: 'no-release', release: false },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["esbenp.prettier-vscode"]
|
||||||
|
}
|
||||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"prettier.requireConfig": true,
|
"prettier.configPath": "./.config/.prettierrc.yml",
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"prettier.ignorePath": "./.config/.prettierignore",
|
||||||
"editor.tabSize": 2
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
}
|
}
|
||||||
|
|||||||
142
README.md
142
README.md
@@ -2,11 +2,19 @@
|
|||||||
|
|
||||||
Retries an Action step on failure or timeout. This is currently intended to replace the `run` step for moody commands.
|
Retries an Action step on failure or timeout. This is currently intended to replace the `run` step for moody commands.
|
||||||
|
|
||||||
|
**NOTE:** Ownership of this project was transferred to my personal account `nick-fields` from my work account `nick-invision`. Details [here](#Ownership)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
|
|
||||||
### `timeout_minutes`
|
### `timeout_minutes`
|
||||||
|
|
||||||
**Required** Minutes to wait before attempt times out
|
**Required** Minutes to wait before attempt times out. Must only specify either minutes or seconds
|
||||||
|
|
||||||
|
### `timeout_seconds`
|
||||||
|
|
||||||
|
**Required** Seconds to wait before attempt times out. Must only specify either minutes or seconds
|
||||||
|
|
||||||
### `max_attempts`
|
### `max_attempts`
|
||||||
|
|
||||||
@@ -20,6 +28,10 @@ Retries an Action step on failure or timeout. This is currently intended to repl
|
|||||||
|
|
||||||
**Optional** Number of seconds to wait before attempting the next retry. Defaults to `10`
|
**Optional** Number of seconds to wait before attempting the next retry. Defaults to `10`
|
||||||
|
|
||||||
|
### `shell`
|
||||||
|
|
||||||
|
**Optional** Shell to use to execute `command`. Defaults to `powershell` on Windows, `bash` otherwise. Supports bash, python, pwsh, sh, cmd, and powershell per [docs](https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell)
|
||||||
|
|
||||||
### `polling_interval_seconds`
|
### `polling_interval_seconds`
|
||||||
|
|
||||||
**Optional** Number of seconds to wait while polling for command result. Defaults to `1`
|
**Optional** Number of seconds to wait while polling for command result. Defaults to `1`
|
||||||
@@ -28,6 +40,26 @@ Retries an Action step on failure or timeout. This is currently intended to repl
|
|||||||
|
|
||||||
**Optional** Event to retry on. Currently supports [any (default), timeout, error].
|
**Optional** Event to retry on. Currently supports [any (default), timeout, error].
|
||||||
|
|
||||||
|
### `warning_on_retry`
|
||||||
|
|
||||||
|
**Optional** Whether to output a warning on retry, or just output to info. Defaults to `true`.
|
||||||
|
|
||||||
|
### `on_retry_command`
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
### `new_command_on_retry`
|
||||||
|
|
||||||
|
**Optional** Command to run if the first attempt fails. This command will be called on all subsequent attempts.
|
||||||
|
|
||||||
|
### `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`
|
||||||
|
|
||||||
|
### `retry_on_exit_code`
|
||||||
|
|
||||||
|
**Optional** Specific exit code to retry on. This will only retry for the given error code and fail immediately other error codes.
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
|
|
||||||
### `total_attempts`
|
### `total_attempts`
|
||||||
@@ -44,10 +76,21 @@ The final error returned by the command
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
### Shell
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: nick-fields/retry@v2
|
||||||
|
with:
|
||||||
|
timeout_minutes: 10
|
||||||
|
max_attempts: 3
|
||||||
|
shell: pwsh
|
||||||
|
command: dir
|
||||||
|
```
|
||||||
|
|
||||||
### Timeout in minutes
|
### Timeout in minutes
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
uses: nick-invision/retry@v2
|
uses: nick-fields/retry@v2
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 10
|
timeout_minutes: 10
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
@@ -57,7 +100,7 @@ with:
|
|||||||
### Timeout in seconds
|
### Timeout in seconds
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
uses: nick-invision/retry@v2
|
uses: nick-fields/retry@v2
|
||||||
with:
|
with:
|
||||||
timeout_seconds: 15
|
timeout_seconds: 15
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
@@ -67,7 +110,7 @@ with:
|
|||||||
### Only retry after timeout
|
### Only retry after timeout
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
uses: nick-invision/retry@v2
|
uses: nick-fields/retry@v2
|
||||||
with:
|
with:
|
||||||
timeout_seconds: 15
|
timeout_seconds: 15
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
@@ -78,7 +121,7 @@ with:
|
|||||||
### Only retry after error
|
### Only retry after error
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
uses: nick-invision/retry@v2
|
uses: nick-fields/retry@v2
|
||||||
with:
|
with:
|
||||||
timeout_seconds: 15
|
timeout_seconds: 15
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
@@ -86,10 +129,32 @@ with:
|
|||||||
command: npm run some-typically-fast-script
|
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
|
```yaml
|
||||||
- uses: nick-invision/retry@v2
|
- uses: nick-fields/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-fields/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: success
|
||||||
|
actual: ${{ steps.retry.outcome }}
|
||||||
|
- name: Assert that action exited with expected exit code
|
||||||
|
uses: nick-fields/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-fields/retry@v2
|
||||||
id: retry
|
id: retry
|
||||||
# see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontinue-on-error
|
# see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontinue-on-error
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
@@ -99,22 +164,79 @@ with:
|
|||||||
retry_on: error
|
retry_on: error
|
||||||
command: node -e 'process.exit(99);'
|
command: node -e 'process.exit(99);'
|
||||||
- name: Assert that action failed
|
- name: Assert that action failed
|
||||||
uses: nick-invision/assert-action@v1
|
uses: nick-fields/assert-action@v1
|
||||||
with:
|
with:
|
||||||
expected: failure
|
expected: failure
|
||||||
actual: ${{ steps.retry.outcome }}
|
actual: ${{ steps.retry.outcome }}
|
||||||
- name: Assert that action exited with expected exit code
|
- name: Assert that action exited with expected exit code
|
||||||
uses: nick-invision/assert-action@v1
|
uses: nick-fields/assert-action@v1
|
||||||
with:
|
with:
|
||||||
expected: 99
|
expected: 99
|
||||||
actual: ${{ steps.retry.outputs.exit_code }}
|
actual: ${{ steps.retry.outputs.exit_code }}
|
||||||
- name: Assert that action made expected number of attempts
|
- name: Assert that action made expected number of attempts
|
||||||
uses: nick-invision/assert-action@v1
|
uses: nick-fields/assert-action@v1
|
||||||
with:
|
with:
|
||||||
expected: 3
|
expected: 3
|
||||||
actual: ${{ steps.retry.outputs.total_attempts }}
|
actual: ${{ steps.retry.outputs.total_attempts }}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Run script after failure but before retry
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: nick-fields/retry@v2
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 3
|
||||||
|
command: npm run some-flaky-script-that-outputs-something
|
||||||
|
on_retry_command: npm run cleanup-flaky-script-output
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run different command after first failure
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: nick-fields/retry@v2
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 3
|
||||||
|
command: npx jest
|
||||||
|
new_command_on_retry: npx jest --onlyFailures
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run multi-line, multi-command script
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Multi-line multi-command Test
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 2
|
||||||
|
command: |
|
||||||
|
Get-ComputerInfo
|
||||||
|
Get-Date
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run multi-line, single-command script
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Multi-line single-command Test
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 2
|
||||||
|
shell: cmd
|
||||||
|
command: >-
|
||||||
|
echo "this is
|
||||||
|
a test"
|
||||||
|
```
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
NodeJS is required for this action to run. This runs without issue on all GitHub hosted runners but if you are running into issues with this on self hosted runners ensure NodeJS is installed.
|
NodeJS is required for this action to run. This runs without issue on all GitHub hosted runners but if you are running into issues with this on self hosted runners ensure NodeJS is installed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Ownership**
|
||||||
|
|
||||||
|
As of 2022/02/15 ownership of this project has been transferred to my personal account `nick-fields` from my work account `nick-invision` due to me leaving InVision. I am the author and have been the primary maintainer since day one and will continue to maintain this as needed.
|
||||||
|
|
||||||
|
Existing workflow references to `nick-invision/retry@<whatever>` no longer work and must be updated to `nick-fields/retry@<whatever>`.
|
||||||
|
|||||||
18
action.yml
18
action.yml
@@ -18,12 +18,30 @@ inputs:
|
|||||||
description: Number of seconds to wait before attempting the next retry
|
description: Number of seconds to wait before attempting the next retry
|
||||||
required: false
|
required: false
|
||||||
default: 10
|
default: 10
|
||||||
|
shell:
|
||||||
|
description: Alternate shell to use (defaults to powershell on windows, bash otherwise). Supports bash, python, pwsh, sh, cmd, and powershell
|
||||||
|
required: false
|
||||||
polling_interval_seconds:
|
polling_interval_seconds:
|
||||||
description: Number of seconds to wait for each check that command has completed running
|
description: Number of seconds to wait for each check that command has completed running
|
||||||
required: false
|
required: false
|
||||||
default: 1
|
default: 1
|
||||||
retry_on:
|
retry_on:
|
||||||
description: Event to retry on. Currently supported [any, timeout, error]
|
description: Event to retry on. Currently supported [any, timeout, error]
|
||||||
|
warning_on_retry:
|
||||||
|
description: Whether to output a warning on retry, or just output to info. Defaults to true
|
||||||
|
default: true
|
||||||
|
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
|
||||||
|
new_command_on_retry:
|
||||||
|
description: Command to run if the first attempt fails. This command will be called on all subsequent attempts.
|
||||||
|
required: false
|
||||||
|
retry_on_exit_code:
|
||||||
|
description: Specific exit code to retry on. This will only retry for the given error code and fail immediately other error codes.
|
||||||
|
required: false
|
||||||
outputs:
|
outputs:
|
||||||
total_attempts:
|
total_attempts:
|
||||||
description: The final number of attempts made
|
description: The final number of attempts made
|
||||||
|
|||||||
12
dist/exec.js
vendored
12
dist/exec.js
vendored
@@ -1,12 +0,0 @@
|
|||||||
const { exec } = require('child_process');
|
|
||||||
const COMMAND = process.argv.splice(2)[0];
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
exec(COMMAND, { stdio: 'inherit' }, (err) => {
|
|
||||||
if (err) {
|
|
||||||
process.exit(err.code);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
2317
dist/index.js
vendored
2317
dist/index.js
vendored
File diff suppressed because it is too large
Load Diff
16734
package-lock.json
generated
16734
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -3,39 +3,56 @@
|
|||||||
"version": "0.0.0-managed-by-semantic-release",
|
"version": "0.0.0-managed-by-semantic-release",
|
||||||
"description": "Retries a GitHub Action step on failure or timeout.",
|
"description": "Retries a GitHub Action step on failure or timeout.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"local": "node -r dotenv/config ./src/index.js",
|
"lint:base": "eslint --config ./.config/.eslintrc.js ",
|
||||||
"prepare": "ncc build src/index.js"
|
"lint": "npm run lint:base -- .",
|
||||||
|
"local": "npm run prepare && node -r dotenv/config ./dist/index.js",
|
||||||
|
"prepare": "ncc build src/index.ts && husky install",
|
||||||
|
"style:base": "prettier --config ./.config/.prettierrc.yml --ignore-path ./.config/.prettierignore --write ",
|
||||||
|
"style": "npm run style:base -- ."
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/nick-invision/retry.git"
|
"url": "git+https://github.com/nick-invision/retry.git"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "Nick Fields",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/nick-invision/retry/issues"
|
"url": "https://github.com/nick-invision/retry/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/nick-invision/retry#readme",
|
"homepage": "https://github.com/nick-invision/retry#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.2.4",
|
"@actions/core": "^1.9.0",
|
||||||
"milliseconds": "^1.0.3",
|
"milliseconds": "^1.0.3",
|
||||||
"tree-kill": "^1.2.2"
|
"tree-kill": "^1.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^8.3.5",
|
"@commitlint/cli": "^16.2.3",
|
||||||
"@commitlint/config-conventional": "^8.3.4",
|
"@commitlint/config-conventional": "^16.2.1",
|
||||||
"@semantic-release/changelog": "^3.0.6",
|
"@semantic-release/changelog": "^6.0.1",
|
||||||
"@semantic-release/git": "^7.0.18",
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"@types/milliseconds": "0.0.30",
|
||||||
|
"@types/node": "^16.11.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.32.0",
|
||||||
|
"@typescript-eslint/parser": "^5.32.0",
|
||||||
"@zeit/ncc": "^0.20.5",
|
"@zeit/ncc": "^0.20.5",
|
||||||
"dotenv": "8.2.0",
|
"dotenv": "8.2.0",
|
||||||
"husky": "^3.1.0",
|
"eslint": "^8.21.0",
|
||||||
"semantic-release": "^17.0.3"
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"husky": "^8.0.1",
|
||||||
|
"lint-staged": "^13.0.3",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"semantic-release": "19.0.3",
|
||||||
|
"ts-node": "9.0.0",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
},
|
},
|
||||||
"husky": {
|
"lint-staged": {
|
||||||
"hooks": {
|
"**/*.ts": [
|
||||||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
|
"npm run style:base --",
|
||||||
"pre-commit": "npm run prepare && git add ."
|
"npm run lint:base --"
|
||||||
}
|
],
|
||||||
|
"**/*.{md,yaml,yml}": [
|
||||||
|
"npm run style:base --"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ INPUT_TIMEOUT_MINUTES=1
|
|||||||
INPUT_MAX_ATTEMPTS=3
|
INPUT_MAX_ATTEMPTS=3
|
||||||
INPUT_COMMAND="node -e 'process.exit(99)'"
|
INPUT_COMMAND="node -e 'process.exit(99)'"
|
||||||
INPUT_RETRY_WAIT_SECONDS=10
|
INPUT_RETRY_WAIT_SECONDS=10
|
||||||
|
SHELL=pwsh
|
||||||
INPUT_POLLING_INTERVAL_SECONDS=1
|
INPUT_POLLING_INTERVAL_SECONDS=1
|
||||||
INPUT_RETRY_ON=any
|
INPUT_RETRY_ON=any
|
||||||
|
|||||||
12
src/exec.js
12
src/exec.js
@@ -1,12 +0,0 @@
|
|||||||
const { exec } = require('child_process');
|
|
||||||
const COMMAND = process.argv.splice(2)[0];
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
exec(COMMAND, { stdio: 'inherit' }, (err) => {
|
|
||||||
if (err) {
|
|
||||||
process.exit(err.code);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
147
src/index.js
147
src/index.js
@@ -1,147 +0,0 @@
|
|||||||
const { getInput, error, warning, info, debug, setOutput } = require('@actions/core');
|
|
||||||
const { spawn } = require('child_process');
|
|
||||||
const { join } = require('path');
|
|
||||||
const ms = require('milliseconds');
|
|
||||||
var kill = require('tree-kill');
|
|
||||||
|
|
||||||
// inputs
|
|
||||||
const TIMEOUT_MINUTES = getInputNumber('timeout_minutes', false);
|
|
||||||
const TIMEOUT_SECONDS = getInputNumber('timeout_seconds', false);
|
|
||||||
const MAX_ATTEMPTS = getInputNumber('max_attempts', true);
|
|
||||||
const COMMAND = getInput('command', { required: true });
|
|
||||||
const RETRY_WAIT_SECONDS = getInputNumber('retry_wait_seconds', false);
|
|
||||||
const POLLING_INTERVAL_SECONDS = getInputNumber('polling_interval_seconds', false);
|
|
||||||
const RETRY_ON = getInput('retry_on') || 'any';
|
|
||||||
|
|
||||||
const OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts';
|
|
||||||
const OUTPUT_EXIT_CODE_KEY = 'exit_code';
|
|
||||||
const OUTPUT_EXIT_ERROR_KEY = 'exit_error';
|
|
||||||
|
|
||||||
var exit;
|
|
||||||
var done;
|
|
||||||
|
|
||||||
function getInputNumber(id, required) {
|
|
||||||
const input = getInput(id, { required });
|
|
||||||
const num = Number.parseInt(input);
|
|
||||||
|
|
||||||
// empty is ok
|
|
||||||
if (!input && !required) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Number.isInteger(num)) {
|
|
||||||
throw `Input ${id} only accepts numbers. Received ${input}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return num;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function wait(ms) {
|
|
||||||
return new Promise((r) => setTimeout(r, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function retryWait() {
|
|
||||||
const waitStart = Date.now();
|
|
||||||
await wait(ms.seconds(RETRY_WAIT_SECONDS));
|
|
||||||
debug(`Waited ${Date.now() - waitStart}ms`);
|
|
||||||
debug(`Configured wait: ${ms.seconds(RETRY_WAIT_SECONDS)}ms`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateInputs() {
|
|
||||||
if ((!TIMEOUT_MINUTES && !TIMEOUT_SECONDS) || (TIMEOUT_MINUTES && TIMEOUT_SECONDS)) {
|
|
||||||
throw new Error('Must specify either timeout_minutes or timeout_seconds inputs');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TIMEOUT_SECONDS && TIMEOUT_SECONDS < RETRY_WAIT_SECONDS) {
|
|
||||||
throw new Error(
|
|
||||||
`timeout_seconds ${TIMEOUT_SECONDS}s less than retry_wait_seconds ${RETRY_WAIT_SECONDS}s`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTimeout() {
|
|
||||||
if (TIMEOUT_MINUTES) {
|
|
||||||
return ms.minutes(TIMEOUT_MINUTES);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ms.seconds(TIMEOUT_SECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runCmd() {
|
|
||||||
const end_time = Date.now() + getTimeout();
|
|
||||||
|
|
||||||
exit = 0;
|
|
||||||
done = false;
|
|
||||||
|
|
||||||
var child = spawn('node', [join(__dirname, 'exec.js'), COMMAND], { stdio: 'inherit' });
|
|
||||||
|
|
||||||
child.on('exit', (code, signal) => {
|
|
||||||
debug(`Code: ${code}`);
|
|
||||||
debug(`Signal: ${signal}`);
|
|
||||||
if (code > 0) {
|
|
||||||
exit = code;
|
|
||||||
}
|
|
||||||
// timeouts are killed manually
|
|
||||||
if (signal === 'SIGTERM') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
done = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
do {
|
|
||||||
await wait(ms.seconds(POLLING_INTERVAL_SECONDS));
|
|
||||||
} while (Date.now() < end_time && !done);
|
|
||||||
|
|
||||||
if (!done) {
|
|
||||||
kill(child.pid);
|
|
||||||
await retryWait();
|
|
||||||
throw new Error(`Timeout of ${getTimeout()}ms hit`);
|
|
||||||
} else if (exit > 0) {
|
|
||||||
await retryWait();
|
|
||||||
throw new Error(`Child_process exited with error code ${exit}`);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runAction() {
|
|
||||||
await validateInputs();
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
||||||
try {
|
|
||||||
// just keep overwriting attempts output
|
|
||||||
setOutput(OUTPUT_TOTAL_ATTEMPTS_KEY, attempt);
|
|
||||||
await runCmd();
|
|
||||||
info(`Command completed after ${attempt} attempt(s).`);
|
|
||||||
break;
|
|
||||||
} catch (error) {
|
|
||||||
if (attempt === MAX_ATTEMPTS) {
|
|
||||||
throw new Error(`Final attempt failed. ${error.message}`);
|
|
||||||
} else if (!done && RETRY_ON === 'error') {
|
|
||||||
// error: timeout
|
|
||||||
throw error;
|
|
||||||
} else if (exit > 0 && RETRY_ON === 'timeout') {
|
|
||||||
// error: error
|
|
||||||
throw error;
|
|
||||||
} else {
|
|
||||||
warning(`Attempt ${attempt} failed. Reason: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runAction()
|
|
||||||
.then(() => {
|
|
||||||
setOutput(OUTPUT_EXIT_CODE_KEY, 0);
|
|
||||||
process.exit(0); // success
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
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);
|
|
||||||
|
|
||||||
// exit with exact error code if available, otherwise just exit with 1
|
|
||||||
process.exit(exit > 0 ? exit : 1);
|
|
||||||
});
|
|
||||||
235
src/index.ts
Normal file
235
src/index.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { getInput, error, warning, info, debug, setOutput } from '@actions/core';
|
||||||
|
import { exec, execSync } from 'child_process';
|
||||||
|
import ms from 'milliseconds';
|
||||||
|
import kill from 'tree-kill';
|
||||||
|
|
||||||
|
import { wait } from './util';
|
||||||
|
|
||||||
|
// inputs
|
||||||
|
const TIMEOUT_MINUTES = getInputNumber('timeout_minutes', false);
|
||||||
|
const TIMEOUT_SECONDS = getInputNumber('timeout_seconds', false);
|
||||||
|
const MAX_ATTEMPTS = getInputNumber('max_attempts', true) || 3;
|
||||||
|
const COMMAND = getInput('command', { required: true });
|
||||||
|
const RETRY_WAIT_SECONDS = getInputNumber('retry_wait_seconds', false) || 10;
|
||||||
|
const SHELL = getInput('shell');
|
||||||
|
const POLLING_INTERVAL_SECONDS = getInputNumber('polling_interval_seconds', false) || 1;
|
||||||
|
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 NEW_COMMAND_ON_RETRY = getInput('new_command_on_retry');
|
||||||
|
const RETRY_ON_EXIT_CODE = getInputNumber('retry_on_exit_code', false);
|
||||||
|
|
||||||
|
const OS = process.platform;
|
||||||
|
const OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts';
|
||||||
|
const OUTPUT_EXIT_CODE_KEY = 'exit_code';
|
||||||
|
const OUTPUT_EXIT_ERROR_KEY = 'exit_error';
|
||||||
|
|
||||||
|
let exit: number;
|
||||||
|
let done: boolean;
|
||||||
|
|
||||||
|
function getInputNumber(id: string, required: boolean): number | undefined {
|
||||||
|
const input = getInput(id, { required });
|
||||||
|
const num = Number.parseInt(input);
|
||||||
|
|
||||||
|
// empty is ok
|
||||||
|
if (!input && !required) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(num)) {
|
||||||
|
throw `Input ${id} only accepts numbers. Received ${input}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
debug(`Waited ${Date.now() - waitStart}ms`);
|
||||||
|
debug(`Configured wait: ${ms.seconds(RETRY_WAIT_SECONDS)}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateInputs() {
|
||||||
|
if ((!TIMEOUT_MINUTES && !TIMEOUT_SECONDS) || (TIMEOUT_MINUTES && TIMEOUT_SECONDS)) {
|
||||||
|
throw new Error('Must specify either timeout_minutes or timeout_seconds inputs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimeout(): number {
|
||||||
|
if (TIMEOUT_MINUTES) {
|
||||||
|
return ms.minutes(TIMEOUT_MINUTES);
|
||||||
|
} else if (TIMEOUT_SECONDS) {
|
||||||
|
return ms.seconds(TIMEOUT_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Must specify either timeout_minutes or timeout_seconds inputs');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExecutable(): string {
|
||||||
|
if (!SHELL) {
|
||||||
|
return OS === 'win32' ? 'powershell' : 'bash';
|
||||||
|
}
|
||||||
|
|
||||||
|
let executable: string;
|
||||||
|
switch (SHELL) {
|
||||||
|
case 'bash':
|
||||||
|
case 'python':
|
||||||
|
case 'pwsh': {
|
||||||
|
executable = SHELL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'sh': {
|
||||||
|
if (OS === 'win32') {
|
||||||
|
throw new Error(`Shell ${SHELL} not allowed on OS ${OS}`);
|
||||||
|
}
|
||||||
|
executable = SHELL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'cmd':
|
||||||
|
case 'powershell': {
|
||||||
|
if (OS !== 'win32') {
|
||||||
|
throw new Error(`Shell ${SHELL} not allowed on OS ${OS}`);
|
||||||
|
}
|
||||||
|
executable = SHELL + '.exe';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(
|
||||||
|
`Shell ${SHELL} not supported. See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell for supported shells`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return executable;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runRetryCmd(): Promise<void> {
|
||||||
|
// if no retry script, just continue
|
||||||
|
if (!ON_RETRY_COMMAND) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execSync(ON_RETRY_COMMAND, { stdio: 'inherit' });
|
||||||
|
// eslint-disable-next-line
|
||||||
|
} catch (error: any) {
|
||||||
|
info(`WARNING: Retry command threw the error ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCmd(attempt: number) {
|
||||||
|
const end_time = Date.now() + getTimeout();
|
||||||
|
const executable = getExecutable();
|
||||||
|
|
||||||
|
exit = 0;
|
||||||
|
done = false;
|
||||||
|
|
||||||
|
debug(`Running command ${COMMAND} on ${OS} using shell ${executable}`);
|
||||||
|
const child =
|
||||||
|
attempt > 1 && NEW_COMMAND_ON_RETRY
|
||||||
|
? exec(NEW_COMMAND_ON_RETRY, { shell: executable })
|
||||||
|
: exec(COMMAND, { shell: executable });
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data) => {
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
debug(`Code: ${code}`);
|
||||||
|
debug(`Signal: ${signal}`);
|
||||||
|
if (code && code > 0) {
|
||||||
|
exit = code;
|
||||||
|
}
|
||||||
|
// timeouts are killed manually
|
||||||
|
if (signal === 'SIGTERM') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
do {
|
||||||
|
await wait(ms.seconds(POLLING_INTERVAL_SECONDS));
|
||||||
|
} while (Date.now() < end_time && !done);
|
||||||
|
|
||||||
|
if (!done && child.pid) {
|
||||||
|
kill(child.pid);
|
||||||
|
await retryWait();
|
||||||
|
throw new Error(`Timeout of ${getTimeout()}ms hit`);
|
||||||
|
} else if (exit > 0) {
|
||||||
|
await retryWait();
|
||||||
|
throw new Error(`Child_process exited with error code ${exit}`);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAction() {
|
||||||
|
await validateInputs();
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||||
|
try {
|
||||||
|
// just keep overwriting attempts output
|
||||||
|
setOutput(OUTPUT_TOTAL_ATTEMPTS_KEY, attempt);
|
||||||
|
await runCmd(attempt);
|
||||||
|
info(`Command completed after ${attempt} attempt(s).`);
|
||||||
|
break;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
} catch (error: any) {
|
||||||
|
if (attempt === MAX_ATTEMPTS) {
|
||||||
|
throw new Error(`Final attempt failed. ${error.message}`);
|
||||||
|
} else if (!done && RETRY_ON === 'error') {
|
||||||
|
// error: timeout
|
||||||
|
throw error;
|
||||||
|
} else if (RETRY_ON_EXIT_CODE && RETRY_ON_EXIT_CODE !== exit) {
|
||||||
|
throw error;
|
||||||
|
} else if (exit > 0 && RETRY_ON === 'timeout') {
|
||||||
|
// error: error
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
await runRetryCmd();
|
||||||
|
if (WARNING_ON_RETRY) {
|
||||||
|
warning(`Attempt ${attempt} failed. Reason: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
info(`Attempt ${attempt} failed. Reason: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runAction()
|
||||||
|
.then(() => {
|
||||||
|
setOutput(OUTPUT_EXIT_CODE_KEY, 0);
|
||||||
|
process.exit(0); // success
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// 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, 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);
|
||||||
|
});
|
||||||
3
src/util.ts
Normal file
3
src/util.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export async function wait(ms: number) {
|
||||||
|
return new Promise((r) => setTimeout(r, ms));
|
||||||
|
}
|
||||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Basic Options */
|
||||||
|
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
|
||||||
|
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||||
|
"noEmit": true /* Do not emit outputs. */,
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
|
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||||
|
|
||||||
|
/* Advanced Options */
|
||||||
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
|
},
|
||||||
|
"exclude": ["**/__tests__", "**/__mocks__"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user