@ronaldphilipsen/local-action 0.0.0-stage → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/.checkov.yml +3 -0
  2. package/.env.example +379 -0
  3. package/.github/codeql/codeql-config.yml +7 -0
  4. package/.github/copilot-instructions.md +79 -0
  5. package/.github/dependabot.yml +32 -0
  6. package/.github/workflows/codeql-analysis.yml +53 -0
  7. package/.github/workflows/continuous-delivery.yml +93 -0
  8. package/.github/workflows/continuous-integration.yml +382 -0
  9. package/.github/workflows/linter.yml +47 -0
  10. package/.github/workflows/release.yaml +99 -0
  11. package/.github/workflows/version-check.yml +44 -0
  12. package/.grype.yml +151 -0
  13. package/.markdown-lint.yml +7 -0
  14. package/.mega-linter.yml +188 -0
  15. package/.node-version +1 -0
  16. package/.prettierignore +4 -0
  17. package/.prettierrc.yml +14 -0
  18. package/.vscode/launch.json +15 -0
  19. package/.vscode/mcp.json +9 -0
  20. package/.vscode/settings.json +15 -0
  21. package/.vscode/tasks.json +51 -0
  22. package/.yaml-lint.yml +10 -0
  23. package/CHANGELOG.md +75 -0
  24. package/CODEOWNERS +3 -0
  25. package/CODE_OF_CONDUCT.md +74 -0
  26. package/CONTRIBUTING.md +140 -0
  27. package/LICENSE +21 -0
  28. package/README.md +285 -2
  29. package/SECURITY.md +42 -0
  30. package/SUPPORT.md +19 -0
  31. package/__fixtures__/@actions/core.ts +14 -0
  32. package/__fixtures__/@actions/exec.ts +9 -0
  33. package/__fixtures__/@actions/github.ts +3 -0
  34. package/__fixtures__/@actions/http-client.ts +9 -0
  35. package/__fixtures__/@actions/io.ts +9 -0
  36. package/__fixtures__/@octokit/rest.ts +18 -0
  37. package/__fixtures__/crypto.ts +7 -0
  38. package/__fixtures__/fs.ts +27 -0
  39. package/__fixtures__/javascript/no-export/.env.fixture +2 -0
  40. package/__fixtures__/javascript/no-export/action.yml +16 -0
  41. package/__fixtures__/javascript/no-export/node_modules/.gitkeep +0 -0
  42. package/__fixtures__/javascript/no-export/package.json +12 -0
  43. package/__fixtures__/javascript/no-export/src/index.js +0 -0
  44. package/__fixtures__/javascript/no-export/src/main.js +0 -0
  45. package/__fixtures__/javascript/no-import/.env.fixture +2 -0
  46. package/__fixtures__/javascript/no-import/action.yml +16 -0
  47. package/__fixtures__/javascript/no-import/node_modules/.gitkeep +0 -0
  48. package/__fixtures__/javascript/no-import/package.json +12 -0
  49. package/__fixtures__/javascript/no-import/src/index.js +3 -0
  50. package/__fixtures__/javascript/no-import/src/main.js +7 -0
  51. package/__fixtures__/javascript/success/.env.fixture +2 -0
  52. package/__fixtures__/javascript/success/action.yml +16 -0
  53. package/__fixtures__/javascript/success/node_modules/.gitkeep +0 -0
  54. package/__fixtures__/javascript/success/package.json +12 -0
  55. package/__fixtures__/javascript/success/post/index.js +3 -0
  56. package/__fixtures__/javascript/success/post/main.js +13 -0
  57. package/__fixtures__/javascript/success/pre/index.js +3 -0
  58. package/__fixtures__/javascript/success/pre/main.js +13 -0
  59. package/__fixtures__/javascript/success/src/index.js +3 -0
  60. package/__fixtures__/javascript/success/src/main.js +13 -0
  61. package/__fixtures__/javascript-esm/no-import/.env.fixture +2 -0
  62. package/__fixtures__/javascript-esm/no-import/action.yml +16 -0
  63. package/__fixtures__/javascript-esm/no-import/node_modules/.gitkeep +0 -0
  64. package/__fixtures__/javascript-esm/no-import/package.json +13 -0
  65. package/__fixtures__/javascript-esm/no-import/src/index.js +3 -0
  66. package/__fixtures__/javascript-esm/no-import/src/main.js +7 -0
  67. package/__fixtures__/javascript-esm/success/.env.fixture +2 -0
  68. package/__fixtures__/javascript-esm/success/action.yml +16 -0
  69. package/__fixtures__/javascript-esm/success/node_modules/.gitkeep +0 -0
  70. package/__fixtures__/javascript-esm/success/package.json +13 -0
  71. package/__fixtures__/javascript-esm/success/post/index.js +3 -0
  72. package/__fixtures__/javascript-esm/success/post/main.js +13 -0
  73. package/__fixtures__/javascript-esm/success/pre/index.js +3 -0
  74. package/__fixtures__/javascript-esm/success/pre/main.js +13 -0
  75. package/__fixtures__/javascript-esm/success/src/index.js +3 -0
  76. package/__fixtures__/javascript-esm/success/src/main.js +13 -0
  77. package/__fixtures__/path.ts +7 -0
  78. package/__fixtures__/payloads/issue_opened.json +33 -0
  79. package/__fixtures__/stream/promises.ts +3 -0
  80. package/__fixtures__/tsconfig-paths.ts +4 -0
  81. package/__fixtures__/typescript/no-import/.env.fixture +2 -0
  82. package/__fixtures__/typescript/no-import/action.yml +16 -0
  83. package/__fixtures__/typescript/no-import/node_modules/.gitkeep +0 -0
  84. package/__fixtures__/typescript/no-import/package.json +15 -0
  85. package/__fixtures__/typescript/no-import/src/index.ts +3 -0
  86. package/__fixtures__/typescript/no-import/src/main.ts +3 -0
  87. package/__fixtures__/typescript/no-import/tsconfig.json +20 -0
  88. package/__fixtures__/typescript/success/.env.fixture +2 -0
  89. package/__fixtures__/typescript/success/action.yml +16 -0
  90. package/__fixtures__/typescript/success/node_modules/.gitkeep +0 -0
  91. package/__fixtures__/typescript/success/package.json +15 -0
  92. package/__fixtures__/typescript/success/post/index.ts +3 -0
  93. package/__fixtures__/typescript/success/post/main.ts +9 -0
  94. package/__fixtures__/typescript/success/pre/index.ts +3 -0
  95. package/__fixtures__/typescript/success/pre/main.ts +9 -0
  96. package/__fixtures__/typescript/success/src/index.ts +3 -0
  97. package/__fixtures__/typescript/success/src/main.ts +9 -0
  98. package/__fixtures__/typescript/success/tsconfig.json +20 -0
  99. package/__fixtures__/typescript/success-yaml/.env.fixture +2 -0
  100. package/__fixtures__/typescript/success-yaml/action.yaml +16 -0
  101. package/__fixtures__/typescript/success-yaml/node_modules/.gitkeep +0 -0
  102. package/__fixtures__/typescript/success-yaml/package.json +15 -0
  103. package/__fixtures__/typescript/success-yaml/post/index.ts +3 -0
  104. package/__fixtures__/typescript/success-yaml/post/main.ts +9 -0
  105. package/__fixtures__/typescript/success-yaml/pre/index.ts +3 -0
  106. package/__fixtures__/typescript/success-yaml/pre/main.ts +9 -0
  107. package/__fixtures__/typescript/success-yaml/src/index.ts +3 -0
  108. package/__fixtures__/typescript/success-yaml/src/main.ts +9 -0
  109. package/__fixtures__/typescript/success-yaml/tsconfig.json +20 -0
  110. package/__fixtures__/typescript-esm/no-export/.env.fixture +2 -0
  111. package/__fixtures__/typescript-esm/no-export/action.yml +17 -0
  112. package/__fixtures__/typescript-esm/no-export/node_modules/.gitkeep +0 -0
  113. package/__fixtures__/typescript-esm/no-export/package.json +17 -0
  114. package/__fixtures__/typescript-esm/no-export/src/index.ts +0 -0
  115. package/__fixtures__/typescript-esm/no-export/src/main.ts +0 -0
  116. package/__fixtures__/typescript-esm/no-export/tsconfig.json +25 -0
  117. package/__fixtures__/typescript-esm/no-import/.env.fixture +2 -0
  118. package/__fixtures__/typescript-esm/no-import/action.yml +17 -0
  119. package/__fixtures__/typescript-esm/no-import/node_modules/.gitkeep +0 -0
  120. package/__fixtures__/typescript-esm/no-import/package.json +17 -0
  121. package/__fixtures__/typescript-esm/no-import/src/index.ts +3 -0
  122. package/__fixtures__/typescript-esm/no-import/src/main.ts +3 -0
  123. package/__fixtures__/typescript-esm/no-import/tsconfig.json +25 -0
  124. package/__fixtures__/typescript-esm/success/.env.fixture +2 -0
  125. package/__fixtures__/typescript-esm/success/action.yml +17 -0
  126. package/__fixtures__/typescript-esm/success/node_modules/.gitkeep +0 -0
  127. package/__fixtures__/typescript-esm/success/package.json +17 -0
  128. package/__fixtures__/typescript-esm/success/post/index.ts +3 -0
  129. package/__fixtures__/typescript-esm/success/post/main.ts +9 -0
  130. package/__fixtures__/typescript-esm/success/pre/index.ts +3 -0
  131. package/__fixtures__/typescript-esm/success/pre/main.ts +9 -0
  132. package/__fixtures__/typescript-esm/success/src/index.ts +3 -0
  133. package/__fixtures__/typescript-esm/success/src/main.ts +9 -0
  134. package/__fixtures__/typescript-esm/success/tsconfig.json +25 -0
  135. package/__fixtures__/typescript-esm/tsconfig-comments/.env.fixture +2 -0
  136. package/__fixtures__/typescript-esm/tsconfig-comments/action.yml +17 -0
  137. package/__fixtures__/typescript-esm/tsconfig-comments/node_modules/.gitkeep +0 -0
  138. package/__fixtures__/typescript-esm/tsconfig-comments/package.json +17 -0
  139. package/__fixtures__/typescript-esm/tsconfig-comments/src/index.ts +3 -0
  140. package/__fixtures__/typescript-esm/tsconfig-comments/src/main.ts +9 -0
  141. package/__fixtures__/typescript-esm/tsconfig-comments/tsconfig.json +21 -0
  142. package/__tests__/command.test.ts +224 -0
  143. package/__tests__/stubs/artifact/internal/client.test.ts +340 -0
  144. package/__tests__/stubs/artifact/internal/delete/delete-artifact.test.ts +152 -0
  145. package/__tests__/stubs/artifact/internal/download/download-artifact.test.ts +119 -0
  146. package/__tests__/stubs/artifact/internal/find/get-artifact.test.ts +237 -0
  147. package/__tests__/stubs/artifact/internal/find/list-artifacts.test.ts +218 -0
  148. package/__tests__/stubs/artifact/internal/find/retry-options.test.ts +54 -0
  149. package/__tests__/stubs/artifact/internal/shared/config.test.ts +91 -0
  150. package/__tests__/stubs/artifact/internal/shared/errors.test.ts +38 -0
  151. package/__tests__/stubs/artifact/internal/shared/user-agent.test.ts +22 -0
  152. package/__tests__/stubs/artifact/internal/upload/path-and-artifact-name-validation.test.ts +53 -0
  153. package/__tests__/stubs/artifact/internal/upload/upload-artifact.test.ts +126 -0
  154. package/__tests__/stubs/artifact/internal/upload/upload-zip-specification.test.ts +49 -0
  155. package/__tests__/stubs/cache/cache.test.ts +357 -0
  156. package/__tests__/stubs/cache/errors.test.ts +55 -0
  157. package/__tests__/stubs/cache/internal/cacheUtils.test.ts +109 -0
  158. package/__tests__/stubs/cache/internal/constants.test.ts +81 -0
  159. package/__tests__/stubs/cache/internal/tar.test.ts +129 -0
  160. package/__tests__/stubs/core/core.test.ts +760 -0
  161. package/__tests__/stubs/core/path-utils.test.ts +45 -0
  162. package/__tests__/stubs/core/platform.test.ts +89 -0
  163. package/__tests__/stubs/core/summary.test.ts +455 -0
  164. package/__tests__/stubs/env.test.ts +72 -0
  165. package/__tests__/stubs/github/context.test.ts +73 -0
  166. package/__tests__/stubs/github/github.test.ts +15 -0
  167. package/__tests__/stubs/github/internal/utils.test.ts +98 -0
  168. package/__tests__/stubs/github/utils.test.ts +16 -0
  169. package/__tests__/utils/config.test.ts +78 -0
  170. package/__tests__/utils/output.test.ts +32 -0
  171. package/__tests__/utils/package.test.ts +33 -0
  172. package/badges/coverage.svg +1 -0
  173. package/bin/local-action.js +121 -0
  174. package/docs/create-a-job-summary.md +32 -0
  175. package/docs/debugging-in-vscode.md +27 -0
  176. package/docs/img/job-summary.png +0 -0
  177. package/docs/supported-functionality.md +126 -0
  178. package/eslint.config.mjs +74 -0
  179. package/jest.config.ts +41 -0
  180. package/package.json +93 -4
  181. package/src/command.ts +171 -0
  182. package/src/commands/run.ts +616 -0
  183. package/src/index.ts +21 -0
  184. package/src/stubs/artifact/artifact.ts +17 -0
  185. package/src/stubs/artifact/internal/client.ts +400 -0
  186. package/src/stubs/artifact/internal/delete/delete-artifact.ts +126 -0
  187. package/src/stubs/artifact/internal/download/download-artifact.ts +309 -0
  188. package/src/stubs/artifact/internal/find/get-artifact.ts +135 -0
  189. package/src/stubs/artifact/internal/find/list-artifacts.ts +188 -0
  190. package/src/stubs/artifact/internal/find/retry-options.ts +57 -0
  191. package/src/stubs/artifact/internal/shared/config.ts +64 -0
  192. package/src/stubs/artifact/internal/shared/errors.ts +53 -0
  193. package/src/stubs/artifact/internal/shared/interfaces.ts +195 -0
  194. package/src/stubs/artifact/internal/shared/user-agent.ts +13 -0
  195. package/src/stubs/artifact/internal/upload/path-and-artifact-name-validation.ts +92 -0
  196. package/src/stubs/artifact/internal/upload/upload-artifact.ts +127 -0
  197. package/src/stubs/artifact/internal/upload/upload-zip-specification.ts +108 -0
  198. package/src/stubs/artifact/internal/upload/zip.ts +122 -0
  199. package/src/stubs/cache/cache.ts +334 -0
  200. package/src/stubs/cache/errors.ts +31 -0
  201. package/src/stubs/cache/interfaces.ts +4 -0
  202. package/src/stubs/cache/internal/cacheUtils.ts +207 -0
  203. package/src/stubs/cache/internal/constants.ts +45 -0
  204. package/src/stubs/cache/internal/tar.ts +374 -0
  205. package/src/stubs/cache/options.ts +91 -0
  206. package/src/stubs/core/core.ts +743 -0
  207. package/src/stubs/core/path-utils.ts +39 -0
  208. package/src/stubs/core/platform.ts +132 -0
  209. package/src/stubs/core/summary.ts +399 -0
  210. package/src/stubs/env.ts +40 -0
  211. package/src/stubs/github/context.ts +94 -0
  212. package/src/stubs/github/github.ts +29 -0
  213. package/src/stubs/github/interfaces.ts +52 -0
  214. package/src/stubs/github/internal/utils.ts +83 -0
  215. package/src/stubs/github/utils.ts +52 -0
  216. package/src/types/quibble.d.ts +5 -0
  217. package/src/types.ts +116 -0
  218. package/src/utils/config.ts +47 -0
  219. package/src/utils/output.ts +20 -0
  220. package/src/utils/package.ts +38 -0
  221. package/tsconfig.base.json +29 -0
  222. package/tsconfig.eslint.json +22 -0
  223. package/tsconfig.json +18 -0
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Last Reviewed Commit: 9b08f07cd305c6060f08e4b64d1805429a773711
3
+ * Last Reviewed Date: 2026-01-16
4
+ */
5
+
6
+ import * as httpClient from '@actions/http-client'
7
+ import * as crypto from 'crypto'
8
+ import fs from 'fs'
9
+ import path from 'path'
10
+ import * as stream from 'stream'
11
+ import { finished } from 'stream/promises'
12
+ import unzip from 'unzip-stream'
13
+ import { EnvMeta } from '../../../../stubs/env.js'
14
+ import * as core from '../../../core/core.js'
15
+ import { getOctokit } from '../../../github/github.js'
16
+ import { getGitHubWorkspaceDir } from '../shared/config.js'
17
+ import { ArtifactNotFoundError } from '../shared/errors.js'
18
+ import type {
19
+ Artifact,
20
+ DownloadArtifactOptions,
21
+ DownloadArtifactResponse
22
+ } from '../shared/interfaces.js'
23
+ import { StreamExtractResponse } from '../shared/interfaces.js'
24
+ import { getUserAgentString } from '../shared/user-agent.js'
25
+
26
+ /**
27
+ * Removes query parameters from a URL.
28
+ *
29
+ * @remarks
30
+ *
31
+ * - Exporting the function to make it available for testing.
32
+ *
33
+ * @param url URL
34
+ * @returns URL without query parameters
35
+ */
36
+ export const scrubQueryParameters = (url: string): string => {
37
+ const parsed = new URL(url)
38
+ parsed.search = ''
39
+ return parsed.toString()
40
+ }
41
+
42
+ /**
43
+ * Checks if a path exists
44
+ *
45
+ * @remarks
46
+ *
47
+ * - Exporting the function to make it available for testing.
48
+ * - Use `accessSync` instead of `access`.
49
+ *
50
+ * @param path Path
51
+ * @returns `true` if the path exists, `false` otherwise
52
+ */
53
+ export function exists(path: string): boolean {
54
+ try {
55
+ fs.accessSync(path)
56
+ return true
57
+ } catch (error: any) {
58
+ if (error.code === 'ENOENT') return false
59
+ else throw error
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Extract the artifact from the given URL to the specified directory.
65
+ *
66
+ * @param url URL
67
+ * @param directory Directory
68
+ */
69
+ /* istanbul ignore next */
70
+ async function streamExtract(
71
+ url: string,
72
+ directory: string
73
+ ): Promise<StreamExtractResponse> {
74
+ let retryCount = 0
75
+ while (retryCount < 5) {
76
+ try {
77
+ return await streamExtractExternal(url, directory)
78
+ } catch (error: any) {
79
+ retryCount++
80
+ core.debug(
81
+ `Failed to download artifact after ${retryCount} retries due to ${error.message}. Retrying in 5 seconds...`
82
+ )
83
+ // wait 5 seconds before retrying
84
+ await new Promise(resolve => setTimeout(resolve, 5000))
85
+ }
86
+ }
87
+
88
+ throw new Error(`Artifact download failed after ${retryCount} retries.`)
89
+ }
90
+
91
+ /**
92
+ * Extract the artifact from the given URL to the specified directory.
93
+ *
94
+ * @remarks
95
+ *
96
+ * - Removed digest verification from the original implementation.
97
+ *
98
+ * @param url URL
99
+ * @param directory Directory
100
+ */
101
+ /* istanbul ignore next */
102
+ export async function streamExtractExternal(
103
+ url: string,
104
+ directory: string,
105
+ opts: { timeout: number } = { timeout: 30 * 1000 }
106
+ ): Promise<StreamExtractResponse> {
107
+ const client = new httpClient.HttpClient(getUserAgentString())
108
+ const response = await client.get(url)
109
+ if (response.message.statusCode !== 200)
110
+ throw new Error(
111
+ `Unexpected HTTP response from blob storage: ${response.message.statusCode} ${response.message.statusMessage}`
112
+ )
113
+
114
+ let sha256Digest: string | undefined = undefined
115
+
116
+ return new Promise((resolve, reject) => {
117
+ const timerFn = (): void => {
118
+ const timeoutError = new Error(
119
+ `Blob storage chunk did not respond in ${opts.timeout}ms`
120
+ )
121
+ response.message.destroy(timeoutError)
122
+ reject(timeoutError)
123
+ }
124
+ const timer = setTimeout(timerFn, opts.timeout)
125
+
126
+ const hashStream = crypto.createHash('sha256').setEncoding('hex')
127
+ const passThrough = new stream.PassThrough()
128
+
129
+ response.message.pipe(passThrough)
130
+ passThrough.pipe(hashStream)
131
+ const extractStream = passThrough
132
+
133
+ extractStream
134
+ .on('data', () => {
135
+ timer.refresh()
136
+ })
137
+ .on('error', (error: Error) => {
138
+ core.debug(
139
+ `response.message: Artifact download failed: ${error.message}`
140
+ )
141
+ clearTimeout(timer)
142
+ reject(error)
143
+ })
144
+ .pipe(unzip.Extract({ path: directory }))
145
+ .on('close', () => {
146
+ clearTimeout(timer)
147
+ if (hashStream) {
148
+ hashStream.end()
149
+ sha256Digest = hashStream.read() as string
150
+ core.info(`SHA256 digest of downloaded artifact is ${sha256Digest}`)
151
+ }
152
+ resolve({ sha256Digest: `sha256:${sha256Digest}` })
153
+ })
154
+ .on('error', (error: Error) => {
155
+ reject(error)
156
+ })
157
+ })
158
+ }
159
+
160
+ /**
161
+ * Download an artifact from the given repository to the specified directory.
162
+ *
163
+ * @remarks
164
+ *
165
+ * - Removed digest verification from the original implementation.
166
+ *
167
+ * @param artifactId Artifact ID
168
+ * @param repositoryOwner Repository Owner
169
+ * @param repositoryName Repository Name
170
+ * @param token Token
171
+ * @param options Options
172
+ * @returns Download Artifact Response
173
+ */
174
+ /* istanbul ignore next */
175
+ export async function downloadArtifactPublic(
176
+ artifactId: number,
177
+ repositoryOwner: string,
178
+ repositoryName: string,
179
+ token: string,
180
+ options?: DownloadArtifactOptions
181
+ ): Promise<DownloadArtifactResponse> {
182
+ const downloadPath = await resolveOrCreateDirectory(options?.path)
183
+
184
+ const api = getOctokit(token)
185
+
186
+ let digestMismatch = false
187
+
188
+ core.info(
189
+ `Downloading artifact '${artifactId}' from '${repositoryOwner}/${repositoryName}'`
190
+ )
191
+
192
+ const { headers, status } = await api.rest.actions.downloadArtifact({
193
+ owner: repositoryOwner,
194
+ repo: repositoryName,
195
+ artifact_id: artifactId,
196
+ archive_format: 'zip',
197
+ request: {
198
+ redirect: 'manual'
199
+ }
200
+ })
201
+
202
+ if (status !== 302)
203
+ throw new Error(`Unable to download artifact. Unexpected status: ${status}`)
204
+
205
+ const { location } = headers
206
+ if (!location) throw new Error(`Unable to redirect to artifact download url`)
207
+
208
+ core.info(
209
+ `Redirecting to blob download url: ${scrubQueryParameters(location)}`
210
+ )
211
+
212
+ try {
213
+ core.info(`Starting download of artifact to: ${downloadPath}`)
214
+ const extractResponse = await streamExtract(location, downloadPath)
215
+ core.info(`Artifact download completed successfully.`)
216
+ if (options?.expectedHash)
217
+ if (options?.expectedHash !== extractResponse.sha256Digest) {
218
+ digestMismatch = true
219
+ core.debug(`Computed digest: ${extractResponse.sha256Digest}`)
220
+ core.debug(`Expected digest: ${options.expectedHash}`)
221
+ }
222
+ } catch (error: any) {
223
+ throw new Error(
224
+ `Unable to download and extract artifact: ${error.message}`,
225
+ { cause: error }
226
+ )
227
+ }
228
+
229
+ return { downloadPath, digestMismatch }
230
+ }
231
+
232
+ /**
233
+ * Downloads an artifact from the current repository to the specified directory.
234
+ *
235
+ * @remarks
236
+ *
237
+ * - Removed digest verification from the original implementation.
238
+ * - Downloads from local filesystem instead of GitHub.
239
+ *
240
+ * @param artifactId Artifact ID
241
+ * @param options Options
242
+ * @returns Download Artifact Response
243
+ */
244
+ export async function downloadArtifactInternal(
245
+ artifactId: number,
246
+ options?: DownloadArtifactOptions
247
+ ): Promise<DownloadArtifactResponse> {
248
+ const downloadPath = await resolveOrCreateDirectory(options?.path)
249
+
250
+ // Check the current list of artifacts for one with a matching ID.
251
+ const artifacts: Artifact[] = EnvMeta.artifacts.filter(
252
+ (artifact: Artifact) => artifact.id === artifactId
253
+ )
254
+
255
+ if (artifacts.length === 0)
256
+ throw new ArtifactNotFoundError(
257
+ `No artifacts found for ID: ${artifactId}\nAre you trying to download from a different run? Try specifying a github-token with \`actions:read\` scope.`
258
+ )
259
+
260
+ if (artifacts.length > 1)
261
+ core.warning('Multiple artifacts found, defaulting to first.')
262
+
263
+ try {
264
+ core.info(`Starting download of artifact to: ${downloadPath}`)
265
+
266
+ // Extract the file to the specified directory. The archive will be
267
+ // available at `${LOCAL_ACTION_ARTIFACT_PATH}/${artifact.id}`.
268
+ const readStream = fs.createReadStream(
269
+ path.join(
270
+ process.env.LOCAL_ACTION_ARTIFACT_PATH!,
271
+ `${artifacts[0].name}.zip`
272
+ )
273
+ )
274
+
275
+ readStream.pipe(unzip.Extract({ path: downloadPath }))
276
+
277
+ await finished(readStream)
278
+
279
+ core.info(`Artifact download completed successfully.`)
280
+ } catch (error: any) {
281
+ throw new Error(
282
+ `Unable to download and extract artifact: ${error.message}`,
283
+ { cause: error }
284
+ )
285
+ }
286
+
287
+ return { downloadPath, digestMismatch: false }
288
+ }
289
+
290
+ /**
291
+ * Resolves or creates a directory.
292
+ *
293
+ * @param downloadPath Download Path
294
+ * @returns Download Path
295
+ */
296
+ /* istanbul ignore next */
297
+ async function resolveOrCreateDirectory(
298
+ downloadPath = getGitHubWorkspaceDir()
299
+ ): Promise<string> {
300
+ if (!exists(downloadPath)) {
301
+ core.debug(
302
+ `Artifact destination folder does not exist, creating: ${downloadPath}`
303
+ )
304
+ fs.mkdirSync(downloadPath, { recursive: true })
305
+ } else
306
+ core.debug(`Artifact destination folder already exists: ${downloadPath}`)
307
+
308
+ return downloadPath
309
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Last Reviewed Commit: d5c8a0fa274d832846b6c3a409b017a6b76b787e
3
+ * Last Reviewed Date: 2026-01-16
4
+ */
5
+
6
+ import type { OctokitOptions } from '@octokit/core'
7
+ import { requestLog } from '@octokit/plugin-request-log'
8
+ import { retry } from '@octokit/plugin-retry'
9
+ import { EnvMeta } from '../../../../stubs/env.js'
10
+ import * as core from '../../../core/core.js'
11
+ import { getOctokit } from '../../../github/github.js'
12
+ import { defaults as defaultGitHubOptions } from '../../../github/utils.js'
13
+ import {
14
+ ArtifactNotFoundError,
15
+ InvalidResponseError
16
+ } from '../shared/errors.js'
17
+ import type { Artifact, GetArtifactResponse } from '../shared/interfaces.js'
18
+ import { getUserAgentString } from '../shared/user-agent.js'
19
+ import { getRetryOptions } from './retry-options.js'
20
+
21
+ /**
22
+ * Gets the artifact from the GitHub API.
23
+ *
24
+ * @remarks
25
+ *
26
+ * - Removed digest from the artifact interface.
27
+ *
28
+ * @param artifactName Artifact Name
29
+ * @param workflowRunId Workflow Run ID
30
+ * @param repositoryOwner Repository Owner
31
+ * @param repositoryName Repository Name
32
+ * @param token Token
33
+ * @returns Get Artifact Response
34
+ */
35
+ export async function getArtifactPublic(
36
+ artifactName: string,
37
+ workflowRunId: number,
38
+ repositoryOwner: string,
39
+ repositoryName: string,
40
+ token: string
41
+ ): Promise<GetArtifactResponse> {
42
+ const [retryOpts, requestOpts] = getRetryOptions(defaultGitHubOptions)
43
+
44
+ const opts: OctokitOptions = {
45
+ log: undefined,
46
+ userAgent: getUserAgentString(),
47
+ previews: undefined,
48
+ retry: retryOpts,
49
+ request: requestOpts
50
+ }
51
+
52
+ const github = getOctokit(token, opts, retry as any, requestLog as any)
53
+
54
+ const getArtifactResp = await github.request(
55
+ 'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts{?name}',
56
+ {
57
+ owner: repositoryOwner,
58
+ repo: repositoryName,
59
+ run_id: workflowRunId,
60
+ name: artifactName
61
+ }
62
+ )
63
+
64
+ if (getArtifactResp.status !== 200)
65
+ throw new InvalidResponseError(
66
+ `Invalid response from GitHub API: ${getArtifactResp.status} (${getArtifactResp?.headers?.['x-github-request-id']})`
67
+ )
68
+
69
+ if (getArtifactResp.data.artifacts.length === 0)
70
+ throw new ArtifactNotFoundError(
71
+ `Artifact not found for name: ${artifactName}
72
+ Please ensure that your artifact is not expired and the artifact was uploaded using a compatible version of toolkit/upload-artifact.
73
+ For more information, visit the GitHub Artifacts FAQ: https://github.com/actions/toolkit/blob/main/packages/artifact/docs/faq.md`
74
+ )
75
+
76
+ let artifact = getArtifactResp.data.artifacts[0]
77
+ if (getArtifactResp.data.artifacts.length > 1) {
78
+ artifact = getArtifactResp.data.artifacts.sort(
79
+ (a: Artifact, b: Artifact) => b.id - a.id
80
+ )[0]
81
+ core.debug(
82
+ `More than one artifact found for a single name, returning newest (id: ${artifact.id})`
83
+ )
84
+ }
85
+
86
+ return {
87
+ artifact: {
88
+ name: artifact.name,
89
+ id: artifact.id,
90
+ size: artifact.size_in_bytes,
91
+ createdAt: artifact.created_at ? new Date(artifact.created_at) : undefined
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Gets the artifact from this workflow run.
98
+ *
99
+ * @remarks
100
+ *
101
+ * - Removed digest from the artifact interface.
102
+ * - Gets artifacts from environment metadata.
103
+ *
104
+ * @param artifactName Artifact Name
105
+ * @returns Get Artifact Response
106
+ */
107
+ export async function getArtifactInternal(
108
+ artifactName: string
109
+ ): Promise<GetArtifactResponse> {
110
+ // Get all artifacts with a matching name, sorted by latest (highest ID).
111
+ const artifacts = EnvMeta.artifacts
112
+ .filter(artifact => artifact.name === artifactName)
113
+ .sort((a, b) => Number(b.id) - Number(a.id))
114
+
115
+ if (artifacts.length === 0)
116
+ throw new ArtifactNotFoundError(
117
+ `Artifact not found for name: ${artifactName}
118
+ Please ensure that your artifact is not expired and the artifact was uploaded using a compatible version of toolkit/upload-artifact.
119
+ For more information, visit the GitHub Artifacts FAQ: https://github.com/actions/toolkit/blob/main/packages/artifact/docs/faq.md`
120
+ )
121
+
122
+ if (artifacts.length > 1)
123
+ core.debug(
124
+ `More than one artifact found for a single name, returning newest (id: ${artifacts[0].id})`
125
+ )
126
+
127
+ return {
128
+ artifact: {
129
+ name: artifacts[0].name,
130
+ id: Number(artifacts[0].id),
131
+ size: Number(artifacts[0].size),
132
+ createdAt: artifacts[0].createdAt
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Last Reviewed Commit: fea4f6b5c527a8f231cfb9e2530a2f367e971faa
3
+ * Last Reviewed Date: 2026-01-16
4
+ */
5
+
6
+ import type { OctokitOptions } from '@octokit/core'
7
+ import { requestLog } from '@octokit/plugin-request-log'
8
+ import { retry } from '@octokit/plugin-retry'
9
+ import { EnvMeta } from '../../../../stubs/env.js'
10
+ import * as core from '../../../core/core.js'
11
+ import { getOctokit } from '../../../github/github.js'
12
+ import { defaults as defaultGitHubOptions } from '../../../github/utils.js'
13
+ import { getMaxArtifactListCount } from '../shared/config.js'
14
+ import type { Artifact, ListArtifactsResponse } from '../shared/interfaces.js'
15
+ import { getUserAgentString } from '../shared/user-agent.js'
16
+ import { getRetryOptions } from './retry-options.js'
17
+
18
+ const paginationCount = 100
19
+
20
+ /**
21
+ * Lists artifacts for a given workflow run.
22
+ *
23
+ * @remarks
24
+ *
25
+ * - Removed digest from the artifact interface.
26
+ *
27
+ * @param workflowRunId Workflow Run ID
28
+ * @param repositoryOwner Repository Owner
29
+ * @param repositoryName Repository Name
30
+ * @param token Token
31
+ * @param latest Latest
32
+ * @returns List Artifacts Response
33
+ */
34
+ export async function listArtifactsPublic(
35
+ workflowRunId: number,
36
+ repositoryOwner: string,
37
+ repositoryName: string,
38
+ token: string,
39
+ latest = false
40
+ ): Promise<ListArtifactsResponse> {
41
+ core.info(
42
+ `Fetching artifact list for workflow run ${workflowRunId} in repository ${repositoryOwner}/${repositoryName}`
43
+ )
44
+
45
+ let artifacts: Artifact[] = []
46
+ const [retryOpts, requestOpts] = getRetryOptions(defaultGitHubOptions)
47
+
48
+ const opts: OctokitOptions = {
49
+ log: undefined,
50
+ userAgent: getUserAgentString(),
51
+ previews: undefined,
52
+ retry: retryOpts,
53
+ request: requestOpts
54
+ }
55
+
56
+ const github = getOctokit(token, opts, retry as any, requestLog as any)
57
+
58
+ let currentPageNumber = 1
59
+
60
+ const { data: listArtifactResponse } =
61
+ await github.rest.actions.listWorkflowRunArtifacts({
62
+ owner: repositoryOwner,
63
+ repo: repositoryName,
64
+ run_id: workflowRunId,
65
+ per_page: paginationCount,
66
+ page: currentPageNumber
67
+ })
68
+
69
+ let numberOfPages = Math.ceil(
70
+ listArtifactResponse.total_count / paginationCount
71
+ )
72
+ const totalArtifactCount = listArtifactResponse.total_count
73
+ if (totalArtifactCount > getMaxArtifactListCount()) {
74
+ core.warning(
75
+ `Workflow run ${workflowRunId} has more than 1000 artifacts. Results will be incomplete as only the first ${getMaxArtifactListCount()} artifacts will be returned`
76
+ )
77
+ numberOfPages = Math.ceil(getMaxArtifactListCount() / paginationCount)
78
+ }
79
+
80
+ // Iterate over the first page
81
+ for (const artifact of listArtifactResponse.artifacts) {
82
+ artifacts.push({
83
+ name: artifact.name,
84
+ id: artifact.id,
85
+ size: artifact.size_in_bytes,
86
+ createdAt: artifact.created_at
87
+ ? new Date(artifact.created_at)
88
+ : undefined,
89
+ digest: (artifact as ArtifactResponse).digest
90
+ })
91
+ }
92
+
93
+ // Move to the next page
94
+ currentPageNumber++
95
+
96
+ // Iterate over any remaining pages
97
+ /* istanbul ignore next */
98
+ for (
99
+ currentPageNumber;
100
+ currentPageNumber <= numberOfPages;
101
+ currentPageNumber++
102
+ ) {
103
+ core.debug(`Fetching page ${currentPageNumber} of artifact list`)
104
+
105
+ const { data: listArtifactResponse } =
106
+ await github.rest.actions.listWorkflowRunArtifacts({
107
+ owner: repositoryOwner,
108
+ repo: repositoryName,
109
+ run_id: workflowRunId,
110
+ per_page: paginationCount,
111
+ page: currentPageNumber
112
+ })
113
+
114
+ for (const artifact of listArtifactResponse.artifacts) {
115
+ artifacts.push({
116
+ name: artifact.name,
117
+ id: artifact.id,
118
+ size: artifact.size_in_bytes,
119
+ createdAt: artifact.created_at
120
+ ? new Date(artifact.created_at)
121
+ : undefined,
122
+ digest: (artifact as ArtifactResponse).digest
123
+ })
124
+ }
125
+ }
126
+
127
+ if (latest) artifacts = filterLatest(artifacts)
128
+
129
+ core.info(`Found ${artifacts.length} artifact(s)`)
130
+
131
+ return {
132
+ artifacts
133
+ }
134
+ }
135
+
136
+ /**
137
+ * List artifacts for this workflow run.
138
+ *
139
+ * @remarks
140
+ *
141
+ * - Uses environment metadata for tracking artifacts.
142
+ *
143
+ * @param latest Latest
144
+ * @returns List Artifacts Response
145
+ */
146
+ export async function listArtifactsInternal(
147
+ latest = false
148
+ ): Promise<ListArtifactsResponse> {
149
+ const artifacts = latest ? filterLatest(EnvMeta.artifacts) : EnvMeta.artifacts
150
+
151
+ core.info(`Found ${artifacts.length} artifact(s)`)
152
+
153
+ return {
154
+ artifacts
155
+ }
156
+ }
157
+
158
+ /**
159
+ * This exists so that we don't have to use 'any' when receiving the artifact
160
+ * list from the GitHub API. The digest field is not present in OpenAPI/types at
161
+ * time of writing, which necessitates this change.
162
+ */
163
+ interface ArtifactResponse {
164
+ name: string
165
+ id: number
166
+ size_in_bytes: number
167
+ created_at?: string
168
+ digest?: string
169
+ }
170
+
171
+ /**
172
+ * Filters a list of artifacts to only include the latest artifact for each name
173
+ *
174
+ * @param artifacts The artifacts to filter
175
+ * @returns The filtered list of artifacts
176
+ */
177
+ function filterLatest(artifacts: Artifact[]): Artifact[] {
178
+ artifacts.sort((a, b) => b.id - a.id)
179
+ const latestArtifacts: Artifact[] = []
180
+ const seenArtifactNames = new Set<string>()
181
+ for (const artifact of artifacts) {
182
+ if (!seenArtifactNames.has(artifact.name)) {
183
+ latestArtifacts.push(artifact)
184
+ seenArtifactNames.add(artifact.name)
185
+ }
186
+ }
187
+ return latestArtifacts
188
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Last Reviewed Commit: 06e751600e2c016997ae8b1393bff0c4c79fc52f
3
+ * Last Reviewed Date: 2026-01-16
4
+ */
5
+
6
+ import type { OctokitOptions } from '@octokit/core'
7
+ import type { RequestRequestOptions } from '@octokit/types'
8
+ import * as core from '../../../core/core.js'
9
+
10
+ export type RetryOptions = {
11
+ doNotRetry?: number[]
12
+ enabled?: boolean
13
+ }
14
+
15
+ // Defaults for fetching artifacts
16
+ const defaultMaxRetryNumber = 5
17
+ const defaultExemptStatusCodes = [400, 401, 403, 404, 422] // https://github.com/octokit/plugin-retry.js/blob/9a2443746c350b3beedec35cf26e197ea318a261/src/index.ts#L14
18
+
19
+ /**
20
+ * Get the retry options for the GitHub client.
21
+ *
22
+ * @param defaultOptions Default Options
23
+ * @param retries Retries
24
+ * @param exemptStatusCodes Exempt Status Codes
25
+ * @returns Retry Options
26
+ */
27
+ export function getRetryOptions(
28
+ defaultOptions: OctokitOptions,
29
+ retries: number = defaultMaxRetryNumber,
30
+ exemptStatusCodes: number[] = defaultExemptStatusCodes
31
+ ): [RetryOptions, RequestRequestOptions | undefined] {
32
+ if (retries <= 0) return [{ enabled: false }, defaultOptions.request]
33
+
34
+ const retryOptions: RetryOptions = {
35
+ enabled: true
36
+ }
37
+
38
+ if (exemptStatusCodes.length > 0) retryOptions.doNotRetry = exemptStatusCodes
39
+
40
+ // The GitHub type has some defaults for `options.request`
41
+ // see: https://github.com/actions/toolkit/blob/4fbc5c941a57249b19562015edbd72add14be93d/packages/github/src/utils.ts#L15
42
+ // We pass these in here so they are not overridden.
43
+ const requestOptions: RequestRequestOptions = {
44
+ ...defaultOptions.request,
45
+ retries
46
+ }
47
+
48
+ core.debug(
49
+ `GitHub client configured with: (retries: ${
50
+ requestOptions.retries
51
+ }, retry-exempt-status-code: ${
52
+ retryOptions.doNotRetry ?? 'octokit default: [400, 401, 403, 404, 422]'
53
+ })`
54
+ )
55
+
56
+ return [retryOptions, requestOptions]
57
+ }