diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1fb3007..510c13a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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" } diff --git a/README.md b/README.md index a2219e7..feb9f44 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/action.yml b/action.yml index e16d8c0..4058486 100644 --- a/action.yml +++ b/action.yml @@ -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' diff --git a/dist/index.js b/dist/index.js index 371b19e..b3764b2 100644 --- a/dist/index.js +++ b/dist/index.js @@ -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 diff --git a/src/constants.ts b/src/constants.ts index 49d800a..3fa2012 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,7 @@ export enum Inputs { Name = 'name', - Path = 'path' + Path = 'path', + WaitTimeout = 'waitTimeout' } export enum Outputs { DownloadPath = 'download-path' diff --git a/src/download-artifact.ts b/src/download-artifact.ts index b32a7be..a7946cf 100644 --- a/src/download-artifact.ts +++ b/src/download-artifact.ts @@ -8,6 +8,27 @@ async function run(): Promise { 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: (action: () => T) => T + // no retry allowed + if (waitTimeoutStr == '') { + runDownload = (action: () => T) => action() + } else { + const waitTimeoutSeconds = parseInt(waitTimeoutStr) + runDownload = (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 { 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 { 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}`