1
0
mirror of https://github.com/actions/upload-artifact.git synced 2026-02-26 13:42:35 +00:00

Upgrade the module to ESM and bump dependencies

This commit is contained in:
Daniel Kennedy
2026-02-25 12:57:27 -05:00
parent 47309c993a
commit f119f587c2
19 changed files with 2811 additions and 2234 deletions

View File

@ -1,16 +0,0 @@
{
"env": { "node": true, "jest": true },
"parser": "@typescript-eslint/parser",
"parserOptions": { "ecmaVersion": 9, "sourceType": "module" },
"extends": [
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"plugin:prettier/recommended"
],
"rules": {
"@typescript-eslint/no-empty-function": "off"
},
"plugins": ["@typescript-eslint", "jest"]
}

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules/ node_modules/
lib/ lib/
__tests__/_temp/ __tests__/_temp/
.DS_Store

View File

@ -1,8 +1,65 @@
import * as core from '@actions/core' import {jest, describe, test, expect, beforeEach} from '@jest/globals'
import artifact from '@actions/artifact'
import {run} from '../src/merge/merge-artifacts' // Mock @actions/github before importing modules that use it
import {Inputs} from '../src/merge/constants' jest.unstable_mockModule('@actions/github', () => ({
import * as search from '../src/shared/search' context: {
repo: {
owner: 'actions',
repo: 'toolkit'
},
runId: 123,
serverUrl: 'https://github.com'
},
getOctokit: jest.fn()
}))
// 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: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}))
// Mock fs/promises
const actualFsPromises = await import('fs/promises')
jest.unstable_mockModule('fs/promises', () => ({
...actualFsPromises,
mkdtemp:
jest.fn<() => Promise<string>>().mockResolvedValue('/tmp/merge-artifact'),
rm: jest.fn<() => Promise<void>>().mockResolvedValue(undefined)
}))
// Mock shared search module
const mockFindFilesToUpload = jest.fn<
() => Promise<{filesToUpload: string[]; rootDirectory: string}>
>()
jest.unstable_mockModule('../src/shared/search.js', () => ({
findFilesToUpload: mockFindFilesToUpload
}))
// Dynamic imports after mocking
const core = await import('@actions/core')
const artifact = await import('@actions/artifact')
const {run} = await import('../src/merge/merge-artifacts.js')
const {Inputs} = await import('../src/merge/constants.js')
const fixtures = { const fixtures = {
artifactName: 'my-merged-artifact', artifactName: 'my-merged-artifact',
@ -34,27 +91,10 @@ const fixtures = {
] ]
} }
jest.mock('@actions/github', () => ({ const mockInputs = (
context: { overrides?: Partial<{[K in (typeof Inputs)[keyof typeof Inputs]]?: any}>
repo: { ) => {
owner: 'actions', const inputs: Record<string, any> = {
repo: 'toolkit'
},
runId: 123,
serverUrl: 'https://github.com'
}
}))
jest.mock('@actions/core')
jest.mock('fs/promises', () => ({
mkdtemp: jest.fn().mockResolvedValue('/tmp/merge-artifact'),
rm: jest.fn().mockResolvedValue(undefined)
}))
/* eslint-disable no-unused-vars */
const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
const inputs = {
[Inputs.Name]: 'my-merged-artifact', [Inputs.Name]: 'my-merged-artifact',
[Inputs.Pattern]: '*', [Inputs.Pattern]: '*',
[Inputs.SeparateDirectories]: false, [Inputs.SeparateDirectories]: false,
@ -64,10 +104,14 @@ const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
...overrides ...overrides
} }
;(core.getInput as jest.Mock).mockImplementation((name: string) => { ;(core.getInput as jest.Mock<typeof core.getInput>).mockImplementation(
return inputs[name] (name: string) => {
}) return inputs[name]
;(core.getBooleanInput as jest.Mock).mockImplementation((name: string) => { }
)
;(
core.getBooleanInput as jest.Mock<typeof core.getBooleanInput>
).mockImplementation((name: string) => {
return inputs[name] return inputs[name]
}) })
@ -77,44 +121,45 @@ const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
describe('merge', () => { describe('merge', () => {
beforeEach(async () => { beforeEach(async () => {
mockInputs() mockInputs()
jest.clearAllMocks()
jest jest
.spyOn(artifact, 'listArtifacts') .spyOn(artifact.default, 'listArtifacts')
.mockResolvedValue({artifacts: fixtures.artifacts}) .mockResolvedValue({artifacts: fixtures.artifacts})
jest.spyOn(artifact, 'downloadArtifact').mockResolvedValue({ jest.spyOn(artifact.default, 'downloadArtifact').mockResolvedValue({
downloadPath: fixtures.tmpDirectory downloadPath: fixtures.tmpDirectory
}) })
jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({ mockFindFilesToUpload.mockResolvedValue({
filesToUpload: fixtures.filesToUpload, filesToUpload: fixtures.filesToUpload,
rootDirectory: fixtures.tmpDirectory rootDirectory: fixtures.tmpDirectory
}) })
jest.spyOn(artifact, 'uploadArtifact').mockResolvedValue({ jest.spyOn(artifact.default, 'uploadArtifact').mockResolvedValue({
size: 123, size: 123,
id: 1337 id: 1337
}) })
jest jest
.spyOn(artifact, 'deleteArtifact') .spyOn(artifact.default, 'deleteArtifact')
.mockImplementation(async artifactName => { .mockImplementation(async (artifactName: string) => {
const artifact = fixtures.artifacts.find(a => a.name === artifactName) const found = fixtures.artifacts.find(a => a.name === artifactName)
if (!artifact) throw new Error(`Artifact ${artifactName} not found`) if (!found) throw new Error(`Artifact ${artifactName} not found`)
return {id: artifact.id} return {id: found.id}
}) })
}) })
it('merges artifacts', async () => { test('merges artifacts', async () => {
await run() await run()
for (const a of fixtures.artifacts) { for (const a of fixtures.artifacts) {
expect(artifact.downloadArtifact).toHaveBeenCalledWith(a.id, { expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(a.id, {
path: fixtures.tmpDirectory path: fixtures.tmpDirectory
}) })
} }
expect(artifact.uploadArtifact).toHaveBeenCalledWith( expect(artifact.default.uploadArtifact).toHaveBeenCalledWith(
fixtures.artifactName, fixtures.artifactName,
fixtures.filesToUpload, fixtures.filesToUpload,
fixtures.tmpDirectory, fixtures.tmpDirectory,
@ -122,23 +167,23 @@ describe('merge', () => {
) )
}) })
it('fails if no artifacts found', async () => { test('fails if no artifacts found', async () => {
mockInputs({[Inputs.Pattern]: 'this-does-not-match'}) mockInputs({[Inputs.Pattern]: 'this-does-not-match'})
expect(run()).rejects.toThrow() await expect(run()).rejects.toThrow()
expect(artifact.uploadArtifact).not.toBeCalled() expect(artifact.default.uploadArtifact).not.toHaveBeenCalled()
expect(artifact.downloadArtifact).not.toBeCalled() expect(artifact.default.downloadArtifact).not.toHaveBeenCalled()
}) })
it('supports custom compression level', async () => { test('supports custom compression level', async () => {
mockInputs({ mockInputs({
[Inputs.CompressionLevel]: 2 [Inputs.CompressionLevel]: 2
}) })
await run() await run()
expect(artifact.uploadArtifact).toHaveBeenCalledWith( expect(artifact.default.uploadArtifact).toHaveBeenCalledWith(
fixtures.artifactName, fixtures.artifactName,
fixtures.filesToUpload, fixtures.filesToUpload,
fixtures.tmpDirectory, fixtures.tmpDirectory,
@ -146,14 +191,14 @@ describe('merge', () => {
) )
}) })
it('supports custom retention days', async () => { test('supports custom retention days', async () => {
mockInputs({ mockInputs({
[Inputs.RetentionDays]: 7 [Inputs.RetentionDays]: 7
}) })
await run() await run()
expect(artifact.uploadArtifact).toHaveBeenCalledWith( expect(artifact.default.uploadArtifact).toHaveBeenCalledWith(
fixtures.artifactName, fixtures.artifactName,
fixtures.filesToUpload, fixtures.filesToUpload,
fixtures.tmpDirectory, fixtures.tmpDirectory,
@ -161,7 +206,7 @@ describe('merge', () => {
) )
}) })
it('supports deleting artifacts after merge', async () => { test('supports deleting artifacts after merge', async () => {
mockInputs({ mockInputs({
[Inputs.DeleteMerged]: true [Inputs.DeleteMerged]: true
}) })
@ -169,7 +214,7 @@ describe('merge', () => {
await run() await run()
for (const a of fixtures.artifacts) { for (const a of fixtures.artifacts) {
expect(artifact.deleteArtifact).toHaveBeenCalledWith(a.name) expect(artifact.default.deleteArtifact).toHaveBeenCalledWith(a.name)
} }
}) })
}) })

View File

@ -1,8 +1,37 @@
import * as core from '@actions/core' import {jest, describe, test, expect, beforeAll} from '@jest/globals'
import * as path from 'path' import * as path from 'path'
import * as io from '@actions/io' import * as io from '@actions/io'
import {promises as fs} from 'fs' import {promises as fs} from 'fs'
import {findFilesToUpload} from '../src/shared/search' import {fileURLToPath} from 'url'
// Mock @actions/core to suppress output during tests
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: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}))
const {findFilesToUpload} = await import('../src/shared/search.js')
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.join(__dirname, '_temp', 'search') const root = path.join(__dirname, '_temp', 'search')
const searchItem1Path = path.join( const searchItem1Path = path.join(
@ -77,11 +106,8 @@ const fileInHiddenFolderInFolderA = path.join(
describe('Search', () => { describe('Search', () => {
beforeAll(async () => { beforeAll(async () => {
// mock all output so that there is less noise when running tests // mock console.log to reduce noise
jest.spyOn(console, 'log').mockImplementation(() => {}) jest.spyOn(console, 'log').mockImplementation(() => {})
jest.spyOn(core, 'debug').mockImplementation(() => {})
jest.spyOn(core, 'info').mockImplementation(() => {})
jest.spyOn(core, 'warning').mockImplementation(() => {})
// clear temp directory // clear temp directory
await io.rmRF(root) await io.rmRF(root)
@ -136,43 +162,9 @@ describe('Search', () => {
await fs.writeFile(hiddenFile, 'hidden file') await fs.writeFile(hiddenFile, 'hidden file')
await fs.writeFile(fileInHiddenFolderPath, 'file in hidden directory') await fs.writeFile(fileInHiddenFolderPath, 'file in hidden directory')
await fs.writeFile(fileInHiddenFolderInFolderA, 'file in hidden directory') await fs.writeFile(fileInHiddenFolderInFolderA, 'file in hidden directory')
/*
Directory structure of files that get created:
root/
.hidden-folder/
folder-in-hidden-folder/
file.txt
folder-a/
.hidden-folder-in-folder-a/
file.txt
folder-b/
folder-c/
search-item1.txt
extraSearch-item1.txt
extra-file-in-folder-c.txt
folder-e/
folder-d/
search-item2.txt
search-item3.txt
search-item4.txt
extraSearch-item2.txt
folder-f/
extraSearch-item3.txt
folder-g/
folder-h/
amazing-item.txt
folder-i/
extraSearch-item4.txt
extraSearch-item5.txt
folder-j/
folder-k/
lonely-file.txt
.hidden-file.txt
search-item5.txt
*/
}) })
it('Single file search - Absolute Path', async () => { test('Single file search - Absolute Path', async () => {
const searchResult = await findFilesToUpload(extraFileInFolderCPath) const searchResult = await findFilesToUpload(extraFileInFolderCPath)
expect(searchResult.filesToUpload.length).toEqual(1) expect(searchResult.filesToUpload.length).toEqual(1)
expect(searchResult.filesToUpload[0]).toEqual(extraFileInFolderCPath) expect(searchResult.filesToUpload[0]).toEqual(extraFileInFolderCPath)
@ -181,7 +173,7 @@ describe('Search', () => {
) )
}) })
it('Single file search - Relative Path', async () => { test('Single file search - Relative Path', async () => {
const relativePath = path.join( const relativePath = path.join(
'__tests__', '__tests__',
'_temp', '_temp',
@ -200,7 +192,7 @@ describe('Search', () => {
) )
}) })
it('Single file using wildcard', async () => { test('Single file using wildcard', async () => {
const expectedRoot = path.join(root, 'folder-h') const expectedRoot = path.join(root, 'folder-h')
const searchPath = path.join(root, 'folder-h', '**/*lonely*') const searchPath = path.join(root, 'folder-h', '**/*lonely*')
const searchResult = await findFilesToUpload(searchPath) const searchResult = await findFilesToUpload(searchPath)
@ -209,7 +201,7 @@ describe('Search', () => {
expect(searchResult.rootDirectory).toEqual(expectedRoot) expect(searchResult.rootDirectory).toEqual(expectedRoot)
}) })
it('Single file using directory', async () => { test('Single file using directory', async () => {
const searchPath = path.join(root, 'folder-h', 'folder-j') const searchPath = path.join(root, 'folder-h', 'folder-j')
const searchResult = await findFilesToUpload(searchPath) const searchResult = await findFilesToUpload(searchPath)
expect(searchResult.filesToUpload.length).toEqual(1) expect(searchResult.filesToUpload.length).toEqual(1)
@ -217,7 +209,7 @@ describe('Search', () => {
expect(searchResult.rootDirectory).toEqual(searchPath) expect(searchResult.rootDirectory).toEqual(searchPath)
}) })
it('Directory search - Absolute Path', async () => { test('Directory search - Absolute Path', async () => {
const searchPath = path.join(root, 'folder-h') const searchPath = path.join(root, 'folder-h')
const searchResult = await findFilesToUpload(searchPath) const searchResult = await findFilesToUpload(searchPath)
expect(searchResult.filesToUpload.length).toEqual(4) expect(searchResult.filesToUpload.length).toEqual(4)
@ -236,7 +228,7 @@ describe('Search', () => {
expect(searchResult.rootDirectory).toEqual(searchPath) expect(searchResult.rootDirectory).toEqual(searchPath)
}) })
it('Directory search - Relative Path', async () => { test('Directory search - Relative Path', async () => {
const searchPath = path.join('__tests__', '_temp', 'search', 'folder-h') const searchPath = path.join('__tests__', '_temp', 'search', 'folder-h')
const expectedRootDirectory = path.join(root, 'folder-h') const expectedRootDirectory = path.join(root, 'folder-h')
const searchResult = await findFilesToUpload(searchPath) const searchResult = await findFilesToUpload(searchPath)
@ -256,7 +248,7 @@ describe('Search', () => {
expect(searchResult.rootDirectory).toEqual(expectedRootDirectory) expect(searchResult.rootDirectory).toEqual(expectedRootDirectory)
}) })
it('Wildcard search - Absolute Path', async () => { test('Wildcard search - Absolute Path', async () => {
const searchPath = path.join(root, '**/*[Ss]earch*') const searchPath = path.join(root, '**/*[Ss]earch*')
const searchResult = await findFilesToUpload(searchPath) const searchResult = await findFilesToUpload(searchPath)
expect(searchResult.filesToUpload.length).toEqual(10) expect(searchResult.filesToUpload.length).toEqual(10)
@ -285,7 +277,7 @@ describe('Search', () => {
expect(searchResult.rootDirectory).toEqual(root) expect(searchResult.rootDirectory).toEqual(root)
}) })
it('Wildcard search - Relative Path', async () => { test('Wildcard search - Relative Path', async () => {
const searchPath = path.join( const searchPath = path.join(
'__tests__', '__tests__',
'_temp', '_temp',
@ -319,7 +311,7 @@ describe('Search', () => {
expect(searchResult.rootDirectory).toEqual(root) expect(searchResult.rootDirectory).toEqual(root)
}) })
it('Multi path search - root directory', async () => { test('Multi path search - root directory', async () => {
const searchPath1 = path.join(root, 'folder-a') const searchPath1 = path.join(root, 'folder-a')
const searchPath2 = path.join(root, 'folder-d') const searchPath2 = path.join(root, 'folder-d')
@ -343,7 +335,7 @@ describe('Search', () => {
) )
}) })
it('Multi path search - with exclude character', async () => { test('Multi path search - with exclude character', async () => {
const searchPath1 = path.join(root, 'folder-a') const searchPath1 = path.join(root, 'folder-a')
const searchPath2 = path.join(root, 'folder-d') const searchPath2 = path.join(root, 'folder-d')
const searchPath3 = path.join(root, 'folder-a', 'folder-b', '**/extra*.txt') const searchPath3 = path.join(root, 'folder-a', 'folder-b', '**/extra*.txt')
@ -363,7 +355,7 @@ describe('Search', () => {
) )
}) })
it('Multi path search - non root directory', async () => { test('Multi path search - non root directory', async () => {
const searchPath1 = path.join(root, 'folder-h', 'folder-i') const searchPath1 = path.join(root, 'folder-h', 'folder-i')
const searchPath2 = path.join(root, 'folder-h', 'folder-j', 'folder-k') const searchPath2 = path.join(root, 'folder-h', 'folder-j', 'folder-k')
const searchPath3 = amazingFileInFolderHPath const searchPath3 = amazingFileInFolderHPath
@ -385,7 +377,7 @@ describe('Search', () => {
expect(searchResult.filesToUpload.includes(lonelyFilePath)).toEqual(true) expect(searchResult.filesToUpload.includes(lonelyFilePath)).toEqual(true)
}) })
it('Hidden files ignored by default', async () => { test('Hidden files ignored by default', async () => {
const searchPath = path.join(root, '**/*') const searchPath = path.join(root, '**/*')
const searchResult = await findFilesToUpload(searchPath) const searchResult = await findFilesToUpload(searchPath)
@ -396,7 +388,7 @@ describe('Search', () => {
) )
}) })
it('Hidden files included', async () => { test('Hidden files included', async () => {
const searchPath = path.join(root, '**/*') const searchPath = path.join(root, '**/*')
const searchResult = await findFilesToUpload(searchPath, true) const searchResult = await findFilesToUpload(searchPath, true)

View File

@ -1,9 +1,58 @@
import * as core from '@actions/core' import {jest, describe, test, expect, beforeEach} from '@jest/globals'
import * as github from '@actions/github'
import artifact, {ArtifactNotFoundError} from '@actions/artifact' // Mock @actions/github before importing modules that use it
import {run} from '../src/upload/upload-artifact' jest.unstable_mockModule('@actions/github', () => ({
import {Inputs} from '../src/upload/constants' context: {
import * as search from '../src/shared/search' repo: {
owner: 'actions',
repo: 'toolkit'
},
runId: 123,
serverUrl: 'https://github.com'
},
getOctokit: jest.fn()
}))
// 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: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}))
// Mock shared search module
const mockFindFilesToUpload = jest.fn<
() => Promise<{filesToUpload: string[]; rootDirectory: string}>
>()
jest.unstable_mockModule('../src/shared/search.js', () => ({
findFilesToUpload: mockFindFilesToUpload
}))
// Dynamic imports after mocking
const core = await import('@actions/core')
const github = await import('@actions/github')
const artifact = await import('@actions/artifact')
const {run} = await import('../src/upload/upload-artifact.js')
const {Inputs} = await import('../src/upload/constants.js')
const {ArtifactNotFoundError} = artifact
const fixtures = { const fixtures = {
artifactName: 'artifact-name', artifactName: 'artifact-name',
@ -14,22 +63,10 @@ const fixtures = {
] ]
} }
jest.mock('@actions/github', () => ({ const mockInputs = (
context: { overrides?: Partial<{[K in (typeof Inputs)[keyof typeof Inputs]]?: any}>
repo: { ) => {
owner: 'actions', const inputs: Record<string, any> = {
repo: 'toolkit'
},
runId: 123,
serverUrl: 'https://github.com'
}
}))
jest.mock('@actions/core')
/* eslint-disable no-unused-vars */
const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
const inputs = {
[Inputs.Name]: 'artifact-name', [Inputs.Name]: 'artifact-name',
[Inputs.Path]: '/some/artifact/path', [Inputs.Path]: '/some/artifact/path',
[Inputs.IfNoFilesFound]: 'warn', [Inputs.IfNoFilesFound]: 'warn',
@ -39,10 +76,14 @@ const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
...overrides ...overrides
} }
;(core.getInput as jest.Mock).mockImplementation((name: string) => { ;(core.getInput as jest.Mock<typeof core.getInput>).mockImplementation(
return inputs[name] (name: string) => {
}) return inputs[name]
;(core.getBooleanInput as jest.Mock).mockImplementation((name: string) => { }
)
;(
core.getBooleanInput as jest.Mock<typeof core.getBooleanInput>
).mockImplementation((name: string) => {
return inputs[name] return inputs[name]
}) })
@ -52,28 +93,29 @@ const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
describe('upload', () => { describe('upload', () => {
beforeEach(async () => { beforeEach(async () => {
mockInputs() mockInputs()
jest.clearAllMocks()
jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({ mockFindFilesToUpload.mockResolvedValue({
filesToUpload: fixtures.filesToUpload, filesToUpload: fixtures.filesToUpload,
rootDirectory: fixtures.rootDirectory rootDirectory: fixtures.rootDirectory
}) })
jest.spyOn(artifact, 'uploadArtifact').mockResolvedValue({ jest.spyOn(artifact.default, 'uploadArtifact').mockResolvedValue({
size: 123, size: 123,
id: 1337, id: 1337,
digest: 'facefeed' digest: 'facefeed'
}) })
}) })
it('uploads a single file', async () => { test('uploads a single file', async () => {
jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({ mockFindFilesToUpload.mockResolvedValue({
filesToUpload: [fixtures.filesToUpload[0]], filesToUpload: [fixtures.filesToUpload[0]],
rootDirectory: fixtures.rootDirectory rootDirectory: fixtures.rootDirectory
}) })
await run() await run()
expect(artifact.uploadArtifact).toHaveBeenCalledWith( expect(artifact.default.uploadArtifact).toHaveBeenCalledWith(
fixtures.artifactName, fixtures.artifactName,
[fixtures.filesToUpload[0]], [fixtures.filesToUpload[0]],
fixtures.rootDirectory, fixtures.rootDirectory,
@ -81,10 +123,10 @@ describe('upload', () => {
) )
}) })
it('uploads multiple files', async () => { test('uploads multiple files', async () => {
await run() await run()
expect(artifact.uploadArtifact).toHaveBeenCalledWith( expect(artifact.default.uploadArtifact).toHaveBeenCalledWith(
fixtures.artifactName, fixtures.artifactName,
fixtures.filesToUpload, fixtures.filesToUpload,
fixtures.rootDirectory, fixtures.rootDirectory,
@ -92,27 +134,25 @@ describe('upload', () => {
) )
}) })
it('sets outputs', async () => { test('sets outputs', async () => {
await run() await run()
expect(core.setOutput).toHaveBeenCalledWith('artifact-id', 1337) expect(core.setOutput).toHaveBeenCalledWith('artifact-id', 1337)
expect(core.setOutput).toHaveBeenCalledWith('artifact-digest', 'facefeed') expect(core.setOutput).toHaveBeenCalledWith('artifact-digest', 'facefeed')
expect(core.setOutput).toHaveBeenCalledWith( expect(core.setOutput).toHaveBeenCalledWith(
'artifact-url', 'artifact-url',
`${github.context.serverUrl}/${github.context.repo.owner}/${ `${github.context.serverUrl}/${github.context.repo.owner}/${github.context.repo.repo}/actions/runs/${github.context.runId}/artifacts/${1337}`
github.context.repo.repo
}/actions/runs/${github.context.runId}/artifacts/${1337}`
) )
}) })
it('supports custom compression level', async () => { test('supports custom compression level', async () => {
mockInputs({ mockInputs({
[Inputs.CompressionLevel]: 2 [Inputs.CompressionLevel]: 2
}) })
await run() await run()
expect(artifact.uploadArtifact).toHaveBeenCalledWith( expect(artifact.default.uploadArtifact).toHaveBeenCalledWith(
fixtures.artifactName, fixtures.artifactName,
fixtures.filesToUpload, fixtures.filesToUpload,
fixtures.rootDirectory, fixtures.rootDirectory,
@ -120,14 +160,14 @@ describe('upload', () => {
) )
}) })
it('supports custom retention days', async () => { test('supports custom retention days', async () => {
mockInputs({ mockInputs({
[Inputs.RetentionDays]: 7 [Inputs.RetentionDays]: 7
}) })
await run() await run()
expect(artifact.uploadArtifact).toHaveBeenCalledWith( expect(artifact.default.uploadArtifact).toHaveBeenCalledWith(
fixtures.artifactName, fixtures.artifactName,
fixtures.filesToUpload, fixtures.filesToUpload,
fixtures.rootDirectory, fixtures.rootDirectory,
@ -135,12 +175,12 @@ describe('upload', () => {
) )
}) })
it('supports warn if-no-files-found', async () => { test('supports warn if-no-files-found', async () => {
mockInputs({ mockInputs({
[Inputs.IfNoFilesFound]: 'warn' [Inputs.IfNoFilesFound]: 'warn'
}) })
jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({ mockFindFilesToUpload.mockResolvedValue({
filesToUpload: [], filesToUpload: [],
rootDirectory: fixtures.rootDirectory rootDirectory: fixtures.rootDirectory
}) })
@ -152,12 +192,12 @@ describe('upload', () => {
) )
}) })
it('supports error if-no-files-found', async () => { test('supports error if-no-files-found', async () => {
mockInputs({ mockInputs({
[Inputs.IfNoFilesFound]: 'error' [Inputs.IfNoFilesFound]: 'error'
}) })
jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({ mockFindFilesToUpload.mockResolvedValue({
filesToUpload: [], filesToUpload: [],
rootDirectory: fixtures.rootDirectory rootDirectory: fixtures.rootDirectory
}) })
@ -169,12 +209,12 @@ describe('upload', () => {
) )
}) })
it('supports ignore if-no-files-found', async () => { test('supports ignore if-no-files-found', async () => {
mockInputs({ mockInputs({
[Inputs.IfNoFilesFound]: 'ignore' [Inputs.IfNoFilesFound]: 'ignore'
}) })
jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({ mockFindFilesToUpload.mockResolvedValue({
filesToUpload: [], filesToUpload: [],
rootDirectory: fixtures.rootDirectory rootDirectory: fixtures.rootDirectory
}) })
@ -186,46 +226,50 @@ describe('upload', () => {
) )
}) })
it('supports overwrite', async () => { test('supports overwrite', async () => {
mockInputs({ mockInputs({
[Inputs.Overwrite]: true [Inputs.Overwrite]: true
}) })
jest.spyOn(artifact, 'deleteArtifact').mockResolvedValue({ jest.spyOn(artifact.default, 'deleteArtifact').mockResolvedValue({
id: 1337 id: 1337
}) })
await run() await run()
expect(artifact.uploadArtifact).toHaveBeenCalledWith( expect(artifact.default.uploadArtifact).toHaveBeenCalledWith(
fixtures.artifactName, fixtures.artifactName,
fixtures.filesToUpload, fixtures.filesToUpload,
fixtures.rootDirectory, fixtures.rootDirectory,
{compressionLevel: 6} {compressionLevel: 6}
) )
expect(artifact.deleteArtifact).toHaveBeenCalledWith(fixtures.artifactName) expect(artifact.default.deleteArtifact).toHaveBeenCalledWith(
fixtures.artifactName
)
}) })
it('supports overwrite and continues if not found', async () => { test('supports overwrite and continues if not found', async () => {
mockInputs({ mockInputs({
[Inputs.Overwrite]: true [Inputs.Overwrite]: true
}) })
jest jest
.spyOn(artifact, 'deleteArtifact') .spyOn(artifact.default, 'deleteArtifact')
.mockRejectedValue(new ArtifactNotFoundError('not found')) .mockRejectedValue(new ArtifactNotFoundError('not found'))
await run() await run()
expect(artifact.uploadArtifact).toHaveBeenCalledWith( expect(artifact.default.uploadArtifact).toHaveBeenCalledWith(
fixtures.artifactName, fixtures.artifactName,
fixtures.filesToUpload, fixtures.filesToUpload,
fixtures.rootDirectory, fixtures.rootDirectory,
{compressionLevel: 6} {compressionLevel: 6}
) )
expect(artifact.deleteArtifact).toHaveBeenCalledWith(fixtures.artifactName) expect(artifact.default.deleteArtifact).toHaveBeenCalledWith(
fixtures.artifactName
)
expect(core.debug).toHaveBeenCalledWith( expect(core.debug).toHaveBeenCalledWith(
`Skipping deletion of '${fixtures.artifactName}', it does not exist` `Skipping deletion of '${fixtures.artifactName}', it does not exist`
) )

53
eslint.config.mjs Normal file
View File

@ -0,0 +1,53 @@
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': ['error', {endOfLine: 'auto'}],
'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-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'
}
}
]

View File

@ -1,12 +0,0 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
roots: ['<rootDir>'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}

24
jest.config.ts Normal file
View File

@ -0,0 +1,24 @@
export default {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
roots: ['<rootDir>'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
useESM: true,
diagnostics: {
ignoreCodes: [151002]
}
}
]
},
extensionsToTreatAsEsm: ['.ts'],
transformIgnorePatterns: ['node_modules/(?!(@actions)/)'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1'
},
verbose: true
}

4426
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
"name": "upload-artifact", "name": "upload-artifact",
"version": "6.0.0", "version": "6.0.0",
"description": "Upload an Actions Artifact in a workflow run", "description": "Upload an Actions Artifact in a workflow run",
"type": "module",
"main": "dist/upload/index.js", "main": "dist/upload/index.js",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
@ -10,7 +11,7 @@
"format": "prettier --write **/*.ts", "format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts", "format-check": "prettier --check **/*.ts",
"lint": "eslint **/*.ts", "lint": "eslint **/*.ts",
"test": "jest --testTimeout 10000" "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testTimeout 10000"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -32,32 +33,29 @@
"node": ">=24" "node": ">=24"
}, },
"dependencies": { "dependencies": {
"@actions/artifact": "^5.0.0", "@actions/artifact": "^6.1.0",
"@actions/core": "^2.0.0", "@actions/core": "^3.0.0",
"@actions/github": "^6.0.0", "@actions/github": "^9.0.0",
"@actions/glob": "^0.5.0", "@actions/glob": "^0.6.1",
"@actions/io": "^2.0.0", "@actions/io": "^3.0.2",
"minimatch": "^9.0.3" "minimatch": "^9.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.2.5", "@types/jest": "^30.0.0",
"@types/node": "^24.1.0", "@types/node": "^25.1.0",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^8.54.0",
"@vercel/ncc": "^0.36.0", "@vercel/ncc": "^0.38.4",
"concurrently": "^7.6.0", "concurrently": "^9.2.1",
"eslint": "^8.31.0", "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8", "eslint-plugin-github": "^6.0.0",
"eslint-plugin-github": "^4.6.0", "eslint-plugin-jest": "^29.12.1",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-jest": "^28.14.0", "jest": "^30.2.0",
"eslint-plugin-prettier": "^5.5.4", "prettier": "^3.8.1",
"glob": "^8.0.3", "ts-jest": "^29.2.6",
"jest": "^29.3.1", "ts-node": "^10.9.2",
"jest-circus": "^29.3.1", "typescript": "^5.3.3"
"prettier": "^3.7.4",
"ts-jest": "^29.0.3",
"typescript": "^5.2.2"
}, },
"overrides": { "overrides": {
"uri-js": "npm:uri-js-replace@^1.0.1" "uri-js": "npm:uri-js-replace@^1.0.1"

View File

@ -1,5 +1,5 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {run} from './merge-artifacts' import {run} from './merge-artifacts.js'
run().catch(error => { run().catch(error => {
core.setFailed((error as Error).message) core.setFailed((error as Error).message)

View File

@ -1,6 +1,6 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Inputs} from './constants' import {Inputs} from './constants.js'
import {MergeInputs} from './merge-inputs' import {MergeInputs} from './merge-inputs.js'
/** /**
* Helper to get all the inputs for the action * Helper to get all the inputs for the action

View File

@ -3,9 +3,9 @@ import {mkdtemp, rm} from 'fs/promises'
import * as core from '@actions/core' import * as core from '@actions/core'
import {Minimatch} from 'minimatch' import {Minimatch} from 'minimatch'
import artifactClient, {UploadArtifactOptions} from '@actions/artifact' import artifactClient, {UploadArtifactOptions} from '@actions/artifact'
import {getInputs} from './input-helper' import {getInputs} from './input-helper.js'
import {uploadArtifact} from '../shared/upload-artifact' import {uploadArtifact} from '../shared/upload-artifact.js'
import {findFilesToUpload} from '../shared/search' import {findFilesToUpload} from '../shared/search.js'
const PARALLEL_DOWNLOADS = 5 const PARALLEL_DOWNLOADS = 5

View File

@ -1,5 +1,5 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {run} from './upload-artifact' import {run} from './upload-artifact.js'
run().catch(error => { run().catch(error => {
core.setFailed((error as Error).message) core.setFailed((error as Error).message)

View File

@ -1,6 +1,6 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Inputs, NoFileOptions} from './constants' import {Inputs, NoFileOptions} from './constants.js'
import {UploadInputs} from './upload-inputs' import {UploadInputs} from './upload-inputs.js'
/** /**
* Helper to get all the inputs for the action * Helper to get all the inputs for the action

View File

@ -3,10 +3,10 @@ import artifact, {
UploadArtifactOptions, UploadArtifactOptions,
ArtifactNotFoundError ArtifactNotFoundError
} from '@actions/artifact' } from '@actions/artifact'
import {findFilesToUpload} from '../shared/search' import {findFilesToUpload} from '../shared/search.js'
import {getInputs} from './input-helper' import {getInputs} from './input-helper.js'
import {NoFileOptions} from './constants' import {NoFileOptions} from './constants.js'
import {uploadArtifact} from '../shared/upload-artifact' import {uploadArtifact} from '../shared/upload-artifact.js'
async function deleteArtifactIfExists(artifactName: string): Promise<void> { async function deleteArtifactIfExists(artifactName: string): Promise<void> {
try { try {

View File

@ -1,4 +1,4 @@
import {NoFileOptions} from './constants' import {NoFileOptions} from './constants.js'
export interface UploadInputs { export interface UploadInputs {
/** /**

8
tsconfig.eslint.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["src/**/*.ts", "__tests__/**/*.ts", "*.ts"],
"exclude": ["node_modules", "lib", "dist"]
}

View File

@ -1,17 +1,13 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es6", "target": "ES2022",
"module": "commonjs", "module": "NodeNext",
"outDir": "./lib", "outDir": "./lib",
"rootDir": "./src", "rootDir": "./src",
"strict": true, "strict": true,
"noImplicitAny": false, "noImplicitAny": false,
"moduleResolution": "node", "moduleResolution": "NodeNext",
"allowSyntheticDefaultImports": true, "esModuleInterop": true
"esModuleInterop": true, },
"declaration": false, "exclude": ["node_modules", "**/*.test.ts", "jest.config.ts", "__tests__"]
"sourceMap": true, }
"lib": ["es6"]
},
"exclude": ["node_modules", "**/*.test.ts"]
}