Skip to content

Commit 52899c8

Browse files
authoredJan 22, 2024
Merge pull request #504 from actions/robherley/reorganize
Reorganize upload code in prep for merge logic & add more tests
2 parents 694cdab + da58a3f commit 52899c8

14 files changed

+593
-256
lines changed
 

‎.github/workflows/check-dist.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
run: npm ci
3535

3636
- name: Rebuild the dist/ directory
37-
run: npm run build
37+
run: npm run release
3838

3939
- name: Compare the expected and actual dist/ directories
4040
run: |

‎__tests__/search.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as core from '@actions/core'
22
import * as path from 'path'
33
import * as io from '@actions/io'
44
import {promises as fs} from 'fs'
5-
import {findFilesToUpload} from '../src/search'
5+
import {findFilesToUpload} from '../src/shared/search'
66

77
const root = path.join(__dirname, '_temp', 'search')
88
const searchItem1Path = path.join(

‎__tests__/upload.test.ts

+231
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import * as core from '@actions/core'
2+
import * as github from '@actions/github'
3+
import artifact, {ArtifactNotFoundError} from '@actions/artifact'
4+
import {run} from '../src/upload/upload-artifact'
5+
import {Inputs} from '../src/upload/constants'
6+
import * as search from '../src/shared/search'
7+
8+
const fixtures = {
9+
artifactName: 'artifact-name',
10+
rootDirectory: '/some/artifact/path',
11+
filesToUpload: [
12+
'/some/artifact/path/file1.txt',
13+
'/some/artifact/path/file2.txt'
14+
]
15+
}
16+
17+
jest.mock('@actions/github', () => ({
18+
context: {
19+
repo: {
20+
owner: 'actions',
21+
repo: 'toolkit'
22+
},
23+
runId: 123,
24+
serverUrl: 'https://github.com'
25+
}
26+
}))
27+
28+
jest.mock('@actions/core')
29+
30+
/* eslint-disable no-unused-vars */
31+
const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
32+
const inputs = {
33+
[Inputs.Name]: 'artifact-name',
34+
[Inputs.Path]: '/some/artifact/path',
35+
[Inputs.IfNoFilesFound]: 'warn',
36+
[Inputs.RetentionDays]: 0,
37+
[Inputs.CompressionLevel]: 6,
38+
[Inputs.Overwrite]: false,
39+
...overrides
40+
}
41+
42+
;(core.getInput as jest.Mock).mockImplementation((name: string) => {
43+
return inputs[name]
44+
})
45+
;(core.getBooleanInput as jest.Mock).mockImplementation((name: string) => {
46+
return inputs[name]
47+
})
48+
49+
return inputs
50+
}
51+
52+
describe('upload', () => {
53+
beforeEach(async () => {
54+
mockInputs()
55+
56+
jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({
57+
filesToUpload: fixtures.filesToUpload,
58+
rootDirectory: fixtures.rootDirectory
59+
})
60+
61+
jest.spyOn(artifact, 'uploadArtifact').mockResolvedValue({
62+
size: 123,
63+
id: 1337
64+
})
65+
})
66+
67+
it('uploads a single file', async () => {
68+
jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({
69+
filesToUpload: [fixtures.filesToUpload[0]],
70+
rootDirectory: fixtures.rootDirectory
71+
})
72+
73+
await run()
74+
75+
expect(artifact.uploadArtifact).toHaveBeenCalledWith(
76+
fixtures.artifactName,
77+
[fixtures.filesToUpload[0]],
78+
fixtures.rootDirectory,
79+
{compressionLevel: 6}
80+
)
81+
})
82+
83+
it('uploads multiple files', async () => {
84+
await run()
85+
86+
expect(artifact.uploadArtifact).toHaveBeenCalledWith(
87+
fixtures.artifactName,
88+
fixtures.filesToUpload,
89+
fixtures.rootDirectory,
90+
{compressionLevel: 6}
91+
)
92+
})
93+
94+
it('sets outputs', async () => {
95+
await run()
96+
97+
expect(core.setOutput).toHaveBeenCalledWith('artifact-id', 1337)
98+
expect(core.setOutput).toHaveBeenCalledWith(
99+
'artifact-url',
100+
`${github.context.serverUrl}/${github.context.repo.owner}/${
101+
github.context.repo.repo
102+
}/actions/runs/${github.context.runId}/artifacts/${1337}`
103+
)
104+
})
105+
106+
it('supports custom compression level', async () => {
107+
mockInputs({
108+
[Inputs.CompressionLevel]: 2
109+
})
110+
111+
await run()
112+
113+
expect(artifact.uploadArtifact).toHaveBeenCalledWith(
114+
fixtures.artifactName,
115+
fixtures.filesToUpload,
116+
fixtures.rootDirectory,
117+
{compressionLevel: 2}
118+
)
119+
})
120+
121+
it('supports custom retention days', async () => {
122+
mockInputs({
123+
[Inputs.RetentionDays]: 7
124+
})
125+
126+
await run()
127+
128+
expect(artifact.uploadArtifact).toHaveBeenCalledWith(
129+
fixtures.artifactName,
130+
fixtures.filesToUpload,
131+
fixtures.rootDirectory,
132+
{retentionDays: 7, compressionLevel: 6}
133+
)
134+
})
135+
136+
it('supports warn if-no-files-found', async () => {
137+
mockInputs({
138+
[Inputs.IfNoFilesFound]: 'warn'
139+
})
140+
141+
jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({
142+
filesToUpload: [],
143+
rootDirectory: fixtures.rootDirectory
144+
})
145+
146+
await run()
147+
148+
expect(core.warning).toHaveBeenCalledWith(
149+
`No files were found with the provided path: ${fixtures.rootDirectory}. No artifacts will be uploaded.`
150+
)
151+
})
152+
153+
it('supports error if-no-files-found', async () => {
154+
mockInputs({
155+
[Inputs.IfNoFilesFound]: 'error'
156+
})
157+
158+
jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({
159+
filesToUpload: [],
160+
rootDirectory: fixtures.rootDirectory
161+
})
162+
163+
await run()
164+
165+
expect(core.setFailed).toHaveBeenCalledWith(
166+
`No files were found with the provided path: ${fixtures.rootDirectory}. No artifacts will be uploaded.`
167+
)
168+
})
169+
170+
it('supports ignore if-no-files-found', async () => {
171+
mockInputs({
172+
[Inputs.IfNoFilesFound]: 'ignore'
173+
})
174+
175+
jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({
176+
filesToUpload: [],
177+
rootDirectory: fixtures.rootDirectory
178+
})
179+
180+
await run()
181+
182+
expect(core.info).toHaveBeenCalledWith(
183+
`No files were found with the provided path: ${fixtures.rootDirectory}. No artifacts will be uploaded.`
184+
)
185+
})
186+
187+
it('supports overwrite', async () => {
188+
mockInputs({
189+
[Inputs.Overwrite]: true
190+
})
191+
192+
jest.spyOn(artifact, 'deleteArtifact').mockResolvedValue({
193+
id: 1337
194+
})
195+
196+
await run()
197+
198+
expect(artifact.uploadArtifact).toHaveBeenCalledWith(
199+
fixtures.artifactName,
200+
fixtures.filesToUpload,
201+
fixtures.rootDirectory,
202+
{compressionLevel: 6}
203+
)
204+
205+
expect(artifact.deleteArtifact).toHaveBeenCalledWith(fixtures.artifactName)
206+
})
207+
208+
it('supports overwrite and continues if not found', async () => {
209+
mockInputs({
210+
[Inputs.Overwrite]: true
211+
})
212+
213+
jest
214+
.spyOn(artifact, 'deleteArtifact')
215+
.mockRejectedValue(new ArtifactNotFoundError('not found'))
216+
217+
await run()
218+
219+
expect(artifact.uploadArtifact).toHaveBeenCalledWith(
220+
fixtures.artifactName,
221+
fixtures.filesToUpload,
222+
fixtures.rootDirectory,
223+
{compressionLevel: 6}
224+
)
225+
226+
expect(artifact.deleteArtifact).toHaveBeenCalledWith(fixtures.artifactName)
227+
expect(core.debug).toHaveBeenCalledWith(
228+
`Skipping deletion of '${fixtures.artifactName}', it does not exist`
229+
)
230+
})
231+
})

‎action.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,4 @@ outputs:
5858
Common uses cases for such a download URL can be adding download links to artifacts in descriptions or comments on pull requests or issues.
5959
runs:
6060
using: 'node20'
61-
main: 'dist/index.js'
61+
main: 'dist/upload/index.js'

‎dist/index.js ‎dist/upload/index.js

+246-157
Large diffs are not rendered by default.

‎package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
"name": "upload-artifact",
33
"version": "4.2.0",
44
"description": "Upload an Actions Artifact in a workflow run",
5-
"main": "dist/index.js",
5+
"main": "dist/upload/index.js",
66
"scripts": {
77
"build": "tsc",
8-
"release": "ncc build src/upload-artifact.ts && git add -f dist/index.js",
8+
"release": "ncc build src/upload/index.ts -o dist/upload",
99
"check-all": "concurrently \"npm:format-check\" \"npm:lint\" \"npm:test\" \"npm:build\"",
1010
"format": "prettier --write **/*.ts",
1111
"format-check": "prettier --check **/*.ts",

‎src/search.ts ‎src/shared/search.ts

File renamed without changes.

‎src/shared/upload-artifact.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as core from '@actions/core'
2+
import * as github from '@actions/github'
3+
import artifact, {UploadArtifactOptions} from '@actions/artifact'
4+
5+
export async function uploadArtifact(
6+
artifactName: string,
7+
filesToUpload: string[],
8+
rootDirectory: string,
9+
options: UploadArtifactOptions
10+
) {
11+
const uploadResponse = await artifact.uploadArtifact(
12+
artifactName,
13+
filesToUpload,
14+
rootDirectory,
15+
options
16+
)
17+
18+
core.info(
19+
`Artifact ${artifactName} has been successfully uploaded! Final size is ${uploadResponse.size} bytes. Artifact ID is ${uploadResponse.id}`
20+
)
21+
core.setOutput('artifact-id', uploadResponse.id)
22+
23+
const repository = github.context.repo
24+
const artifactURL = `${github.context.serverUrl}/${repository.owner}/${repository.repo}/actions/runs/${github.context.runId}/artifacts/${uploadResponse.id}`
25+
26+
core.info(`Artifact download URL: ${artifactURL}`)
27+
core.setOutput('artifact-url', artifactURL)
28+
}

‎src/upload-artifact.ts

-94
This file was deleted.
File renamed without changes.

‎src/upload/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import * as core from '@actions/core'
2+
import {run} from './upload-artifact'
3+
4+
run().catch(error => {
5+
core.setFailed((error as Error).message)
6+
})
File renamed without changes.

‎src/upload/upload-artifact.ts

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as core from '@actions/core'
2+
import artifact, {
3+
UploadArtifactOptions,
4+
ArtifactNotFoundError
5+
} from '@actions/artifact'
6+
import {findFilesToUpload} from '../shared/search'
7+
import {getInputs} from './input-helper'
8+
import {NoFileOptions} from './constants'
9+
import {uploadArtifact} from '../shared/upload-artifact'
10+
11+
async function deleteArtifactIfExists(artifactName: string): Promise<void> {
12+
try {
13+
await artifact.deleteArtifact(artifactName)
14+
} catch (error) {
15+
if (error instanceof ArtifactNotFoundError) {
16+
core.debug(`Skipping deletion of '${artifactName}', it does not exist`)
17+
return
18+
}
19+
20+
// Best effort, we don't want to fail the action if this fails
21+
core.debug(`Unable to delete artifact: ${(error as Error).message}`)
22+
}
23+
}
24+
25+
export async function run(): Promise<void> {
26+
const inputs = getInputs()
27+
const searchResult = await findFilesToUpload(inputs.searchPath)
28+
if (searchResult.filesToUpload.length === 0) {
29+
// No files were found, different use cases warrant different types of behavior if nothing is found
30+
switch (inputs.ifNoFilesFound) {
31+
case NoFileOptions.warn: {
32+
core.warning(
33+
`No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.`
34+
)
35+
break
36+
}
37+
case NoFileOptions.error: {
38+
core.setFailed(
39+
`No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.`
40+
)
41+
break
42+
}
43+
case NoFileOptions.ignore: {
44+
core.info(
45+
`No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.`
46+
)
47+
break
48+
}
49+
}
50+
} else {
51+
const s = searchResult.filesToUpload.length === 1 ? '' : 's'
52+
core.info(
53+
`With the provided path, there will be ${searchResult.filesToUpload.length} file${s} uploaded`
54+
)
55+
core.debug(`Root artifact directory is ${searchResult.rootDirectory}`)
56+
57+
if (inputs.overwrite) {
58+
await deleteArtifactIfExists(inputs.artifactName)
59+
}
60+
61+
const options: UploadArtifactOptions = {}
62+
if (inputs.retentionDays) {
63+
options.retentionDays = inputs.retentionDays
64+
}
65+
66+
if (typeof inputs.compressionLevel !== 'undefined') {
67+
options.compressionLevel = inputs.compressionLevel
68+
}
69+
70+
await uploadArtifact(
71+
inputs.artifactName,
72+
searchResult.filesToUpload,
73+
searchResult.rootDirectory,
74+
options
75+
)
76+
}
77+
}
File renamed without changes.

0 commit comments

Comments
 (0)
Please sign in to comment.