mirror of
https://github.com/nick-fields/retry.git
synced 2026-02-10 07:05:29 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
c803451cc1 | ||
|
|
3f5463b526 | ||
|
|
915303cda5 | ||
|
|
86ecaf34fa | ||
|
|
ec785f59e1 | ||
|
|
d2b20569e3 | ||
|
|
dea74f0715 | ||
|
|
7841cadab1 | ||
|
|
87ec0a8a32 | ||
|
|
fc84966019 |
20
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
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');
|
||||||
223
.github/workflows/ci_cd.yml
vendored
223
.github/workflows/ci_cd.yml
vendored
@@ -18,13 +18,27 @@ jobs:
|
|||||||
node-version: 12
|
node-version: 12
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: happy-path
|
- name: happy-path
|
||||||
|
id: happy_path
|
||||||
uses: ./
|
uses: ./
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 1
|
timeout_minutes: 1
|
||||||
max_attempts: 2
|
max_attempts: 2
|
||||||
command: npm -v
|
command: npm -v
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: true
|
||||||
|
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
|
||||||
uses: ./
|
uses: ./
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
@@ -32,20 +46,229 @@ jobs:
|
|||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
retry_wait_seconds: 15
|
retry_wait_seconds: 15
|
||||||
command: npm install this-isnt-a-real-package-name-zzz
|
command: npm install this-isnt-a-real-package-name-zzz
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 3
|
||||||
|
actual: ${{ steps.sad_path_wait_sec.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.sad_path_wait_sec.outcome }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 'Final attempt failed'
|
||||||
|
actual: ${{ steps.sad_path_wait_sec.outputs.exit_error }}
|
||||||
|
comparison: contains
|
||||||
|
|
||||||
|
- 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: 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
|
||||||
uses: ./
|
uses: ./
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 1
|
timeout_minutes: 1
|
||||||
max_attempts: 2
|
max_attempts: 2
|
||||||
command: node -e "process.exit(1)"
|
command: node -e "process.exit(1)"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.sad_path_error.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.sad_path_error.outcome }}
|
||||||
|
|
||||||
|
- name: retry_on (timeout) fails early if error encountered
|
||||||
|
id: retry_on_timeout_fail
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: timeout
|
||||||
|
command: node -e "process.exit(2)"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 1
|
||||||
|
actual: ${{ steps.retry_on_timeout_fail.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.retry_on_timeout_fail.outcome }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.retry_on_timeout_fail.outputs.exit_code }}
|
||||||
|
|
||||||
|
- name: retry_on (error)
|
||||||
|
id: retry_on_error
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 2
|
||||||
|
retry_on: error
|
||||||
|
command: node -e "process.exit(2)"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.retry_on_error.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.retry_on_error.outcome }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.retry_on_error.outputs.exit_code }}
|
||||||
|
|
||||||
|
|
||||||
|
# timeout tests (takes longer to run so run last)
|
||||||
- name: sad-path (timeout)
|
- name: sad-path (timeout)
|
||||||
|
id: sad_path_timeout
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 2
|
||||||
|
command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.sad_path_timeout.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.sad_path_timeout.outcome }}
|
||||||
|
|
||||||
|
- name: retry_on (timeout)
|
||||||
|
id: retry_on_timeout
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 2
|
||||||
|
retry_on: timeout
|
||||||
|
command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.retry_on_timeout.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.retry_on_timeout.outcome }}
|
||||||
|
|
||||||
|
- name: retry_on (error) fails early if timeout encountered
|
||||||
|
id: retry_on_error_fail
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 2
|
||||||
|
retry_on: error
|
||||||
|
command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 1
|
||||||
|
actual: ${{ steps.retry_on_error_fail.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.retry_on_error_fail.outcome }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 1
|
||||||
|
actual: ${{ steps.retry_on_error_fail.outputs.exit_code }}
|
||||||
|
|
||||||
|
- name: sad-path (timeout minutes)
|
||||||
|
id: sad_path_timeout_minutes
|
||||||
uses: ./
|
uses: ./
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 1
|
timeout_minutes: 1
|
||||||
max_attempts: 2
|
max_attempts: 2
|
||||||
command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()"
|
command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.sad_path_timeout.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
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: 12
|
||||||
|
- 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')
|
||||||
|
|
||||||
# runs on push to master only
|
# runs on push to master only
|
||||||
cd:
|
cd:
|
||||||
|
|||||||
@@ -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 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
131
README.md
131
README.md
@@ -6,7 +6,11 @@ Retries an Action step on failure or timeout. This is currently intended to repl
|
|||||||
|
|
||||||
### `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,16 +24,135 @@ 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`
|
||||||
|
|
||||||
## Example usage
|
### `retry_on`
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
### `total_attempts`
|
||||||
|
|
||||||
|
The final number of attempts made
|
||||||
|
|
||||||
|
### `exit_code`
|
||||||
|
|
||||||
|
The final exit code returned by the command
|
||||||
|
|
||||||
|
### `exit_error`
|
||||||
|
|
||||||
|
The final error returned by the command
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Shell
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
uses: nick-invision/retry@v1
|
uses: nick-invision/retry@v2
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 10
|
timeout_minutes: 10
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
command: npm install
|
shell: pwsh
|
||||||
|
command: dir
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Timeout in minutes
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: nick-invision/retry@v2
|
||||||
|
with:
|
||||||
|
timeout_minutes: 10
|
||||||
|
max_attempts: 3
|
||||||
|
command: npm run some-typically-slow-script
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeout in seconds
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: nick-invision/retry@v2
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 3
|
||||||
|
command: npm run some-typically-fast-script
|
||||||
|
```
|
||||||
|
|
||||||
|
### Only retry after timeout
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: nick-invision/retry@v2
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: timeout
|
||||||
|
command: npm run some-typically-fast-script
|
||||||
|
```
|
||||||
|
|
||||||
|
### Only retry after error
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: nick-invision/retry@v2
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: error
|
||||||
|
command: npm run some-typically-fast-script
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retry but allow failure and do something with output
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: nick-invision/retry@v2
|
||||||
|
id: retry
|
||||||
|
# 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
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: error
|
||||||
|
command: node -e 'process.exit(99);'
|
||||||
|
- name: Assert that action failed
|
||||||
|
uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
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 }}
|
||||||
|
- name: Assert that action made expected number of attempts
|
||||||
|
uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 3
|
||||||
|
actual: ${{ steps.retry.outputs.total_attempts }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run script after failure but before retry
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: nick-invision/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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|||||||
25
action.yml
25
action.yml
@@ -2,8 +2,11 @@ name: Retry Step
|
|||||||
description: 'Retry a step on failure or timeout'
|
description: 'Retry a step on failure or timeout'
|
||||||
inputs:
|
inputs:
|
||||||
timeout_minutes:
|
timeout_minutes:
|
||||||
description: Minutes to wait before attempt times out
|
description: Minutes to wait before attempt times out. Must only specify either minutes or seconds
|
||||||
required: true
|
required: false
|
||||||
|
timeout_seconds:
|
||||||
|
description: Seconds to wait before attempt times out. Must only specify either minutes or seconds
|
||||||
|
required: false
|
||||||
max_attempts:
|
max_attempts:
|
||||||
description: Number of attempts to make before failing the step
|
description: Number of attempts to make before failing the step
|
||||||
required: true
|
required: true
|
||||||
@@ -15,10 +18,28 @@ 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:
|
||||||
|
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
|
||||||
|
outputs:
|
||||||
|
total_attempts:
|
||||||
|
description: The final number of attempts made
|
||||||
|
exit_code:
|
||||||
|
description: The final exit code returned by the command
|
||||||
|
exit_error:
|
||||||
|
description: The final error returned by the command
|
||||||
runs:
|
runs:
|
||||||
using: 'node12'
|
using: 'node12'
|
||||||
main: 'dist/index.js'
|
main: 'dist/index.js'
|
||||||
8
dist/exec.js
vendored
8
dist/exec.js
vendored
@@ -1,8 +0,0 @@
|
|||||||
const { execSync } = require('child_process');
|
|
||||||
const COMMAND = process.argv.splice(2)[0];
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
execSync(COMMAND, { stdio: 'inherit' });
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
547
dist/index.js
vendored
547
dist/index.js
vendored
@@ -34,7 +34,7 @@ module.exports =
|
|||||||
/******/ // the startup function
|
/******/ // the startup function
|
||||||
/******/ function startup() {
|
/******/ function startup() {
|
||||||
/******/ // Load entry module and return exports
|
/******/ // Load entry module and return exports
|
||||||
/******/ return __webpack_require__(676);
|
/******/ return __webpack_require__(325);
|
||||||
/******/ };
|
/******/ };
|
||||||
/******/
|
/******/
|
||||||
/******/ // run startup
|
/******/ // run startup
|
||||||
@@ -43,6 +43,32 @@ module.exports =
|
|||||||
/************************************************************************/
|
/************************************************************************/
|
||||||
/******/ ({
|
/******/ ({
|
||||||
|
|
||||||
|
/***/ 82:
|
||||||
|
/***/ (function(__unusedmodule, exports) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// We use any as a valid input type
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
/**
|
||||||
|
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||||
|
* @param input input to sanitize into a string
|
||||||
|
*/
|
||||||
|
function toCommandValue(input) {
|
||||||
|
if (input === null || input === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
else if (typeof input === 'string' || input instanceof String) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
return JSON.stringify(input);
|
||||||
|
}
|
||||||
|
exports.toCommandValue = toCommandValue;
|
||||||
|
//# sourceMappingURL=utils.js.map
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
/***/ 87:
|
/***/ 87:
|
||||||
/***/ (function(module) {
|
/***/ (function(module) {
|
||||||
|
|
||||||
@@ -50,6 +76,42 @@ module.exports = require("os");
|
|||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 102:
|
||||||
|
/***/ (function(__unusedmodule, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// For internal use, subject to change.
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||||
|
result["default"] = mod;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
// We use any as a valid input type
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
const fs = __importStar(__webpack_require__(747));
|
||||||
|
const os = __importStar(__webpack_require__(87));
|
||||||
|
const utils_1 = __webpack_require__(82);
|
||||||
|
function issueCommand(command, message) {
|
||||||
|
const filePath = process.env[`GITHUB_${command}`];
|
||||||
|
if (!filePath) {
|
||||||
|
throw new Error(`Unable to find environment variable for file command ${command}`);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
throw new Error(`Missing file at path: ${filePath}`);
|
||||||
|
}
|
||||||
|
fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, {
|
||||||
|
encoding: 'utf8'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.issueCommand = issueCommand;
|
||||||
|
//# sourceMappingURL=file-command.js.map
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
/***/ 129:
|
/***/ 129:
|
||||||
/***/ (function(module) {
|
/***/ (function(module) {
|
||||||
|
|
||||||
@@ -74,6 +136,362 @@ module.exports = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 322:
|
||||||
|
/***/ (function(__unusedmodule, exports) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||||
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||||
|
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||||
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||||
|
function step(op) {
|
||||||
|
if (f) throw new TypeError("Generator is already executing.");
|
||||||
|
while (_) try {
|
||||||
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||||
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||||
|
switch (op[0]) {
|
||||||
|
case 0: case 1: t = op; break;
|
||||||
|
case 4: _.label++; return { value: op[1], done: false };
|
||||||
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||||
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||||
|
default:
|
||||||
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||||
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||||
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||||
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||||
|
if (t[2]) _.ops.pop();
|
||||||
|
_.trys.pop(); continue;
|
||||||
|
}
|
||||||
|
op = body.call(thisArg, _);
|
||||||
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||||
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.wait = void 0;
|
||||||
|
function wait(ms) {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
return [2 /*return*/, new Promise(function (r) { return setTimeout(r, ms); })];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.wait = wait;
|
||||||
|
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 325:
|
||||||
|
/***/ (function(__unusedmodule, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||||
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||||
|
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||||
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||||
|
function step(op) {
|
||||||
|
if (f) throw new TypeError("Generator is already executing.");
|
||||||
|
while (_) try {
|
||||||
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||||
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||||
|
switch (op[0]) {
|
||||||
|
case 0: case 1: t = op; break;
|
||||||
|
case 4: _.label++; return { value: op[1], done: false };
|
||||||
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||||
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||||
|
default:
|
||||||
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||||
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||||
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||||
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||||
|
if (t[2]) _.ops.pop();
|
||||||
|
_.trys.pop(); continue;
|
||||||
|
}
|
||||||
|
op = body.call(thisArg, _);
|
||||||
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||||
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
var core_1 = __webpack_require__(470);
|
||||||
|
var child_process_1 = __webpack_require__(129);
|
||||||
|
var milliseconds_1 = __importDefault(__webpack_require__(156));
|
||||||
|
var tree_kill_1 = __importDefault(__webpack_require__(791));
|
||||||
|
var util_1 = __webpack_require__(322);
|
||||||
|
// inputs
|
||||||
|
var TIMEOUT_MINUTES = getInputNumber('timeout_minutes', false);
|
||||||
|
var TIMEOUT_SECONDS = getInputNumber('timeout_seconds', false);
|
||||||
|
var MAX_ATTEMPTS = getInputNumber('max_attempts', true) || 3;
|
||||||
|
var COMMAND = core_1.getInput('command', { required: true });
|
||||||
|
var RETRY_WAIT_SECONDS = getInputNumber('retry_wait_seconds', false) || 10;
|
||||||
|
var SHELL = core_1.getInput('shell');
|
||||||
|
var POLLING_INTERVAL_SECONDS = getInputNumber('polling_interval_seconds', false) || 1;
|
||||||
|
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 OS = process.platform;
|
||||||
|
var OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts';
|
||||||
|
var OUTPUT_EXIT_CODE_KEY = 'exit_code';
|
||||||
|
var OUTPUT_EXIT_ERROR_KEY = 'exit_error';
|
||||||
|
var exit;
|
||||||
|
var done;
|
||||||
|
function getInputNumber(id, required) {
|
||||||
|
var input = core_1.getInput(id, { required: required });
|
||||||
|
var 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 retryWait() {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
var waitStart;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0:
|
||||||
|
waitStart = Date.now();
|
||||||
|
return [4 /*yield*/, util_1.wait(milliseconds_1.default.seconds(RETRY_WAIT_SECONDS))];
|
||||||
|
case 1:
|
||||||
|
_a.sent();
|
||||||
|
core_1.debug("Waited " + (Date.now() - waitStart) + "ms");
|
||||||
|
core_1.debug("Configured wait: " + milliseconds_1.default.seconds(RETRY_WAIT_SECONDS) + "ms");
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function validateInputs() {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
if ((!TIMEOUT_MINUTES && !TIMEOUT_SECONDS) || (TIMEOUT_MINUTES && TIMEOUT_SECONDS)) {
|
||||||
|
throw new Error('Must specify either timeout_minutes or timeout_seconds inputs');
|
||||||
|
}
|
||||||
|
return [2 /*return*/];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getTimeout() {
|
||||||
|
if (TIMEOUT_MINUTES) {
|
||||||
|
return milliseconds_1.default.minutes(TIMEOUT_MINUTES);
|
||||||
|
}
|
||||||
|
else if (TIMEOUT_SECONDS) {
|
||||||
|
return milliseconds_1.default.seconds(TIMEOUT_SECONDS);
|
||||||
|
}
|
||||||
|
throw new Error('Must specify either timeout_minutes or timeout_seconds inputs');
|
||||||
|
}
|
||||||
|
function getExecutable() {
|
||||||
|
if (!SHELL) {
|
||||||
|
return OS === 'win32' ? 'powershell' : 'bash';
|
||||||
|
}
|
||||||
|
var executable;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
function runRetryCmd() {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
var error_1;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0:
|
||||||
|
// if no retry script, just continue
|
||||||
|
if (!ON_RETRY_COMMAND) {
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
_a.label = 1;
|
||||||
|
case 1:
|
||||||
|
_a.trys.push([1, 3, , 4]);
|
||||||
|
return [4 /*yield*/, child_process_1.execSync(ON_RETRY_COMMAND, { stdio: 'inherit' })];
|
||||||
|
case 2:
|
||||||
|
_a.sent();
|
||||||
|
return [3 /*break*/, 4];
|
||||||
|
case 3:
|
||||||
|
error_1 = _a.sent();
|
||||||
|
core_1.info("WARNING: Retry command threw the error " + error_1.message);
|
||||||
|
return [3 /*break*/, 4];
|
||||||
|
case 4: return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function runCmd() {
|
||||||
|
var _a, _b;
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
var end_time, executable, child;
|
||||||
|
return __generator(this, function (_c) {
|
||||||
|
switch (_c.label) {
|
||||||
|
case 0:
|
||||||
|
end_time = Date.now() + getTimeout();
|
||||||
|
executable = getExecutable();
|
||||||
|
exit = 0;
|
||||||
|
done = false;
|
||||||
|
core_1.debug("Running command " + COMMAND + " on " + OS + " using shell " + executable);
|
||||||
|
child = child_process_1.exec(COMMAND, { 'shell': executable });
|
||||||
|
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', function (data) {
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
(_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', function (data) {
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
child.on('exit', function (code, signal) {
|
||||||
|
core_1.debug("Code: " + code);
|
||||||
|
core_1.debug("Signal: " + signal);
|
||||||
|
if (code && code > 0) {
|
||||||
|
exit = code;
|
||||||
|
}
|
||||||
|
// timeouts are killed manually
|
||||||
|
if (signal === 'SIGTERM') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
});
|
||||||
|
_c.label = 1;
|
||||||
|
case 1: return [4 /*yield*/, util_1.wait(milliseconds_1.default.seconds(POLLING_INTERVAL_SECONDS))];
|
||||||
|
case 2:
|
||||||
|
_c.sent();
|
||||||
|
_c.label = 3;
|
||||||
|
case 3:
|
||||||
|
if (Date.now() < end_time && !done) return [3 /*break*/, 1];
|
||||||
|
_c.label = 4;
|
||||||
|
case 4:
|
||||||
|
if (!!done) return [3 /*break*/, 6];
|
||||||
|
tree_kill_1.default(child.pid);
|
||||||
|
return [4 /*yield*/, retryWait()];
|
||||||
|
case 5:
|
||||||
|
_c.sent();
|
||||||
|
throw new Error("Timeout of " + getTimeout() + "ms hit");
|
||||||
|
case 6:
|
||||||
|
if (!(exit > 0)) return [3 /*break*/, 8];
|
||||||
|
return [4 /*yield*/, retryWait()];
|
||||||
|
case 7:
|
||||||
|
_c.sent();
|
||||||
|
throw new Error("Child_process exited with error code " + exit);
|
||||||
|
case 8: return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function runAction() {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
var attempt, error_2;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0: return [4 /*yield*/, validateInputs()];
|
||||||
|
case 1:
|
||||||
|
_a.sent();
|
||||||
|
attempt = 1;
|
||||||
|
_a.label = 2;
|
||||||
|
case 2:
|
||||||
|
if (!(attempt <= MAX_ATTEMPTS)) return [3 /*break*/, 12];
|
||||||
|
_a.label = 3;
|
||||||
|
case 3:
|
||||||
|
_a.trys.push([3, 5, , 11]);
|
||||||
|
// just keep overwriting attempts output
|
||||||
|
core_1.setOutput(OUTPUT_TOTAL_ATTEMPTS_KEY, attempt);
|
||||||
|
return [4 /*yield*/, runCmd()];
|
||||||
|
case 4:
|
||||||
|
_a.sent();
|
||||||
|
core_1.info("Command completed after " + attempt + " attempt(s).");
|
||||||
|
return [3 /*break*/, 12];
|
||||||
|
case 5:
|
||||||
|
error_2 = _a.sent();
|
||||||
|
if (!(attempt === MAX_ATTEMPTS)) return [3 /*break*/, 6];
|
||||||
|
throw new Error("Final attempt failed. " + error_2.message);
|
||||||
|
case 6:
|
||||||
|
if (!(!done && RETRY_ON === 'error')) return [3 /*break*/, 7];
|
||||||
|
// error: timeout
|
||||||
|
throw error_2;
|
||||||
|
case 7:
|
||||||
|
if (!(exit > 0 && RETRY_ON === 'timeout')) return [3 /*break*/, 8];
|
||||||
|
// error: error
|
||||||
|
throw error_2;
|
||||||
|
case 8: return [4 /*yield*/, runRetryCmd()];
|
||||||
|
case 9:
|
||||||
|
_a.sent();
|
||||||
|
if (WARNING_ON_RETRY) {
|
||||||
|
core_1.warning("Attempt " + attempt + " failed. Reason: " + error_2.message);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
core_1.info("Attempt " + attempt + " failed. Reason: " + error_2.message);
|
||||||
|
}
|
||||||
|
_a.label = 10;
|
||||||
|
case 10: return [3 /*break*/, 11];
|
||||||
|
case 11:
|
||||||
|
attempt++;
|
||||||
|
return [3 /*break*/, 2];
|
||||||
|
case 12: return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
runAction()
|
||||||
|
.then(function () {
|
||||||
|
core_1.setOutput(OUTPUT_EXIT_CODE_KEY, 0);
|
||||||
|
process.exit(0); // success
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 431:
|
/***/ 431:
|
||||||
@@ -90,6 +508,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
const os = __importStar(__webpack_require__(87));
|
const os = __importStar(__webpack_require__(87));
|
||||||
|
const utils_1 = __webpack_require__(82);
|
||||||
/**
|
/**
|
||||||
* Commands
|
* Commands
|
||||||
*
|
*
|
||||||
@@ -143,28 +562,14 @@ class Command {
|
|||||||
return cmdStr;
|
return cmdStr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
|
||||||
* @param input input to sanitize into a string
|
|
||||||
*/
|
|
||||||
function toCommandValue(input) {
|
|
||||||
if (input === null || input === undefined) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
else if (typeof input === 'string' || input instanceof String) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
return JSON.stringify(input);
|
|
||||||
}
|
|
||||||
exports.toCommandValue = toCommandValue;
|
|
||||||
function escapeData(s) {
|
function escapeData(s) {
|
||||||
return toCommandValue(s)
|
return utils_1.toCommandValue(s)
|
||||||
.replace(/%/g, '%25')
|
.replace(/%/g, '%25')
|
||||||
.replace(/\r/g, '%0D')
|
.replace(/\r/g, '%0D')
|
||||||
.replace(/\n/g, '%0A');
|
.replace(/\n/g, '%0A');
|
||||||
}
|
}
|
||||||
function escapeProperty(s) {
|
function escapeProperty(s) {
|
||||||
return toCommandValue(s)
|
return utils_1.toCommandValue(s)
|
||||||
.replace(/%/g, '%25')
|
.replace(/%/g, '%25')
|
||||||
.replace(/\r/g, '%0D')
|
.replace(/\r/g, '%0D')
|
||||||
.replace(/\n/g, '%0A')
|
.replace(/\n/g, '%0A')
|
||||||
@@ -198,6 +603,8 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
const command_1 = __webpack_require__(431);
|
const command_1 = __webpack_require__(431);
|
||||||
|
const file_command_1 = __webpack_require__(102);
|
||||||
|
const utils_1 = __webpack_require__(82);
|
||||||
const os = __importStar(__webpack_require__(87));
|
const os = __importStar(__webpack_require__(87));
|
||||||
const path = __importStar(__webpack_require__(622));
|
const path = __importStar(__webpack_require__(622));
|
||||||
/**
|
/**
|
||||||
@@ -224,9 +631,17 @@ var ExitCode;
|
|||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function exportVariable(name, val) {
|
function exportVariable(name, val) {
|
||||||
const convertedVal = command_1.toCommandValue(val);
|
const convertedVal = utils_1.toCommandValue(val);
|
||||||
process.env[name] = convertedVal;
|
process.env[name] = convertedVal;
|
||||||
|
const filePath = process.env['GITHUB_ENV'] || '';
|
||||||
|
if (filePath) {
|
||||||
|
const delimiter = '_GitHubActionsFileCommandDelimeter_';
|
||||||
|
const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`;
|
||||||
|
file_command_1.issueCommand('ENV', commandValue);
|
||||||
|
}
|
||||||
|
else {
|
||||||
command_1.issueCommand('set-env', { name }, convertedVal);
|
command_1.issueCommand('set-env', { name }, convertedVal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
exports.exportVariable = exportVariable;
|
exports.exportVariable = exportVariable;
|
||||||
/**
|
/**
|
||||||
@@ -242,7 +657,13 @@ exports.setSecret = setSecret;
|
|||||||
* @param inputPath
|
* @param inputPath
|
||||||
*/
|
*/
|
||||||
function addPath(inputPath) {
|
function addPath(inputPath) {
|
||||||
|
const filePath = process.env['GITHUB_PATH'] || '';
|
||||||
|
if (filePath) {
|
||||||
|
file_command_1.issueCommand('PATH', inputPath);
|
||||||
|
}
|
||||||
|
else {
|
||||||
command_1.issueCommand('add-path', {}, inputPath);
|
command_1.issueCommand('add-path', {}, inputPath);
|
||||||
|
}
|
||||||
process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`;
|
process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`;
|
||||||
}
|
}
|
||||||
exports.addPath = addPath;
|
exports.addPath = addPath;
|
||||||
@@ -411,94 +832,10 @@ module.exports = require("path");
|
|||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 676:
|
/***/ 747:
|
||||||
/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) {
|
/***/ (function(module) {
|
||||||
|
|
||||||
const { getInput, error, warning, info, debug } = __webpack_require__(470);
|
|
||||||
const { spawn } = __webpack_require__(129);
|
|
||||||
const { join } = __webpack_require__(622);
|
|
||||||
const ms = __webpack_require__(156);
|
|
||||||
var kill = __webpack_require__(791);
|
|
||||||
|
|
||||||
function getInputNumber(id, required) {
|
|
||||||
const input = getInput(id, { required });
|
|
||||||
const num = Number.parseInt(input);
|
|
||||||
|
|
||||||
if (!Number.isInteger(num)) {
|
|
||||||
throw `Input ${id} only accepts numbers. Received ${input}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return num;
|
|
||||||
}
|
|
||||||
|
|
||||||
// inputs
|
|
||||||
const TIMEOUT_MINUTES = getInputNumber('timeout_minutes', true);
|
|
||||||
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);
|
|
||||||
|
|
||||||
async function wait(ms) {
|
|
||||||
return new Promise((r) => setTimeout(r, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runCmd() {
|
|
||||||
const end_time = Date.now() + ms.minutes(TIMEOUT_MINUTES);
|
|
||||||
var done, exit;
|
|
||||||
|
|
||||||
var child = spawn('node', [__webpack_require__.ab + "exec.js", COMMAND], { stdio: 'inherit' });
|
|
||||||
|
|
||||||
child.on('exit', (code) => {
|
|
||||||
if (code > 0) {
|
|
||||||
exit = code;
|
|
||||||
}
|
|
||||||
done = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
do {
|
|
||||||
await wait(ms.seconds(POLLING_INTERVAL_SECONDS));
|
|
||||||
} while (Date.now() < end_time && !done && !exit);
|
|
||||||
|
|
||||||
if (!done) {
|
|
||||||
kill(child.pid);
|
|
||||||
await retryWait();
|
|
||||||
throw new Error(`Timeout of ${TIMEOUT_MINUTES}m hit`);
|
|
||||||
} else if (exit > 0) {
|
|
||||||
await retryWait();
|
|
||||||
throw new Error(`Child_process exited with error`);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 runAction() {
|
|
||||||
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
||||||
try {
|
|
||||||
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 {
|
|
||||||
warning(`Attempt ${attempt} failed. Reason:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runAction().catch((err) => {
|
|
||||||
error(err.message);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
module.exports = require("fs");
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
|||||||
3100
package-lock.json
generated
3100
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -3,7 +3,8 @@
|
|||||||
"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": {
|
||||||
"prepare": "ncc build src/index.js"
|
"local": "npm run prepare && node -r dotenv/config ./dist/index.js",
|
||||||
|
"prepare": "ncc build src/index.ts"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -17,18 +18,23 @@
|
|||||||
},
|
},
|
||||||
"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.2.6",
|
||||||
"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": "11.0.0",
|
||||||
"@commitlint/config-conventional": "^8.3.4",
|
"@commitlint/config-conventional": "11.0.0",
|
||||||
"@semantic-release/changelog": "^3.0.6",
|
"@semantic-release/changelog": "5.0.1",
|
||||||
"@semantic-release/git": "^7.0.18",
|
"@semantic-release/git": "9.0.0",
|
||||||
|
"@types/milliseconds": "0.0.30",
|
||||||
|
"@types/node": "14.14.7",
|
||||||
"@zeit/ncc": "^0.20.5",
|
"@zeit/ncc": "^0.20.5",
|
||||||
"husky": "^3.1.0",
|
"dotenv": "8.2.0",
|
||||||
"semantic-release": "^17.0.3"
|
"husky": "4.3.0",
|
||||||
|
"semantic-release": "17.2.3",
|
||||||
|
"ts-node": "9.0.0",
|
||||||
|
"typescript": "4.0.5"
|
||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
|
|||||||
7
sample.env
Normal file
7
sample.env
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
INPUT_TIMEOUT_MINUTES=1
|
||||||
|
INPUT_MAX_ATTEMPTS=3
|
||||||
|
INPUT_COMMAND="node -e 'process.exit(99)'"
|
||||||
|
INPUT_RETRY_WAIT_SECONDS=10
|
||||||
|
SHELL=pwsh
|
||||||
|
INPUT_POLLING_INTERVAL_SECONDS=1
|
||||||
|
INPUT_RETRY_ON=any
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const { execSync } = require('child_process');
|
|
||||||
const COMMAND = process.argv.splice(2)[0];
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
execSync(COMMAND, { stdio: 'inherit' });
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
84
src/index.js
84
src/index.js
@@ -1,84 +0,0 @@
|
|||||||
const { getInput, error, warning, info, debug } = require('@actions/core');
|
|
||||||
const { spawn } = require('child_process');
|
|
||||||
const { join } = require('path');
|
|
||||||
const ms = require('milliseconds');
|
|
||||||
var kill = require('tree-kill');
|
|
||||||
|
|
||||||
function getInputNumber(id, required) {
|
|
||||||
const input = getInput(id, { required });
|
|
||||||
const num = Number.parseInt(input);
|
|
||||||
|
|
||||||
if (!Number.isInteger(num)) {
|
|
||||||
throw `Input ${id} only accepts numbers. Received ${input}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return num;
|
|
||||||
}
|
|
||||||
|
|
||||||
// inputs
|
|
||||||
const TIMEOUT_MINUTES = getInputNumber('timeout_minutes', true);
|
|
||||||
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);
|
|
||||||
|
|
||||||
async function wait(ms) {
|
|
||||||
return new Promise((r) => setTimeout(r, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runCmd() {
|
|
||||||
const end_time = Date.now() + ms.minutes(TIMEOUT_MINUTES);
|
|
||||||
var done, exit;
|
|
||||||
|
|
||||||
var child = spawn('node', [join(__dirname, 'exec.js'), COMMAND], { stdio: 'inherit' });
|
|
||||||
|
|
||||||
child.on('exit', (code) => {
|
|
||||||
if (code > 0) {
|
|
||||||
exit = code;
|
|
||||||
}
|
|
||||||
done = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
do {
|
|
||||||
await wait(ms.seconds(POLLING_INTERVAL_SECONDS));
|
|
||||||
} while (Date.now() < end_time && !done && !exit);
|
|
||||||
|
|
||||||
if (!done) {
|
|
||||||
kill(child.pid);
|
|
||||||
await retryWait();
|
|
||||||
throw new Error(`Timeout of ${TIMEOUT_MINUTES}m hit`);
|
|
||||||
} else if (exit > 0) {
|
|
||||||
await retryWait();
|
|
||||||
throw new Error(`Child_process exited with error`);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 runAction() {
|
|
||||||
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
||||||
try {
|
|
||||||
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 {
|
|
||||||
warning(`Attempt ${attempt} failed. Reason:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runAction().catch((err) => {
|
|
||||||
error(err.message);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
206
src/index.ts
Normal file
206
src/index.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
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 OS = process.platform;
|
||||||
|
const OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts';
|
||||||
|
const OUTPUT_EXIT_CODE_KEY = 'exit_code';
|
||||||
|
const OUTPUT_EXIT_ERROR_KEY = 'exit_error';
|
||||||
|
|
||||||
|
var exit: number;
|
||||||
|
var 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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' });
|
||||||
|
} catch (error) {
|
||||||
|
info(`WARNING: Retry command threw the error ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCmd() {
|
||||||
|
const end_time = Date.now() + getTimeout();
|
||||||
|
const executable = getExecutable();
|
||||||
|
|
||||||
|
exit = 0;
|
||||||
|
done = false;
|
||||||
|
|
||||||
|
debug(`Running command ${COMMAND} on ${OS} using shell ${executable}`)
|
||||||
|
var child = 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) {
|
||||||
|
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 {
|
||||||
|
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) => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
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