Compare commits

..
15 Commits
57 changed files with 70697 additions and 70296 deletions
-3
View File
@@ -1,3 +0,0 @@
node_modules/
lib/
dist/
-15
View File
@@ -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"]
}
+25 -2
View File
@@ -63,14 +63,14 @@ jobs:
path: path/to/artifact-B
# Test downloading a single artifact
- name: Download artifact A
- name: Download artifact A (absolute path)
uses: ./
with:
name: Artifact-A-${{ matrix.runs-on }}
path: some/new/path
# Test downloading an artifact using tilde expansion
- name: Download artifact A
- name: Download artifact A (tilde expansion)
uses: ./
with:
name: Artifact-A-${{ matrix.runs-on }}
@@ -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-A-${{ matrix.runs-on }}.zip"
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
+3 -1
View File
@@ -2,4 +2,6 @@
node_modules/
# Ignore js files that are transpiled from ts files in src/
lib/
lib/
.DS_Store
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+157 -40
View File
@@ -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,
@@ -209,11 +243,11 @@ 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 run()
@@ -237,7 +271,7 @@ describe('download', () => {
[Inputs.ArtifactIds]: '456'
})
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
jest.spyOn(artifact.default, 'listArtifacts').mockImplementation(() =>
Promise.resolve({
artifacts: [mockArtifact]
})
@@ -247,8 +281,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 +304,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 +316,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 +339,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 +351,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 +361,7 @@ describe('download', () => {
[Inputs.ArtifactIds]: '123, 456'
})
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
jest.spyOn(artifact.default, 'listArtifacts').mockImplementation(() =>
Promise.resolve({
artifacts: []
})
@@ -389,7 +423,7 @@ describe('download', () => {
[Inputs.Path]: testPath
})
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
jest.spyOn(artifact.default, 'listArtifacts').mockImplementation(() =>
Promise.resolve({
artifacts: [mockArtifact]
})
@@ -398,7 +432,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 +440,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})
)
})
})
+5
View File
@@ -35,6 +35,11 @@ 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'
outputs:
download-path:
description: 'Path of artifact download'
+68543 -66191
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+58
View 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'
}
}
]
+15 -3
View File
@@ -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
}
+1850 -4019
View File
File diff suppressed because it is too large Load Diff
+18 -15
View File
@@ -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"
+2 -1
View File
@@ -6,7 +6,8 @@ export enum Inputs {
RunID = 'run-id',
Pattern = 'pattern',
MergeMultiple = 'merge-multiple',
ArtifactIds = 'artifact-ids'
ArtifactIds = 'artifact-ids',
SkipDecompress = 'skip-decompress'
}
export enum Outputs {
+7 -3
View File
@@ -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} from './constants.js'
const PARALLEL_DOWNLOADS = 5
@@ -26,7 +26,10 @@ 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
})
}
if (!inputs.path) {
@@ -179,7 +182,8 @@ export async function run(): Promise<void> {
artifacts.length === 1
? resolvedPath
: path.join(resolvedPath, artifact.name),
expectedHash: artifact.digest
expectedHash: artifact.digest,
skipDecompress: inputs.skipDecompress
})
}))
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["src/**/*.ts", "__tests__/**/*.ts", "*.ts"],
"exclude": ["node_modules", "lib", "dist"]
}
+3 -3
View File
@@ -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__"]