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 run build
# Test downloading a single artifact
- name: Download artifact A
uses: ./
with:
name: 'Artifact-wait-${{ matrix.runs-on }}'
path: some/new/path
waitTimeout: 600
# Test downloading an artifact using tilde expansion
- name: Download artifact A
@ -169,6 +171,7 @@ jobs:
with:
name: 'Artifact-wait-${{ matrix.runs-on }}'
path: ~/some/path/with/a/tilde
# no need for a timeout here
- name: Verify successful download
run: |
@ -184,21 +187,43 @@ jobs:
}
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
uses: ./
with:
path: some/other/path
waitTimeout: 600
- name: Verify successful download
run: |
$fileA = "some/other/path/Artifact-A/file-A.txt"
$fileB = "some/other/path/Artifact-B/file-B.txt"
if(!(Test-Path -path $fileA) -or !(Test-Path -path $fileB))
if(!(Test-Path -path $fileA))
{
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"
}

View file

@ -26,7 +26,7 @@ steps:
- uses: actions/download-artifact@v3
with:
name: my-artifact
- name: Display structure of downloaded files
run: ls -R
```
@ -40,7 +40,7 @@ steps:
with:
name: my-artifact
path: path/to/artifact
- name: Display structure of downloaded files
run: ls -R
working-directory: path/to/artifact
@ -98,7 +98,7 @@ steps:
- uses: actions/download-artifact@v3
with:
path: path/to/artifacts
- name: Display structure of downloaded files
run: ls -R
working-directory: path/to/artifacts
@ -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`)
# 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
### Permission Loss
@ -157,7 +196,7 @@ If file permissions and case sensitivity are required, you can `tar` all of your
uses: actions/upload-artifact@v2
with:
name: my-artifact
path: my_files.tar
path: my_files.tar
```
# @actions/artifact package

View file

@ -1,13 +1,16 @@
name: 'Download a Build Artifact'
description: 'Download a build artifact that was previously uploaded in the workflow by the upload-artifact action'
author: 'GitHub'
inputs:
inputs:
name:
description: 'Artifact name'
required: false
path:
description: 'Destination path'
required: false
waitTimeout:
description: 'Wait for the artifact to become available (timeout in seconds)'
required: false
runs:
using: 'node16'
main: 'dist/index.js'

29
dist/index.js vendored
View file

@ -6954,6 +6954,7 @@ var Inputs;
(function (Inputs) {
Inputs["Name"] = "name";
Inputs["Path"] = "path";
Inputs["WaitTimeout"] = "waitTimeout";
})(Inputs = exports.Inputs || (exports.Inputs = {}));
var Outputs;
(function (Outputs) {
@ -7009,6 +7010,28 @@ function run() {
try {
const name = core.getInput(constants_1.Inputs.Name, { 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;
// resolve tilde expansions, path.replace only replaces the first occurrence of a pattern
if (path.startsWith(`~`)) {
@ -7023,7 +7046,7 @@ function run() {
// download all artifacts
core.info('No artifact name specified, downloading all artifacts');
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`);
for (const artifact of downloadResponse) {
core.info(`Artifact ${artifact.artifactName} was downloaded to ${artifact.downloadPath}`);
@ -7035,7 +7058,9 @@ function run() {
const downloadOptions = {
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}`);
}
// output the directory that the artifact(s) was/were downloaded to

View file

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

View file

@ -8,6 +8,27 @@ async function run(): Promise<void> {
try {
const name = core.getInput(Inputs.Name, {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
// resolve tilde expansions, path.replace only replaces the first occurrence of a pattern
@ -25,8 +46,8 @@ async function run(): Promise<void> {
core.info(
'Creating an extra directory for each artifact that is being downloaded'
)
const downloadResponse = await artifactClient.downloadAllArtifacts(
resolvedPath
const downloadResponse = await runDownload(
async () => await artifactClient.downloadAllArtifacts(resolvedPath)
)
core.info(`There were ${downloadResponse.length} artifacts downloaded`)
for (const artifact of downloadResponse) {
@ -40,10 +61,13 @@ async function run(): Promise<void> {
const downloadOptions = {
createArtifactFolder: false
}
const downloadResponse = await artifactClient.downloadArtifact(
name,
resolvedPath,
downloadOptions
const downloadResponse = await runDownload(
async () =>
await artifactClient.downloadArtifact(
name,
resolvedPath,
downloadOptions
)
)
core.info(
`Artifact ${downloadResponse.artifactName} was downloaded to ${downloadResponse.downloadPath}`