Compare commits

..

54 Commits
v4.1.8 ... main

Author SHA1 Message Date
Rob Herley
d3f86a106a
Merge pull request from actions/robherley/v4.3.0
Prep for v4.3.0 release
2025-04-24 12:25:03 -04:00
Rob Herley
fc02353415
prep for v4.3.0 release 2025-04-24 11:21:41 -04:00
Josh Gross
77454371a4
Merge pull request from actions/joshmgross/download-by-id-example
Fix workflow example for downloading by artifact ID
2025-04-24 11:04:38 -04:00
Josh Gross
84fc7a0a35
Remove path filters from Check dist workflow 2025-04-23 10:32:04 -04:00
Josh Gross
67f2bc382f
Fix workflow example for downloading by artifact ID 2025-04-23 10:27:20 -04:00
Grant Birkinbine
8ea3c2c174
Merge pull request from actions/download-by-id
feat: implement new `artifact-ids` input
2025-04-22 08:16:56 -07:00
GrantBirki
d219c630f6
add supporting unit tests for artifact downloads with ids 2025-04-17 13:14:21 -07:00
GrantBirki
54124fbd88
revert getArtifact() changes - for now we have to list and filter by artifact-ids until a getArtifactById() public method exists 2025-04-17 12:30:12 -07:00
GrantBirki
b83057b90d
bundle 2025-04-17 12:20:46 -07:00
GrantBirki
171183c7dc
use the same artifactClient.getArtifact structure as seen above in isSingleArtifactDownload logic 2025-04-17 12:18:37 -07:00
GrantBirki
e463631f66
bundle 2025-04-17 11:49:32 -07:00
GrantBirki
ec378bcca1
when only one artifact-id is given, use getArtifact and check the resulting id returned 2025-04-17 11:48:44 -07:00
GrantBirki
42aef06f22
apply https://github.com/actions/download-artifact/pull/401#discussion_r2048225048 suggestion 2025-04-17 09:55:13 -07:00
Grant Birkinbine
ac35f995fe implement new artifact-ids input 2025-04-17 04:47:03 +00:00
Josh Gross
95815c38cf
Merge pull request from GhadimiR/main
Fix bug introduced in 4.2.0
2025-03-19 11:14:44 -04:00
Ryan Ghadimi
278fca438a Move log statements 2025-03-19 15:06:13 +00:00
Ryan Ghadimi
68909842a1
Merge branch 'main' into main 2025-03-19 15:04:09 +00:00
Josh Gross
f9415c0ec3 Run unit tests in CI 2025-03-19 15:01:26 +00:00
Josh Gross
76a6eb5cbc
Merge pull request from GhadimiR/add_unit_tests
Add unit tests
2025-03-19 10:52:21 -04:00
Josh Gross
a2426d7c45
Merge branch 'main' into add_unit_tests 2025-03-19 10:48:52 -04:00
Ryan Ghadimi
3ffa694f6f lint 2025-03-19 12:26:46 +00:00
Ryan Ghadimi
53f6aa5f93 Add extra assertion to download single artifact test 2025-03-19 12:25:37 +00:00
Ryan Ghadimi
b456700053 lint 2025-03-19 11:31:11 +00:00
Ryan Ghadimi
9eab798a98 Configure tsconfig 2025-03-19 11:25:29 +00:00
Ryan Ghadimi
a39a661f39 Unfix error 2025-03-19 11:22:27 +00:00
Ryan Ghadimi
9a869e9c49 Lint 2025-03-19 11:21:30 +00:00
Ryan Ghadimi
96a6f165f4 Add tests & test dependencies 2025-03-19 11:17:03 +00:00
Ryan Ghadimi
df4ad15cb8
Merge branch 'main' into main 2025-03-18 22:02:13 +00:00
Ryan Ghadimi
c7cfc3a2a3 dist & package.json 2025-03-18 21:28:08 +00:00
Ryan Ghadimi
2439186eed Fix bug introduced in 4.2.0 2025-03-18 21:25:22 +00:00
JoannaaKL
b14cf4c926
Merge pull request from GhadimiR/main
Bump artifact version, do digest check
2025-03-18 16:31:00 +01:00
Ryan Ghadimi
c5804ef743 Update dist 2025-03-18 15:23:08 +00:00
Ryan Ghadimi
956811a503 Update artifact to 2.3.2 2025-03-18 15:21:17 +00:00
Ryan Ghadimi
af3c6d3e5b Update artifact license 2025-03-18 13:23:53 +00:00
Ryan Ghadimi
4dd97f8f21 Bump artifact package 2025-03-18 11:57:35 +00:00
Ryan Ghadimi
da9985dde6
Merge branch 'main' into main 2025-03-17 10:26:24 +00:00
Larissa Fortuna
81ba80daa4
Merge pull request from actions/lkfortuna-patch-1
Update README.md
2025-03-13 08:12:46 -07:00
Larissa Fortuna
727afbf2b0
Update README.md
removing deprecation warning
2025-03-12 15:02:16 -07:00
Ryan Ghadimi
56c2d7ea8c Make work as intended 2025-03-12 16:21:59 +00:00
Ryan Ghadimi
7797bfcd59 run release 2025-03-12 16:20:09 +00:00
Ryan Ghadimi
9ff67cb2d2 Break the thing, also log the expected digest 2025-03-12 16:17:52 +00:00
Ryan Ghadimi
049eba1e9a unbreak testing code 2025-03-12 16:04:02 +00:00
Ryan Ghadimi
503e7a18ae Refactor loop, break for testing 2025-03-12 15:40:05 +00:00
Ryan Ghadimi
a8a786b097 update dist 2025-03-12 14:13:07 +00:00
Ryan Ghadimi
24aef17bbf Refactor loop 2025-03-12 14:02:51 +00:00
Ryan Ghadimi
b81a615862 Bump artifact version, do digest check 2025-03-12 13:47:20 +00:00
Yang Cao
cc20338598
Merge pull request from actions/yacaovsnc/release_4_1_9
Update artifact package to 2.2.2
2025-02-25 16:23:11 -05:00
Yang Cao
1fc0fee191 Update artifact package to 2.2.2 2025-02-25 16:10:45 -05:00
Rob Herley
7fba95161a
Merge pull request from andyfeller/patch-1
Update MIGRATION.md
2025-01-10 12:01:23 -05:00
Andy Feller
f9ceb7763b
Update MIGRATION.md
Fixing the broken link pointing to `actions/upload-artifact` content outside of the repository.

cc: @pedrolacerda
2025-01-10 11:56:44 -05:00
Josh Gross
533298bc57
Merge pull request from froblesmartin/patch-1
docs: small migration fix
2024-12-23 13:12:54 -05:00
Francisco Robles Martín
d06289e120
docs: small migration fix 2024-12-23 14:32:00 +01:00
Joel Ambass
d0ce8fd116
Merge pull request from actions/Jcambass-patch-1
Add workflow file for publishing releases to immutable action package
2024-09-26 08:20:04 +02:00
Joel Ambass
1ce0d91ace
Add workflow file for publishing releases to immutable action package
This workflow file publishes new action releases to the immutable action package of the same name as this repo.

This is part of the Immutable Actions project which is not yet fully released to the public. First party actions like this one are part of our initial testing of this feature.
2024-09-24 15:12:00 +02:00
15 changed files with 9379 additions and 2662 deletions

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

@ -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

@ -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

@ -1,6 +1,6 @@
---
name: "@actions/artifact"
version: 2.1.8
version: 2.3.2
type: npm
summary: Actions artifact lib
homepage: https://github.com/actions/toolkit/tree/main/packages/artifact

@ -1,11 +1,5 @@
# `@actions/download-artifact`
> [!WARNING]
> actions/download-artifact@v3 is scheduled for deprecation on **November 30, 2024**. [Learn more.](https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/)
> Similarly, v1/v2 are scheduled for deprecation on **June 30, 2024**.
> Please update your workflow to use v4 of the artifact actions.
> This deprecation will not impact any existing versions of GitHub Enterprise Server being used by customers.
Download [Actions Artifacts](https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts) from your Workflow Runs. Internally powered by the [@actions/artifact](https://github.com/actions/toolkit/tree/main/packages/artifact) package.
See also [upload-artifact](https://github.com/actions/upload-artifact).
@ -19,6 +13,7 @@ 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)
@ -59,6 +54,11 @@ For assistance with breaking changes, see [MIGRATION.md](docs/MIGRATION.md).
# 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:
@ -123,6 +123,32 @@ 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

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."
)
})
})

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

2754
dist/index.js vendored

File diff suppressed because it is too large Load Diff

@ -4,6 +4,7 @@
- [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`.
@ -189,7 +190,8 @@ jobs:
- 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@v3
+ uses: actions/upload-artifact@v4
with:
- name: all-my-files
+ name: my-artifact-${{ matrix.runs-on }}
@ -205,4 +207,47 @@ jobs:
+ 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](../merge/README.md).
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

@ -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
}

8675
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "download-artifact",
"version": "4.1.7",
"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,12 +29,13 @@
},
"homepage": "https://github.com/actions/download-artifact#readme",
"dependencies": {
"@actions/artifact": "^2.1.8",
"@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": "^6.14.0",
"@vercel/ncc": "^0.33.4",
@ -41,7 +43,10 @@
"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"
}
}

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

@ -15,7 +15,7 @@ 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}),
@ -23,7 +23,10 @@ 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})
mergeMultiple: core.getBooleanInput(Inputs.MergeMultiple, {
required: false
}),
artifactIds: core.getInput(Inputs.ArtifactIds, {required: false})
}
if (!inputs.path) {
@ -34,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}`)
@ -56,6 +67,7 @@ async function run(): Promise<void> {
}
let artifacts: Artifact[] = []
let artifactIds: number[] = []
if (isSingleArtifactDownload) {
core.info(`Downloading single artifact`)
@ -74,6 +86,52 @@ 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,
@ -92,7 +150,7 @@ async function run(): Promise<void> {
)
} else {
core.info(
'No input name or pattern filtered specified, downloading all artifacts'
'No input name, artifact-ids or pattern filtered specified, downloading all artifacts'
)
if (!inputs.mergeMultiple) {
core.info(
@ -106,26 +164,39 @@ async function run(): Promise<void> {
core.info(`Preparing to download the following artifacts:`)
artifacts.forEach(artifact => {
core.info(
`- ${artifact.name} (ID: ${artifact.id}, Size: ${artifact.size})`
`- ${artifact.name} (ID: ${artifact.id}, Size: ${artifact.size}, Expected Digest: ${artifact.digest})`
)
})
}
const downloadPromises = artifacts.map(artifact =>
artifactClient.downloadArtifact(artifact.id, {
const downloadPromises = artifacts.map(artifact => ({
name: artifact.name,
promise: artifactClient.downloadArtifact(artifact.id, {
...options,
path:
isSingleArtifactDownload || inputs.mergeMultiple
? resolvedPath
: path.join(resolvedPath, artifact.name)
: 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__"]
}