Compare commits

..

No commits in common. "main" and "v4.2.1" have entirely different histories.
main ... v4.2.1

10 changed files with 14 additions and 357 deletions

View File

@ -10,7 +10,11 @@ on:
push:
branches:
- main
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
workflow_dispatch:
jobs:

View File

@ -13,7 +13,6 @@ 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)
@ -42,24 +41,6 @@ For more information, see the [`@actions/artifact`](https://github.com/actions/t
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 were working on and what stage theyre 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
@ -72,11 +53,6 @@ You are welcome to still raise bugs in this repo.
# 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. Default is $GITHUB_WORKSPACE
path:
@ -141,32 +117,6 @@ 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

View File

@ -112,7 +112,7 @@ describe('download', () => {
await run()
expect(core.info).toHaveBeenCalledWith(
'No input name, artifact-ids or pattern filtered specified, downloading all artifacts'
'No input name or pattern filtered specified, downloading all artifacts'
)
expect(core.info).toHaveBeenCalledWith('Total of 2 artifact(s) downloaded')
@ -221,154 +221,4 @@ describe('download', () => {
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."
)
})
})

View File

@ -5,9 +5,6 @@ inputs:
name:
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

47
dist/index.js vendored
View File

@ -118710,7 +118710,6 @@ var Inputs;
Inputs["RunID"] = "run-id";
Inputs["Pattern"] = "pattern";
Inputs["MergeMultiple"] = "merge-multiple";
Inputs["ArtifactIds"] = "artifact-ids";
})(Inputs || (exports.Inputs = Inputs = {}));
var Outputs;
(function (Outputs) {
@ -118784,10 +118783,7 @@ function run() {
repository: core.getInput(constants_1.Inputs.Repository, { required: false }),
runID: parseInt(core.getInput(constants_1.Inputs.RunID, { required: false })),
pattern: core.getInput(constants_1.Inputs.Pattern, { required: false }),
mergeMultiple: core.getBooleanInput(constants_1.Inputs.MergeMultiple, {
required: false
}),
artifactIds: core.getInput(constants_1.Inputs.ArtifactIds, { required: false })
mergeMultiple: core.getBooleanInput(constants_1.Inputs.MergeMultiple, { required: false })
};
if (!inputs.path) {
inputs.path = process.env['GITHUB_WORKSPACE'] || process.cwd();
@ -118795,12 +118791,7 @@ function run() {
if (inputs.path.startsWith(`~`)) {
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}`);
const options = {};
@ -118817,7 +118808,6 @@ function run() {
};
}
let artifacts = [];
let artifactIds = [];
if (isSingleArtifactDownload) {
core.info(`Downloading single artifact`);
const { artifact: targetArtifact } = yield artifact_1.default.getArtifact(inputs.name, options);
@ -118827,37 +118817,6 @@ function run() {
core.debug(`Found named artifact '${inputs.name}' (ID: ${targetArtifact.id}, Size: ${targetArtifact.size})`);
artifacts = [targetArtifact];
}
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 = yield artifact_1.default.listArtifacts(Object.assign({ latest: true }, options));
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 ${artifacts.length} artifacts by ID`);
}
else {
const listArtifactResponse = yield artifact_1.default.listArtifacts(Object.assign({ latest: true }, options));
artifacts = listArtifactResponse.artifacts;
@ -118869,7 +118828,7 @@ function run() {
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');
core.info('No input name 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');
}
@ -128958,4 +128917,4 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"]
/******/ module.exports = __webpack_exports__;
/******/
/******/ })()
;
;

View File

@ -4,7 +4,6 @@
- [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`.
@ -208,46 +207,3 @@ jobs:
```
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.

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "download-artifact",
"version": "4.3.0",
"version": "4.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "download-artifact",
"version": "4.3.0",
"version": "4.2.0",
"license": "MIT",
"dependencies": {
"@actions/artifact": "^2.3.2",

View File

@ -1,6 +1,6 @@
{
"name": "download-artifact",
"version": "4.3.0",
"version": "4.2.0",
"description": "Download an Actions Artifact from a workflow run",
"main": "dist/index.js",
"scripts": {

View File

@ -5,8 +5,7 @@ export enum Inputs {
Repository = 'repository',
RunID = 'run-id',
Pattern = 'pattern',
MergeMultiple = 'merge-multiple',
ArtifactIds = 'artifact-ids'
MergeMultiple = 'merge-multiple'
}
export enum Outputs {

View File

@ -23,10 +23,7 @@ export async function run(): Promise<void> {
repository: core.getInput(Inputs.Repository, {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})
mergeMultiple: core.getBooleanInput(Inputs.MergeMultiple, {required: false})
}
if (!inputs.path) {
@ -37,15 +34,7 @@ export 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}`)
@ -67,7 +56,6 @@ export async function run(): Promise<void> {
}
let artifacts: Artifact[] = []
let artifactIds: number[] = []
if (isSingleArtifactDownload) {
core.info(`Downloading single artifact`)
@ -86,52 +74,6 @@ export async function run(): Promise<void> {
)
artifacts = [targetArtifact]
} 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
})
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 ${artifacts.length} artifacts by ID`)
} else {
const listArtifactResponse = await artifactClient.listArtifacts({
latest: true,
@ -150,7 +92,7 @@ export async function run(): Promise<void> {
)
} else {
core.info(
'No input name, artifact-ids or pattern filtered specified, downloading all artifacts'
'No input name or pattern filtered specified, downloading all artifacts'
)
if (!inputs.mergeMultiple) {
core.info(