Compare commits

..

30 Commits
v4.2.0 ... 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
13 changed files with 8809 additions and 113 deletions

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

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

@ -13,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)
@ -53,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:
@ -117,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

56
dist/index.js vendored

@ -118710,6 +118710,7 @@ 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) {
@ -118760,7 +118761,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.chunk = void 0;
exports.run = exports.chunk = void 0;
const os = __importStar(__nccwpck_require__(22037));
const path = __importStar(__nccwpck_require__(71017));
const core = __importStar(__nccwpck_require__(42186));
@ -118783,7 +118784,10 @@ 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 })
mergeMultiple: core.getBooleanInput(constants_1.Inputs.MergeMultiple, {
required: false
}),
artifactIds: core.getInput(constants_1.Inputs.ArtifactIds, { required: false })
};
if (!inputs.path) {
inputs.path = process.env['GITHUB_WORKSPACE'] || process.cwd();
@ -118791,7 +118795,12 @@ 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 = {};
@ -118808,6 +118817,7 @@ function run() {
};
}
let artifacts = [];
let artifactIds = [];
if (isSingleArtifactDownload) {
core.info(`Downloading single artifact`);
const { artifact: targetArtifact } = yield artifact_1.default.getArtifact(inputs.name, options);
@ -118817,6 +118827,37 @@ 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;
@ -118828,7 +118869,7 @@ function run() {
core.debug(`Filtered from ${listArtifactResponse.artifacts.length} to ${artifacts.length} artifacts`);
}
else {
core.info('No input name or pattern filtered specified, downloading all artifacts');
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');
}
@ -118857,12 +118898,13 @@ function run() {
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(constants_1.Outputs.DownloadPath, resolvedPath);
core.info('Download artifact has finished successfully');
}
core.info(`Total of ${artifacts.length} artifact(s) downloaded`);
core.setOutput(constants_1.Outputs.DownloadPath, resolvedPath);
core.info('Download artifact has finished successfully');
});
}
exports.run = run;
run().catch(err => core.setFailed(`Unable to download artifact(s): ${err.message}`));
@ -128916,4 +128958,4 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"]
/******/ module.exports = __webpack_exports__;
/******/
/******/ })()
;
;

@ -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`.
@ -207,3 +208,46 @@ 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.

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
}

8309
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "download-artifact",
"version": "4.2.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",
@ -34,6 +35,7 @@
"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(
@ -138,11 +196,10 @@ async function run(): Promise<void> {
)
}
}
core.info(`Total of ${artifacts.length} artifact(s) downloaded`)
core.setOutput(Outputs.DownloadPath, resolvedPath)
core.info('Download artifact has finished successfully')
}
core.info(`Total of ${artifacts.length} artifact(s) downloaded`)
core.setOutput(Outputs.DownloadPath, resolvedPath)
core.info('Download artifact has finished successfully')
}
run().catch(err =>

@ -9,5 +9,5 @@
"moduleResolution": "node",
"esModuleInterop": true
},
"exclude": ["node_modules", "**/*.test.ts"]
"exclude": ["node_modules", "**/*.test.ts", "jest.config.ts", "__tests__"]
}