@ronaldphilipsen/local-action 0.0.0-stage → 7.0.1

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 +100 -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 +277 -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,122 @@
1
+ /**
2
+ * Last Reviewed Commit: https://github.com/actions/toolkit/blob/ee91adfbc4f4d1470e176a8d568c0cbdabaf98e0/packages/artifact/src/internal/upload/zip.ts
3
+ * Last Reviewed Date: 2026-01-16
4
+ */
5
+
6
+ import archiver from 'archiver'
7
+ import { realpath } from 'fs/promises'
8
+ import * as stream from 'stream'
9
+ import * as core from '../../../core/core.js'
10
+ import { getUploadChunkSize } from '../shared/config.js'
11
+ import type { UploadZipSpecification } from './upload-zip-specification.js'
12
+
13
+ /* istanbul ignore next */
14
+ export const DEFAULT_COMPRESSION_LEVEL = 6
15
+
16
+ // Custom stream transformer so we can set the highWaterMark property
17
+ // See https://github.com/nodejs/node/issues/8855
18
+ /* istanbul ignore next */
19
+ export class ZipUploadStream extends stream.Transform {
20
+ constructor(bufferSize: number) {
21
+ super({
22
+ highWaterMark: bufferSize
23
+ })
24
+ }
25
+
26
+ _transform(chunk: any, enc: any, cb: any): void {
27
+ cb(null, chunk)
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Creates a zip upload stream.
33
+ *
34
+ * @param uploadSpecification Upload Specification
35
+ * @param compressionLevel Compression Level
36
+ * @returns Zip Upload Stream
37
+ */
38
+ /* istanbul ignore next */
39
+ export async function createZipUploadStream(
40
+ uploadSpecification: UploadZipSpecification[],
41
+ compressionLevel: number = DEFAULT_COMPRESSION_LEVEL
42
+ ): Promise<ZipUploadStream> {
43
+ core.debug(
44
+ `Creating Artifact archive with compressionLevel: ${compressionLevel}`
45
+ )
46
+
47
+ const zip = archiver.create('zip', {
48
+ highWaterMark: getUploadChunkSize(),
49
+ zlib: { level: compressionLevel }
50
+ })
51
+
52
+ // register callbacks for various events during the zip lifecycle
53
+ zip.on('error', zipErrorCallback)
54
+ zip.on('warning', zipWarningCallback)
55
+ zip.on('finish', zipFinishCallback)
56
+ zip.on('end', zipEndCallback)
57
+
58
+ for (const file of uploadSpecification) {
59
+ if (file.sourcePath !== null) {
60
+ // Check if symlink and resolve the source path
61
+ let sourcePath = file.sourcePath
62
+ if (file.stats.isSymbolicLink())
63
+ sourcePath = await realpath(file.sourcePath)
64
+
65
+ // Add the file to the zip
66
+ zip.file(sourcePath, {
67
+ name: file.destinationPath
68
+ })
69
+ } else {
70
+ // Add a directory to the zip
71
+ zip.append('', { name: file.destinationPath })
72
+ }
73
+ }
74
+
75
+ const bufferSize = getUploadChunkSize()
76
+ const zipUploadStream = new ZipUploadStream(bufferSize)
77
+
78
+ core.debug(
79
+ `Zip write high watermark value ${zipUploadStream.writableHighWaterMark}`
80
+ )
81
+ core.debug(
82
+ `Zip read high watermark value ${zipUploadStream.readableHighWaterMark}`
83
+ )
84
+
85
+ zip.pipe(zipUploadStream)
86
+ zip.finalize()
87
+
88
+ return zipUploadStream
89
+ }
90
+
91
+ /* istanbul ignore next */
92
+ const zipErrorCallback = (error: any): void => {
93
+ core.error('An error has occurred while creating the zip file for upload')
94
+ core.info(error)
95
+
96
+ throw new Error('An error has occurred during zip creation for the artifact')
97
+ }
98
+
99
+ /* istanbul ignore next */
100
+ const zipWarningCallback = (error: any): void => {
101
+ if (error.code === 'ENOENT') {
102
+ core.warning(
103
+ 'ENOENT warning during artifact zip creation. No such file or directory'
104
+ )
105
+ core.info(error)
106
+ } else {
107
+ core.warning(
108
+ `A non-blocking warning has occurred during artifact zip creation: ${error.code}`
109
+ )
110
+ core.info(error)
111
+ }
112
+ }
113
+
114
+ /* istanbul ignore next */
115
+ const zipFinishCallback = (): void => {
116
+ core.debug('Zip stream for upload has finished.')
117
+ }
118
+
119
+ /* istanbul ignore next */
120
+ const zipEndCallback = (): void => {
121
+ core.debug('Zip stream for upload has ended.')
122
+ }
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Last Reviewed Commit: 06f7fd9df15088233d2593af69ea710eb2fc516b
3
+ * Last Reviewed Date: 2026-01-16
4
+ */
5
+
6
+ import * as core from '@actions/core'
7
+ import * as fs from 'fs'
8
+ import * as path from 'path'
9
+ import { EnvMeta } from '../env.js'
10
+ import { ReserveCacheError, ValidationError } from './errors.js'
11
+ import * as utils from './internal/cacheUtils.js'
12
+ import { createTar, extractTar, listTar } from './internal/tar.js'
13
+ import { DownloadOptions, UploadOptions } from './options.js'
14
+
15
+ export const CACHE_STUBS = {
16
+ restoreCache,
17
+ saveCache
18
+ }
19
+
20
+ /**
21
+ * Validates at least one path is provided.
22
+ *
23
+ * @param paths List of paths to check
24
+ */
25
+ /* istanbul ignore next */
26
+ function checkPaths(paths: string[]): void {
27
+ if (!paths || paths.length === 0)
28
+ throw new ValidationError(
29
+ `Path Validation Error: At least one directory or file path is required`
30
+ )
31
+ }
32
+
33
+ /**
34
+ * Validates the provided key is sufficient length and contains valid characters
35
+ *
36
+ * @param key Key to check
37
+ */
38
+ /* istanbul ignore next */
39
+ function checkKey(key: string): void {
40
+ if (key.length > 512)
41
+ throw new ValidationError(
42
+ `Key Validation Error: ${key} cannot be larger than 512 characters.`
43
+ )
44
+
45
+ const regex = /^[^,]*$/
46
+
47
+ if (!regex.test(key))
48
+ throw new ValidationError(
49
+ `Key Validation Error: ${key} cannot contain commas.`
50
+ )
51
+ }
52
+
53
+ /**
54
+ * Restores cache from keys
55
+ *
56
+ * @param paths List of file paths to restore from the cache
57
+ * @param primaryKey Explicit key for restoring the cache. Lookup is done with prefix matching.
58
+ * @param restoreKeys Optional ordered list of keys to use for restoring the cache if no cache hit occurred for primaryKey
59
+ * @param downloadOptions Cache download options
60
+ * @param enableCrossOsArchive Optional boolean enabled to restore on Windows any cache created on any platform
61
+ * @returns Key for the cache hit, otherwise undefined
62
+ *
63
+ * @remarks
64
+ *
65
+ * - Checks for required environment variables.
66
+ * - Caches are only managed locally...removed check for service version.
67
+ */
68
+ export async function restoreCache(
69
+ paths: string[],
70
+ primaryKey: string,
71
+ restoreKeys?: string[],
72
+ options?: DownloadOptions,
73
+ enableCrossOsArchive = false
74
+ ): Promise<string | undefined> {
75
+ if (!process.env.LOCAL_ACTION_WORKSPACE)
76
+ throw new Error(
77
+ 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/cache!'
78
+ )
79
+ if (!process.env.LOCAL_ACTION_CACHE_PATH)
80
+ throw new Error(
81
+ 'LOCAL_ACTION_CACHE_PATH must be set when interacting with @actions/cache!'
82
+ )
83
+
84
+ checkPaths(paths)
85
+
86
+ return await restoreCacheFromFilesystem(
87
+ paths,
88
+ primaryKey,
89
+ restoreKeys,
90
+ options,
91
+ enableCrossOsArchive
92
+ )
93
+ }
94
+
95
+ /**
96
+ * Restores cache from local filesystem
97
+ *
98
+ * @param paths List of file paths to restore from the cache
99
+ * @param primaryKey Explicit key for restoring the cache. Lookup is done with prefix matching
100
+ * @param restoreKeys Optional ordered list of keys to use for restoring the cache if no cache hit occurred for primaryKey
101
+ * @param downloadOptions Cache download options
102
+ * @param enableCrossOsArchive Optional boolean enabled to restore on Windows any cache created on any platform
103
+ * @returns Key for the cache hit, otherwise undefined
104
+ *
105
+ * @remarks
106
+ *
107
+ * - Replaces v1 and v2 caching implementations.
108
+ * - Download options, other than `lookupOnly`, are ignored.
109
+ */
110
+ async function restoreCacheFromFilesystem(
111
+ paths: string[],
112
+ primaryKey: string,
113
+ restoreKeys?: string[],
114
+ options?: DownloadOptions,
115
+ /* istanbul ignore next */
116
+ enableCrossOsArchive = false
117
+ ): Promise<string | undefined> {
118
+ const keys = [primaryKey, ...(restoreKeys ?? [])]
119
+
120
+ core.debug('Resolved Keys:')
121
+ core.debug(JSON.stringify(keys))
122
+
123
+ if (keys.length > 10)
124
+ throw new ValidationError(
125
+ `Key Validation Error: Keys are limited to a maximum of 10.`
126
+ )
127
+
128
+ for (const key of keys) checkKey(key)
129
+
130
+ let archivePath: string
131
+ let matchedKey = undefined
132
+ let matchedCache = undefined
133
+
134
+ try {
135
+ const compressionMethod = await utils.getCompressionMethod()
136
+ const cacheVersion = utils.getCacheVersion(
137
+ paths,
138
+ compressionMethod,
139
+ enableCrossOsArchive
140
+ )
141
+
142
+ const caches = fs
143
+ .readdirSync(process.env.LOCAL_ACTION_CACHE_PATH!)
144
+ .filter(file => file.endsWith('.cache'))
145
+ .map(file => file.replace('.cache', ''))
146
+
147
+ // Check the caches for a matching key
148
+ for (const key of keys) {
149
+ const cache = caches.find(c => c === key || c.startsWith(key))
150
+
151
+ if (cache) {
152
+ matchedKey = key
153
+ matchedCache = cache
154
+ break
155
+ }
156
+ }
157
+
158
+ if (matchedCache === undefined) {
159
+ core.debug(
160
+ `Cache not found for version ${cacheVersion} of keys: ${keys.join(
161
+ ', '
162
+ )}`
163
+ )
164
+
165
+ return undefined
166
+ }
167
+
168
+ if (primaryKey !== matchedKey)
169
+ core.info(`Cache hit for restore-key: ${matchedKey}`)
170
+ else core.info(`Cache hit for: ${matchedKey}`)
171
+
172
+ if (options?.lookupOnly) {
173
+ core.info('Lookup only - skipping download')
174
+ return matchedKey
175
+ }
176
+
177
+ archivePath = path.join(
178
+ process.env.LOCAL_ACTION_CACHE_PATH!,
179
+ `${matchedCache}.cache`
180
+ )
181
+ core.debug(`Archive path: ${archivePath}`)
182
+
183
+ const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
184
+ core.info(
185
+ `Cache Size: ~${Math.round(
186
+ archiveFileSize / (1024 * 1024)
187
+ )} MB (${archiveFileSize} B)`
188
+ )
189
+
190
+ if (core.isDebug()) await listTar(archivePath, compressionMethod)
191
+ await extractTar(archivePath, compressionMethod)
192
+
193
+ core.info('Cache restored successfully')
194
+ return matchedKey
195
+ } catch (error: any) {
196
+ core.warning(`Failed to restore: ${error.message}`)
197
+ }
198
+
199
+ return undefined
200
+ }
201
+
202
+ /**
203
+ * Saves a list of files with the specified key
204
+ *
205
+ * @param paths List of file paths to be cached
206
+ * @param key Explicit key for restoring the cache
207
+ * @param enableCrossOsArchive Optional boolean enabled to save cache on Windows which could be restored on any platform
208
+ * @param options Cache upload options
209
+ * @returns Cache ID if the cache was saved successfully
210
+ * @throws Error if save fails
211
+ *
212
+ * @remarks
213
+ *
214
+ * - Checks for required environment variables.
215
+ * - Caches are only managed locally...removed check for service version.
216
+ */
217
+ /* istanbul ignore next */
218
+ export async function saveCache(
219
+ paths: string[],
220
+ key: string,
221
+ options?: UploadOptions,
222
+ enableCrossOsArchive = false
223
+ ): Promise<number> {
224
+ if (!process.env.LOCAL_ACTION_WORKSPACE)
225
+ throw new Error(
226
+ 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/cache!'
227
+ )
228
+ if (!process.env.LOCAL_ACTION_CACHE_PATH)
229
+ throw new Error(
230
+ 'LOCAL_ACTION_CACHE_PATH must be set when interacting with @actions/cache!'
231
+ )
232
+
233
+ checkPaths(paths)
234
+ checkKey(key)
235
+
236
+ return await saveCacheToFilesystem(paths, key, options, enableCrossOsArchive)
237
+ }
238
+
239
+ /**
240
+ * Saves cache to local filesystem
241
+ *
242
+ * @param paths List of file paths to be cached
243
+ * @param key Explicit key for restoring the cache
244
+ * @param options Cache upload options
245
+ * @param enableCrossOsArchive Optional boolean enabled to save cache on Windows which could be restored on any platform
246
+ * @returns Cache ID if the cache was saved successfully
247
+ *
248
+ * @remarks
249
+ *
250
+ * - Replaces v1 and v2 caching implementations.
251
+ * - Azure SDK settings and upload options are ignored.
252
+ * - Cache reserving is replaced with file conflict checks.
253
+ */
254
+ async function saveCacheToFilesystem(
255
+ paths: string[],
256
+ key: string,
257
+ options?: UploadOptions,
258
+ /* istanbul ignore next */
259
+ enableCrossOsArchive = false
260
+ ): Promise<number> {
261
+ const compressionMethod = await utils.getCompressionMethod()
262
+ const cachePaths = await utils.resolvePaths(paths)
263
+
264
+ core.debug('Cache Paths:')
265
+ core.debug(`${JSON.stringify(cachePaths)}`)
266
+
267
+ if (cachePaths.length === 0)
268
+ throw new Error(
269
+ `Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.`
270
+ )
271
+
272
+ const archiveFolder = await utils.createTempDirectory()
273
+ const archivePath = path.join(
274
+ archiveFolder,
275
+ utils.getCacheFileName(compressionMethod)
276
+ )
277
+
278
+ core.debug(`Archive Path: ${archivePath}`)
279
+
280
+ try {
281
+ await createTar(archiveFolder, cachePaths, compressionMethod)
282
+
283
+ if (core.isDebug()) await listTar(archivePath, compressionMethod)
284
+
285
+ const archiveSizeBytes = utils.getArchiveFileSizeInBytes(archivePath)
286
+ core.debug(`File Size: ${archiveSizeBytes}`)
287
+
288
+ // Attempt to reserve the cache. In local-action, this is done by checking
289
+ // if there is an existing file with the same name. If so, we fail the save.
290
+ core.debug('Reserving Cache')
291
+ const version = utils.getCacheVersion(
292
+ paths,
293
+ compressionMethod,
294
+ enableCrossOsArchive
295
+ )
296
+
297
+ if (
298
+ fs.existsSync(
299
+ path.join(
300
+ process.env.LOCAL_ACTION_CACHE_PATH!,
301
+ `${key}-${version}.cache`
302
+ )
303
+ )
304
+ )
305
+ throw new ReserveCacheError(
306
+ `Unable to reserve cache with key ${key}, another job may be creating this cache.`
307
+ )
308
+
309
+ // Copy the archive to the cache folder. In local-action, this is simply
310
+ // copying the file.
311
+ core.debug(`Attempting to upload cache located at: ${archivePath}`)
312
+ fs.copyFileSync(
313
+ archivePath,
314
+ path.join(process.env.LOCAL_ACTION_CACHE_PATH!, `${key}-${version}.cache`)
315
+ )
316
+
317
+ // Generate a cache ID. This can just be the next available ID based on
318
+ // existing caches.
319
+ return (
320
+ Math.max(...Object.keys(EnvMeta.caches).map(key => parseInt(key))) + 1
321
+ )
322
+ } catch (error: any) {
323
+ core.warning(`Failed to save: ${error.message}`)
324
+ } finally {
325
+ // Try to delete the original archive to save space
326
+ try {
327
+ await utils.unlinkFile(archivePath)
328
+ } catch (error) {
329
+ core.debug(`Failed to delete archive: ${error}`)
330
+ }
331
+ }
332
+
333
+ return -1
334
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Caching validation error
3
+ */
4
+ export class ValidationError extends Error {
5
+ constructor(message: string) {
6
+ super(message)
7
+ this.name = 'ValidationError'
8
+ Object.setPrototypeOf(this, ValidationError.prototype)
9
+ }
10
+ }
11
+
12
+ /**
13
+ * Cache reservation error
14
+ */
15
+ export class ReserveCacheError extends Error {
16
+ constructor(message: string) {
17
+ super(message)
18
+ this.name = 'ReserveCacheError'
19
+ Object.setPrototypeOf(this, ReserveCacheError.prototype)
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Invalid response error
25
+ */
26
+ export class InvalidResponseError extends Error {
27
+ constructor(message: string) {
28
+ super(message)
29
+ this.name = 'InvalidResponseError'
30
+ }
31
+ }
@@ -0,0 +1,4 @@
1
+ export interface ArchiveTool {
2
+ path: string
3
+ type: string
4
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Last Reviewed Commit: 267841d7bd659368f7382ca22d5ca0b2af68f0b3
3
+ * Last Reviewed Date: 2026-01-16
4
+ *
5
+ * @remarks
6
+ *
7
+ * - Deleted assertDefined function
8
+ * - Deleted getRuntimeToken function
9
+ */
10
+ import * as core from '@actions/core'
11
+ import * as exec from '@actions/exec'
12
+ import * as glob from '@actions/glob'
13
+ import * as io from '@actions/io'
14
+ import * as crypto from 'crypto'
15
+ import * as fs from 'fs'
16
+ import * as path from 'path'
17
+ import * as semver from 'semver'
18
+ import * as util from 'util'
19
+ import {
20
+ CacheFilename,
21
+ CompressionMethod,
22
+ GnuTarPathOnWindows
23
+ } from './constants.js'
24
+
25
+ const versionSalt = '1.0'
26
+
27
+ /**
28
+ * Creates a temporary directory and returns the path
29
+ *
30
+ * @returns Path to the temporary directory
31
+ *
32
+ * @remarks
33
+ *
34
+ * - Replaces location with LOCAL_ACTION_WORKSPACE/actions/temp
35
+ */
36
+ export async function createTempDirectory(): Promise<string> {
37
+ const tempDirectory = path.join(
38
+ process.env.LOCAL_ACTION_WORKSPACE!,
39
+ 'actions',
40
+ 'temp'
41
+ )
42
+ const dest = path.join(tempDirectory, crypto.randomUUID())
43
+
44
+ await io.mkdirP(dest)
45
+
46
+ return dest
47
+ }
48
+
49
+ /**
50
+ * Gets the size of the archive file in bytes
51
+ *
52
+ * @param filePath Path to the archive file
53
+ * @returns Size of the archive file in bytes
54
+ */
55
+ /* istanbul ignore next */
56
+ export function getArchiveFileSizeInBytes(filePath: string): number {
57
+ return fs.statSync(filePath).size
58
+ }
59
+
60
+ /**
61
+ * Resolves the paths to be cached
62
+ *
63
+ * @param patterns Array of glob patterns
64
+ * @returns Array of resolved paths
65
+ */
66
+ /* istanbul ignore next */
67
+ export async function resolvePaths(patterns: string[]): Promise<string[]> {
68
+ const paths: string[] = []
69
+ const workspace = process.env['GITHUB_WORKSPACE'] ?? process.cwd()
70
+ const globber = await glob.create(patterns.join('\n'), {
71
+ implicitDescendants: false
72
+ })
73
+
74
+ for await (const file of globber.globGenerator()) {
75
+ const relativeFile = path
76
+ .relative(workspace, file)
77
+ .replace(new RegExp(`\\${path.sep}`, 'g'), '/')
78
+ core.debug(`Matched: ${relativeFile}`)
79
+
80
+ // Paths are made relative so the tar entries are all relative to the root
81
+ // of the workspace.
82
+ if (relativeFile === '') paths.push('.')
83
+ else paths.push(`${relativeFile}`)
84
+ }
85
+
86
+ return paths
87
+ }
88
+
89
+ /**
90
+ * Deletes a file
91
+ *
92
+ * @param filePath Path to the file
93
+ * @returns Promise that resolves when the file is deleted
94
+ */
95
+ /* istanbul ignore next */
96
+ export async function unlinkFile(filePath: fs.PathLike): Promise<void> {
97
+ return util.promisify(fs.unlink)(filePath)
98
+ }
99
+
100
+ /**
101
+ * Gets the version of an application by executing it with the provided
102
+ * arguments
103
+ *
104
+ * @param app Application to get the version of
105
+ * @param additionalArgs Additional arguments to pass to the application
106
+ * @returns Version of the application
107
+ */
108
+ /* istanbul ignore next */
109
+ async function getVersion(
110
+ app: string,
111
+ additionalArgs: string[] = []
112
+ ): Promise<string> {
113
+ let versionOutput = ''
114
+ additionalArgs.push('--version')
115
+ core.debug(`Checking ${app} ${additionalArgs.join(' ')}`)
116
+
117
+ try {
118
+ await exec.exec(`${app}`, additionalArgs, {
119
+ ignoreReturnCode: true,
120
+ silent: true,
121
+ listeners: {
122
+ stdout: (data: Buffer): string => (versionOutput += data.toString()),
123
+ stderr: (data: Buffer): string => (versionOutput += data.toString())
124
+ }
125
+ })
126
+ } catch (err: any) {
127
+ core.debug(err.message)
128
+ }
129
+
130
+ versionOutput = versionOutput.trim()
131
+ core.debug(versionOutput)
132
+
133
+ return versionOutput
134
+ }
135
+
136
+ /**
137
+ * Gets the compression method to use when creating caches.
138
+ *
139
+ * Use zstandard if possible to maximize cache performance.
140
+ */
141
+ /* istanbul ignore next */
142
+ export async function getCompressionMethod(): Promise<CompressionMethod> {
143
+ const versionOutput = await getVersion('zstd', ['--quiet'])
144
+ const version = semver.clean(versionOutput)
145
+
146
+ core.debug(`zstd version: ${version}`)
147
+
148
+ if (versionOutput === '') return CompressionMethod.Gzip
149
+ else return CompressionMethod.ZstdWithoutLong
150
+ }
151
+
152
+ /**
153
+ * Gets the cache file name based on the compression method
154
+ *
155
+ * @param compressionMethod Compression method
156
+ * @returns Cache file name
157
+ */
158
+ /* istanbul ignore next */
159
+ export function getCacheFileName(compressionMethod: CompressionMethod): string {
160
+ return compressionMethod === CompressionMethod.Gzip
161
+ ? CacheFilename.Gzip
162
+ : CacheFilename.Zstd
163
+ }
164
+
165
+ /**
166
+ * Gets the path to GNU tar on Windows, if available
167
+ *
168
+ * @returns Path to GNU tar on Windows, or empty string if not available
169
+ */
170
+ /* istanbul ignore next */
171
+ export async function getGnuTarPathOnWindows(): Promise<string> {
172
+ if (fs.existsSync(GnuTarPathOnWindows)) return GnuTarPathOnWindows
173
+
174
+ const versionOutput = await getVersion('tar')
175
+ return versionOutput.toLowerCase().includes('gnu tar') ? io.which('tar') : ''
176
+ }
177
+
178
+ /**
179
+ * Get the cache version
180
+ *
181
+ * @param paths Paths included in the cache
182
+ * @param compressionMethod Compression method
183
+ * @param enableCrossOsArchive Enable cross OS archive
184
+ * @returns Cache version
185
+ */
186
+ /* istanbul ignore next */
187
+ export function getCacheVersion(
188
+ paths: string[],
189
+ compressionMethod?: CompressionMethod,
190
+ enableCrossOsArchive = false
191
+ ): string {
192
+ // don't pass changes upstream
193
+ const components = paths.slice()
194
+
195
+ // Add compression method to cache version to restore
196
+ // compressed cache as per compression method
197
+ if (compressionMethod) components.push(compressionMethod)
198
+
199
+ // Only check for windows platforms if enableCrossOsArchive is false
200
+ if (process.platform === 'win32' && !enableCrossOsArchive)
201
+ components.push('windows-only')
202
+
203
+ // Add salt to cache version to support breaking changes in cache entry
204
+ components.push(versionSalt)
205
+
206
+ return crypto.createHash('sha256').update(components.join('|')).digest('hex')
207
+ }