@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,53 @@
1
+ import { jest } from '@jest/globals'
2
+ import * as core from '../../../../../__fixtures__/@actions/core.js'
3
+
4
+ jest.unstable_mockModule('../../../../../src/stubs/core/core.js', () => core)
5
+
6
+ const pathAndArtifactNameValidation =
7
+ await import('../../../../../src/stubs/artifact/internal/upload/path-and-artifact-name-validation.js')
8
+
9
+ describe('path-and-artifact-name-validation', () => {
10
+ afterEach(() => {
11
+ jest.resetAllMocks()
12
+ })
13
+
14
+ describe('validateArtifactName', () => {
15
+ it('Throws if no name is provided', () => {
16
+ expect(() =>
17
+ pathAndArtifactNameValidation.validateArtifactName('')
18
+ ).toThrow('Provided artifact name input during validation is empty')
19
+ })
20
+
21
+ it('Throws if name contains invalid characters', () => {
22
+ expect(() =>
23
+ pathAndArtifactNameValidation.validateArtifactName('invalid<name')
24
+ ).toThrow()
25
+ })
26
+
27
+ it('Does not throw if name is valid', () => {
28
+ expect(() =>
29
+ pathAndArtifactNameValidation.validateArtifactName('valid-name')
30
+ ).not.toThrow()
31
+ })
32
+ })
33
+
34
+ describe('validateFilePath', () => {
35
+ it('Throws if no path is provided', () => {
36
+ expect(() => pathAndArtifactNameValidation.validateFilePath('')).toThrow(
37
+ 'Provided file path input during validation is empty'
38
+ )
39
+ })
40
+
41
+ it('Throws if path contains invalid characters', () => {
42
+ expect(() =>
43
+ pathAndArtifactNameValidation.validateFilePath('invalid<path')
44
+ ).toThrow()
45
+ })
46
+
47
+ it('Does not throw if path is valid', () => {
48
+ expect(() =>
49
+ pathAndArtifactNameValidation.validateFilePath('valid/path')
50
+ ).not.toThrow()
51
+ })
52
+ })
53
+ })
@@ -0,0 +1,126 @@
1
+ import { jest } from '@jest/globals'
2
+ import { type Stats } from 'fs'
3
+ import * as core from '../../../../../__fixtures__/@actions/core.js'
4
+ import * as crypto from '../../../../../__fixtures__/crypto.js'
5
+ import * as fs from '../../../../../__fixtures__/fs.js'
6
+ import * as stream from '../../../../../__fixtures__/stream/promises.js'
7
+ import { ResetCoreMetadata } from '../../../../../src/stubs/core/core.js'
8
+ import { EnvMeta, ResetEnvMetadata } from '../../../../../src/stubs/env.js'
9
+
10
+ const validateArtifactName = jest.fn()
11
+ const validateRootDirectory = jest.fn()
12
+ const getUploadZipSpecification = jest.fn()
13
+ const createZipUploadStream = jest.fn()
14
+ const zipUploadStream = {
15
+ on: () => zipUploadStream,
16
+ pipe: () => zipUploadStream,
17
+ setEncoding: () => zipUploadStream
18
+ }
19
+ const hashStream = {
20
+ end: () => hashStream,
21
+ read: () => hashStream
22
+ }
23
+ const writeStream = {
24
+ on: () => writeStream
25
+ }
26
+
27
+ jest.unstable_mockModule('crypto', () => crypto)
28
+ jest.unstable_mockModule('fs', () => fs)
29
+ jest.unstable_mockModule('stream/promises', () => stream)
30
+ jest.unstable_mockModule('../../../../../src/stubs/core/core.js', () => core)
31
+ jest.unstable_mockModule(
32
+ '../../../../../src/stubs/artifact/internal/upload/upload-zip-specification.js',
33
+ () => ({
34
+ validateRootDirectory,
35
+ getUploadZipSpecification
36
+ })
37
+ )
38
+ jest.unstable_mockModule(
39
+ '../../../../../src/stubs/artifact/internal/upload/path-and-artifact-name-validation.js',
40
+ () => ({
41
+ validateArtifactName
42
+ })
43
+ )
44
+ jest.unstable_mockModule(
45
+ '../../../../../src/stubs/artifact/internal/upload/zip.js',
46
+ () => ({
47
+ createZipUploadStream
48
+ })
49
+ )
50
+
51
+ const uploadArtifact =
52
+ await import('../../../../../src/stubs/artifact/internal/upload/upload-artifact.js')
53
+
54
+ describe('upload-artifacts', () => {
55
+ beforeEach(() => {
56
+ // Reset environment variables
57
+ process.env.LOCAL_ACTION_ARTIFACT_PATH = '/tmp/artifacts'
58
+
59
+ // Reset metadata
60
+ ResetEnvMetadata()
61
+ ResetCoreMetadata()
62
+
63
+ // Reset mocks
64
+ getUploadZipSpecification.mockImplementation(() => [
65
+ {
66
+ sourcePath: 'source-path',
67
+ destinationPath: 'destination-path',
68
+ stats: {} as Stats
69
+ }
70
+ ])
71
+ createZipUploadStream.mockImplementation(() => zipUploadStream)
72
+ crypto.createHash.mockImplementation(() => hashStream)
73
+ fs.createWriteStream.mockImplementation(() => writeStream)
74
+ })
75
+
76
+ afterEach(() => {
77
+ jest.resetAllMocks()
78
+
79
+ // Unset environment variables
80
+ delete process.env.LOCAL_ACTION_ARTIFACT_PATH
81
+ })
82
+
83
+ describe('uploadArtifact', () => {
84
+ it('Uploads an artifact', async () => {
85
+ await uploadArtifact.uploadArtifact(
86
+ 'artifact-name',
87
+ ['file1', 'file2'],
88
+ 'root'
89
+ )
90
+
91
+ expect(getUploadZipSpecification).toHaveBeenCalledTimes(1)
92
+ expect(createZipUploadStream).toHaveBeenCalledTimes(1)
93
+ expect(fs.createWriteStream).toHaveBeenCalledTimes(1)
94
+ expect(crypto.createHash).toHaveBeenCalledTimes(1)
95
+ expect(stream.finished).toHaveBeenCalledTimes(1)
96
+ })
97
+
98
+ it('Throws if no files are included in the artifact', async () => {
99
+ getUploadZipSpecification.mockImplementation(() => [])
100
+
101
+ await expect(
102
+ uploadArtifact.uploadArtifact('artifact-name', [], 'root')
103
+ ).rejects.toThrow()
104
+ })
105
+
106
+ it('Throws if an artifact exists with the same name', async () => {
107
+ EnvMeta.artifacts = [{ name: 'artifact-name', id: 1, size: 0 }]
108
+
109
+ await expect(
110
+ uploadArtifact.uploadArtifact('artifact-name', ['file'], 'root')
111
+ ).rejects.toThrow()
112
+ })
113
+
114
+ it('Throws if more than 10 artifacts are created', async () => {
115
+ EnvMeta.artifacts = Array.from({ length: 10 }, (_, i) => ({
116
+ name: `artifact-${i}`,
117
+ id: i + 1,
118
+ size: 0
119
+ }))
120
+
121
+ await expect(
122
+ uploadArtifact.uploadArtifact('new-artifact', ['file'], 'root')
123
+ ).rejects.toThrow()
124
+ })
125
+ })
126
+ })
@@ -0,0 +1,49 @@
1
+ import { jest } from '@jest/globals'
2
+ import * as core from '../../../../../__fixtures__/@actions/core.js'
3
+ import * as fs from '../../../../../__fixtures__/fs.js'
4
+
5
+ jest.unstable_mockModule('fs', () => fs)
6
+ jest.unstable_mockModule('../../../../../src/stubs/core/core.js', () => core)
7
+
8
+ const uploadZipSpecification =
9
+ await import('../../../../../src/stubs/artifact/internal/upload/upload-zip-specification.js')
10
+
11
+ describe('upload-zip-specification', () => {
12
+ afterEach(() => {
13
+ jest.resetAllMocks()
14
+ })
15
+
16
+ describe('validateRootDirectory', () => {
17
+ it('Throws if the directory does not exist', () => {
18
+ fs.existsSync.mockReturnValue(false)
19
+
20
+ expect(() =>
21
+ uploadZipSpecification.validateRootDirectory('/invalid/path')
22
+ ).toThrow('The provided rootDirectory /invalid/path does not exist')
23
+ })
24
+
25
+ it('Throws if the path is not a directory', () => {
26
+ fs.existsSync.mockReturnValue(true)
27
+ fs.statSync.mockReturnValue({
28
+ isDirectory: () => false
29
+ })
30
+
31
+ expect(() =>
32
+ uploadZipSpecification.validateRootDirectory('/invalid/path')
33
+ ).toThrow(
34
+ 'The provided rootDirectory /invalid/path is not a valid directory'
35
+ )
36
+ })
37
+
38
+ it('Does not throw if the directory is valid', () => {
39
+ fs.existsSync.mockReturnValue(true)
40
+ fs.statSync.mockReturnValue({
41
+ isDirectory: () => true
42
+ })
43
+
44
+ expect(() =>
45
+ uploadZipSpecification.validateRootDirectory('/valid/path')
46
+ ).not.toThrow()
47
+ })
48
+ })
49
+ })
@@ -0,0 +1,357 @@
1
+ import { jest } from '@jest/globals'
2
+ import * as core from '../../../__fixtures__/@actions/core.js'
3
+ import * as fs from '../../../__fixtures__/fs.js'
4
+
5
+ jest.unstable_mockModule('@actions/core', () => core)
6
+ jest.unstable_mockModule('fs', () => fs)
7
+
8
+ // Mock the internal dependencies
9
+ const mockTar = {
10
+ createTar: jest.fn() as jest.MockedFunction<any>,
11
+ extractTar: jest.fn() as jest.MockedFunction<any>,
12
+ listTar: jest.fn() as jest.MockedFunction<any>
13
+ }
14
+
15
+ jest.unstable_mockModule(
16
+ '../../../src/stubs/cache/internal/tar.js',
17
+ () => mockTar
18
+ )
19
+
20
+ const mockUtils = {
21
+ createTempDirectory: jest.fn() as jest.MockedFunction<() => Promise<string>>,
22
+ getArchiveFileSizeInBytes: jest.fn() as jest.MockedFunction<
23
+ (path: string) => number
24
+ >,
25
+ getCacheFileName: jest.fn() as jest.MockedFunction<
26
+ (compression: string) => string
27
+ >,
28
+ getCacheVersion: jest.fn() as jest.MockedFunction<
29
+ (paths: string[], compression: string, crossOs: boolean) => string
30
+ >,
31
+ getCompressionMethod: jest.fn() as jest.MockedFunction<() => Promise<string>>,
32
+ resolvePaths: jest.fn() as jest.MockedFunction<
33
+ (paths: string[]) => Promise<string[]>
34
+ >,
35
+ unlinkFile: jest.fn() as jest.MockedFunction<(path: string) => Promise<void>>
36
+ }
37
+
38
+ jest.unstable_mockModule(
39
+ '../../../src/stubs/cache/internal/cacheUtils.js',
40
+ () => mockUtils
41
+ )
42
+
43
+ // Mock env
44
+ const mockEnvMeta = {
45
+ caches: {}
46
+ }
47
+
48
+ jest.unstable_mockModule('../../../src/stubs/env.js', () => ({
49
+ EnvMeta: mockEnvMeta
50
+ }))
51
+
52
+ // Import after mocking
53
+ const { CACHE_STUBS, restoreCache, saveCache } =
54
+ await import('../../../src/stubs/cache/cache.js')
55
+
56
+ describe('cache/cache', () => {
57
+ const originalWorkspace = process.env.LOCAL_ACTION_WORKSPACE
58
+ const originalCachePath = process.env.LOCAL_ACTION_CACHE_PATH
59
+
60
+ beforeEach(() => {
61
+ jest.clearAllMocks()
62
+
63
+ process.env.LOCAL_ACTION_WORKSPACE = '/tmp/workspace'
64
+ process.env.LOCAL_ACTION_CACHE_PATH = '/tmp/cache'
65
+
66
+ // Reset default mocks
67
+ mockUtils.getArchiveFileSizeInBytes.mockReturnValue(1024 * 1024) // 1MB
68
+ mockUtils.getCacheFileName.mockReturnValue('cache.tgz')
69
+ mockUtils.getCacheVersion.mockReturnValue('abc123')
70
+ mockUtils.getCompressionMethod.mockResolvedValue('gzip')
71
+ mockUtils.resolvePaths.mockResolvedValue(['/path/to/files'])
72
+ mockUtils.createTempDirectory.mockResolvedValue('/tmp/archive')
73
+ mockUtils.unlinkFile.mockResolvedValue()
74
+
75
+ core.isDebug.mockReturnValue(false)
76
+ fs.readdirSync.mockReturnValue([])
77
+ fs.existsSync.mockReturnValue(false)
78
+
79
+ mockTar.createTar.mockResolvedValue(undefined)
80
+ mockTar.extractTar.mockResolvedValue(undefined)
81
+ mockTar.listTar.mockResolvedValue(undefined)
82
+
83
+ mockEnvMeta.caches = {}
84
+ })
85
+
86
+ afterEach(() => {
87
+ process.env.LOCAL_ACTION_WORKSPACE = originalWorkspace
88
+ process.env.LOCAL_ACTION_CACHE_PATH = originalCachePath
89
+ })
90
+
91
+ describe('CACHE_STUBS', () => {
92
+ it('Exports the correct cache stub functions', () => {
93
+ expect(CACHE_STUBS).toHaveProperty('restoreCache')
94
+ expect(CACHE_STUBS).toHaveProperty('saveCache')
95
+ expect(typeof CACHE_STUBS.restoreCache).toBe('function')
96
+ expect(typeof CACHE_STUBS.saveCache).toBe('function')
97
+ })
98
+ })
99
+
100
+ describe('restoreCache', () => {
101
+ it('Throws if LOCAL_ACTION_WORKSPACE is not set', async () => {
102
+ delete process.env.LOCAL_ACTION_WORKSPACE
103
+
104
+ await expect(restoreCache(['src/'], 'cache-key')).rejects.toThrow(
105
+ 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/cache!'
106
+ )
107
+ })
108
+
109
+ it('Throws if LOCAL_ACTION_CACHE_PATH is not set', async () => {
110
+ delete process.env.LOCAL_ACTION_CACHE_PATH
111
+
112
+ await expect(restoreCache(['src/'], 'cache-key')).rejects.toThrow(
113
+ 'LOCAL_ACTION_CACHE_PATH must be set when interacting with @actions/cache!'
114
+ )
115
+ })
116
+
117
+ it('Throws if no paths are provided', async () => {
118
+ await expect(restoreCache([], 'cache-key')).rejects.toThrow(
119
+ 'Path Validation Error: At least one directory or file path is required'
120
+ )
121
+ })
122
+
123
+ it('Throws if too many keys are provided', async () => {
124
+ const manyKeys = Array(11).fill('key')
125
+
126
+ await expect(
127
+ restoreCache(['src/'], 'primary-key', manyKeys)
128
+ ).rejects.toThrow(
129
+ 'Key Validation Error: Keys are limited to a maximum of 10.'
130
+ )
131
+ })
132
+
133
+ it('Returns undefined if no cache is found', async () => {
134
+ fs.readdirSync.mockReturnValue([])
135
+
136
+ const result = await restoreCache(['src/'], 'cache-key')
137
+
138
+ expect(result).toBeUndefined()
139
+ })
140
+
141
+ it('Returns matched key for exact cache hit', async () => {
142
+ // Mock file system to return cache files - the key must match exactly
143
+ fs.readdirSync.mockReturnValue(['cache-key-abc123def456.cache'])
144
+
145
+ const result = await restoreCache(['src/'], 'cache-key')
146
+
147
+ expect(result).toBe('cache-key')
148
+ expect(core.info).toHaveBeenCalledWith('Cache hit for: cache-key')
149
+ })
150
+
151
+ it('Handles undefined restore keys (nullish coalescing coverage)', async () => {
152
+ fs.readdirSync.mockReturnValue(['cache-key-abc123def456.cache'])
153
+
154
+ const result = await restoreCache(['src/'], 'cache-key', undefined)
155
+
156
+ expect(result).toBe('cache-key')
157
+ expect(core.info).toHaveBeenCalledWith('Cache hit for: cache-key')
158
+ })
159
+
160
+ it('Returns matched key for restore key hit', async () => {
161
+ // Mock file system to return cache files that match restore key
162
+ fs.readdirSync.mockReturnValue(['restore-key-abc123def456.cache'])
163
+
164
+ const result = await restoreCache(['src/'], 'primary-key', [
165
+ 'restore-key'
166
+ ])
167
+
168
+ expect(result).toBe('restore-key')
169
+ expect(core.info).toHaveBeenCalledWith(
170
+ 'Cache hit for restore-key: restore-key'
171
+ )
172
+ })
173
+
174
+ it('Handles lookup only option', async () => {
175
+ fs.readdirSync.mockReturnValue(['cache-key-abc123def456.cache'])
176
+
177
+ const result = await restoreCache(['src/'], 'cache-key', [], {
178
+ lookupOnly: true
179
+ })
180
+
181
+ expect(result).toBe('cache-key')
182
+ expect(core.info).toHaveBeenCalledWith('Lookup only - skipping download')
183
+ expect(mockTar.extractTar).not.toHaveBeenCalled()
184
+ })
185
+
186
+ it('Extracts cache when found', async () => {
187
+ fs.readdirSync.mockReturnValue(['cache-key-abc123def456.cache'])
188
+
189
+ const result = await restoreCache(['src/'], 'cache-key')
190
+
191
+ expect(result).toBe('cache-key')
192
+ expect(mockTar.extractTar).toHaveBeenCalled()
193
+ expect(core.info).toHaveBeenCalledWith('Cache restored successfully')
194
+ })
195
+
196
+ it('Lists tar contents in debug mode', async () => {
197
+ fs.readdirSync.mockReturnValue(['cache-key-abc123def456.cache'])
198
+ core.isDebug.mockReturnValue(true)
199
+
200
+ await restoreCache(['src/'], 'cache-key')
201
+
202
+ expect(mockTar.listTar).toHaveBeenCalled()
203
+ })
204
+
205
+ it('Handles errors during restoration', async () => {
206
+ fs.readdirSync.mockReturnValue(['cache-key-abc123def456.cache'])
207
+ mockTar.extractTar.mockRejectedValue(new Error('Extraction failed'))
208
+
209
+ const result = await restoreCache(['src/'], 'cache-key')
210
+
211
+ expect(result).toBeUndefined()
212
+ expect(core.warning).toHaveBeenCalledWith(
213
+ 'Failed to restore: Extraction failed'
214
+ )
215
+ })
216
+
217
+ it('Finds cache with partial key match', async () => {
218
+ // Reset to clean state and setup specific mock for this test
219
+ fs.readdirSync.mockReturnValue(['cache-key-extra-abc123def456.cache'])
220
+
221
+ const result = await restoreCache(['src/'], 'cache-key')
222
+
223
+ expect(result).toBe('cache-key')
224
+ })
225
+ })
226
+
227
+ describe('saveCache', () => {
228
+ beforeEach(() => {
229
+ mockEnvMeta.caches = { 1: 'existing-cache.cache' }
230
+ })
231
+
232
+ it('Throws if LOCAL_ACTION_WORKSPACE is not set', async () => {
233
+ delete process.env.LOCAL_ACTION_WORKSPACE
234
+
235
+ await expect(saveCache(['src/'], 'cache-key')).rejects.toThrow(
236
+ 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/cache!'
237
+ )
238
+ })
239
+
240
+ it('Throws if LOCAL_ACTION_CACHE_PATH is not set', async () => {
241
+ delete process.env.LOCAL_ACTION_CACHE_PATH
242
+
243
+ await expect(saveCache(['src/'], 'cache-key')).rejects.toThrow(
244
+ 'LOCAL_ACTION_CACHE_PATH must be set when interacting with @actions/cache!'
245
+ )
246
+ })
247
+
248
+ it('Throws if no paths are provided', async () => {
249
+ await expect(saveCache([], 'cache-key')).rejects.toThrow(
250
+ 'Path Validation Error: At least one directory or file path is required'
251
+ )
252
+ })
253
+
254
+ it('Throws if key is too long', async () => {
255
+ const longKey = 'a'.repeat(513)
256
+
257
+ await expect(saveCache(['src/'], longKey)).rejects.toThrow(
258
+ `Key Validation Error: ${longKey} cannot be larger than 512 characters.`
259
+ )
260
+ })
261
+
262
+ it('Throws if key contains commas', async () => {
263
+ const invalidKey = 'cache,key'
264
+
265
+ await expect(saveCache(['src/'], invalidKey)).rejects.toThrow(
266
+ `Key Validation Error: ${invalidKey} cannot contain commas.`
267
+ )
268
+ })
269
+
270
+ it('Returns -1 if no paths resolve', async () => {
271
+ mockUtils.resolvePaths.mockResolvedValue([])
272
+
273
+ // The implementation throws an error when no paths resolve
274
+ await expect(saveCache(['src/'], 'cache-key')).rejects.toThrow(
275
+ 'Path(s) specified in the action for caching do(es) not exist'
276
+ )
277
+ })
278
+
279
+ it('Throws if cache already exists', async () => {
280
+ // Setup successful path resolution but existing cache
281
+ mockUtils.resolvePaths.mockResolvedValue(['/resolved/path'])
282
+ fs.existsSync.mockReturnValue(true)
283
+
284
+ const result = await saveCache(['src/'], 'cache-key')
285
+
286
+ // The implementation catches the error and returns -1
287
+ expect(result).toBe(-1)
288
+ })
289
+
290
+ it('Successfully saves cache', async () => {
291
+ // Setup successful path resolution and no existing cache
292
+ mockUtils.resolvePaths.mockResolvedValue(['/resolved/path'])
293
+ fs.existsSync.mockReturnValue(false)
294
+
295
+ const result = await saveCache(['src/'], 'cache-key')
296
+
297
+ expect(result).toBe(2) // Next ID after existing cache
298
+ expect(mockTar.createTar).toHaveBeenCalled()
299
+ expect(fs.copyFileSync).toHaveBeenCalled()
300
+ })
301
+
302
+ it('Lists tar contents in debug mode', async () => {
303
+ mockUtils.resolvePaths.mockResolvedValue(['/resolved/path'])
304
+ fs.existsSync.mockReturnValue(false)
305
+ core.isDebug.mockReturnValue(true)
306
+
307
+ await saveCache(['src/'], 'cache-key')
308
+
309
+ expect(mockTar.listTar).toHaveBeenCalled()
310
+ })
311
+
312
+ it('Handles errors during save', async () => {
313
+ mockUtils.resolvePaths.mockResolvedValue(['/resolved/path'])
314
+ fs.existsSync.mockReturnValue(false)
315
+ mockTar.createTar.mockRejectedValue(new Error('Create failed'))
316
+
317
+ const result = await saveCache(['src/'], 'cache-key')
318
+
319
+ expect(result).toBe(-1)
320
+ expect(core.warning).toHaveBeenCalledWith('Failed to save: Create failed')
321
+ })
322
+
323
+ it('Cleans up archive file after save', async () => {
324
+ mockUtils.resolvePaths.mockResolvedValue(['/resolved/path'])
325
+ fs.existsSync.mockReturnValue(false)
326
+
327
+ await saveCache(['src/'], 'cache-key')
328
+
329
+ expect(mockUtils.unlinkFile).toHaveBeenCalled()
330
+ })
331
+
332
+ it('Handles archive cleanup errors', async () => {
333
+ mockUtils.resolvePaths.mockResolvedValue(['/resolved/path'])
334
+ fs.existsSync.mockReturnValue(false)
335
+ mockUtils.unlinkFile.mockRejectedValue(new Error('Cleanup failed'))
336
+
337
+ await saveCache(['src/'], 'cache-key')
338
+
339
+ expect(core.debug).toHaveBeenCalledWith(
340
+ 'Failed to delete archive: Error: Cleanup failed'
341
+ )
342
+ })
343
+
344
+ it('Uses cross OS archive setting', async () => {
345
+ mockUtils.resolvePaths.mockResolvedValue(['/resolved/path'])
346
+ fs.existsSync.mockReturnValue(false)
347
+
348
+ await saveCache(['src/'], 'cache-key', {}, true)
349
+
350
+ expect(mockUtils.getCacheVersion).toHaveBeenCalledWith(
351
+ ['src/'],
352
+ 'gzip',
353
+ true
354
+ )
355
+ })
356
+ })
357
+ })
@@ -0,0 +1,55 @@
1
+ import {
2
+ InvalidResponseError,
3
+ ReserveCacheError,
4
+ ValidationError
5
+ } from '../../../src/stubs/cache/errors.js'
6
+
7
+ describe('cache/errors', () => {
8
+ describe('ValidationError', () => {
9
+ it('Creates a ValidationError with the correct message', () => {
10
+ const message = 'Test validation error'
11
+ const error = new ValidationError(message)
12
+
13
+ expect(error.message).toBe(message)
14
+ expect(error.name).toBe('ValidationError')
15
+ expect(error).toBeInstanceOf(ValidationError)
16
+ expect(error).toBeInstanceOf(Error)
17
+ })
18
+
19
+ it('Sets the prototype correctly', () => {
20
+ const error = new ValidationError('test')
21
+
22
+ expect(Object.getPrototypeOf(error)).toBe(ValidationError.prototype)
23
+ })
24
+ })
25
+
26
+ describe('ReserveCacheError', () => {
27
+ it('Creates a ReserveCacheError with the correct message', () => {
28
+ const message = 'Test reserve cache error'
29
+ const error = new ReserveCacheError(message)
30
+
31
+ expect(error.message).toBe(message)
32
+ expect(error.name).toBe('ReserveCacheError')
33
+ expect(error).toBeInstanceOf(ReserveCacheError)
34
+ expect(error).toBeInstanceOf(Error)
35
+ })
36
+
37
+ it('Sets the prototype correctly', () => {
38
+ const error = new ReserveCacheError('test')
39
+
40
+ expect(Object.getPrototypeOf(error)).toBe(ReserveCacheError.prototype)
41
+ })
42
+ })
43
+
44
+ describe('InvalidResponseError', () => {
45
+ it('Creates an InvalidResponseError with the correct message', () => {
46
+ const message = 'Test invalid response error'
47
+ const error = new InvalidResponseError(message)
48
+
49
+ expect(error.message).toBe(message)
50
+ expect(error.name).toBe('InvalidResponseError')
51
+ expect(error).toBeInstanceOf(InvalidResponseError)
52
+ expect(error).toBeInstanceOf(Error)
53
+ })
54
+ })
55
+ })