Skip to content

Commit 1b5783f

Browse files
authored
Add Support for @actions/artifact (#136)
This PR introduces support for stubbing the functionality of the [`@actions/artifact`](https://github.com/actions/toolkit/blob/main/packages/artifact/README.md) package. This is accomplished via the following: - Copies all required components of the `@actions/artifact` package. - Adds a new environment variable, `LOCAL_ACTION_ARTIFACT_PATH`, which can be used to set the local directory where `@actions/artifact` will _"upload"_ and _"download"_ artifacts. - Adds a check for the `LOCAL_ACTION_ARTIFACT_PATH` environment variable whenever `@actions/artifact` operations are performed. - Adds new properties to environment metadata for tracking artifacts that have been created as part of a `local-action` run (currently no cleanup of those artifacts is performed). > [!IMPORTANT] > > Operations on artifacts in external repositories is left implemented **as-is**. If you specify the `findBy` options as noted [here](https://github.com/actions/toolkit/blob/main/packages/artifact/README.md#downloading-from-other-workflow-runs-or-repos), the corresponding GitHub API will be called.
2 parents 9c84a45 + 45c064a commit 1b5783f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+4208
-92
lines changed

.env.example

+7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ ACTIONS_STEP_DEBUG=true
99
# Hyphens should not be converted to underscores!
1010
INPUT_MILLISECONDS=2400
1111

12+
# Environment variables specific to the @github/local-action tool.
13+
#
14+
# LOCAL_ACTION_ARTIFACT_PATH: Local path where any artifacts will be saved. Will
15+
# throw an error if the action attempts to use the
16+
# @actions/artifact package without setting this.
17+
LOCAL_ACTION_ARTIFACT_PATH=""
18+
1219
# GitHub Actions default environment variables. These are set for every run of a
1320
# workflow and can be used in your actions. Setting the value here will override
1421
# any value set by the local-action tool.

__fixtures__/crypto.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { jest } from '@jest/globals'
2+
3+
export const createHash = jest.fn()
4+
5+
export default {
6+
createHash
7+
}

__fixtures__/fs.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { jest } from '@jest/globals'
2+
3+
export const accessSync = jest.fn()
4+
export const createWriteStream = jest.fn()
5+
export const createReadStream = jest.fn()
6+
export const mkdirSync = jest.fn()
7+
export const rmSync = jest.fn()
8+
9+
export default {
10+
accessSync,
11+
createWriteStream,
12+
createReadStream,
13+
mkdirSync,
14+
rmSync
15+
}

__fixtures__/stream/promises.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { jest } from '@jest/globals'
2+
3+
export const finished = jest.fn()

__tests__/command.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { jest } from '@jest/globals'
22
import { Command } from 'commander'
3-
import { ResetCoreMetadata } from '../src/stubs/core-stubs.js'
4-
import { ResetEnvMetadata } from '../src/stubs/env-stubs.js'
3+
import { ResetCoreMetadata } from '../src/stubs/core.js'
4+
import { ResetEnvMetadata } from '../src/stubs/env.js'
55

66
const action = jest.fn()
77

__tests__/commands/run.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { jest } from '@jest/globals'
22
import * as core from '../../__fixtures__/core.js'
3-
import { ResetCoreMetadata } from '../../src/stubs/core-stubs.js'
4-
import { EnvMeta, ResetEnvMetadata } from '../../src/stubs/env-stubs.js'
3+
import { ResetCoreMetadata } from '../../src/stubs/core.js'
4+
import { EnvMeta, ResetEnvMetadata } from '../../src/stubs/env.js'
55

66
const quibbleEsm = jest.fn().mockImplementation(() => {})
77
const quibbleDefault = jest.fn().mockImplementation(() => {})

__tests__/index.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { jest } from '@jest/globals'
2-
import { ResetCoreMetadata } from '../src/stubs/core-stubs.js'
3-
import { ResetEnvMetadata } from '../src/stubs/env-stubs.js'
2+
import { ResetCoreMetadata } from '../src/stubs/core.js'
3+
import { ResetEnvMetadata } from '../src/stubs/env.js'
44

55
const makeProgram = jest.fn().mockResolvedValue({
66
parse: jest.fn()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import { jest } from '@jest/globals'
2+
import * as core from '../../../../__fixtures__/core.js'
3+
import { ResetCoreMetadata } from '../../../../src/stubs/core.js'
4+
import { EnvMeta, ResetEnvMetadata } from '../../../../src/stubs/env.js'
5+
6+
const isGhes = jest.fn().mockReturnValue(false)
7+
const getGitHubWorkspaceDir = jest.fn().mockReturnValue('/github/workspace')
8+
const getUploadChunkSize = jest.fn().mockReturnValue(8 * 1024 * 1024)
9+
const uploadArtifact = jest.fn()
10+
const downloadArtifactInternal = jest.fn()
11+
const downloadArtifactPublic = jest.fn()
12+
const listArtifactsInternal = jest.fn()
13+
const listArtifactsPublic = jest.fn()
14+
const getArtifactInternal = jest.fn()
15+
const getArtifactPublic = jest.fn()
16+
const deleteArtifactInternal = jest.fn()
17+
const deleteArtifactPublic = jest.fn()
18+
19+
jest.unstable_mockModule(
20+
'../../../../src/stubs/artifact/internal/shared/config.js',
21+
() => ({
22+
isGhes,
23+
getGitHubWorkspaceDir,
24+
getUploadChunkSize
25+
})
26+
)
27+
jest.unstable_mockModule(
28+
'../../../../src/stubs/artifact/internal/upload/upload-artifact.js',
29+
() => ({
30+
uploadArtifact
31+
})
32+
)
33+
jest.unstable_mockModule(
34+
'../../../../src/stubs/artifact/internal/download/download-artifact.js',
35+
() => ({
36+
downloadArtifactPublic,
37+
downloadArtifactInternal
38+
})
39+
)
40+
jest.unstable_mockModule(
41+
'../../../../src/stubs/artifact/internal/find/list-artifacts.js',
42+
() => ({
43+
listArtifactsInternal,
44+
listArtifactsPublic
45+
})
46+
)
47+
jest.unstable_mockModule(
48+
'../../../../src/stubs/artifact/internal/find/get-artifact.js',
49+
() => ({
50+
getArtifactInternal,
51+
getArtifactPublic
52+
})
53+
)
54+
jest.unstable_mockModule(
55+
'../../../../src/stubs/artifact/internal/delete/delete-artifact.js',
56+
() => ({
57+
deleteArtifactInternal,
58+
deleteArtifactPublic
59+
})
60+
)
61+
jest.unstable_mockModule('../../../../src/stubs/core.js', () => core)
62+
63+
const { DefaultArtifactClient } = await import(
64+
'../../../../src/stubs/artifact/internal/client.js'
65+
)
66+
67+
describe('DefaultArtifactClient', () => {
68+
beforeEach(() => {
69+
// Set environment variables
70+
process.env.LOCAL_ACTION_ARTIFACT_PATH = '/tmp/artifacts'
71+
72+
// Reset metadata
73+
ResetEnvMetadata()
74+
ResetCoreMetadata()
75+
76+
EnvMeta.artifacts = [{ name: 'artifact-name', id: 1, size: 0 }]
77+
})
78+
79+
afterEach(() => {
80+
// Reset all spies
81+
jest.resetAllMocks()
82+
83+
// Unset environment variables
84+
delete process.env.LOCAL_ACTION_ARTIFACT_PATH
85+
})
86+
87+
describe('uploadArtifact', () => {
88+
it('Throws if LOCAL_ACTION_ARTIFACT_PATH is not set', async () => {
89+
delete process.env.LOCAL_ACTION_ARTIFACT_PATH
90+
91+
const client = new DefaultArtifactClient()
92+
93+
await expect(
94+
client.uploadArtifact('artifact-name', ['file1', 'file2'], 'root')
95+
).rejects.toThrow()
96+
})
97+
98+
it('Throws if running on GHES', async () => {
99+
isGhes.mockReturnValue(true)
100+
101+
const client = new DefaultArtifactClient()
102+
103+
await expect(
104+
client.uploadArtifact('artifact-name', ['file1', 'file2'], 'root')
105+
).rejects.toThrow()
106+
})
107+
108+
it('Uploads an artifact', async () => {
109+
const client = new DefaultArtifactClient()
110+
111+
await client.uploadArtifact('artifact-name', ['file1', 'file2'], 'root')
112+
113+
expect(uploadArtifact).toHaveBeenCalled()
114+
})
115+
})
116+
117+
describe('downloadArtifact', () => {
118+
it('Throws if LOCAL_ACTION_ARTIFACT_PATH is not set', async () => {
119+
delete process.env.LOCAL_ACTION_ARTIFACT_PATH
120+
121+
const client = new DefaultArtifactClient()
122+
123+
await expect(client.downloadArtifact(1)).rejects.toThrow()
124+
})
125+
126+
it('Throws if running on GHES', async () => {
127+
isGhes.mockReturnValue(true)
128+
129+
const client = new DefaultArtifactClient()
130+
131+
await expect(client.downloadArtifact(1)).rejects.toThrow()
132+
})
133+
134+
it('Downloads an artifact (internal)', async () => {
135+
const client = new DefaultArtifactClient()
136+
137+
await client.downloadArtifact(1)
138+
139+
expect(downloadArtifactInternal).toHaveBeenCalled()
140+
expect(downloadArtifactPublic).not.toHaveBeenCalled()
141+
})
142+
143+
it('Downloads an artifact (public)', async () => {
144+
const client = new DefaultArtifactClient()
145+
146+
await client.downloadArtifact(1, {
147+
findBy: {
148+
repositoryOwner: 'owner',
149+
repositoryName: 'repo',
150+
workflowRunId: 1,
151+
token: 'token'
152+
}
153+
})
154+
155+
expect(downloadArtifactInternal).not.toHaveBeenCalled()
156+
expect(downloadArtifactPublic).toHaveBeenCalled()
157+
})
158+
})
159+
160+
describe('listArtifacts', () => {
161+
it('Throws if LOCAL_ACTION_ARTIFACT_PATH is not set', async () => {
162+
delete process.env.LOCAL_ACTION_ARTIFACT_PATH
163+
164+
const client = new DefaultArtifactClient()
165+
166+
await expect(client.listArtifacts()).rejects.toThrow()
167+
})
168+
169+
it('Throws if running on GHES', async () => {
170+
isGhes.mockReturnValue(true)
171+
172+
const client = new DefaultArtifactClient()
173+
174+
await expect(client.listArtifacts()).rejects.toThrow()
175+
})
176+
177+
it('Lists artifacts (internal)', async () => {
178+
const client = new DefaultArtifactClient()
179+
180+
await client.listArtifacts()
181+
182+
expect(listArtifactsInternal).toHaveBeenCalled()
183+
expect(listArtifactsPublic).not.toHaveBeenCalled()
184+
})
185+
186+
it('Lists artifacts (public)', async () => {
187+
const client = new DefaultArtifactClient()
188+
189+
await client.listArtifacts({
190+
findBy: {
191+
repositoryOwner: 'owner',
192+
repositoryName: 'repo',
193+
workflowRunId: 1,
194+
token: 'token'
195+
}
196+
})
197+
198+
expect(listArtifactsInternal).not.toHaveBeenCalled()
199+
expect(listArtifactsPublic).toHaveBeenCalled()
200+
})
201+
})
202+
203+
describe('getArtifact', () => {
204+
it('Throws if LOCAL_ACTION_ARTIFACT_PATH is not set', async () => {
205+
delete process.env.LOCAL_ACTION_ARTIFACT_PATH
206+
207+
const client = new DefaultArtifactClient()
208+
209+
await expect(client.getArtifact('artifact-name')).rejects.toThrow()
210+
})
211+
212+
it('Throws if running on GHES', async () => {
213+
isGhes.mockReturnValue(true)
214+
215+
const client = new DefaultArtifactClient()
216+
217+
await expect(client.getArtifact('artifact-name')).rejects.toThrow()
218+
})
219+
220+
it('Gets an artifact (internal)', async () => {
221+
const client = new DefaultArtifactClient()
222+
223+
await client.getArtifact('artifact-name')
224+
225+
expect(getArtifactInternal).toHaveBeenCalled()
226+
expect(getArtifactPublic).not.toHaveBeenCalled()
227+
})
228+
229+
it('Gets an artifact (public)', async () => {
230+
const client = new DefaultArtifactClient()
231+
232+
await client.getArtifact('artifact-name', {
233+
findBy: {
234+
repositoryOwner: 'owner',
235+
repositoryName: 'repo',
236+
workflowRunId: 1,
237+
token: 'token'
238+
}
239+
})
240+
241+
expect(getArtifactInternal).not.toHaveBeenCalled()
242+
expect(getArtifactPublic).toHaveBeenCalled()
243+
})
244+
})
245+
246+
describe('deleteArtifact', () => {
247+
it('Throws if LOCAL_ACTION_ARTIFACT_PATH is not set', async () => {
248+
delete process.env.LOCAL_ACTION_ARTIFACT_PATH
249+
250+
const client = new DefaultArtifactClient()
251+
252+
await expect(client.deleteArtifact('artifact-name')).rejects.toThrow()
253+
})
254+
255+
it('Throws if running on GHES', async () => {
256+
isGhes.mockReturnValue(true)
257+
258+
const client = new DefaultArtifactClient()
259+
260+
await expect(client.deleteArtifact('artifact-name')).rejects.toThrow()
261+
})
262+
263+
it('Deletes an artifact (internal)', async () => {
264+
const client = new DefaultArtifactClient()
265+
266+
await client.deleteArtifact('artifact-name')
267+
268+
expect(deleteArtifactInternal).toHaveBeenCalled()
269+
expect(deleteArtifactPublic).not.toHaveBeenCalled()
270+
})
271+
272+
it('Deletes an artifact (public)', async () => {
273+
const client = new DefaultArtifactClient()
274+
275+
await client.deleteArtifact('artifact-name', {
276+
findBy: {
277+
repositoryOwner: 'owner',
278+
repositoryName: 'repo',
279+
workflowRunId: 1,
280+
token: 'token'
281+
}
282+
})
283+
284+
expect(deleteArtifactInternal).not.toHaveBeenCalled()
285+
expect(deleteArtifactPublic).toHaveBeenCalled()
286+
})
287+
})
288+
})

0 commit comments

Comments
 (0)