mirror of
https://gitea.com/actions/download-artifact.git
synced 2026-02-02 04:14:29 +07:00
Add a setting to specify what to do on hash mismatch and default it to error
This commit is contained in:
@@ -234,7 +234,7 @@ describe('download', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('warns when digest validation fails', async () => {
|
test('errors when digest validation fails (default behavior)', async () => {
|
||||||
const mockArtifact = {
|
const mockArtifact = {
|
||||||
id: 123,
|
id: 123,
|
||||||
name: 'corrupted-artifact',
|
name: 'corrupted-artifact',
|
||||||
@@ -242,6 +242,31 @@ describe('download', () => {
|
|||||||
digest: 'abc123'
|
digest: 'abc123'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'getArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'downloadArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({digestMismatch: true}))
|
||||||
|
|
||||||
|
await expect(run()).rejects.toThrow(
|
||||||
|
"Digest validation failed for artifact(s): corrupted-artifact"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('warns when digest validation fails with digest-mismatch set to warn', async () => {
|
||||||
|
const mockArtifact = {
|
||||||
|
id: 123,
|
||||||
|
name: 'corrupted-artifact',
|
||||||
|
size: 1024,
|
||||||
|
digest: 'abc123'
|
||||||
|
}
|
||||||
|
|
||||||
|
mockInputs({
|
||||||
|
[Inputs.DigestMismatch]: 'warn'
|
||||||
|
})
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(artifact.default, 'getArtifact')
|
.spyOn(artifact.default, 'getArtifact')
|
||||||
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||||
@@ -257,6 +282,61 @@ describe('download', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('logs info when digest validation fails with digest-mismatch set to info', async () => {
|
||||||
|
const mockArtifact = {
|
||||||
|
id: 123,
|
||||||
|
name: 'corrupted-artifact',
|
||||||
|
size: 1024,
|
||||||
|
digest: 'abc123'
|
||||||
|
}
|
||||||
|
|
||||||
|
mockInputs({
|
||||||
|
[Inputs.DigestMismatch]: 'info'
|
||||||
|
})
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'getArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'downloadArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({digestMismatch: true}))
|
||||||
|
|
||||||
|
await run()
|
||||||
|
|
||||||
|
expect(core.info).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('digest validation failed')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('silently continues when digest validation fails with digest-mismatch set to ignore', async () => {
|
||||||
|
const mockArtifact = {
|
||||||
|
id: 123,
|
||||||
|
name: 'corrupted-artifact',
|
||||||
|
size: 1024,
|
||||||
|
digest: 'abc123'
|
||||||
|
}
|
||||||
|
|
||||||
|
mockInputs({
|
||||||
|
[Inputs.DigestMismatch]: 'ignore'
|
||||||
|
})
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'getArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'downloadArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({digestMismatch: true}))
|
||||||
|
|
||||||
|
await run()
|
||||||
|
|
||||||
|
expect(core.warning).not.toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('digest validation failed')
|
||||||
|
)
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Total of 1 artifact(s) downloaded')
|
||||||
|
})
|
||||||
|
|
||||||
test('downloads a single artifact by ID', async () => {
|
test('downloads a single artifact by ID', async () => {
|
||||||
const mockArtifact = {
|
const mockArtifact = {
|
||||||
id: 456,
|
id: 456,
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ inputs:
|
|||||||
This is useful when you want to handle the artifact as-is without extraction.'
|
This is useful when you want to handle the artifact as-is without extraction.'
|
||||||
required: false
|
required: false
|
||||||
default: 'false'
|
default: 'false'
|
||||||
|
digest-mismatch:
|
||||||
|
description: 'The behavior when a downloaded artifact''s digest does not match the expected digest.
|
||||||
|
Options: ignore, info, warn, error. Default is error which will fail the action.'
|
||||||
|
required: false
|
||||||
|
default: 'error'
|
||||||
outputs:
|
outputs:
|
||||||
download-path:
|
download-path:
|
||||||
description: 'Path of artifact download'
|
description: 'Path of artifact download'
|
||||||
|
|||||||
41
dist/index.js
vendored
41
dist/index.js
vendored
@@ -129353,7 +129353,15 @@ var Inputs;
|
|||||||
Inputs["MergeMultiple"] = "merge-multiple";
|
Inputs["MergeMultiple"] = "merge-multiple";
|
||||||
Inputs["ArtifactIds"] = "artifact-ids";
|
Inputs["ArtifactIds"] = "artifact-ids";
|
||||||
Inputs["SkipDecompress"] = "skip-decompress";
|
Inputs["SkipDecompress"] = "skip-decompress";
|
||||||
|
Inputs["DigestMismatch"] = "digest-mismatch";
|
||||||
})(Inputs || (Inputs = {}));
|
})(Inputs || (Inputs = {}));
|
||||||
|
var DigestMismatchBehavior;
|
||||||
|
(function (DigestMismatchBehavior) {
|
||||||
|
DigestMismatchBehavior["Ignore"] = "ignore";
|
||||||
|
DigestMismatchBehavior["Info"] = "info";
|
||||||
|
DigestMismatchBehavior["Warn"] = "warn";
|
||||||
|
DigestMismatchBehavior["Error"] = "error";
|
||||||
|
})(DigestMismatchBehavior || (DigestMismatchBehavior = {}));
|
||||||
var Outputs;
|
var Outputs;
|
||||||
(function (Outputs) {
|
(function (Outputs) {
|
||||||
Outputs["DownloadPath"] = "download-path";
|
Outputs["DownloadPath"] = "download-path";
|
||||||
@@ -129386,8 +129394,15 @@ async function run() {
|
|||||||
artifactIds: getInput(Inputs.ArtifactIds, { required: false }),
|
artifactIds: getInput(Inputs.ArtifactIds, { required: false }),
|
||||||
skipDecompress: getBooleanInput(Inputs.SkipDecompress, {
|
skipDecompress: getBooleanInput(Inputs.SkipDecompress, {
|
||||||
required: false
|
required: false
|
||||||
})
|
}),
|
||||||
|
digestMismatch: (getInput(Inputs.DigestMismatch, { required: false }) ||
|
||||||
|
DigestMismatchBehavior.Error)
|
||||||
};
|
};
|
||||||
|
// Validate digest-mismatch input
|
||||||
|
const validBehaviors = Object.values(DigestMismatchBehavior);
|
||||||
|
if (!validBehaviors.includes(inputs.digestMismatch)) {
|
||||||
|
throw new Error(`Invalid value for 'digest-mismatch': '${inputs.digestMismatch}'. Valid options are: ${validBehaviors.join(', ')}`);
|
||||||
|
}
|
||||||
if (!inputs.path) {
|
if (!inputs.path) {
|
||||||
inputs.path = process.env['GITHUB_WORKSPACE'] || process.cwd();
|
inputs.path = process.env['GITHUB_WORKSPACE'] || process.cwd();
|
||||||
}
|
}
|
||||||
@@ -129500,6 +129515,7 @@ async function run() {
|
|||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
const chunkedPromises = chunk(downloadPromises, PARALLEL_DOWNLOADS);
|
const chunkedPromises = chunk(downloadPromises, PARALLEL_DOWNLOADS);
|
||||||
|
const digestMismatches = [];
|
||||||
for (const chunk of chunkedPromises) {
|
for (const chunk of chunkedPromises) {
|
||||||
const chunkPromises = chunk.map(item => item.promise);
|
const chunkPromises = chunk.map(item => item.promise);
|
||||||
const results = await Promise.all(chunkPromises);
|
const results = await Promise.all(chunkPromises);
|
||||||
@@ -129507,10 +129523,31 @@ async function run() {
|
|||||||
const outcome = results[i];
|
const outcome = results[i];
|
||||||
const artifactName = chunk[i].name;
|
const artifactName = chunk[i].name;
|
||||||
if (outcome.digestMismatch) {
|
if (outcome.digestMismatch) {
|
||||||
warning(`Artifact '${artifactName}' digest validation failed. Please verify the integrity of the artifact.`);
|
digestMismatches.push(artifactName);
|
||||||
|
const message = `Artifact '${artifactName}' digest validation failed. Please verify the integrity of the artifact.`;
|
||||||
|
switch (inputs.digestMismatch) {
|
||||||
|
case DigestMismatchBehavior.Ignore:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
case DigestMismatchBehavior.Info:
|
||||||
|
info(message);
|
||||||
|
break;
|
||||||
|
case DigestMismatchBehavior.Warn:
|
||||||
|
warning(message);
|
||||||
|
break;
|
||||||
|
case DigestMismatchBehavior.Error:
|
||||||
|
// Collect all errors and fail at the end
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// If there were digest mismatches and behavior is 'error', fail the action
|
||||||
|
if (digestMismatches.length > 0 &&
|
||||||
|
inputs.digestMismatch === DigestMismatchBehavior.Error) {
|
||||||
|
throw new Error(`Digest validation failed for artifact(s): ${digestMismatches.join(', ')}. ` +
|
||||||
|
`Use 'digest-mismatch: warn' to continue on mismatch.`);
|
||||||
|
}
|
||||||
info(`Total of ${artifacts.length} artifact(s) downloaded`);
|
info(`Total of ${artifacts.length} artifact(s) downloaded`);
|
||||||
setOutput(Outputs.DownloadPath, resolvedPath);
|
setOutput(Outputs.DownloadPath, resolvedPath);
|
||||||
info('Download artifact has finished successfully');
|
info('Download artifact has finished successfully');
|
||||||
|
|||||||
@@ -7,7 +7,15 @@ export enum Inputs {
|
|||||||
Pattern = 'pattern',
|
Pattern = 'pattern',
|
||||||
MergeMultiple = 'merge-multiple',
|
MergeMultiple = 'merge-multiple',
|
||||||
ArtifactIds = 'artifact-ids',
|
ArtifactIds = 'artifact-ids',
|
||||||
SkipDecompress = 'skip-decompress'
|
SkipDecompress = 'skip-decompress',
|
||||||
|
DigestMismatch = 'digest-mismatch'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DigestMismatchBehavior {
|
||||||
|
Ignore = 'ignore',
|
||||||
|
Info = 'info',
|
||||||
|
Warn = 'warn',
|
||||||
|
Error = 'error'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Outputs {
|
export enum Outputs {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as core from '@actions/core'
|
|||||||
import artifactClient from '@actions/artifact'
|
import artifactClient from '@actions/artifact'
|
||||||
import type {Artifact, FindOptions} from '@actions/artifact'
|
import type {Artifact, FindOptions} from '@actions/artifact'
|
||||||
import {Minimatch} from 'minimatch'
|
import {Minimatch} from 'minimatch'
|
||||||
import {Inputs, Outputs} from './constants.js'
|
import {Inputs, Outputs, DigestMismatchBehavior} from './constants.js'
|
||||||
|
|
||||||
const PARALLEL_DOWNLOADS = 5
|
const PARALLEL_DOWNLOADS = 5
|
||||||
|
|
||||||
@@ -29,7 +29,17 @@ export async function run(): Promise<void> {
|
|||||||
artifactIds: core.getInput(Inputs.ArtifactIds, {required: false}),
|
artifactIds: core.getInput(Inputs.ArtifactIds, {required: false}),
|
||||||
skipDecompress: core.getBooleanInput(Inputs.SkipDecompress, {
|
skipDecompress: core.getBooleanInput(Inputs.SkipDecompress, {
|
||||||
required: false
|
required: false
|
||||||
})
|
}),
|
||||||
|
digestMismatch: (core.getInput(Inputs.DigestMismatch, {required: false}) ||
|
||||||
|
DigestMismatchBehavior.Error) as DigestMismatchBehavior
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate digest-mismatch input
|
||||||
|
const validBehaviors = Object.values(DigestMismatchBehavior)
|
||||||
|
if (!validBehaviors.includes(inputs.digestMismatch)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid value for 'digest-mismatch': '${inputs.digestMismatch}'. Valid options are: ${validBehaviors.join(', ')}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inputs.path) {
|
if (!inputs.path) {
|
||||||
@@ -188,6 +198,8 @@ export async function run(): Promise<void> {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const chunkedPromises = chunk(downloadPromises, PARALLEL_DOWNLOADS)
|
const chunkedPromises = chunk(downloadPromises, PARALLEL_DOWNLOADS)
|
||||||
|
const digestMismatches: string[] = []
|
||||||
|
|
||||||
for (const chunk of chunkedPromises) {
|
for (const chunk of chunkedPromises) {
|
||||||
const chunkPromises = chunk.map(item => item.promise)
|
const chunkPromises = chunk.map(item => item.promise)
|
||||||
const results = await Promise.all(chunkPromises)
|
const results = await Promise.all(chunkPromises)
|
||||||
@@ -197,12 +209,38 @@ export async function run(): Promise<void> {
|
|||||||
const artifactName = chunk[i].name
|
const artifactName = chunk[i].name
|
||||||
|
|
||||||
if (outcome.digestMismatch) {
|
if (outcome.digestMismatch) {
|
||||||
core.warning(
|
digestMismatches.push(artifactName)
|
||||||
`Artifact '${artifactName}' digest validation failed. Please verify the integrity of the artifact.`
|
const message = `Artifact '${artifactName}' digest validation failed. Please verify the integrity of the artifact.`
|
||||||
|
|
||||||
|
switch (inputs.digestMismatch) {
|
||||||
|
case DigestMismatchBehavior.Ignore:
|
||||||
|
// Do nothing
|
||||||
|
break
|
||||||
|
case DigestMismatchBehavior.Info:
|
||||||
|
core.info(message)
|
||||||
|
break
|
||||||
|
case DigestMismatchBehavior.Warn:
|
||||||
|
core.warning(message)
|
||||||
|
break
|
||||||
|
case DigestMismatchBehavior.Error:
|
||||||
|
// Collect all errors and fail at the end
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there were digest mismatches and behavior is 'error', fail the action
|
||||||
|
if (
|
||||||
|
digestMismatches.length > 0 &&
|
||||||
|
inputs.digestMismatch === DigestMismatchBehavior.Error
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Digest validation failed for artifact(s): ${digestMismatches.join(', ')}. ` +
|
||||||
|
`Use 'digest-mismatch: warn' to continue on mismatch.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
core.info(`Total of ${artifacts.length} artifact(s) downloaded`)
|
core.info(`Total of ${artifacts.length} artifact(s) downloaded`)
|
||||||
core.setOutput(Outputs.DownloadPath, resolvedPath)
|
core.setOutput(Outputs.DownloadPath, resolvedPath)
|
||||||
core.info('Download artifact has finished successfully')
|
core.info('Download artifact has finished successfully')
|
||||||
|
|||||||
Reference in New Issue
Block a user