mirror of
https://github.com/actions/download-artifact.git
synced 2026-02-09 03:45:22 +00:00
Compare commits
9 Commits
v7.0.0
...
danwkenned
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcacd8b0f6 | ||
|
|
f0a2a2d898 | ||
|
|
c9ff097e79 | ||
|
|
882c7072ac | ||
|
|
6d4a0de04f | ||
|
|
f45b79f03c | ||
|
|
1d636af56d | ||
|
|
43c2394920 | ||
|
|
3d65dc0e0b |
@@ -1,3 +0,0 @@
|
||||
node_modules/
|
||||
lib/
|
||||
dist/
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"env": { "node": true, "jest": true },
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": { "ecmaVersion": 9, "sourceType": "module" },
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"plugins": ["@typescript-eslint"]
|
||||
}
|
||||
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
@@ -131,3 +131,26 @@ jobs:
|
||||
Write-Error "File contents of downloaded artifacts are incorrect"
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
# Test downloading artifact without decompressing (skip-decompress)
|
||||
- name: Download artifact A without decompressing
|
||||
uses: ./
|
||||
with:
|
||||
name: Artifact-A-${{ matrix.runs-on }}
|
||||
path: skip-decompress-test
|
||||
skip-decompress: true
|
||||
|
||||
- name: Verify skip-decompress download
|
||||
run: |
|
||||
$rawFile = "skip-decompress-test/artifact"
|
||||
if(!(Test-Path -path $rawFile))
|
||||
{
|
||||
Write-Error "Expected raw artifact file does not exist at $rawFile"
|
||||
}
|
||||
$fileInfo = Get-Item $rawFile
|
||||
if($fileInfo.Length -eq 0)
|
||||
{
|
||||
Write-Error "Downloaded artifact file is empty"
|
||||
}
|
||||
Write-Host "Successfully downloaded artifact without decompressing: $rawFile (size: $($fileInfo.Length) bytes)"
|
||||
shell: pwsh
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,4 +2,6 @@
|
||||
node_modules/
|
||||
|
||||
# Ignore js files that are transpiled from ts files in src/
|
||||
lib/
|
||||
lib/
|
||||
|
||||
.DS_Store
|
||||
@@ -1,10 +1,8 @@
|
||||
import * as core from '@actions/core'
|
||||
import {jest, describe, test, expect, beforeEach} from '@jest/globals'
|
||||
import * as path from 'path'
|
||||
import artifact, {ArtifactNotFoundError} from '@actions/artifact'
|
||||
import {run} from '../src/download-artifact'
|
||||
import {Inputs} from '../src/constants'
|
||||
|
||||
jest.mock('@actions/github', () => ({
|
||||
// Mock @actions/github before importing modules that use it
|
||||
jest.unstable_mockModule('@actions/github', () => ({
|
||||
context: {
|
||||
repo: {
|
||||
owner: 'actions',
|
||||
@@ -12,14 +10,46 @@ jest.mock('@actions/github', () => ({
|
||||
},
|
||||
runId: 123,
|
||||
serverUrl: 'https://github.com'
|
||||
}
|
||||
},
|
||||
getOctokit: jest.fn()
|
||||
}))
|
||||
|
||||
jest.mock('@actions/core')
|
||||
// Mock @actions/core
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
getInput: jest.fn(),
|
||||
getBooleanInput: jest.fn(),
|
||||
setOutput: jest.fn(),
|
||||
setFailed: jest.fn(),
|
||||
setSecret: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
notice: jest.fn(),
|
||||
startGroup: jest.fn(),
|
||||
endGroup: jest.fn(),
|
||||
isDebug: jest.fn(() => false),
|
||||
getState: jest.fn(),
|
||||
saveState: jest.fn(),
|
||||
exportVariable: jest.fn(),
|
||||
addPath: jest.fn(),
|
||||
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
|
||||
toPlatformPath: jest.fn(p => p),
|
||||
toWin32Path: jest.fn(p => p),
|
||||
toPosixPath: jest.fn(p => p)
|
||||
}))
|
||||
|
||||
/* eslint-disable no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
|
||||
const inputs = {
|
||||
// Dynamic imports after mocking
|
||||
const core = await import('@actions/core')
|
||||
const artifact = await import('@actions/artifact')
|
||||
const {run} = await import('../src/download-artifact.js')
|
||||
const {Inputs} = await import('../src/constants.js')
|
||||
const {ArtifactNotFoundError} = artifact
|
||||
|
||||
const mockInputs = (
|
||||
overrides?: Partial<{[K in (typeof Inputs)[keyof typeof Inputs]]?: any}>
|
||||
) => {
|
||||
const inputs: Record<string, any> = {
|
||||
[Inputs.Name]: 'artifact-name',
|
||||
[Inputs.Path]: '/some/artifact/path',
|
||||
[Inputs.GitHubToken]: 'warn',
|
||||
@@ -29,10 +59,14 @@ const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
|
||||
...overrides
|
||||
}
|
||||
|
||||
;(core.getInput as jest.Mock).mockImplementation((name: string) => {
|
||||
return inputs[name]
|
||||
})
|
||||
;(core.getBooleanInput as jest.Mock).mockImplementation((name: string) => {
|
||||
;(core.getInput as jest.Mock<typeof core.getInput>).mockImplementation(
|
||||
(name: string) => {
|
||||
return inputs[name]
|
||||
}
|
||||
)
|
||||
;(
|
||||
core.getBooleanInput as jest.Mock<typeof core.getBooleanInput>
|
||||
).mockImplementation((name: string) => {
|
||||
return inputs[name]
|
||||
})
|
||||
|
||||
@@ -46,13 +80,13 @@ describe('download', () => {
|
||||
|
||||
// Mock artifact client methods
|
||||
jest
|
||||
.spyOn(artifact, 'listArtifacts')
|
||||
.spyOn(artifact.default, 'listArtifacts')
|
||||
.mockImplementation(() => Promise.resolve({artifacts: []}))
|
||||
jest.spyOn(artifact, 'getArtifact').mockImplementation(name => {
|
||||
jest.spyOn(artifact.default, 'getArtifact').mockImplementation(name => {
|
||||
throw new ArtifactNotFoundError(`Artifact '${name}' not found`)
|
||||
})
|
||||
jest
|
||||
.spyOn(artifact, 'downloadArtifact')
|
||||
.spyOn(artifact.default, 'downloadArtifact')
|
||||
.mockImplementation(() => Promise.resolve({digestMismatch: false}))
|
||||
})
|
||||
|
||||
@@ -65,12 +99,12 @@ describe('download', () => {
|
||||
}
|
||||
|
||||
jest
|
||||
.spyOn(artifact, 'getArtifact')
|
||||
.spyOn(artifact.default, 'getArtifact')
|
||||
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||
|
||||
await run()
|
||||
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||
mockArtifact.id,
|
||||
expect.objectContaining({
|
||||
expectedHash: mockArtifact.digest
|
||||
@@ -102,12 +136,12 @@ describe('download', () => {
|
||||
|
||||
// Set up artifact mock after clearing mocks
|
||||
jest
|
||||
.spyOn(artifact, 'listArtifacts')
|
||||
.spyOn(artifact.default, 'listArtifacts')
|
||||
.mockImplementation(() => Promise.resolve({artifacts: mockArtifacts}))
|
||||
|
||||
// Reset downloadArtifact mock as well
|
||||
jest
|
||||
.spyOn(artifact, 'downloadArtifact')
|
||||
.spyOn(artifact.default, 'downloadArtifact')
|
||||
.mockImplementation(() => Promise.resolve({digestMismatch: false}))
|
||||
|
||||
await run()
|
||||
@@ -117,7 +151,7 @@ describe('download', () => {
|
||||
)
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith('Total of 2 artifact(s) downloaded')
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(2)
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('sets download path output even when no artifacts are found', async () => {
|
||||
@@ -144,7 +178,7 @@ describe('download', () => {
|
||||
]
|
||||
|
||||
jest
|
||||
.spyOn(artifact, 'listArtifacts')
|
||||
.spyOn(artifact.default, 'listArtifacts')
|
||||
.mockImplementation(() => Promise.resolve({artifacts: mockArtifacts}))
|
||||
|
||||
mockInputs({
|
||||
@@ -154,8 +188,8 @@ describe('download', () => {
|
||||
|
||||
await run()
|
||||
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(1)
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledTimes(1)
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||
123,
|
||||
expect.anything()
|
||||
)
|
||||
@@ -172,12 +206,12 @@ describe('download', () => {
|
||||
})
|
||||
|
||||
jest
|
||||
.spyOn(artifact, 'listArtifacts')
|
||||
.spyOn(artifact.default, 'listArtifacts')
|
||||
.mockImplementation(() => Promise.resolve({artifacts: []}))
|
||||
|
||||
await run()
|
||||
|
||||
expect(artifact.listArtifacts).toHaveBeenCalledWith(
|
||||
expect(artifact.default.listArtifacts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
findBy: {
|
||||
token,
|
||||
@@ -200,7 +234,7 @@ describe('download', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('warns when digest validation fails', async () => {
|
||||
test('errors when digest validation fails (default behavior)', async () => {
|
||||
const mockArtifact = {
|
||||
id: 123,
|
||||
name: 'corrupted-artifact',
|
||||
@@ -209,11 +243,36 @@ describe('download', () => {
|
||||
}
|
||||
|
||||
jest
|
||||
.spyOn(artifact, 'getArtifact')
|
||||
.spyOn(artifact.default, 'getArtifact')
|
||||
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||
|
||||
jest
|
||||
.spyOn(artifact, 'downloadArtifact')
|
||||
.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
|
||||
.spyOn(artifact.default, 'getArtifact')
|
||||
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||
|
||||
jest
|
||||
.spyOn(artifact.default, 'downloadArtifact')
|
||||
.mockImplementation(() => Promise.resolve({digestMismatch: true}))
|
||||
|
||||
await run()
|
||||
@@ -223,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 () => {
|
||||
const mockArtifact = {
|
||||
id: 456,
|
||||
@@ -237,7 +351,7 @@ describe('download', () => {
|
||||
[Inputs.ArtifactIds]: '456'
|
||||
})
|
||||
|
||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
||||
jest.spyOn(artifact.default, 'listArtifacts').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
artifacts: [mockArtifact]
|
||||
})
|
||||
@@ -247,8 +361,8 @@ describe('download', () => {
|
||||
|
||||
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(
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledTimes(1)
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||
456,
|
||||
expect.objectContaining({
|
||||
expectedHash: mockArtifact.digest
|
||||
@@ -270,7 +384,7 @@ describe('download', () => {
|
||||
[Inputs.ArtifactIds]: '123, 456, 789'
|
||||
})
|
||||
|
||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
||||
jest.spyOn(artifact.default, 'listArtifacts').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
artifacts: mockArtifacts
|
||||
})
|
||||
@@ -282,9 +396,9 @@ describe('download', () => {
|
||||
expect(core.debug).toHaveBeenCalledWith(
|
||||
'Parsed artifact IDs: ["123","456","789"]'
|
||||
)
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(3)
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledTimes(3)
|
||||
mockArtifacts.forEach(mockArtifact => {
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||
mockArtifact.id,
|
||||
expect.objectContaining({
|
||||
expectedHash: mockArtifact.digest
|
||||
@@ -305,7 +419,7 @@ describe('download', () => {
|
||||
[Inputs.ArtifactIds]: '123, 456, 789'
|
||||
})
|
||||
|
||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
||||
jest.spyOn(artifact.default, 'listArtifacts').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
artifacts: mockArtifacts
|
||||
})
|
||||
@@ -317,7 +431,7 @@ describe('download', () => {
|
||||
'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.default.downloadArtifact).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('throws error when no artifacts with requested IDs are found', async () => {
|
||||
@@ -327,7 +441,7 @@ describe('download', () => {
|
||||
[Inputs.ArtifactIds]: '123, 456'
|
||||
})
|
||||
|
||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
||||
jest.spyOn(artifact.default, 'listArtifacts').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
artifacts: []
|
||||
})
|
||||
@@ -389,7 +503,7 @@ describe('download', () => {
|
||||
[Inputs.Path]: testPath
|
||||
})
|
||||
|
||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
||||
jest.spyOn(artifact.default, 'listArtifacts').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
artifacts: [mockArtifact]
|
||||
})
|
||||
@@ -398,7 +512,7 @@ describe('download', () => {
|
||||
await run()
|
||||
|
||||
// Verify it downloads directly to the specified path (not nested in artifact name subdirectory)
|
||||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||
456,
|
||||
expect.objectContaining({
|
||||
path: path.resolve(testPath), // Should be the resolved path directly, not nested
|
||||
@@ -406,4 +520,87 @@ describe('download', () => {
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('passes skipDecompress option when skip-decompress input is true', async () => {
|
||||
const mockArtifact = {
|
||||
id: 123,
|
||||
name: 'artifact-name',
|
||||
size: 1024,
|
||||
digest: 'abc123'
|
||||
}
|
||||
|
||||
mockInputs({
|
||||
[Inputs.SkipDecompress]: true
|
||||
})
|
||||
|
||||
jest
|
||||
.spyOn(artifact.default, 'getArtifact')
|
||||
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||
|
||||
await run()
|
||||
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||
mockArtifact.id,
|
||||
expect.objectContaining({
|
||||
skipDecompress: true,
|
||||
expectedHash: mockArtifact.digest
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('does not pass skipDecompress when skip-decompress input is false', async () => {
|
||||
const mockArtifact = {
|
||||
id: 123,
|
||||
name: 'artifact-name',
|
||||
size: 1024,
|
||||
digest: 'abc123'
|
||||
}
|
||||
|
||||
mockInputs({
|
||||
[Inputs.SkipDecompress]: false
|
||||
})
|
||||
|
||||
jest
|
||||
.spyOn(artifact.default, 'getArtifact')
|
||||
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||
|
||||
await run()
|
||||
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||
mockArtifact.id,
|
||||
expect.objectContaining({
|
||||
skipDecompress: false,
|
||||
expectedHash: mockArtifact.digest
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('passes skipDecompress for multiple artifact downloads', async () => {
|
||||
mockInputs({
|
||||
[Inputs.Name]: '',
|
||||
[Inputs.Pattern]: '',
|
||||
[Inputs.SkipDecompress]: true
|
||||
})
|
||||
|
||||
const mockArtifacts = [
|
||||
{id: 123, name: 'artifact1', size: 1024, digest: 'abc123'},
|
||||
{id: 456, name: 'artifact2', size: 2048, digest: 'def456'}
|
||||
]
|
||||
|
||||
jest
|
||||
.spyOn(artifact.default, 'listArtifacts')
|
||||
.mockImplementation(() => Promise.resolve({artifacts: mockArtifacts}))
|
||||
|
||||
await run()
|
||||
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledTimes(2)
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||
123,
|
||||
expect.objectContaining({skipDecompress: true})
|
||||
)
|
||||
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||
456,
|
||||
expect.objectContaining({skipDecompress: true})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
10
action.yml
10
action.yml
@@ -35,6 +35,16 @@ inputs:
|
||||
If github-token is specified, this is the run that artifacts will be downloaded from.'
|
||||
required: false
|
||||
default: ${{ github.run_id }}
|
||||
skip-decompress:
|
||||
description: 'If true, the downloaded artifact will not be automatically extracted/decompressed.
|
||||
This is useful when you want to handle the artifact as-is without extraction.'
|
||||
required: 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:
|
||||
download-path:
|
||||
description: 'Path of artifact download'
|
||||
|
||||
134771
dist/index.js
vendored
134771
dist/index.js
vendored
File diff suppressed because one or more lines are too long
3
dist/package.json
vendored
Normal file
3
dist/package.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
58
eslint.config.mjs
Normal file
58
eslint.config.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
import github from 'eslint-plugin-github'
|
||||
import jest from 'eslint-plugin-jest'
|
||||
import prettier from 'eslint-plugin-prettier/recommended'
|
||||
|
||||
const githubConfigs = github.getFlatConfigs()
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['**/node_modules/**', '**/lib/**', '**/dist/**']
|
||||
},
|
||||
githubConfigs.recommended,
|
||||
...githubConfigs.typescript,
|
||||
prettier,
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: './tsconfig.eslint.json'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// Prettier
|
||||
'prettier/prettier': ['error', {endOfLine: 'auto'}],
|
||||
|
||||
// Disable rules that conflict with project style
|
||||
'eslint-comments/no-use': 'off',
|
||||
'github/no-then': 'off',
|
||||
'github/filenames-match-regex': 'off',
|
||||
'github/array-foreach': 'off',
|
||||
'import/no-namespace': 'off',
|
||||
'import/no-commonjs': 'off',
|
||||
'import/named': 'off',
|
||||
'import/no-unresolved': 'off',
|
||||
'i18n-text/no-en': 'off',
|
||||
'filenames/match-regex': 'off',
|
||||
'no-shadow': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'no-undef': 'off',
|
||||
camelcase: 'off',
|
||||
|
||||
// TypeScript rules
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'@typescript-eslint/array-type': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/__tests__/**/*.ts'],
|
||||
...jest.configs['flat/recommended'],
|
||||
rules: {
|
||||
...jest.configs['flat/recommended'].rules,
|
||||
'jest/expect-expect': 'off',
|
||||
'jest/no-conditional-expect': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,12 +1,24 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
roots: ['<rootDir>'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
testRunner: 'jest-circus/runner',
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
'^.+\\.ts$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true,
|
||||
diagnostics: {
|
||||
ignoreCodes: [151002]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
transformIgnorePatterns: ['node_modules/(?!(@actions)/)'],
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1'
|
||||
},
|
||||
verbose: true
|
||||
}
|
||||
|
||||
5869
package-lock.json
generated
5869
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@@ -2,6 +2,7 @@
|
||||
"name": "download-artifact",
|
||||
"version": "7.0.0",
|
||||
"description": "Download an Actions Artifact from a workflow run",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
},
|
||||
@@ -13,7 +14,7 @@
|
||||
"format": "prettier --write **/*.ts",
|
||||
"format-check": "prettier --check **/*.ts",
|
||||
"lint": "eslint **/*.ts",
|
||||
"test": "jest"
|
||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -32,22 +33,24 @@
|
||||
},
|
||||
"homepage": "https://github.com/actions/download-artifact#readme",
|
||||
"dependencies": {
|
||||
"@actions/artifact": "^5.0.0",
|
||||
"@actions/core": "^2.0.0",
|
||||
"@actions/github": "^6.0.1",
|
||||
"minimatch": "^9.0.3"
|
||||
"@actions/artifact": "^6.1.0",
|
||||
"@actions/core": "^3.0.0",
|
||||
"minimatch": "^10.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^24.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@vercel/ncc": "^0.33.4",
|
||||
"concurrently": "^5.2.0",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-github": "^4.10.1",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.1.1",
|
||||
"@actions/github": "^9.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^25.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^8.54.0",
|
||||
"@vercel/ncc": "^0.38.4",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-github": "^6.0.0",
|
||||
"eslint-plugin-jest": "^29.12.1",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"jest": "^30.2.0",
|
||||
"prettier": "^3.8.1",
|
||||
"ts-jest": "^29.2.6",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
|
||||
@@ -6,7 +6,16 @@ export enum Inputs {
|
||||
RunID = 'run-id',
|
||||
Pattern = 'pattern',
|
||||
MergeMultiple = 'merge-multiple',
|
||||
ArtifactIds = 'artifact-ids'
|
||||
ArtifactIds = 'artifact-ids',
|
||||
SkipDecompress = 'skip-decompress',
|
||||
DigestMismatch = 'digest-mismatch'
|
||||
}
|
||||
|
||||
export enum DigestMismatchBehavior {
|
||||
Ignore = 'ignore',
|
||||
Info = 'info',
|
||||
Warn = 'warn',
|
||||
Error = 'error'
|
||||
}
|
||||
|
||||
export enum Outputs {
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as core from '@actions/core'
|
||||
import artifactClient from '@actions/artifact'
|
||||
import type {Artifact, FindOptions} from '@actions/artifact'
|
||||
import {Minimatch} from 'minimatch'
|
||||
import {Inputs, Outputs} from './constants'
|
||||
import {Inputs, Outputs, DigestMismatchBehavior} from './constants.js'
|
||||
|
||||
const PARALLEL_DOWNLOADS = 5
|
||||
|
||||
@@ -26,7 +26,20 @@ export async function run(): Promise<void> {
|
||||
mergeMultiple: core.getBooleanInput(Inputs.MergeMultiple, {
|
||||
required: false
|
||||
}),
|
||||
artifactIds: core.getInput(Inputs.ArtifactIds, {required: false})
|
||||
artifactIds: core.getInput(Inputs.ArtifactIds, {required: false}),
|
||||
skipDecompress: core.getBooleanInput(Inputs.SkipDecompress, {
|
||||
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) {
|
||||
@@ -179,11 +192,14 @@ export async function run(): Promise<void> {
|
||||
artifacts.length === 1
|
||||
? resolvedPath
|
||||
: path.join(resolvedPath, artifact.name),
|
||||
expectedHash: artifact.digest
|
||||
expectedHash: artifact.digest,
|
||||
skipDecompress: inputs.skipDecompress
|
||||
})
|
||||
}))
|
||||
|
||||
const chunkedPromises = chunk(downloadPromises, PARALLEL_DOWNLOADS)
|
||||
const digestMismatches: string[] = []
|
||||
|
||||
for (const chunk of chunkedPromises) {
|
||||
const chunkPromises = chunk.map(item => item.promise)
|
||||
const results = await Promise.all(chunkPromises)
|
||||
@@ -193,12 +209,38 @@ export async function run(): Promise<void> {
|
||||
const artifactName = chunk[i].name
|
||||
|
||||
if (outcome.digestMismatch) {
|
||||
core.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:
|
||||
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.setOutput(Outputs.DownloadPath, resolvedPath)
|
||||
core.info('Download artifact has finished successfully')
|
||||
|
||||
8
tsconfig.eslint.json
Normal file
8
tsconfig.eslint.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src/**/*.ts", "__tests__/**/*.ts", "*.ts"],
|
||||
"exclude": ["node_modules", "lib", "dist"]
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": ["node_modules", "**/*.test.ts", "jest.config.ts", "__tests__"]
|
||||
|
||||
Reference in New Issue
Block a user