mirror of
https://github.com/actions/download-artifact.git
synced 2025-07-08 19:32:40 +08:00
Compare commits
115 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
448e3f862a | ||
|
47225c44b3 | ||
|
d3f86a106a | ||
|
fc02353415 | ||
|
77454371a4 | ||
|
84fc7a0a35 | ||
|
67f2bc382f | ||
|
8ea3c2c174 | ||
|
d219c630f6 | ||
|
54124fbd88 | ||
|
b83057b90d | ||
|
171183c7dc | ||
|
e463631f66 | ||
|
ec378bcca1 | ||
|
42aef06f22 | ||
|
ac35f995fe | ||
|
95815c38cf | ||
|
278fca438a | ||
|
68909842a1 | ||
|
f9415c0ec3 | ||
|
76a6eb5cbc | ||
|
a2426d7c45 | ||
|
3ffa694f6f | ||
|
53f6aa5f93 | ||
|
b456700053 | ||
|
9eab798a98 | ||
|
a39a661f39 | ||
|
9a869e9c49 | ||
|
96a6f165f4 | ||
|
df4ad15cb8 | ||
|
c7cfc3a2a3 | ||
|
2439186eed | ||
|
b14cf4c926 | ||
|
c5804ef743 | ||
|
956811a503 | ||
|
af3c6d3e5b | ||
|
4dd97f8f21 | ||
|
da9985dde6 | ||
|
81ba80daa4 | ||
|
727afbf2b0 | ||
|
56c2d7ea8c | ||
|
7797bfcd59 | ||
|
9ff67cb2d2 | ||
|
049eba1e9a | ||
|
503e7a18ae | ||
|
a8a786b097 | ||
|
24aef17bbf | ||
|
b81a615862 | ||
|
cc20338598 | ||
|
1fc0fee191 | ||
|
7fba95161a | ||
|
f9ceb7763b | ||
|
533298bc57 | ||
|
d06289e120 | ||
|
d0ce8fd116 | ||
|
1ce0d91ace | ||
|
fa0a91b85d | ||
|
b54d0883e1 | ||
|
65a9edc588 | ||
|
fdd1595981 | ||
|
c13dba102f | ||
|
0daa75ebea | ||
|
9c19ed7fe5 | ||
|
3d3ea8741e | ||
|
89af5db821 | ||
|
b4aefff88e | ||
|
8caf195ad4 | ||
|
d7a2ec411d | ||
|
e56a1d48ef | ||
|
1fcda58b3a | ||
|
325a10d8b7 | ||
|
f8aaee4a21 | ||
|
d98334b11d | ||
|
c850b930e6 | ||
|
6fd111f15a | ||
|
87c55149d9 | ||
|
47f9ce604f | ||
|
127824d34c | ||
|
6dd49bff0a | ||
|
f71c0e3da3 | ||
|
7c63dfde29 | ||
|
67d37cd346 | ||
|
348754975e | ||
|
eaceaf801f | ||
|
81eafdc926 | ||
|
9ac5cad9e2 | ||
|
3ad8411bbd | ||
|
1de464352c | ||
|
bb3fa7fd35 | ||
|
a244de5a62 | ||
|
355659bff2 | ||
|
6b208ae046 | ||
|
6c5b5806e1 | ||
|
5f5015dc38 | ||
|
1fddaaf0f1 | ||
|
8aa9e2115b | ||
|
657edd9b81 | ||
|
555a2fc129 | ||
|
4fc4d70d4c | ||
|
072ac9dceb | ||
|
038dc0329f | ||
|
3f40cd641f | ||
|
82c243c150 | ||
|
f44cd7b40b | ||
|
3181fe853d | ||
|
aaaac7b403 | ||
|
7c9182f0e1 | ||
|
b94e701556 | ||
|
0b55470e40 | ||
|
0b51c2ef90 | ||
|
c4c6db724f | ||
|
1bd0606e08 | ||
|
eff4d42b1f | ||
|
4e2cbd8192 | ||
|
bf8f7d968a |
@ -9,8 +9,7 @@
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier/@typescript-eslint"
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"plugins": ["@typescript-eslint"]
|
||||
}
|
||||
}
|
||||
|
6
.github/workflows/check-dist.yml
vendored
6
.github/workflows/check-dist.yml
vendored
@ -10,11 +10,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@ -46,7 +42,7 @@ jobs:
|
||||
id: diff
|
||||
|
||||
# If index.js was different than expected, upload the expected version as an artifact
|
||||
- uses: actions/upload-artifact@v4-beta
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
|
||||
with:
|
||||
name: dist
|
||||
|
20
.github/workflows/publish-immutable-actions.yml
vendored
Normal file
20
.github/workflows/publish-immutable-actions.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
name: 'Publish Immutable Action Version'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checking out
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish
|
||||
id: publish
|
||||
uses: actions/publish-immutable-action@0.0.3
|
@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update the ${{ env.TAG_NAME }} tag
|
||||
uses: actions/publish-action@v0.2.1
|
||||
uses: actions/publish-action@v0.3.0
|
||||
with:
|
||||
source-tag: ${{ env.TAG_NAME }}
|
||||
slack-webhook: ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
29
.github/workflows/test.yml
vendored
29
.github/workflows/test.yml
vendored
@ -40,6 +40,9 @@ jobs:
|
||||
- name: Format
|
||||
run: npm run format-check
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: npm test
|
||||
|
||||
- name: Create artifacts
|
||||
run: |
|
||||
mkdir -p path/to/artifact-A
|
||||
@ -48,13 +51,13 @@ jobs:
|
||||
echo "Hello world from file B" > path/to/artifact-B/file-B.txt
|
||||
|
||||
- name: Upload artifact A
|
||||
uses: actions/upload-artifact@v4-beta
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Artifact-A-${{ matrix.runs-on }}
|
||||
path: path/to/artifact-A
|
||||
|
||||
- name: Upload artifact B
|
||||
uses: actions/upload-artifact@v4-beta
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Artifact-B-${{ matrix.runs-on }}
|
||||
path: path/to/artifact-B
|
||||
@ -106,3 +109,25 @@ jobs:
|
||||
Write-Error "File contents of downloaded artifacts are incorrect"
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
# Test glob downloading both artifacts to same directory
|
||||
- name: Download all Artifacts
|
||||
uses: ./
|
||||
with:
|
||||
pattern: Artifact-*
|
||||
path: single/directory
|
||||
merge-multiple: true
|
||||
|
||||
- name: Verify successful download
|
||||
run: |
|
||||
$fileA = "single/directory/file-A.txt"
|
||||
$fileB = "single/directory/file-B.txt"
|
||||
if(!(Test-Path -path $fileA) -or !(Test-Path -path $fileB))
|
||||
{
|
||||
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"))
|
||||
{
|
||||
Write-Error "File contents of downloaded artifacts are incorrect"
|
||||
}
|
||||
shell: pwsh
|
||||
|
2
.licenses/npm/@actions/artifact.dep.yml
generated
2
.licenses/npm/@actions/artifact.dep.yml
generated
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/artifact"
|
||||
version: 2.0.0
|
||||
version: 2.3.2
|
||||
type: npm
|
||||
summary: Actions artifact lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/artifact
|
||||
|
2
.licenses/npm/@actions/core.dep.yml
generated
2
.licenses/npm/@actions/core.dep.yml
generated
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/core"
|
||||
version: 1.10.0
|
||||
version: 1.10.1
|
||||
type: npm
|
||||
summary: Actions core lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/core
|
||||
|
26
.licenses/npm/minimatch-9.0.3.dep.yml
generated
Normal file
26
.licenses/npm/minimatch-9.0.3.dep.yml
generated
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
name: minimatch
|
||||
version: 9.0.3
|
||||
type: npm
|
||||
summary: a glob matcher in javascript
|
||||
homepage:
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) 2011-2023 Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
notices: []
|
26
.licenses/npm/minimatch.dep.yml
generated
Normal file
26
.licenses/npm/minimatch.dep.yml
generated
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
name: minimatch
|
||||
version: 9.0.3
|
||||
type: npm
|
||||
summary:
|
||||
homepage:
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) 2011-2023 Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
notices: []
|
134
README.md
134
README.md
@ -13,7 +13,9 @@ See also [upload-artifact](https://github.com/actions/upload-artifact).
|
||||
- [Outputs](#outputs)
|
||||
- [Examples](#examples)
|
||||
- [Download Single Artifact](#download-single-artifact)
|
||||
- [Download Artifacts by ID](#download-artifacts-by-id)
|
||||
- [Download All Artifacts](#download-all-artifacts)
|
||||
- [Download multiple (filtered) Artifacts to the same directory](#download-multiple-filtered-artifacts-to-the-same-directory)
|
||||
- [Download Artifacts from other Workflow Runs or Repositories](#download-artifacts-from-other-workflow-runs-or-repositories)
|
||||
- [Limitations](#limitations)
|
||||
- [Permission Loss](#permission-loss)
|
||||
@ -38,6 +40,26 @@ For more information, see the [`@actions/artifact`](https://github.com/actions/t
|
||||
1. On self hosted runners, additional [firewall rules](https://github.com/actions/toolkit/tree/main/packages/artifact#breaking-changes) may be required.
|
||||
2. Downloading artifacts that were created from `action/upload-artifact@v3` and below are not supported.
|
||||
|
||||
For assistance with breaking changes, see [MIGRATION.md](docs/MIGRATION.md).
|
||||
|
||||
## Note
|
||||
|
||||
Thank you for your interest in this GitHub repo, however, right now we are not taking contributions.
|
||||
|
||||
We continue to focus our resources on strategic areas that help our customers be successful while making developers' lives easier. While GitHub Actions remains a key part of this vision, we are allocating resources towards other areas of Actions and are not taking contributions to this repository at this time. The GitHub public roadmap is the best place to follow along for any updates on features we’re working on and what stage they’re in.
|
||||
|
||||
We are taking the following steps to better direct requests related to GitHub Actions, including:
|
||||
|
||||
1. We will be directing questions and support requests to our [Community Discussions area](https://github.com/orgs/community/discussions/categories/actions)
|
||||
|
||||
2. High Priority bugs can be reported through Community Discussions or you can report these to our support team https://support.github.com/contact/bug-report.
|
||||
|
||||
3. Security Issues should be handled as per our [security.md](SECURITY.md).
|
||||
|
||||
We will still provide security updates for this project and fix major breaking changes during this time.
|
||||
|
||||
You are welcome to still raise bugs in this repo.
|
||||
|
||||
## Usage
|
||||
|
||||
### Inputs
|
||||
@ -46,13 +68,30 @@ For more information, see the [`@actions/artifact`](https://github.com/actions/t
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
# Name of the artifact to download.
|
||||
# Optional. If unspecified, all artifacts for the run are downloaded.
|
||||
# If unspecified, all artifacts for the run are downloaded.
|
||||
# Optional.
|
||||
name:
|
||||
|
||||
# IDs of the artifacts to download, comma-separated.
|
||||
# Either inputs `artifact-ids` or `name` can be used, but not both.
|
||||
# Optional.
|
||||
artifact-ids:
|
||||
|
||||
# Destination path. Supports basic tilde expansion.
|
||||
# Optional. Defaults is $GITHUB_WORKSPACE
|
||||
# Optional. Default is $GITHUB_WORKSPACE
|
||||
path:
|
||||
|
||||
# A glob pattern to the artifacts that should be downloaded.
|
||||
# Ignored if name is specified.
|
||||
# Optional.
|
||||
pattern:
|
||||
|
||||
# When multiple artifacts are matched, this changes the behavior of the destination directories.
|
||||
# If true, the downloaded artifacts will be in the same directory specified by path.
|
||||
# If false, the downloaded artifacts will be extracted into individual named directories within the specified path.
|
||||
# Optional. Default is 'false'
|
||||
merge-multiple:
|
||||
|
||||
# The GitHub token used to authenticate with the GitHub API.
|
||||
# This is required when downloading artifacts from a different repository or from a different workflow run.
|
||||
# Optional. If unspecified, the action will download artifacts from the current repo and the current workflow run.
|
||||
@ -65,7 +104,7 @@ For more information, see the [`@actions/artifact`](https://github.com/actions/t
|
||||
|
||||
# The id of the workflow run where the desired download artifact was uploaded from.
|
||||
# If github-token is specified, this is the run that artifacts will be downloaded from.
|
||||
# Optional. Default is ${{ github.repository }}
|
||||
# Optional. Default is ${{ github.run_id }}
|
||||
run-id:
|
||||
```
|
||||
|
||||
@ -102,10 +141,36 @@ steps:
|
||||
run: ls -R your/destination/dir
|
||||
```
|
||||
|
||||
### Download Artifacts by ID
|
||||
|
||||
The `artifact-ids` input allows downloading artifacts using their unique ID rather than name. This is particularly useful when working with immutable artifacts from `actions/upload-artifact@v4` which assigns a unique ID to each artifact.
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
artifact-ids: 12345
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
```
|
||||
|
||||
Multiple artifacts can be downloaded by providing a comma-separated list of IDs:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
artifact-ids: 12345,67890
|
||||
path: path/to/artifacts
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R path/to/artifacts
|
||||
```
|
||||
|
||||
This will download multiple artifacts to separate directories (similar to downloading multiple artifacts by name).
|
||||
|
||||
### Download All Artifacts
|
||||
|
||||
If the `name` input parameter is not provided, all artifacts will be downloaded. **To differentiate between downloaded artifacts, a directory denoted by the artifacts name will be created for each individual artifact.**
|
||||
If the `name` input parameter is not provided, all artifacts will be downloaded. To differentiate between downloaded artifacts, by default a directory denoted by the artifacts name will be created for each individual artifact. This behavior can be changed with the `merge-multiple` input parameter.
|
||||
|
||||
Example, if there are two artifacts `Artifact-A` and `Artifact-B`, and the directory is `etc/usr/artifacts/`, the directory structure will look like this:
|
||||
|
||||
@ -137,6 +202,67 @@ steps:
|
||||
run: ls -R path/to/artifacts
|
||||
```
|
||||
|
||||
To download them to the _same_ directory:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: path/to/artifacts
|
||||
merge-multiple: true
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R path/to/artifacts
|
||||
```
|
||||
|
||||
Which will result in:
|
||||
|
||||
```
|
||||
path/to/artifacts/
|
||||
... contents of Artifact-A
|
||||
... contents of Artifact-B
|
||||
```
|
||||
|
||||
### Download multiple (filtered) Artifacts to the same directory
|
||||
|
||||
In multiple arch/os scenarios, you may have Artifacts built in different jobs. To download all Artifacts to the same directory (or matching a glob pattern), you can use the `pattern` and `merge-multiple` inputs.
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
upload:
|
||||
strategy:
|
||||
matrix:
|
||||
runs-on: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- name: Create a File
|
||||
run: echo "hello from ${{ matrix.runs-on }}" > file-${{ matrix.runs-on }}.txt
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: my-artifact-${{ matrix.runs-on }}
|
||||
path: file-${{ matrix.runs-on }}.txt
|
||||
download:
|
||||
needs: upload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download All Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: my-artifact
|
||||
pattern: my-artifact-*
|
||||
merge-multiple: true
|
||||
- run: ls -R my-artifact
|
||||
```
|
||||
|
||||
This results in a directory like so:
|
||||
|
||||
```
|
||||
my-artifact/
|
||||
file-macos-latest.txt
|
||||
file-ubuntu-latest.txt
|
||||
file-windows-latest.txt
|
||||
```
|
||||
|
||||
### Download Artifacts from other Workflow Runs or Repositories
|
||||
|
||||
It may be useful to download Artifacts from other workflow runs, or even other repositories. By default, the permissions are scoped so they can only download Artifacts within the current workflow run. To elevate permissions for this scenario, you can specify a `github-token` along with other repository and run identifiers:
|
||||
|
374
__tests__/download.test.ts
Normal file
374
__tests__/download.test.ts
Normal file
@ -0,0 +1,374 @@
|
||||
import * as core from '@actions/core'
|
||||
import artifact, {ArtifactNotFoundError} from '@actions/artifact'
|
||||
import {run} from '../src/download-artifact'
|
||||
import {Inputs} from '../src/constants'
|
||||
|
||||
jest.mock('@actions/github', () => ({
|
||||
context: {
|
||||
repo: {
|
||||
owner: 'actions',
|
||||
repo: 'toolkit'
|
||||
},
|
||||
runId: 123,
|
||||
serverUrl: 'https://github.com'
|
||||
}
|
||||
}))
|
||||
|
||||
jest.mock('@actions/core')
|
||||
|
||||
/* eslint-disable no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
|
||||
const inputs = {
|
||||
[Inputs.Name]: 'artifact-name',
|
||||
[Inputs.Path]: '/some/artifact/path',
|
||||
[Inputs.GitHubToken]: 'warn',
|
||||
[Inputs.Repository]: 'owner/some-repository',
|
||||
[Inputs.RunID]: 'some-run-id',
|
||||
[Inputs.Pattern]: 'some-pattern',
|
||||
...overrides
|
||||
}
|
||||
|
||||
;(core.getInput as jest.Mock).mockImplementation((name: string) => {
|
||||
return inputs[name]
|
||||
})
|
||||
;(core.getBooleanInput as jest.Mock).mockImplementation((name: string) => {
|
||||
return inputs[name]
|
||||
})
|
||||
|
||||
return inputs
|
||||
}
|
||||
|
||||
describe('download', () => {
|
||||
beforeEach(async () => {
|
||||
mockInputs()
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Mock artifact client methods
|
||||
jest
|
||||
.spyOn(artifact, 'listArtifacts')
|
||||
.mockImplementation(() => Promise.resolve({artifacts: []}))
|
||||
jest.spyOn(artifact, 'getArtifact').mockImplementation(name => {
|
||||
throw new ArtifactNotFoundError(`Artifact '${name}' not found`)
|
||||
})
|
||||
jest
|
||||
.spyOn(artifact, 'downloadArtifact')
|
||||
.mockImplementation(() => Promise.resolve({digestMismatch: false}))
|
||||
})
|
||||
|
||||
test('downloads a single artifact by name', async () => {
|
||||
const mockArtifact = {
|
||||
id: 123,
|
||||
name: 'artifact-name',
|
||||
size: 1024,
|
||||
digest: 'abc123'
|
||||
}
|
||||
|
||||
jest
|
||||
.spyOn(artifact, 'getArtifact')
|
||||
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||
|
||||
await run()
|
||||
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
|
||||
mockArtifact.id,
|
||||
expect.objectContaining({
|
||||
expectedHash: mockArtifact.digest
|
||||
})
|
||||
)
|
||||
expect(core.info).toHaveBeenCalledWith('Total of 1 artifact(s) downloaded')
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith(
|
||||
'download-path',
|
||||
expect.any(String)
|
||||
)
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
'Download artifact has finished successfully'
|
||||
)
|
||||
})
|
||||
|
||||
test('downloads multiple artifacts when no name or pattern provided', async () => {
|
||||
jest.clearAllMocks()
|
||||
mockInputs({
|
||||
[Inputs.Name]: '',
|
||||
[Inputs.Pattern]: ''
|
||||
})
|
||||
|
||||
const mockArtifacts = [
|
||||
{id: 123, name: 'artifact1', size: 1024, digest: 'abc123'},
|
||||
{id: 456, name: 'artifact2', size: 2048, digest: 'def456'}
|
||||
]
|
||||
|
||||
// Set up artifact mock after clearing mocks
|
||||
jest
|
||||
.spyOn(artifact, 'listArtifacts')
|
||||
.mockImplementation(() => Promise.resolve({artifacts: mockArtifacts}))
|
||||
|
||||
// Reset downloadArtifact mock as well
|
||||
jest
|
||||
.spyOn(artifact, 'downloadArtifact')
|
||||
.mockImplementation(() => Promise.resolve({digestMismatch: false}))
|
||||
|
||||
await run()
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
'No input name, artifact-ids or pattern filtered specified, downloading all artifacts'
|
||||
)
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith('Total of 2 artifact(s) downloaded')
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('sets download path output even when no artifacts are found', async () => {
|
||||
mockInputs({[Inputs.Name]: ''})
|
||||
|
||||
await run()
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith(
|
||||
'download-path',
|
||||
expect.any(String)
|
||||
)
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
'Download artifact has finished successfully'
|
||||
)
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith('Total of 0 artifact(s) downloaded')
|
||||
})
|
||||
|
||||
test('filters artifacts by pattern', async () => {
|
||||
const mockArtifacts = [
|
||||
{id: 123, name: 'test-artifact', size: 1024, digest: 'abc123'},
|
||||
{id: 456, name: 'prod-artifact', size: 2048, digest: 'def456'}
|
||||
]
|
||||
|
||||
jest
|
||||
.spyOn(artifact, 'listArtifacts')
|
||||
.mockImplementation(() => Promise.resolve({artifacts: mockArtifacts}))
|
||||
|
||||
mockInputs({
|
||||
[Inputs.Name]: '',
|
||||
[Inputs.Pattern]: 'test-*'
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(1)
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
|
||||
123,
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
|
||||
test('uses token and repository information when provided', async () => {
|
||||
const token = 'ghp_testtoken123'
|
||||
|
||||
mockInputs({
|
||||
[Inputs.Name]: '',
|
||||
[Inputs.GitHubToken]: token,
|
||||
[Inputs.Repository]: 'myorg/myrepo',
|
||||
[Inputs.RunID]: '789'
|
||||
})
|
||||
|
||||
jest
|
||||
.spyOn(artifact, 'listArtifacts')
|
||||
.mockImplementation(() => Promise.resolve({artifacts: []}))
|
||||
|
||||
await run()
|
||||
|
||||
expect(artifact.listArtifacts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
findBy: {
|
||||
token,
|
||||
workflowRunId: 789,
|
||||
repositoryName: 'myrepo',
|
||||
repositoryOwner: 'myorg'
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('throws error when repository format is invalid', async () => {
|
||||
mockInputs({
|
||||
[Inputs.GitHubToken]: 'some-token',
|
||||
[Inputs.Repository]: 'invalid-format' // Missing the owner/repo format
|
||||
})
|
||||
|
||||
await expect(run()).rejects.toThrow(
|
||||
"Invalid repository: 'invalid-format'. Must be in format owner/repo"
|
||||
)
|
||||
})
|
||||
|
||||
test('warns when digest validation fails', async () => {
|
||||
const mockArtifact = {
|
||||
id: 123,
|
||||
name: 'corrupted-artifact',
|
||||
size: 1024,
|
||||
digest: 'abc123'
|
||||
}
|
||||
|
||||
jest
|
||||
.spyOn(artifact, 'getArtifact')
|
||||
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||
|
||||
jest
|
||||
.spyOn(artifact, 'downloadArtifact')
|
||||
.mockImplementation(() => Promise.resolve({digestMismatch: true}))
|
||||
|
||||
await run()
|
||||
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
expect.stringContaining('digest validation failed')
|
||||
)
|
||||
})
|
||||
|
||||
test('downloads a single artifact by ID', async () => {
|
||||
const mockArtifact = {
|
||||
id: 456,
|
||||
name: 'artifact-by-id',
|
||||
size: 1024,
|
||||
digest: 'def456'
|
||||
}
|
||||
|
||||
mockInputs({
|
||||
[Inputs.Name]: '',
|
||||
[Inputs.Pattern]: '',
|
||||
[Inputs.ArtifactIds]: '456'
|
||||
})
|
||||
|
||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
artifacts: [mockArtifact]
|
||||
})
|
||||
)
|
||||
|
||||
await run()
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith('Downloading artifacts by ID')
|
||||
expect(core.debug).toHaveBeenCalledWith('Parsed artifact IDs: ["456"]')
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(1)
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
|
||||
456,
|
||||
expect.objectContaining({
|
||||
expectedHash: mockArtifact.digest
|
||||
})
|
||||
)
|
||||
expect(core.info).toHaveBeenCalledWith('Total of 1 artifact(s) downloaded')
|
||||
})
|
||||
|
||||
test('downloads multiple artifacts by ID', async () => {
|
||||
const mockArtifacts = [
|
||||
{id: 123, name: 'first-artifact', size: 1024, digest: 'abc123'},
|
||||
{id: 456, name: 'second-artifact', size: 2048, digest: 'def456'},
|
||||
{id: 789, name: 'third-artifact', size: 3072, digest: 'ghi789'}
|
||||
]
|
||||
|
||||
mockInputs({
|
||||
[Inputs.Name]: '',
|
||||
[Inputs.Pattern]: '',
|
||||
[Inputs.ArtifactIds]: '123, 456, 789'
|
||||
})
|
||||
|
||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
artifacts: mockArtifacts
|
||||
})
|
||||
)
|
||||
|
||||
await run()
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith('Downloading artifacts by ID')
|
||||
expect(core.debug).toHaveBeenCalledWith(
|
||||
'Parsed artifact IDs: ["123","456","789"]'
|
||||
)
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(3)
|
||||
mockArtifacts.forEach(mockArtifact => {
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
|
||||
mockArtifact.id,
|
||||
expect.objectContaining({
|
||||
expectedHash: mockArtifact.digest
|
||||
})
|
||||
)
|
||||
})
|
||||
expect(core.info).toHaveBeenCalledWith('Total of 3 artifact(s) downloaded')
|
||||
})
|
||||
|
||||
test('warns when some artifact IDs are not found', async () => {
|
||||
const mockArtifacts = [
|
||||
{id: 123, name: 'found-artifact', size: 1024, digest: 'abc123'}
|
||||
]
|
||||
|
||||
mockInputs({
|
||||
[Inputs.Name]: '',
|
||||
[Inputs.Pattern]: '',
|
||||
[Inputs.ArtifactIds]: '123, 456, 789'
|
||||
})
|
||||
|
||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
artifacts: mockArtifacts
|
||||
})
|
||||
)
|
||||
|
||||
await run()
|
||||
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
'Could not find the following artifact IDs: 456, 789'
|
||||
)
|
||||
expect(core.debug).toHaveBeenCalledWith('Found 1 artifacts by ID')
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('throws error when no artifacts with requested IDs are found', async () => {
|
||||
mockInputs({
|
||||
[Inputs.Name]: '',
|
||||
[Inputs.Pattern]: '',
|
||||
[Inputs.ArtifactIds]: '123, 456'
|
||||
})
|
||||
|
||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
artifacts: []
|
||||
})
|
||||
)
|
||||
|
||||
await expect(run()).rejects.toThrow(
|
||||
'None of the provided artifact IDs were found'
|
||||
)
|
||||
})
|
||||
|
||||
test('throws error when artifact-ids input is empty', async () => {
|
||||
mockInputs({
|
||||
[Inputs.Name]: '',
|
||||
[Inputs.Pattern]: '',
|
||||
[Inputs.ArtifactIds]: ' '
|
||||
})
|
||||
|
||||
await expect(run()).rejects.toThrow(
|
||||
"No valid artifact IDs provided in 'artifact-ids' input"
|
||||
)
|
||||
})
|
||||
|
||||
test('throws error when some artifact IDs are not valid numbers', async () => {
|
||||
mockInputs({
|
||||
[Inputs.Name]: '',
|
||||
[Inputs.Pattern]: '',
|
||||
[Inputs.ArtifactIds]: '123, abc, 456'
|
||||
})
|
||||
|
||||
await expect(run()).rejects.toThrow(
|
||||
"Invalid artifact ID: 'abc'. Must be a number."
|
||||
)
|
||||
})
|
||||
|
||||
test('throws error when both name and artifact-ids are provided', async () => {
|
||||
mockInputs({
|
||||
[Inputs.Name]: 'some-artifact',
|
||||
[Inputs.ArtifactIds]: '123'
|
||||
})
|
||||
|
||||
await expect(run()).rejects.toThrow(
|
||||
"Inputs 'name' and 'artifact-ids' cannot be used together. Please specify only one."
|
||||
)
|
||||
})
|
||||
})
|
14
action.yml
14
action.yml
@ -3,11 +3,23 @@ description: 'Download a build artifact that was previously uploaded in the work
|
||||
author: 'GitHub'
|
||||
inputs:
|
||||
name:
|
||||
description: 'Name of the artifact to download. If unspecified, all artifacts for the run are downloaded'
|
||||
description: 'Name of the artifact to download. If unspecified, all artifacts for the run are downloaded.'
|
||||
required: false
|
||||
artifact-ids:
|
||||
description: 'IDs of the artifacts to download, comma-separated. Either inputs `artifact-ids` or `name` can be used, but not both.'
|
||||
required: false
|
||||
path:
|
||||
description: 'Destination path. Supports basic tilde expansion. Defaults to $GITHUB_WORKSPACE'
|
||||
required: false
|
||||
pattern:
|
||||
description: 'A glob pattern matching the artifacts that should be downloaded. Ignored if name is specified.'
|
||||
required: false
|
||||
merge-multiple:
|
||||
description: 'When multiple artifacts are matched, this changes the behavior of the destination directories.
|
||||
If true, the downloaded artifacts will be in the same directory specified by path.
|
||||
If false, the downloaded artifacts will be extracted into individual named directories within the specified path.'
|
||||
required: false
|
||||
default: 'false'
|
||||
github-token:
|
||||
description: 'The GitHub token used to authenticate with the GitHub API.
|
||||
This is required when downloading artifacts from a different repository or from a different workflow run.
|
||||
|
39550
dist/index.js
vendored
39550
dist/index.js
vendored
File diff suppressed because one or more lines are too long
253
docs/MIGRATION.md
Normal file
253
docs/MIGRATION.md
Normal file
@ -0,0 +1,253 @@
|
||||
# Migration
|
||||
|
||||
- [Migration](#migration)
|
||||
- [Multiple uploads to the same named Artifact](#multiple-uploads-to-the-same-named-artifact)
|
||||
- [Overwriting an Artifact](#overwriting-an-artifact)
|
||||
- [Merging multiple artifacts](#merging-multiple-artifacts)
|
||||
- [Working with Immutable Artifacts](#working-with-immutable-artifacts)
|
||||
|
||||
Several behavioral differences exist between Artifact actions `v3` and below vs `v4`. This document outlines common scenarios in `v3`, and how they would be handled in `v4`.
|
||||
|
||||
## Multiple uploads to the same named Artifact
|
||||
|
||||
In `v3`, Artifacts are _mutable_ so it's possible to write workflow scenarios where multiple jobs upload to the same Artifact like so:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
upload:
|
||||
strategy:
|
||||
matrix:
|
||||
runs-on: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- name: Create a File
|
||||
run: echo "hello from ${{ matrix.runs-on }}" > file-${{ matrix.runs-on }}.txt
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: my-artifact # NOTE: same artifact name
|
||||
path: file-${{ matrix.runs-on }}.txt
|
||||
download:
|
||||
needs: upload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download All Artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: my-artifact
|
||||
path: my-artifact
|
||||
- run: ls -R my-artifact
|
||||
```
|
||||
|
||||
This results in a directory like so:
|
||||
|
||||
```
|
||||
my-artifact/
|
||||
file-macos-latest.txt
|
||||
file-ubuntu-latest.txt
|
||||
file-windows-latest.txt
|
||||
```
|
||||
|
||||
In v4, Artifacts are immutable (unless deleted). So you must change each of the uploaded Artifacts to have a different name and filter the downloads by name to achieve the same effect:
|
||||
|
||||
```diff
|
||||
jobs:
|
||||
upload:
|
||||
strategy:
|
||||
matrix:
|
||||
runs-on: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- name: Create a File
|
||||
run: echo "hello from ${{ matrix.runs-on }}" > file-${{ matrix.runs-on }}.txt
|
||||
- name: Upload Artifact
|
||||
- uses: actions/upload-artifact@v3
|
||||
+ uses: actions/upload-artifact@v4
|
||||
with:
|
||||
- name: my-artifact
|
||||
+ name: my-artifact-${{ matrix.runs-on }}
|
||||
path: file-${{ matrix.runs-on }}.txt
|
||||
download:
|
||||
needs: upload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download All Artifacts
|
||||
- uses: actions/download-artifact@v3
|
||||
+ uses: actions/download-artifact@v4
|
||||
with:
|
||||
- name: my-artifact
|
||||
path: my-artifact
|
||||
+ pattern: my-artifact-*
|
||||
+ merge-multiple: true
|
||||
- run: ls -R my-artifact
|
||||
```
|
||||
|
||||
In `v4`, the new `pattern:` input will filter the downloaded Artifacts to match the name specified. The new `merge-multiple:` input will support downloading multiple Artifacts to the same directory. If the files within the Artifacts have the same name, the last writer wins.
|
||||
|
||||
## Overwriting an Artifact
|
||||
|
||||
In `v3`, the contents of an Artifact were mutable so something like the following was possible:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create a file
|
||||
run: echo "hello world" > my-file.txt
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: my-artifact # NOTE: same artifact name
|
||||
path: my-file.txt
|
||||
upload-again:
|
||||
needs: upload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create a different file
|
||||
run: echo "goodbye world" > my-file.txt
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: my-artifact # NOTE: same artifact name
|
||||
path: my-file.txt
|
||||
```
|
||||
|
||||
The resulting `my-file.txt` in `my-artifact` will have "goodbye world" as the content.
|
||||
|
||||
In `v4`, Artifacts are immutable unless deleted. To achieve this same behavior, you can use `overwrite: true` to delete the Artifact before a new one is created:
|
||||
|
||||
```diff
|
||||
jobs:
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create a file
|
||||
run: echo "hello world" > my-file.txt
|
||||
- name: Upload Artifact
|
||||
- uses: actions/upload-artifact@v3
|
||||
+ uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: my-artifact # NOTE: same artifact name
|
||||
path: my-file.txt
|
||||
upload-again:
|
||||
needs: upload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create a different file
|
||||
run: echo "goodbye world" > my-file.txt
|
||||
- name: Upload Artifact
|
||||
- uses: actions/upload-artifact@v3
|
||||
+ uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: my-artifact # NOTE: same artifact name
|
||||
path: my-file.txt
|
||||
+ overwrite: true
|
||||
```
|
||||
|
||||
Note that this will create an _entirely_ new Artifact, with a different ID from the previous.
|
||||
|
||||
## Merging multiple artifacts
|
||||
|
||||
In `v3`, multiple uploads from multiple jobs could be done to the same Artifact. This would result in a single archive, which could be useful for sending to upstream systems outside of Actions via API or UI downloads.
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
upload:
|
||||
strategy:
|
||||
matrix:
|
||||
runs-on: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- name: Create a File
|
||||
run: echo "hello from ${{ matrix.runs-on }}" > file-${{ matrix.runs-on }}.txt
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: all-my-files # NOTE: same artifact name
|
||||
path: file-${{ matrix.runs-on }}.txt
|
||||
```
|
||||
|
||||
The single `all-my-files` artifact would contain the following:
|
||||
|
||||
```
|
||||
.
|
||||
∟ file-ubuntu-latest.txt
|
||||
∟ file-macos-latest.txt
|
||||
∟ file-windows-latest.txt
|
||||
```
|
||||
|
||||
To achieve the same in `v4` you can change it like so:
|
||||
|
||||
```diff
|
||||
jobs:
|
||||
upload:
|
||||
strategy:
|
||||
matrix:
|
||||
runs-on: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- name: Create a File
|
||||
run: echo "hello from ${{ matrix.runs-on }}" > file-${{ matrix.runs-on }}.txt
|
||||
- name: Upload Artifact
|
||||
- uses: actions/upload-artifact@v3
|
||||
+ uses: actions/upload-artifact@v4
|
||||
with:
|
||||
- name: all-my-files
|
||||
+ name: my-artifact-${{ matrix.runs-on }}
|
||||
path: file-${{ matrix.runs-on }}.txt
|
||||
+ merge:
|
||||
+ runs-on: ubuntu-latest
|
||||
+ needs: upload
|
||||
+ steps:
|
||||
+ - name: Merge Artifacts
|
||||
+ uses: actions/upload-artifact/merge@v4
|
||||
+ with:
|
||||
+ name: all-my-files
|
||||
+ pattern: my-artifact-*
|
||||
```
|
||||
|
||||
Note that this will download all artifacts to a temporary directory and reupload them as a single artifact. For more information on inputs and other use cases for `actions/upload-artifact/merge@v4`, see [the action documentation](https://github.com/actions/upload-artifact/blob/main/merge/README.md).
|
||||
|
||||
## Working with Immutable Artifacts
|
||||
|
||||
In `v4`, artifacts are immutable by default and each artifact gets a unique ID when uploaded. When an artifact with the same name is uploaded again (with or without `overwrite: true`), it gets a new artifact ID.
|
||||
|
||||
To take advantage of this immutability for security purposes (to avoid potential TOCTOU issues where an artifact might be replaced between upload and download), the new `artifact-ids` input allows you to download artifacts by their specific ID rather than by name:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Make the artifact ID available to the download job
|
||||
outputs:
|
||||
artifact-id: ${{ steps.upload-step.outputs.artifact-id }}
|
||||
|
||||
steps:
|
||||
- name: Create a file
|
||||
run: echo "hello world" > my-file.txt
|
||||
- name: Upload Artifact
|
||||
id: upload-step
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: my-artifact
|
||||
path: my-file.txt
|
||||
# The upload step outputs the artifact ID
|
||||
- name: Print Artifact ID
|
||||
run: echo "Artifact ID is ${{ steps.upload-step.outputs.artifact-id }}"
|
||||
|
||||
download:
|
||||
needs: upload
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download Artifact by ID
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
# Use the artifact ID directly, not the name, to ensure you get exactly the artifact you expect
|
||||
artifact-ids: ${{ needs.upload.outputs.artifact-id }}
|
||||
```
|
||||
|
||||
This approach provides stronger guarantees about which artifact version you're downloading compared to using just the artifact name.
|
12
jest.config.ts
Normal file
12
jest.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
roots: ['<rootDir>'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
testRunner: 'jest-circus/runner',
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
15605
package-lock.json
generated
15605
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "download-artifact",
|
||||
"version": "4.0.0",
|
||||
"version": "4.3.0",
|
||||
"description": "Download an Actions Artifact from a workflow run",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
@ -9,7 +9,8 @@
|
||||
"check-all": "concurrently \"npm:format-check\" \"npm:lint\" \"npm:build\"",
|
||||
"format": "prettier --write **/*.ts",
|
||||
"format-check": "prettier --check **/*.ts",
|
||||
"lint": "eslint **/*.ts"
|
||||
"lint": "eslint **/*.ts",
|
||||
"test": "jest"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -28,18 +29,24 @@
|
||||
},
|
||||
"homepage": "https://github.com/actions/download-artifact#readme",
|
||||
"dependencies": {
|
||||
"@actions/artifact": "^2.0.0",
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/github": "^5.1.1"
|
||||
"@actions/artifact": "^2.3.2",
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^5.1.1",
|
||||
"minimatch": "^9.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^12.12.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@vercel/ncc": "^0.33.4",
|
||||
"concurrently": "^5.2.0",
|
||||
"eslint": "^7.4.0",
|
||||
"eslint-plugin-github": "^4.1.1",
|
||||
"prettier": "^2.0.5",
|
||||
"typescript": "^3.8.3"
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-github": "^4.10.1",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.1.1",
|
||||
"ts-jest": "^29.2.6",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,10 @@ export enum Inputs {
|
||||
Path = 'path',
|
||||
GitHubToken = 'github-token',
|
||||
Repository = 'repository',
|
||||
RunID = 'run-id'
|
||||
RunID = 'run-id',
|
||||
Pattern = 'pattern',
|
||||
MergeMultiple = 'merge-multiple',
|
||||
ArtifactIds = 'artifact-ids'
|
||||
}
|
||||
|
||||
export enum Outputs {
|
||||
|
@ -3,6 +3,7 @@ import * as path from 'path'
|
||||
import * as core from '@actions/core'
|
||||
import artifactClient from '@actions/artifact'
|
||||
import type {Artifact, FindOptions} from '@actions/artifact'
|
||||
import {Minimatch} from 'minimatch'
|
||||
import {Inputs, Outputs} from './constants'
|
||||
|
||||
const PARALLEL_DOWNLOADS = 5
|
||||
@ -14,13 +15,18 @@ export const chunk = <T>(arr: T[], n: number): T[][] =>
|
||||
return acc
|
||||
}, [] as T[][])
|
||||
|
||||
async function run(): Promise<void> {
|
||||
export async function run(): Promise<void> {
|
||||
const inputs = {
|
||||
name: core.getInput(Inputs.Name, {required: false}),
|
||||
path: core.getInput(Inputs.Path, {required: false}),
|
||||
token: core.getInput(Inputs.GitHubToken, {required: false}),
|
||||
repository: core.getInput(Inputs.Repository, {required: false}),
|
||||
runID: parseInt(core.getInput(Inputs.RunID, {required: false}))
|
||||
runID: parseInt(core.getInput(Inputs.RunID, {required: false})),
|
||||
pattern: core.getInput(Inputs.Pattern, {required: false}),
|
||||
mergeMultiple: core.getBooleanInput(Inputs.MergeMultiple, {
|
||||
required: false
|
||||
}),
|
||||
artifactIds: core.getInput(Inputs.ArtifactIds, {required: false})
|
||||
}
|
||||
|
||||
if (!inputs.path) {
|
||||
@ -31,7 +37,15 @@ async function run(): Promise<void> {
|
||||
inputs.path = inputs.path.replace('~', os.homedir())
|
||||
}
|
||||
|
||||
// Check for mutually exclusive inputs
|
||||
if (inputs.name && inputs.artifactIds) {
|
||||
throw new Error(
|
||||
`Inputs 'name' and 'artifact-ids' cannot be used together. Please specify only one.`
|
||||
)
|
||||
}
|
||||
|
||||
const isSingleArtifactDownload = !!inputs.name
|
||||
const isDownloadByIds = !!inputs.artifactIds
|
||||
const resolvedPath = path.resolve(inputs.path)
|
||||
core.debug(`Resolved path is ${resolvedPath}`)
|
||||
|
||||
@ -53,6 +67,7 @@ async function run(): Promise<void> {
|
||||
}
|
||||
|
||||
let artifacts: Artifact[] = []
|
||||
let artifactIds: number[] = []
|
||||
|
||||
if (isSingleArtifactDownload) {
|
||||
core.info(`Downloading single artifact`)
|
||||
@ -71,40 +86,117 @@ async function run(): Promise<void> {
|
||||
)
|
||||
|
||||
artifacts = [targetArtifact]
|
||||
} else {
|
||||
core.info(
|
||||
`No input name specified, downloading all artifacts. Extra directory with the artifact name will be created for each download`
|
||||
)
|
||||
} else if (isDownloadByIds) {
|
||||
core.info(`Downloading artifacts by ID`)
|
||||
|
||||
const artifactIdList = inputs.artifactIds
|
||||
.split(',')
|
||||
.map(id => id.trim())
|
||||
.filter(id => id !== '')
|
||||
|
||||
if (artifactIdList.length === 0) {
|
||||
throw new Error(`No valid artifact IDs provided in 'artifact-ids' input`)
|
||||
}
|
||||
|
||||
core.debug(`Parsed artifact IDs: ${JSON.stringify(artifactIdList)}`)
|
||||
|
||||
// Parse the artifact IDs
|
||||
artifactIds = artifactIdList.map(id => {
|
||||
const numericId = parseInt(id, 10)
|
||||
if (isNaN(numericId)) {
|
||||
throw new Error(`Invalid artifact ID: '${id}'. Must be a number.`)
|
||||
}
|
||||
return numericId
|
||||
})
|
||||
|
||||
// We need to fetch all artifacts to get metadata for the specified IDs
|
||||
const listArtifactResponse = await artifactClient.listArtifacts({
|
||||
latest: true,
|
||||
...options
|
||||
})
|
||||
|
||||
if (listArtifactResponse.artifacts.length === 0) {
|
||||
throw new Error(
|
||||
`No artifacts found for run '${inputs.runID}' in '${inputs.repository}'`
|
||||
artifacts = listArtifactResponse.artifacts.filter(artifact =>
|
||||
artifactIds.includes(artifact.id)
|
||||
)
|
||||
|
||||
if (artifacts.length === 0) {
|
||||
throw new Error(`None of the provided artifact IDs were found`)
|
||||
}
|
||||
|
||||
if (artifacts.length < artifactIds.length) {
|
||||
const foundIds = artifacts.map(a => a.id)
|
||||
const missingIds = artifactIds.filter(id => !foundIds.includes(id))
|
||||
core.warning(
|
||||
`Could not find the following artifact IDs: ${missingIds.join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
core.debug(`Found ${listArtifactResponse.artifacts.length} artifacts`)
|
||||
core.debug(`Found ${artifacts.length} artifacts by ID`)
|
||||
} else {
|
||||
const listArtifactResponse = await artifactClient.listArtifacts({
|
||||
latest: true,
|
||||
...options
|
||||
})
|
||||
artifacts = listArtifactResponse.artifacts
|
||||
|
||||
core.debug(`Found ${artifacts.length} artifacts in run`)
|
||||
|
||||
if (inputs.pattern) {
|
||||
core.info(`Filtering artifacts by pattern '${inputs.pattern}'`)
|
||||
const matcher = new Minimatch(inputs.pattern)
|
||||
artifacts = artifacts.filter(artifact => matcher.match(artifact.name))
|
||||
core.debug(
|
||||
`Filtered from ${listArtifactResponse.artifacts.length} to ${artifacts.length} artifacts`
|
||||
)
|
||||
} else {
|
||||
core.info(
|
||||
'No input name, artifact-ids or pattern filtered specified, downloading all artifacts'
|
||||
)
|
||||
if (!inputs.mergeMultiple) {
|
||||
core.info(
|
||||
'An extra directory with the artifact name will be created for each download'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const downloadPromises = artifacts.map(artifact =>
|
||||
artifactClient.downloadArtifact(artifact.id, {
|
||||
...options,
|
||||
path: isSingleArtifactDownload
|
||||
? resolvedPath
|
||||
: path.join(resolvedPath, artifact.name)
|
||||
if (artifacts.length) {
|
||||
core.info(`Preparing to download the following artifacts:`)
|
||||
artifacts.forEach(artifact => {
|
||||
core.info(
|
||||
`- ${artifact.name} (ID: ${artifact.id}, Size: ${artifact.size}, Expected Digest: ${artifact.digest})`
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const downloadPromises = artifacts.map(artifact => ({
|
||||
name: artifact.name,
|
||||
promise: artifactClient.downloadArtifact(artifact.id, {
|
||||
...options,
|
||||
path:
|
||||
isSingleArtifactDownload || inputs.mergeMultiple
|
||||
? resolvedPath
|
||||
: path.join(resolvedPath, artifact.name),
|
||||
expectedHash: artifact.digest
|
||||
})
|
||||
}))
|
||||
|
||||
const chunkedPromises = chunk(downloadPromises, PARALLEL_DOWNLOADS)
|
||||
for (const chunk of chunkedPromises) {
|
||||
await Promise.all(chunk)
|
||||
}
|
||||
const chunkPromises = chunk.map(item => item.promise)
|
||||
const results = await Promise.all(chunkPromises)
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const outcome = results[i]
|
||||
const artifactName = chunk[i].name
|
||||
|
||||
if (outcome.digestMismatch) {
|
||||
core.warning(
|
||||
`Artifact '${artifactName}' digest validation failed. Please verify the integrity of the artifact.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
core.info(`Total of ${artifacts.length} artifact(s) downloaded`)
|
||||
core.setOutput(Outputs.DownloadPath, resolvedPath)
|
||||
core.info('Download artifact has finished successfully')
|
||||
|
@ -9,5 +9,5 @@
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
"exclude": ["node_modules", "**/*.test.ts", "jest.config.ts", "__tests__"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user