feat: implement waitTimeout

This commit is contained in:
Ivan Dlugos 2022-03-03 17:56:42 +01:00
parent 880511a8d8
commit 7825bea020
6 changed files with 135 additions and 18 deletions

View file

@ -156,12 +156,14 @@ jobs:
- run: npm install - run: npm install
- run: npm run build - run: npm run build
# Test downloading a single artifact # Test downloading a single artifact
- name: Download artifact A - name: Download artifact A
uses: ./ uses: ./
with: with:
name: 'Artifact-wait-${{ matrix.runs-on }}' name: 'Artifact-wait-${{ matrix.runs-on }}'
path: some/new/path path: some/new/path
waitTimeout: 600
# Test downloading an artifact using tilde expansion # Test downloading an artifact using tilde expansion
- name: Download artifact A - name: Download artifact A
@ -169,6 +171,7 @@ jobs:
with: with:
name: 'Artifact-wait-${{ matrix.runs-on }}' name: 'Artifact-wait-${{ matrix.runs-on }}'
path: ~/some/path/with/a/tilde path: ~/some/path/with/a/tilde
# no need for a timeout here
- name: Verify successful download - name: Verify successful download
run: | run: |
@ -184,21 +187,43 @@ jobs:
} }
shell: pwsh shell: pwsh
# Test downloading both artifacts at once # Test downloading all artifacts at once
test-wait-consumer-all:
name: 'Test: wait (consumer - all)'
strategy:
matrix:
runs-on: [ubuntu-latest, macos-latest, windows-latest]
fail-fast: false
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v2
- name: Set Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- run: npm install
- run: npm run build
- name: Download all Artifacts - name: Download all Artifacts
uses: ./ uses: ./
with: with:
path: some/other/path path: some/other/path
waitTimeout: 600
- name: Verify successful download - name: Verify successful download
run: | run: |
$fileA = "some/other/path/Artifact-A/file-A.txt" $fileA = "some/other/path/Artifact-A/file-A.txt"
$fileB = "some/other/path/Artifact-B/file-B.txt" if(!(Test-Path -path $fileA))
if(!(Test-Path -path $fileA) -or !(Test-Path -path $fileB))
{ {
Write-Error "Expected files do not exist" Write-Error "Expected files do not exist"
} }
if(!((Get-Content $fileA) -ceq "Lorem ipsum dolor sit amet") -or !((Get-Content $fileB) -ceq "Hello world from file B")) if(!(Get-Content $fileA) -ceq "Lorem ipsum dolor sit amet"))
{ {
Write-Error "File contents of downloaded artifacts are incorrect" Write-Error "File contents of downloaded artifacts are incorrect"
} }

View file

@ -135,6 +135,45 @@ steps:
> Note: The `id` defined in the `download/artifact` step must match the `id` defined in the `echo` step (i.e `steps.[ID].outputs.download-path`) > Note: The `id` defined in the `download/artifact` step must match the `id` defined in the `echo` step (i.e `steps.[ID].outputs.download-path`)
# Waiting for the artifact to be available
You can specify `waitTimeout` (seconds) to instruct the download/artifact to retry until the artifact is available.
This is useful if you want to launch the job before its dependency job has finished, e.g. if the dependant requires some time-consuming steps.
You can do this by removing the `needs` dependency and relying on the retry logic of download-artifact to fetch the artifact after it's uploaded.
Beware that GitHub actions come with a limited number of runners available, so if your workflow uses up the limt on the "dependant" jobs,
your artifact-source jobs may never be scheduled.
```yaml
jobs:
producer-job:
name: This job produces an artifact
steps:
# ... do something
# ... then upload an artifact as usual
- uses: actions/upload-artifact@v1
with:
name: artifact-name
path: path/to/artifact
dependant-job:
name: This job has some long running preparation that can run before the artifact is necessary
steps:
# your long-running steps come first - they're run in parallel with the `producer-job` above (given you have enouth GH actions runners available)
run: # e.g. install some large SDK
# then when you finally need the artifact to be downloaded
uses: actions/download-artifact@v2
with:
name: artifact-name
path: output-path
# wait for 300 seconds
waitTimeout: 300
```
> Note: The `id` defined in the `download/artifact` step must match the `id` defined in the `echo` step (i.e `steps.[ID].outputs.download-path`)
# Limitations # Limitations
### Permission Loss ### Permission Loss

View file

@ -8,6 +8,9 @@ inputs:
path: path:
description: 'Destination path' description: 'Destination path'
required: false required: false
waitTimeout:
description: 'Wait for the artifact to become available (timeout in seconds)'
required: false
runs: runs:
using: 'node16' using: 'node16'
main: 'dist/index.js' main: 'dist/index.js'

29
dist/index.js vendored
View file

@ -6954,6 +6954,7 @@ var Inputs;
(function (Inputs) { (function (Inputs) {
Inputs["Name"] = "name"; Inputs["Name"] = "name";
Inputs["Path"] = "path"; Inputs["Path"] = "path";
Inputs["WaitTimeout"] = "waitTimeout";
})(Inputs = exports.Inputs || (exports.Inputs = {})); })(Inputs = exports.Inputs || (exports.Inputs = {}));
var Outputs; var Outputs;
(function (Outputs) { (function (Outputs) {
@ -7009,6 +7010,28 @@ function run() {
try { try {
const name = core.getInput(constants_1.Inputs.Name, { required: false }); const name = core.getInput(constants_1.Inputs.Name, { required: false });
const path = core.getInput(constants_1.Inputs.Path, { required: false }); const path = core.getInput(constants_1.Inputs.Path, { required: false });
const waitTimeoutStr = core.getInput(constants_1.Inputs.WaitTimeout, { required: false });
let runDownload;
// no retry allowed
if (waitTimeoutStr == '') {
runDownload = (action) => action();
}
else {
const waitTimeoutSeconds = parseInt(waitTimeoutStr);
runDownload = (action) => {
const waitUntil = new Date().getSeconds() + waitTimeoutSeconds;
let lastError;
do {
try {
return action();
}
catch (e) {
lastError = e;
}
} while (new Date().getSeconds() < waitUntil);
throw Error('Timeout reached. Latest error: ' + lastError);
};
}
let resolvedPath; let resolvedPath;
// resolve tilde expansions, path.replace only replaces the first occurrence of a pattern // resolve tilde expansions, path.replace only replaces the first occurrence of a pattern
if (path.startsWith(`~`)) { if (path.startsWith(`~`)) {
@ -7023,7 +7046,7 @@ function run() {
// download all artifacts // download all artifacts
core.info('No artifact name specified, downloading all artifacts'); core.info('No artifact name specified, downloading all artifacts');
core.info('Creating an extra directory for each artifact that is being downloaded'); core.info('Creating an extra directory for each artifact that is being downloaded');
const downloadResponse = yield artifactClient.downloadAllArtifacts(resolvedPath); const downloadResponse = yield runDownload(() => __awaiter(this, void 0, void 0, function* () { return yield artifactClient.downloadAllArtifacts(resolvedPath); }));
core.info(`There were ${downloadResponse.length} artifacts downloaded`); core.info(`There were ${downloadResponse.length} artifacts downloaded`);
for (const artifact of downloadResponse) { for (const artifact of downloadResponse) {
core.info(`Artifact ${artifact.artifactName} was downloaded to ${artifact.downloadPath}`); core.info(`Artifact ${artifact.artifactName} was downloaded to ${artifact.downloadPath}`);
@ -7035,7 +7058,9 @@ function run() {
const downloadOptions = { const downloadOptions = {
createArtifactFolder: false createArtifactFolder: false
}; };
const downloadResponse = yield artifactClient.downloadArtifact(name, resolvedPath, downloadOptions); const downloadResponse = yield runDownload(() => __awaiter(this, void 0, void 0, function* () {
return yield artifactClient.downloadArtifact(name, resolvedPath, downloadOptions);
}));
core.info(`Artifact ${downloadResponse.artifactName} was downloaded to ${downloadResponse.downloadPath}`); core.info(`Artifact ${downloadResponse.artifactName} was downloaded to ${downloadResponse.downloadPath}`);
} }
// output the directory that the artifact(s) was/were downloaded to // output the directory that the artifact(s) was/were downloaded to

View file

@ -1,6 +1,7 @@
export enum Inputs { export enum Inputs {
Name = 'name', Name = 'name',
Path = 'path' Path = 'path',
WaitTimeout = 'waitTimeout'
} }
export enum Outputs { export enum Outputs {
DownloadPath = 'download-path' DownloadPath = 'download-path'

View file

@ -8,6 +8,27 @@ async function run(): Promise<void> {
try { try {
const name = core.getInput(Inputs.Name, {required: false}) const name = core.getInput(Inputs.Name, {required: false})
const path = core.getInput(Inputs.Path, {required: false}) const path = core.getInput(Inputs.Path, {required: false})
const waitTimeoutStr = core.getInput(Inputs.WaitTimeout, {required: false})
let runDownload: <T extends unknown>(action: () => T) => T
// no retry allowed
if (waitTimeoutStr == '') {
runDownload = <T extends unknown>(action: () => T) => action()
} else {
const waitTimeoutSeconds = parseInt(waitTimeoutStr)
runDownload = <T extends unknown>(action: () => T) => {
const waitUntil = new Date().getSeconds() + waitTimeoutSeconds
let lastError
do {
try {
return action()
} catch (e) {
lastError = e
}
} while (new Date().getSeconds() < waitUntil)
throw Error('Timeout reached. Latest error: ' + lastError)
}
}
let resolvedPath let resolvedPath
// resolve tilde expansions, path.replace only replaces the first occurrence of a pattern // resolve tilde expansions, path.replace only replaces the first occurrence of a pattern
@ -25,8 +46,8 @@ async function run(): Promise<void> {
core.info( core.info(
'Creating an extra directory for each artifact that is being downloaded' 'Creating an extra directory for each artifact that is being downloaded'
) )
const downloadResponse = await artifactClient.downloadAllArtifacts( const downloadResponse = await runDownload(
resolvedPath async () => await artifactClient.downloadAllArtifacts(resolvedPath)
) )
core.info(`There were ${downloadResponse.length} artifacts downloaded`) core.info(`There were ${downloadResponse.length} artifacts downloaded`)
for (const artifact of downloadResponse) { for (const artifact of downloadResponse) {
@ -40,11 +61,14 @@ async function run(): Promise<void> {
const downloadOptions = { const downloadOptions = {
createArtifactFolder: false createArtifactFolder: false
} }
const downloadResponse = await artifactClient.downloadArtifact( const downloadResponse = await runDownload(
async () =>
await artifactClient.downloadArtifact(
name, name,
resolvedPath, resolvedPath,
downloadOptions downloadOptions
) )
)
core.info( core.info(
`Artifact ${downloadResponse.artifactName} was downloaded to ${downloadResponse.downloadPath}` `Artifact ${downloadResponse.artifactName} was downloaded to ${downloadResponse.downloadPath}`
) )