Compare commits

...

10 Commits

Author SHA1 Message Date
Rob Herley
d3f86a106a
Merge pull request #404 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 #402 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 #401 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
7 changed files with 110 additions and 161 deletions

View File

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

View File

@ -25,8 +25,6 @@ const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
[Inputs.Repository]: 'owner/some-repository',
[Inputs.RunID]: 'some-run-id',
[Inputs.Pattern]: 'some-pattern',
[Inputs.MergeMultiple]: false,
[Inputs.ArtifactIds]: '',
...overrides
}
@ -224,42 +222,9 @@ describe('download', () => {
)
})
test('throws error when both name and artifact-ids are provided', async () => {
mockInputs({
[Inputs.Name]: 'artifact-name',
[Inputs.ArtifactIds]: '123'
})
await expect(run()).rejects.toThrow(
"Inputs 'name' and 'artifact-ids' cannot be used together. Please specify only one."
)
})
test('throws error when artifact-ids is empty', async () => {
mockInputs({
[Inputs.Name]: '',
[Inputs.ArtifactIds]: ' , '
})
await expect(run()).rejects.toThrow(
"No valid artifact IDs provided in 'artifact-ids' input"
)
})
test('throws error when artifact-id is not a number', async () => {
mockInputs({
[Inputs.Name]: '',
[Inputs.ArtifactIds]: '123,abc,456'
})
await expect(run()).rejects.toThrow(
"Invalid artifact ID: 'abc'. Must be a number."
)
})
test('downloads a single artifact by ID', async () => {
const mockArtifact = {
id: 123,
id: 456,
name: 'artifact-by-id',
size: 1024,
digest: 'def456'
@ -267,120 +232,143 @@ describe('download', () => {
mockInputs({
[Inputs.Name]: '',
[Inputs.ArtifactIds]: '123'
[Inputs.Pattern]: '',
[Inputs.ArtifactIds]: '456'
})
jest
.spyOn(artifact, 'getArtifact')
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
Promise.resolve({
artifacts: [mockArtifact]
})
)
await run()
expect(core.debug).toHaveBeenCalledWith(
'Only one artifact ID provided. Fetching latest artifact by its name and checking the ID'
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(artifact.getArtifact).toHaveBeenCalled()
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(artifact.listArtifacts).not.toHaveBeenCalled()
expect(core.info).toHaveBeenCalledWith('Total of 1 artifact(s) downloaded')
})
test('throws error when single artifact ID is not found', async () => {
mockInputs({
[Inputs.Name]: '',
[Inputs.ArtifactIds]: '999'
})
jest.spyOn(artifact, 'getArtifact').mockImplementation(() => {
return Promise.resolve({artifact: null} as any)
})
await expect(run()).rejects.toThrow(
"Artifact with ID '999' not found. Please check the ID."
)
})
test('downloads multiple artifacts by IDs', async () => {
const mockArtifacts = [
{id: 123, name: 'artifact1', size: 1024, digest: 'abc123'},
{id: 456, name: 'artifact2', size: 2048, digest: 'def456'},
{id: 789, name: 'artifact3', size: 3072, digest: 'ghi789'}
]
mockInputs({
[Inputs.Name]: '',
[Inputs.ArtifactIds]: '123, 456'
})
jest
.spyOn(artifact, 'listArtifacts')
.mockImplementation(() => Promise.resolve({artifacts: mockArtifacts}))
await run()
expect(core.info).toHaveBeenCalledWith(
'Multiple artifact IDs provided. Fetching all artifacts to filter by ID'
)
expect(artifact.getArtifact).not.toHaveBeenCalled()
expect(artifact.listArtifacts).toHaveBeenCalled()
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(2)
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
123,
expect.anything()
)
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
456,
expect.anything()
)
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: 'artifact1', size: 1024, digest: 'abc123'}
{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}))
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)
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
123,
expect.anything()
)
})
test('throws error when none of the provided artifact IDs are found', async () => {
const mockArtifacts = [
{id: 999, name: 'other-artifact', size: 1024, digest: 'xyz999'}
]
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: mockArtifacts}))
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
dist/index.js vendored
View File

@ -118845,23 +118845,9 @@ function run() {
}
return numericId;
});
// if the length of artifactIds exactly 1 fetch the latest artifact by its name and check the ID
if (artifactIds.length === 1) {
core.debug(`Only one artifact ID provided. Fetching latest artifact by its name and checking the ID`);
const getArtifactResponse = yield artifact_1.default.getArtifact(inputs.name, Object.assign({}, options));
if (!getArtifactResponse || !getArtifactResponse.artifact) {
throw new Error(`Artifact with ID '${artifactIds[0]}' not found. Please check the ID.`);
}
const artifact = getArtifactResponse.artifact;
core.debug(`Found artifact by ID '${artifact.name}' (ID: ${artifact.id}, Size: ${artifact.size})`);
artifacts = [artifact];
}
else {
core.info(`Multiple artifact IDs provided. Fetching all artifacts to filter by ID`);
// 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`);
}

View File

@ -219,21 +219,29 @@ To take advantage of this immutability for security purposes (to avoid potential
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
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.outputs.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

4
package-lock.json generated
View File

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

View File

@ -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": {

View File

@ -109,34 +109,6 @@ export async function run(): Promise<void> {
return numericId
})
// if the length of artifactIds exactly 1 fetch the latest artifact by its name and check the ID
if (artifactIds.length === 1) {
core.debug(
`Only one artifact ID provided. Fetching latest artifact by its name and checking the ID`
)
const getArtifactResponse = await artifactClient.getArtifact(
inputs.name,
{...options}
)
if (!getArtifactResponse || !getArtifactResponse.artifact) {
throw new Error(
`Artifact with ID '${artifactIds[0]}' not found. Please check the ID.`
)
}
const artifact = getArtifactResponse.artifact
core.debug(
`Found artifact by ID '${artifact.name}' (ID: ${artifact.id}, Size: ${artifact.size})`
)
artifacts = [artifact]
} else {
core.info(
`Multiple artifact IDs provided. Fetching all artifacts to filter by ID`
)
// We need to fetch all artifacts to get metadata for the specified IDs
const listArtifactResponse = await artifactClient.listArtifacts({
latest: true,
@ -146,7 +118,6 @@ export async function run(): Promise<void> {
artifacts = listArtifactResponse.artifacts.filter(artifact =>
artifactIds.includes(artifact.id)
)
}
if (artifacts.length === 0) {
throw new Error(`None of the provided artifact IDs were found`)