Compare commits

...

359 Commits

Author SHA1 Message Date
Aiqiao Yan 0607d7a54b release new versions for a few packages 2026-04-21 17:15:02 +00:00
Aiqiao Yan 36d90eb54c Merge pull request #2356 from actions/dependabot/npm_and_yarn/flatted-3.4.2
chore(deps-dev): bump flatted from 3.3.3 to 3.4.2
2026-04-21 12:59:58 -04:00
Aiqiao Yan 4ee32849b4 Merge pull request #2346 from actions/dependabot/npm_and_yarn/packages/github/undici-6.24.0
chore(deps): bump undici from 6.23.0 to 6.24.0 in /packages/github
2026-04-21 12:55:26 -04:00
Aiqiao Yan d76f9fe99a Merge pull request #2348 from actions/dependabot/npm_and_yarn/packages/core/undici-6.24.1
chore(deps): bump undici from 6.23.0 to 6.24.1 in /packages/core
2026-04-21 12:54:48 -04:00
Aiqiao Yan 7e08d73d76 Merge pull request #2345 from actions/dependabot/npm_and_yarn/packages/glob/undici-6.24.0
chore(deps): bump undici from 6.23.0 to 6.24.0 in /packages/glob
2026-04-21 12:42:56 -04:00
Aiqiao Yan 8b842d839b Merge pull request #2355 from shogo82148/bump-minimatch-v10
@actions/glob: bump minimatch from v3.0.4 to v10.2.5
2026-04-21 12:38:52 -04:00
ICHINOSE Shogo 16cd46c365 Merge branch 'main' into bump-minimatch-v10 2026-04-21 21:43:27 +09:00
Aiqiao Yan 75b8dd1009 Merge pull request #2369 from actions/dependabot/npm_and_yarn/packages/glob/brace-expansion-1.1.13
chore(deps): bump brace-expansion from 1.1.12 to 1.1.13 in /packages/glob
2026-04-20 17:44:57 -04:00
Aiqiao Yan a7c6618070 Merge pull request #2381 from actions/dependabot/npm_and_yarn/axios-1.15.1
chore(deps-dev): bump axios from 1.12.2 to 1.15.1
2026-04-20 17:44:12 -04:00
Aiqiao Yan 54ad3ca9ba Merge pull request #2347 from actions/dependabot/npm_and_yarn/packages/http-client/undici-6.24.0
chore(deps): bump undici from 6.23.0 to 6.24.0 in /packages/http-client
2026-04-20 17:34:56 -04:00
dependabot[bot] 3c424f0d63 chore(deps-dev): bump axios from 1.12.2 to 1.15.1
Bumps [axios](https://github.com/axios/axios) from 1.12.2 to 1.15.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.12.2...v1.15.1)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 21:34:33 +00:00
Aiqiao Yan d9346d8d93 Merge pull request #2378 from actions/dependabot/npm_and_yarn/follow-redirects-1.16.0
chore(deps-dev): bump follow-redirects from 1.15.11 to 1.16.0
2026-04-20 17:33:08 -04:00
dependabot[bot] 1f375f130a chore(deps-dev): bump follow-redirects from 1.15.11 to 1.16.0
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.11 to 1.16.0.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.11...v1.16.0)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-version: 1.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-15 18:58:03 +00:00
dependabot[bot] 140509034c chore(deps): bump brace-expansion in /packages/glob
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.13.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-08 20:54:11 +00:00
Salman Chishti b68d046fe3 Merge pull request #2366 from salmanmkc/salmanmkc/github-9.1.0-release
chore: bump @actions/github to 9.1.0 for release
2026-04-08 21:11:16 +01:00
Salman Chishti e4598e374b chore: bump @actions/github to 9.1.0 for release
- Version bump 9.0.0 → 9.1.0 in package.json
- Update RELEASES.md with idempotency guard note and PR link
2026-04-08 20:07:19 +00:00
Salman Chishti 14a090004e Merge pull request #2364 from salmanmkc/salmanmkc/orchestration-id-support
feat(github): add orchestration ID to user-agent in getOctokitOptions
2026-04-08 20:54:57 +01:00
Salman Chishti 3643ce2db4 style: fix prettier formatting in orchestration tests 2026-04-08 19:38:31 +00:00
Salman Chishti ffeb50bd02 fix: prevent duplicate orchestration ID in user-agent
Add idempotency check to getUserAgentWithOrchestrationId — if the
tag is already present in baseUserAgent, return it unchanged. This
prevents doubling when both the exported helper and getOctokitOptions
run for the same client.
2026-04-08 16:49:32 +00:00
Salman Chishti b0917c5a37 style: fix prettier formatting in orchestration tests 2026-04-07 16:35:32 +00:00
Salman Chishti a8ea745713 feat(github): append orchestration ID to user-agent in getOctokitOptions
When ACTIONS_ORCHESTRATION_ID is set, appends
actions_orchestration_id/{sanitizedId} to the user-agent string.

- Add getUserAgentWithOrchestrationId() to internal/utils.ts
- Wire into getOctokitOptions() so all getOctokit() calls include it
- Re-export helper from @actions/github/lib/utils for downstream consumers
- 14 deterministic unit tests covering helper, integration, edge cases
2026-04-07 16:16:11 +00:00
Salman Chishti 0df75b91ff Merge pull request #2359 from actions/dependabot/npm_and_yarn/picomatch-2.3.2
chore(deps-dev): bump picomatch from 2.3.1 to 2.3.2
2026-03-26 12:25:19 +00:00
dependabot[bot] 233d556477 chore(deps-dev): bump picomatch from 2.3.1 to 2.3.2
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 22:41:36 +00:00
dependabot[bot] 23cbecacad chore(deps-dev): bump flatted from 3.3.3 to 3.4.2
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-21 10:32:03 +00:00
ICHINOSE Shogo 74fcfdbd10 @actions/glob: add some comments for the regression testing
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 19:56:02 +09:00
ICHINOSE Shogo 20647b6bcf @actions/core: update regression test with minimatch v3 2026-03-20 19:24:19 +09:00
ICHINOSE Shogo 6bd5e50ee1 @actions/glob: bump minimatch from v3.0.4 to v10.2.4 2026-03-20 18:00:34 +09:00
Bassem Dghaidi 44d43b5490 Merge pull request #2351 from actions/Link-/add-releases-docs
Add docs for the releases workflow
2026-03-16 16:45:42 +01:00
Bassem Dghaidi 76ac4dd95f Update contributing.md 2026-03-16 08:39:20 -07:00
Bassem Dghaidi 632b2cbff6 Add screenshot 2026-03-16 08:33:25 -07:00
Bassem Dghaidi 73bdb59ac2 Explain the new releases workflow 2026-03-16 08:29:53 -07:00
Bassem Dghaidi 22d35395d4 Merge pull request #2350 from actions/Link-/fix-tests-in-releases
Scope tests to the package being published
2026-03-16 15:01:41 +01:00
Bassem Dghaidi ed4fdc98c4 Add input to run isolated tests 2026-03-16 06:50:25 -07:00
Bassem Dghaidi 6211ca9cbd Merge pull request #2349 from actions/Link-/update-release-workflow
Update release workflow to permit shipping from non main branches
2026-03-16 14:29:21 +01:00
Bassem Dghaidi 99dfdab194 Fix the description of npm-tag 2026-03-16 06:14:29 -07:00
Bassem Dghaidi 6ec76cbf3d Update release workflow to permit shipping from non main branches 2026-03-16 06:05:25 -07:00
dependabot[bot] 7c6cc28ed5 chore(deps): bump undici from 6.23.0 to 6.24.1 in /packages/core
Bumps [undici](https://github.com/nodejs/undici) from 6.23.0 to 6.24.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.23.0...v6.24.1)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.24.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-14 09:09:41 +00:00
dependabot[bot] 8f62bc23d1 chore(deps): bump undici from 6.23.0 to 6.24.0 in /packages/http-client
Bumps [undici](https://github.com/nodejs/undici) from 6.23.0 to 6.24.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.23.0...v6.24.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.24.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-14 05:22:04 +00:00
dependabot[bot] bbaffb4bb3 chore(deps): bump undici from 6.23.0 to 6.24.0 in /packages/github
Bumps [undici](https://github.com/nodejs/undici) from 6.23.0 to 6.24.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.23.0...v6.24.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.24.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-14 04:56:28 +00:00
dependabot[bot] c23cc6e61c chore(deps): bump undici from 6.23.0 to 6.24.0 in /packages/glob
Bumps [undici](https://github.com/nodejs/undici) from 6.23.0 to 6.24.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.23.0...v6.24.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.24.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-14 04:40:06 +00:00
Salman Chishti 943ff82d3d Merge pull request #2344 from actions/dependabot/npm_and_yarn/packages/artifact/undici-6.24.0
chore(deps): bump undici from 6.23.0 to 6.24.0 in /packages/artifact
2026-03-14 04:35:35 +00:00
dependabot[bot] 06bca4509d chore(deps): bump undici from 6.23.0 to 6.24.0 in /packages/artifact
Bumps [undici](https://github.com/nodejs/undici) from 6.23.0 to 6.24.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.23.0...v6.24.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.24.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-14 04:17:32 +00:00
Daniel Kennedy 21229dc09e Artifact: support downloading artifacts with CJK characters in their name (#2341)
* Artifact: support downloading artifacts with CJK characters in their name

* Fix some linting/PR comments

* One more linting fix
2026-03-11 09:30:15 -04:00
Meredith Lancaster 6fd292ebdd Merge pull request #2337 from actions/dependabot/npm_and_yarn/packages/attest/tar-7.5.10
chore(deps): bump tar from 7.5.7 to 7.5.10 in /packages/attest
2026-03-06 06:54:55 -08:00
dependabot[bot] 89f01c9125 chore(deps): bump tar from 7.5.7 to 7.5.10 in /packages/attest
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.7 to 7.5.10.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.7...v7.5.10)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 13:29:24 +00:00
Zachary Taylor 85466c0f54 Merge pull request #2323 from actions/zaataylor-update-artifact-storage-err-msg
Update artifact storage error message
2026-02-26 14:37:38 -05:00
Zachary Taylor bd4fb086f1 Update UsageError in cache 2026-02-26 14:31:12 -05:00
Zachary Taylor 49c3d09c01 Update error message 2026-02-26 14:28:46 -05:00
Brian DeHamer 91d76bea50 Merge pull request #2321 from actions/bdehamer/storage-record-orchestration-id
Custom user-agent string for storage record API reqs
2026-02-26 10:52:42 -08:00
Brian DeHamer 69f29a1b1c Update packages/attest/src/artifactMetadata.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 17:40:10 -08:00
Brian DeHamer 7987771a2b new user-agent string for storage record API reqs
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2026-02-25 15:36:27 -08:00
Brian DeHamer 605cc18397 Merge pull request #2320 from actions/bdehamer/attest-orchestration-id
custom user-agent string for attestation API reqs
2026-02-25 11:25:53 -08:00
Brian DeHamer 27e5a955bf custom user-agent string for attestation API reqs
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2026-02-25 11:20:54 -08:00
Daniel Kennedy 6fe3c0f3e6 Artifact upload: support uploading single un-zipped files (#2256)
* Artifact upload: support uploading single un-zipped files

* Fix linters

* Fix lint again

* Fix tests

* Check for 0 sized artifact lists

* Add some more stream tests and handle an upload failure gracefully

* Add CI tests for non-zipped artifacts

* Add an html report to test rendering in the browser

* Fix linting issue

* Artifact: bump the version and add release notes

* Fix Windows tests

* Fix linting

* stream: switch the error details to error type

* Refactor the validation logic in `uploadArtifact` a bit

* Added more details about how the name parameter is handled
2026-02-25 11:01:38 -05:00
Daniel Kennedy 8c90e2297a fix(tests): close sockets to remove a Jest warning about resources outliving their tests (#2279) 2026-02-13 12:05:37 -05:00
dependabot[bot] 8351a5d84d chore(deps): bump fast-xml-parser in /packages/artifact (#2285)
Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases)
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: fast-xml-parser
  dependency-version: 5.3.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-30 16:26:32 -05:00
Daniel Kennedy 5203b671f1 Releases: use ubuntu-latest instead of macos-latest-large (#2284) 2026-01-30 16:04:52 -05:00
Daniel Kennedy 975fcbd402 Artifact download: don't unzip non-zip artifacts (#2253)
* Download artifact: don't extract the downloaded file if the content-type isn't a zip

* Remove unused `import`

* Add support for specifying whether to skip decompressing

* Prevent path traversal attacks

* Fix indenting

* Update packages/artifact/__tests__/download-artifact.test.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Parse the mime type out of the content-type header

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix some linting issues

* Swap `zip` for `application/zip-compressed`

* Test: negative check for malicious paths

* Increase the timeout on one of the tests

* Check the URL path for `.zip` to see if we can auto-decompress

* Fix linting issue

* Bump the package version and add release notes

* Remove `launch.json`

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-30 12:18:38 -05:00
Tingting Wang ffae274475 Merge pull request #2268 from actions/dependabot/npm_and_yarn/packages/attest/tar-7.5.7
chore(deps): bump tar from 7.5.6 to 7.5.7 in /packages/attest
2026-01-29 13:01:33 -08:00
dependabot[bot] 1c20378379 chore(deps): bump tar from 7.5.6 to 7.5.7 in /packages/attest
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.6 to 7.5.7.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.6...v7.5.7)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 20:20:49 +00:00
Daniel Kennedy 0be0a6ef89 @actions/attest: convert to an ESM module (#2278) 2026-01-29 15:19:39 -05:00
Daniel Kennedy ae29a2751b @actions/cache: convert to an ESM module (#2275)
* `@actions/cache`: convert to an ESM module

* Update the fixture to ESM syntax

* Update the cache workflows

* Bump `@actions/glob` to `0.6.1`

* Fix awaiting in the cache unit tests

* Fix a type issues in contracts

* Export the `DownloadOptions`/`UploadOptions` like before

* More cache test fixes

* Make the cache units tests better

* Add some more logging

* Add retries to restore-cache.mjs
2026-01-29 14:23:32 -05:00
Daniel Kennedy b48854e1ac @actions/glob: fix minimatch imports (#2276) 2026-01-29 13:30:54 -05:00
Daniel Kennedy 9d912b1840 @actions/tool-cache: convert to an ESM module (#2274)
* `@actions/tool-cache`: convert to an ESM module

* Fix jest config

* Downgrade `nock` since it's conflicting with `@actions/attest`'s version
2026-01-29 11:26:14 -05:00
Daniel Kennedy 7a0147b5c6 @actions/glob: convert to an ESM module (#2273)
* `@actions/glob`: convert to an ESM module

* Update packages/glob/RELEASES.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 10:41:33 -05:00
Daniel Kennedy 5793b08cd9 @actions/artifact: convert to an ESM module (#2266)
* `@actions/artifact`: convert to an ESM module

* Update the package-lock.json

* Undo the GHES ignores

* Fix the reference to `@actions/http-client` in the lock file

* Bump `@actions/core` to `3.0.0`

* Remove `jest.config.cjs`

* Import `OctoKitOptions` from `@octokit/core/types`

* Pull the package version from `package.json`

* Workaround getting the package version for the user-agent

* Fix the `archiver` import

* Fix linting
2026-01-29 09:52:09 -05:00
Daniel Kennedy ed3ea3b5ba @actions/core: convert to ESM module 2026-01-28 20:50:58 -05:00
Daniel Kennedy c9c663babe Bump @actions/io to 3.0.2 2026-01-28 15:59:40 -05:00
Daniel Kennedy 0fc1805b46 @actions/exec: convert to ESM module 2026-01-28 15:59:40 -05:00
Daniel Kennedy a6e9f4bab2 @actions/io: update lock file version 2026-01-28 14:36:03 -05:00
Daniel Kennedy 758b556388 @actions/io: export lib/io-util 2026-01-28 14:08:19 -05:00
Daniel Kennedy 9e060cb3e1 Add release notes 2026-01-28 13:33:17 -05:00
Daniel Kennedy 5501ba08b7 @actions/io: convert to ESM module 2026-01-28 13:33:17 -05:00
Daniel Kennedy 4446f00fc7 Add a release entry for 4.0.0 2026-01-28 10:27:09 -05:00
Daniel Kennedy 965dcc7493 Fix a JSON lint issue 2026-01-28 10:27:09 -05:00
Daniel Kennedy d464f9dd60 Add proxy/interfaces exports 2026-01-28 10:27:09 -05:00
Daniel Kennedy c9ab4f9548 http-client: convert to ESM 2026-01-28 10:27:09 -05:00
Lokesh Gopu a2986ee511 Merge pull request #2260 from actions/lokesh755-actions-github-v9-esm
ESM-only with updated @octokit dependencies
2026-01-27 15:58:18 -05:00
Daniel Kennedy e827417593 Bump @actions/glob to 0.5.1 in @actions/cache 2026-01-27 15:43:44 -05:00
Lokesh Gopu b05d26b3fa ESM-only with updated @octokit dependencies 2026-01-27 15:35:32 -05:00
Daniel Kennedy ecdfc18bf2 Bump @actions/glob version to 0.5.1 2026-01-27 14:55:56 -05:00
Daniel Kennedy e8e0ce7ad8 Bump @actions/core to 2.0.3 on @actions/glob 2026-01-27 14:55:56 -05:00
Daniel Kennedy dc6427f3c3 Attest: undo the @actions/github/@octokit bumps 2026-01-27 13:31:31 -05:00
Daniel Kennedy 76339b5f68 Bump @actions/http-client and @actions/github on all packages 2026-01-27 13:31:31 -05:00
Daniel Kennedy c0ef67ec49 Release @actions/github v8.0.1 2026-01-27 10:29:06 -05:00
Daniel Kennedy 968fd7f8d3 Bump undici to v6.23.0 and @actions/http-client to v3.0.2 in @actions/github 2026-01-27 10:29:06 -05:00
Daniel Kennedy 9b27fa97f9 Release @actions/http-client version 3.0.2 2026-01-27 09:53:50 -05:00
Daniel Kennedy 065cf9f0b1 Bump undici to v6.23.0 in @actions/http-client 2026-01-27 09:38:40 -05:00
Lokesh Gopu b77f226465 Merge pull request #2249 from actions/fix/upgrade-octokit-dependencies
upgrade octokit dependencies
2026-01-22 14:46:12 -05:00
Lokesh Gopu f61ae48376 upgrade octokit dependencies 2026-01-22 11:59:59 -05:00
Salman Chishti 4236fc3e78 Merge pull request #2246 from actions/dependabot/npm_and_yarn/packages/attest/tar-7.5.6
chore(deps): bump tar from 7.5.2 to 7.5.6 in /packages/attest
2026-01-22 15:53:06 +00:00
Salman Chishti f366966232 Merge pull request #2248 from actions/dependabot/npm_and_yarn/lodash-4.17.23
chore(deps): bump lodash from 4.17.21 to 4.17.23
2026-01-22 14:48:46 +00:00
dependabot[bot] bd561a6765 chore(deps): bump lodash from 4.17.21 to 4.17.23
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-22 00:03:54 +00:00
dependabot[bot] 26490f0d3b chore(deps): bump tar from 7.5.2 to 7.5.6 in /packages/attest
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.2 to 7.5.6.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.2...v7.5.6)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-21 21:04:00 +00:00
Ryan Ghadimi ee91adfbc4 Merge pull request #2243 from GhadimiR/main
Don't retry 429s returned from the cache service
2026-01-16 10:43:35 +00:00
Ryan Ghadimi a039cff4a1 Add comment for rate limiting handling
Added a comment regarding rate limiting and retry behavior.
2026-01-16 10:26:45 +00:00
Ryan Ghadimi 9dd77993e7 Update packages/cache/RELEASES.md
Co-authored-by: Bassem Dghaidi <568794+Link-@users.noreply.github.com>
2026-01-16 10:25:44 +00:00
Ryan Ghadimi dd1bb93c72 New error type for cache RL 2026-01-16 10:00:15 +00:00
Ryan Ghadimi 7292b3508f update release doc 2026-01-16 09:51:21 +00:00
Ryan Ghadimi 4a47af6481 bump pkg 2026-01-16 09:46:28 +00:00
Ryan Ghadimi a68693e20a tests & releases 2026-01-16 09:44:57 +00:00
Ryan Ghadimi acf4bd70fb Add handling for 429s from cache service 2026-01-14 17:01:59 +00:00
Tingluo Huang 7ae5c2f423 Merge pull request #2237 from actions/users/tihuang/bump
Bump version to 2.2.0 and update dependencies for @actions/core, @actions/github, and @actions/http-client
2026-01-08 11:41:34 -05:00
Tingluo Huang d5470e6023 . 2026-01-08 16:19:53 +00:00
Tingluo Huang e68ab4b91a Bump version to 3.0.0 and update dependencies for @actions/core, @actions/github, and @actions/http-client 2026-01-08 14:34:05 +00:00
Tingluo Huang d3a48dd52f Merge pull request #2233 from actions/copilot/update-http-client-dependency
Update selected packages to consume @actions/http-client@3.0.1
2026-01-08 09:04:43 -05:00
Tingluo Huang c4d47c1922 Merge pull request #2236 from actions/users/tihuang/bump
Bump dependency version for actions/github and actions/tool-cache.
2026-01-08 08:38:35 -05:00
Tingluo Huang 5fc5cdde44 Bump dependency version for actions/github and actions/tool-cache. 2026-01-08 04:58:48 +00:00
copilot-swe-agent[bot] 2a9d836b08 Revert @actions/attest http-client dependency to ^2.2.3
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-07 23:17:02 +00:00
copilot-swe-agent[bot] 3a3b073ef2 Revert changes to github, tool-cache packages and .gitignore
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-07 22:38:20 +00:00
copilot-swe-agent[bot] d3a0fb260e Revert @actions/attest version to 2.1.0 - no new release needed
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-07 22:33:05 +00:00
copilot-swe-agent[bot] be4fdc505f Fix package-lock.json to mark typescript as dev dependency
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-07 22:10:30 +00:00
copilot-swe-agent[bot] 6be37922c5 Add .nx/ to .gitignore and remove cached build artifacts
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-07 22:04:45 +00:00
copilot-swe-agent[bot] dfc20acda2 Verify changes with tests - all package-specific tests passing
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-07 22:03:56 +00:00
copilot-swe-agent[bot] dc1fec82a6 Update all packages to use @actions/http-client@^3.0.1 and bump patch versions
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-07 21:55:41 +00:00
copilot-swe-agent[bot] 9339b3573b Initial plan 2026-01-07 21:51:58 +00:00
Tingluo Huang 67a08de5c7 Merge pull request #2231 from actions/copilot/prepare-http-client-release
Release @actions/http-client v3.0.1
2026-01-07 15:35:32 -05:00
copilot-swe-agent[bot] d73fffceed Combine duplicate RELEASES.md entries into single line
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-07 18:44:34 +00:00
copilot-swe-agent[bot] 16f0b3d28e Revert .gitignore change as requested
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-07 18:40:39 +00:00
copilot-swe-agent[bot] 398e2cb68b Change version from 3.1.0 to 3.0.1 as requested
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-07 18:31:01 +00:00
copilot-swe-agent[bot] 4b9031fa77 Bump http-client version to 3.1.0 and update RELEASES.md
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-07 18:13:07 +00:00
copilot-swe-agent[bot] 959cb66bd5 Initial plan 2026-01-07 18:02:29 +00:00
Tingluo Huang 3e0b611f99 Merge pull request #2229 from actions/copilot/extend-user-agent-http-requests
Extend user-agent with orchestration ID and add default user-agent
2026-01-07 10:42:56 -05:00
copilot-swe-agent[bot] 83c13c81ba Improve test cleanup and add missing test coverage
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 19:19:35 +00:00
copilot-swe-agent[bot] bccbba401a Fix lint error: remove unnecessary escape in regex
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 18:30:18 +00:00
copilot-swe-agent[bot] 3a191eecf6 Add default user-agent 'actions/http-client' when none provided
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 18:10:12 +00:00
copilot-swe-agent[bot] 97f5a6f0dc Simplify constructor by handling undefined in helper method
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 18:03:26 +00:00
copilot-swe-agent[bot] 48a7cdbf9c Remove .gitignore change for .nx/ directory
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 17:54:40 +00:00
copilot-swe-agent[bot] 3f1933edf9 Revert unrelated version bumps in package-lock.json files
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 17:50:28 +00:00
copilot-swe-agent[bot] 2215c8e5aa Restrict sanitization to only allow 0-9, a-z, _, -, .
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 17:37:28 +00:00
copilot-swe-agent[bot] af6de2cb95 Move orchestration ID logic to constructor for efficiency
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 17:30:28 +00:00
copilot-swe-agent[bot] 1dc58e3080 Rename github_orchestration_id to actions_orchestration_id
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 17:20:43 +00:00
copilot-swe-agent[bot] 20596c1d96 Move hyphen to end of character class for cleaner regex
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 16:30:09 +00:00
copilot-swe-agent[bot] 557f80fd03 Fix regex to properly escape hyphen in character class
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 16:28:52 +00:00
copilot-swe-agent[bot] 32c52bb78a Use product/version format and sanitize orchestration ID
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 16:26:29 +00:00
copilot-swe-agent[bot] 6d9a3fe547 Format code and update .gitignore to exclude .nx cache
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 15:36:15 +00:00
copilot-swe-agent[bot] 4e1c194b34 Add ACTIONS_ORCHESTRATION_ID support to http-client user-agent
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 15:32:55 +00:00
copilot-swe-agent[bot] 09cb71a033 Initial plan 2026-01-06 15:22:42 +00:00
Salman Chishti 2506e78e82 Merge pull request #2212 from actions/artifact-v5.0.1-release
docs(artifact): release @actions/artifact v5.0.1
2025-12-12 15:39:59 +00:00
Salman Muin Kayser Chishti f8003d52ff docs: add PR reference to artifact v5.0.1 release notes 2025-12-12 15:33:41 +00:00
Salman Muin Kayser Chishti e263dfb89d Merge remote-tracking branch 'origin/main' into artifact-v5.0.1-release 2025-12-12 15:33:18 +00:00
Salman Chishti 2e53bd8485 Merge pull request #2214 from actions/cache-v5.0.1-release
docs(cache): release @actions/cache v5.0.1
2025-12-12 15:30:16 +00:00
Salman Chishti b5e3b25b34 Merge pull request #2211 from actions/fix-artifact-storage-blob
fix(artifact): update @azure/storage-blob to fix Node.js 24 punycode deprecation
2025-12-12 15:29:23 +00:00
Salman Muin Kayser Chishti b71834a510 fix(artifact): update @azure/storage-blob to ^12.29.1 and remove deprecated packages 2025-12-12 15:01:31 +00:00
Salman Muin Kayser Chishti c6f0239e63 Merge remote-tracking branch 'origin/main' into fix-artifact-storage-blob 2025-12-12 14:59:31 +00:00
Salman Muin Kayser Chishti c655f38a0f docs(cache): add PR reference to v5.0.1 release notes 2025-12-12 14:53:26 +00:00
Salman Muin Kayser Chishti cf8caa4e0d Merge remote-tracking branch 'origin/main' into cache-v5.0.1-release 2025-12-12 14:50:27 +00:00
Salman Chishti 8734e578c6 Merge pull request #2213 from actions/fix-cache-storage-blob
fix(cache): update @azure/storage-blob to fix Node.js 24 punycode deprecation
2025-12-12 14:40:41 +00:00
Salman Muin Kayser Chishti 74ac6db523 fix(cache): update @azure/storage-blob to ^12.29.1 to address punycode deprecation 2025-12-12 14:05:14 +00:00
Salman Muin Kayser Chishti 6fc2f678c8 docs(cache): bump to v5.0.1 and add release notes 2025-12-12 13:42:58 +00:00
Salman Muin Kayser Chishti 5ef62e14dd fix(cache): update @azure/storage-blob to ^12.29.1 to fix punycode deprecation
- Updated @azure/storage-blob from ^12.13.0 to ^12.29.1
- Newer storage-blob uses @azure/core-rest-pipeline instead of deprecated @azure/core-http
- Fixes Node.js 24 deprecation warning for punycode module
2025-12-12 13:41:31 +00:00
Salman Muin Kayser Chishti 7b29e67278 docs(artifact): bump to v5.0.1 and add release notes 2025-12-12 13:40:07 +00:00
Salman Muin Kayser Chishti 9d2227dbb0 fix(artifact): update @azure/storage-blob to ^12.29.1 to fix punycode deprecation
- Removed direct @azure/core-http dependency
- Updated @azure/storage-blob from ^12.15.0 to ^12.29.1
- Newer storage-blob uses @azure/core-rest-pipeline instead of deprecated @azure/core-http
- Fixes Node.js 24 deprecation warning for punycode module
2025-12-12 13:38:13 +00:00
Salman Chishti 5a8462ec27 Merge pull request #2210 from actions/prepare-artifact-release
docs(artifact): add v5.0.0 release notes
2025-12-11 21:42:59 +00:00
Salman Muin Kayser Chishti fcaf488df6 chore(artifact): bump version to v5.0.0 2025-12-11 20:45:49 +00:00
Salman Muin Kayser Chishti 2b48e40e62 docs(artifact): add v5.0.0 release notes 2025-12-11 20:37:18 +00:00
Salman Chishti 44ec738e27 Merge pull request #2209 from actions/version-bumps
chore(artifact): bump dependencies for Node.js 24 support
2025-12-11 20:23:43 +00:00
Salman Muin Kayser Chishti 3af0128b01 chore(artifact): bump dependencies for Node.js 24 support 2025-12-11 19:20:51 +00:00
Salman Chishti e74405f68c Merge pull request #2194 from actions/prepare-cache-release-v5.0.0
Prepare cache v5 release
2025-12-11 16:00:48 +00:00
Salman Muin Kayser Chishti cc6abe3c3a chore(releases): update release notes for v5.0.0 to remove punycode deprecation warning 2025-12-11 14:43:23 +00:00
Salman Muin Kayser Chishti c6502bc679 PR number update in releases 2025-12-11 14:33:30 +00:00
Salman Muin Kayser Chishti bdd6eb4293 update releases 2025-12-11 14:32:58 +00:00
Salman Muin Kayser Chishti 6785788751 Merge remote-tracking branch 'origin/main' into prepare-cache-release-v5.0.0 2025-12-11 14:27:41 +00:00
Salman Chishti ddf2d52556 Merge pull request #2198 from actions/cache-bump-deps
chore(cache): bump @actions/* dependencies to v2/v3
2025-12-11 14:25:35 +00:00
Salman Muin Kayser Chishti 7c1b12a15e chore(cache): update @actions/core to 2.0.1 2025-12-11 13:57:58 +00:00
Salman Muin Kayser Chishti fdbf9e3ec2 Merge remote-tracking branch 'origin/main' into cache-bump-deps 2025-12-11 13:54:20 +00:00
Salman Muin Kayser Chishti 369aa55cdc update to core 2.0.1 which has exec 2.0.0 2025-12-11 13:54:17 +00:00
Brian DeHamer e1191599bb Merge pull request #2203 from actions/node-update-for-publish
Use node24 for publishing
2025-12-10 14:19:00 -08:00
Brian DeHamer c043714a35 use node24 for publishing
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2025-12-10 14:14:15 -08:00
Brian DeHamer 3ac6e0fdf2 Merge pull request #2201 from actions/npm-trusted-publishing
Enable npm trusted publishing
2025-12-10 13:32:35 -08:00
Brian DeHamer d9f9074fee npm trusted publishing
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2025-12-10 13:27:16 -08:00
Salman Chishti 2c52220624 Merge pull request #2199 from actions/core-bump-exec
prepare @actions/exec 2.0.1 relesae + chore(core): bump @actions/exec from ^1.1.1 to ^2.0.0
2025-12-10 12:08:47 +00:00
Salman Muin Kayser Chishti b2e6a5a284 chore(core): bump @actions/exec from ^1.1.1 to ^2.0.0
Aligns with cache package which already uses exec@2.0.0, avoiding nested duplicate dependencies.
2025-12-10 11:54:17 +00:00
Salman Muin Kayser Chishti e48877e66c chore(cache): bump @actions/* dependencies to v2/v3
- @actions/core: ^1.11.1 → ^2.0.0
- @actions/exec: ^1.0.1 → ^2.0.0
- @actions/glob: ^0.1.0 → ^0.5.0
- @actions/http-client: ^2.1.1 → ^3.0.0
- @actions/io: ^1.0.1 → ^2.0.0
2025-12-10 11:38:25 +00:00
Salman Chishti bdddd872e3 Merge pull request #2197 from actions/cache-remove-ms-rest-js
fix(cache): replace @azure/ms-rest-js with @azure/core-rest-pipeline
2025-12-10 11:36:53 +00:00
Salman Muin Kayser Chishti 8a2701f328 fix(cache): replace @azure/ms-rest-js with @azure/core-rest-pipeline
Remove abandoned @azure/ms-rest-js dependency which pulls in node-fetch@v2, causing punycode deprecation warnings on Node.js 24+.

The TransferProgressEvent type is now imported from @azure/core-rest-pipeline instead.
2025-12-10 11:23:06 +00:00
Salman Muin Kayser Chishti eb7ff8401e chore(cache): regenerate package-lock.json with Node 24 2025-12-10 11:09:56 +00:00
Salman Muin Kayser Chishti 45ec4a2087 chore: update dependencies and remove deprecated package
- Removed `@azure/ms-rest-js` dependency to fix Node.js 24+ punycode deprecation warning.
  - The `TransferProgressEvent` type is now imported from `@azure/core-rest-pipeline`.
- Updated `package.json` to reflect the new dependency.
- Updated tests to import `TransferProgressEvent` from the new package.
- Updated `package-lock.json` to remove `@azure/ms-rest-js` and include `@azure/core-rest-pipeline`.
- Bumped versions of several dependencies including `@azure/storage-blob` and `@azure/storage-common`.
2025-12-10 11:04:34 +00:00
Meredith Lancaster 02869fefb4 Merge pull request #2196 from actions/update-attest-storage-record-params
Refactor attest package createStorageRecord function params
2025-12-09 11:49:14 -08:00
Meredith Lancaster 701191f50e fix linter issues
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-09 11:40:40 -08:00
Meredith Lancaster 539724611c param name
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-09 11:39:12 -08:00
Meredith Lancaster 3d01d7ed69 Update packages/attest/README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-09 11:38:06 -08:00
Meredith Lancaster d75223fd4a split mega param into several different ones
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-09 11:37:04 -08:00
Meredith Lancaster 056c217a52 Merge pull request #2192 from actions/malancas/create-artifact-metadata-storage-record
Add the `createStorageRecord` function to the `attest` package for creating artifact storage records
2025-12-09 08:55:21 -08:00
Meredith Lancaster d795a0ad0d linter fix
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-09 08:32:31 -08:00
Salman Muin Kayser Chishti b0464628c0 Prepare cache v5 release 2025-12-09 16:11:32 +00:00
Meredith Lancaster 0380590fdd fix expected endpoint response
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-09 08:02:38 -08:00
Salman Chishti 5e183dabac Merge pull request #2193 from actions/fix-audit-vulnerabilities
chore: fix npm audit vulnerabilities (glob, js-yaml)
2025-12-09 15:39:33 +00:00
Meredith Lancaster 97b7fa81c8 regenerate package lock
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 19:22:04 -08:00
Meredith Lancaster 87afd16bb2 bump to next minor version
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 19:19:29 -08:00
Meredith Lancaster c40fa0d905 formatting
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 19:19:11 -08:00
Meredith Lancaster dc9f635a0d Update packages/attest/src/artifactMetadata.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 16:30:37 -08:00
Meredith Lancaster 7847d31696 Update packages/attest/README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 16:30:25 -08:00
Meredith Lancaster 10d3b034e0 fix linter issues
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 16:22:59 -08:00
Meredith Lancaster 8eca440361 fix test and function calls
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 15:59:25 -08:00
Meredith Lancaster 6ec87f46b7 add back param parsing function
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 15:39:26 -08:00
Meredith Lancaster d1f9584cda fix test calls
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 15:33:01 -08:00
Meredith Lancaster b8933d0495 reorganize function options and document
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 15:25:34 -08:00
Meredith Lancaster 0a988d204e rename file
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 15:16:26 -08:00
Meredith Lancaster 136f9dfe37 fix header link
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 14:07:17 -08:00
Meredith Lancaster ed78411ffb fix expected response
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 14:03:23 -08:00
Meredith Lancaster dd097c7f4e add section on createStorageRecord func
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 13:57:00 -08:00
Meredith Lancaster f01262913d table of contents
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 13:55:24 -08:00
Meredith Lancaster c034e76488 fix function exporting and test results
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 13:49:54 -08:00
Meredith Lancaster 9ca26d4946 regenerate package lock
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 13:17:18 -08:00
Meredith Lancaster 417dbfff73 use parameter objects and add tests
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 13:17:08 -08:00
Salman Muin Kayser Chishti 8883833d6d chore: fix npm audit vulnerabilities (glob, js-yaml) 2025-12-08 21:14:00 +00:00
Meredith Lancaster 79efd648ac condense parameters
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 11:02:59 -08:00
Meredith Lancaster e8c242695d add function for creating storage record
Signed-off-by: Meredith Lancaster <malancas@github.com>
2025-12-08 10:49:24 -08:00
Salman Chishti 48f166f6d5 Merge pull request #2175 from actions/prepare-core-1.12.0-release
Prepare @actions/core 2.0.0 release
2025-12-08 16:28:47 +00:00
Salman Muin Kayser Chishti 4bc377e1b4 Timeout time back to normal, as it was like this for debugging 2025-12-08 12:47:19 +00:00
Salman Muin Kayser Chishti bf1b64008f fix: use detached:true on all platforms for exec stream tests
On Windows, detached:true is needed to properly keep stdio handles
open after the parent process exits.
2025-12-08 12:35:33 +00:00
Salman Muin Kayser Chishti 894f77901e fix: improve exec stream tests cross-platform handling
- Update spawn-wait-for-file.js to use proper stdio inheritance
- Add small delay before exit to ensure child process inherits handles
- Simplify test code to use the helper script instead of shell commands
2025-12-08 12:28:52 +00:00
Tingting Wang 7993066184 Merge pull request #2174 from actions/dependabot/npm_and_yarn/packages/attest/tar-7.5.2
Bump tar from 7.5.1 to 7.5.2 in /packages/attest
2025-12-05 17:13:09 -08:00
dependabot[bot] f014075da9 Bump tar from 7.5.1 to 7.5.2 in /packages/attest
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.1 to 7.5.2.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.1...v7.5.2)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 01:06:03 +00:00
Tingting Wang 4500de75c1 Merge pull request #2187 from actions/dependabot/npm_and_yarn/packages/attest/glob-10.5.0
Bump glob from 10.4.5 to 10.5.0 in /packages/attest
2025-12-05 17:04:14 -08:00
dependabot[bot] 47017fa24b Bump glob from 10.4.5 to 10.5.0 in /packages/attest
Bumps [glob](https://github.com/isaacs/node-glob) from 10.4.5 to 10.5.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 17:36:35 +00:00
Salman Muin Kayser Chishti d97deb1f60 tests(exec): use platform-aware spawn options in spawn-wait-for-file script (detach on Unix, hide window on Windows) 2025-11-18 15:45:59 +00:00
Salman Muin Kayser Chishti df111e1104 tests(glob): set GITHUB_WORKSPACE to __dirname in hash-files.test 2025-11-18 15:32:19 +00:00
Salman Muin Kayser Chishti a3588a70ba update hash files test 2025-11-18 14:21:47 +00:00
Salman Chishti 6b63a2bfc3 Merge pull request #2176 from actions/prepare-exec-2.0.0-release
Prepare @actions/exec 2.0.0 release
2025-11-18 14:13:24 +00:00
Salman Muin Kayser Chishti 290017ff81 update package json 2025-11-04 13:53:28 +00:00
Salman Chishti 2a876cd69d Update packages/exec/RELEASES.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-04 13:50:24 +00:00
Salman Muin Kayser Chishti 7cba4c8084 npm install 2025-10-31 16:02:54 +00:00
Salman Muin Kayser Chishti f79b906406 Prepare @actions/exec 2.0.0 release 2025-10-31 15:55:29 +00:00
Salman Muin Kayser Chishti 1bcc453b44 Prepare @actions/core 2.0.0 release 2025-10-31 15:52:21 +00:00
Salman Chishti dcae869a03 Merge pull request #2167 from actions/prepare-http-client-3.0.0-release
Prepare @actions/http-client 3.0.0 release
2025-10-31 15:27:38 +00:00
Salman Chishti 23769d04c7 Merge pull request #2166 from actions/prepare-io-2.0.0-release
Prepare @actions/io 2.0.0 release
2025-10-31 15:27:26 +00:00
Daniel Kennedy d3ab50471b Merge pull request #2168 from actions/danwkennedy/prepare-4.0.0
Artifact: prepare `v4.0.0`
2025-10-24 13:38:36 -04:00
Daniel Kennedy 1388fd1cac Artifact: prepare 4.0.0 2025-10-24 13:28:26 -04:00
Bassem Dghaidi 5b446d2657 Merge pull request #2165 from austenstone/max-list-artifact-2k
fix: artifact pagination bugs and configurable artifact count limits
2025-10-24 17:23:44 +02:00
Austen Stone 006d6978c1 linting 2025-10-22 11:44:21 -04:00
Austen Stone 02afeb1577 style: wrap ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT env var assignment for readability 2025-10-22 11:42:01 -04:00
Salman Chishti d47594b536 Update packages/http-client/RELEASES.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-22 15:44:09 +01:00
Salman Muin Kayser Chishti 2823824b94 Prepare @actions/http-client 3.0.0 release 2025-10-22 15:40:17 +01:00
Austen Stone cbc06d6766 fix: ensure max artifact count variable is treated as a string 2025-10-22 07:47:51 -04:00
Austen Stone 9bb6708527 fix: remove redundant check for max artifact count variable 2025-10-22 07:41:31 -04:00
Austen Stone be1151df02 Apply suggestion from @Link-
Co-authored-by: Bassem Dghaidi <568794+Link-@users.noreply.github.com>
2025-10-22 07:39:45 -04:00
Salman Muin Kayser Chishti 130842f4e8 Prepare @actions/io 2.0.0 release 2025-10-21 15:55:10 +01:00
Salman Chishti ab82301c62 Merge pull request #2164 from actions/prepare-attest-2.0.0-release
Prepare @actions/attest 2.0.0 release
2025-10-21 15:08:28 +01:00
Austen Stone fea4f6b5c5 fix: resolve critical pagination bugs and add comprehensive testing
- Fix off-by-one error in pagination loop (< to <=) that prevented fetching last page
- Add Math.ceil() to maxNumberOfPages calculation for proper limit handling
- Replace hardcoded 2000 limit with configurable getMaxArtifactListCount()
- Add pagination test for multi-page artifact listing
- Add environment variable test for ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT
- Add comprehensive test coverage for getMaxArtifactListCount() function

Fixes compound bug where pagination and limit logic capped results at 900 artifacts instead of intended 1000.
2025-10-21 09:22:11 -04:00
Salman Muin Kayser Chishti d3ade9ecfc Prepare @actions/attest 2.0.0 release 2025-10-20 12:07:20 +01:00
functionstackx fb592eec03 Update packages/artifact/src/internal/find/list-artifacts.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-17 18:05:06 -04:00
functionstackx 70e79399a2 fix: bumping max list artifact to 2k 2025-10-16 23:13:34 -04:00
Eugene acb230b99a Merge pull request #2160 from actions/ejahnGithub-patch-1
Remove unnecessary Buffer to Uint8Array conversion
2025-10-16 12:22:23 -04:00
Eugene 5e0fa1aaaa Remove unnecessary Buffer to Uint8Array conversion
Removed unnecessary conversion of Buffer to Uint8Array for compatibility.
2025-10-16 12:08:05 -04:00
Salman Chishti ac2468e605 Support Nodejs.24 - Merge pull request #2110 from actions/salmanmkc/node24
Support Node.js 24
2025-10-16 16:25:47 +01:00
Salman Muin Kayser Chishti 3c8fcfce19 del file 2025-10-16 14:37:05 +01:00
Salman Muin Kayser Chishti 45467b9199 LInt 2025-10-16 14:34:09 +01:00
Salman Muin Kayser Chishti 700a55077d spacing 2025-10-16 14:27:39 +01:00
Salman Muin Kayser Chishti 6fa8f07827 Update based on testing to add trailing back slash to all results 2025-10-16 13:52:43 +01:00
Salman Muin Kayser Chishti d16e86a709 Add workflow to test readlink behavior on Windows across Node versions 2025-10-16 13:03:06 +01:00
Salman Muin Kayser Chishti ae3ac0db0c change back to lstat 2025-10-15 17:25:34 +01:00
Salman Muin Kayser Chishti b319d6afff Add comment to explain the method and return types 2025-10-15 17:14:54 +01:00
Salman Muin Kayser Chishti b8ac8fc14a lint 2025-10-15 17:08:33 +01:00
Salman Muin Kayser Chishti 028d621193 Merge remote-tracking branch 'origin/main' into salmanmkc/node24 2025-10-15 16:41:54 +01:00
Salman Muin Kayser Chishti b0d901f9c2 rebase led to this changing so reverting 2025-10-15 16:37:38 +01:00
Salman Muin Kayser Chishti 394e804dc8 remove skip lib check 2025-10-15 16:28:21 +01:00
Salman Muin Kayser Chishti d402248c45 Lint fix 2025-10-15 16:28:21 +01:00
Salman Muin Kayser Chishti 66e8437b3e Revert "Io util package usage update"
This reverts commit 783332a4b57e9455ec3a361c4e16f659a35f3a97.
2025-10-15 16:28:21 +01:00
Salman Muin Kayser Chishti 9c7501a5f3 Io util package usage update 2025-10-15 16:28:21 +01:00
Salman Muin Kayser Chishti 3b4b5725f0 Update packages, core doesn't need updates and update to use IO util update 2025-10-15 16:28:21 +01:00
Salman Muin Kayser Chishti 9a364e607b update io utils 2025-10-15 16:28:21 +01:00
Salman Muin Kayser Chishti 625c3f4856 change version for http-client 2025-10-15 16:28:21 +01:00
Salman Muin Kayser Chishti 1c3a637017 Update documentation 2025-10-15 16:28:21 +01:00
Salman Muin Kayser Chishti ec0ca1b19b fix typo 2025-10-15 16:28:21 +01:00
Salman Muin Kayser Chishti 57cd003e61 Update tests to use HTTPS for postman-echo.com and adjust proxy environment variable 2025-10-15 16:28:21 +01:00
Salman Muin Kayser Chishti b5befc6c6d Update HTTP tests to use HTTPS for postman-echo.com 2025-10-15 16:28:21 +01:00
Salman Muin Kayser Chishti 88a490d2ce override for node-fetch 2025-10-15 16:28:21 +01:00
Salman Muin Kayser Chishti a8d1fb0687 remove node 18 2025-10-15 16:27:29 +01:00
Salman Muin Kayser Chishti 347c887e54 package json 2025-10-15 16:27:29 +01:00
Salman Muin Kayser Chishti d5af54ee78 Update package versions 2025-10-15 16:27:29 +01:00
Salman Muin Kayser Chishti 44b9401378 Remove the need to update packages/core 2025-10-15 16:27:29 +01:00
Salman Muin Kayser Chishti fb5ae2a0e0 Keep attest at the same version 2025-10-15 16:27:29 +01:00
Salman Muin Kayser Chishti 8024983ab0 Update workflows and documentation to use the latest versions of first party actions that are available (checkout, setup-node, github-script)
=
2025-10-15 16:27:29 +01:00
Salman Muin Kayser Chishti d44f9b8f13 update some version numbers, will revise in a bit 2025-10-15 16:27:29 +01:00
Daniel Kennedy 9b4ee219ef fix: only mock the cpus() function on the os module instead of the whole module 2025-10-15 16:26:39 +01:00
Daniel Kennedy ee5d8970ad Take a direct dependency on @azure/core-http 2025-10-15 16:26:39 +01:00
dependabot[bot] 2874e3a741 Bump the artifact-minor-patch group in /packages/artifact with 5 updates
Bumps the artifact-minor-patch group in /packages/artifact with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) | `1.10.0` | `1.11.1` |
| [@azure/storage-blob](https://github.com/Azure/azure-sdk-for-js) | `12.15.0` | `12.28.0` |
| [@protobuf-ts/plugin](https://github.com/timostamm/protobuf-ts/tree/HEAD/packages/plugin) | `2.9.1` | `2.11.1` |
| [typedoc](https://github.com/TypeStrong/TypeDoc) | `0.25.4` | `0.28.13` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.2.2` | `5.9.2` |

Updates `@actions/core` from 1.10.0 to 1.11.1
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core)

Updates `@azure/storage-blob` from 12.15.0 to 12.28.0
- [Release notes](https://github.com/Azure/azure-sdk-for-js/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-js/blob/main/documentation/Changelog-for-next-generation.md)
- [Commits](https://github.com/Azure/azure-sdk-for-js/compare/@azure/storage-blob_12.15.0...@azure/storage-blob_12.28.0)

Updates `@protobuf-ts/plugin` from 2.9.1 to 2.11.1
- [Release notes](https://github.com/timostamm/protobuf-ts/releases)
- [Commits](https://github.com/timostamm/protobuf-ts/commits/v2.11.1/packages/plugin)

Updates `typedoc` from 0.25.4 to 0.28.13
- [Release notes](https://github.com/TypeStrong/TypeDoc/releases)
- [Changelog](https://github.com/TypeStrong/typedoc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.25.4...v0.28.13)

Updates `typescript` from 5.2.2 to 5.9.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.2.2...v5.9.2)

---
updated-dependencies:
- dependency-name: "@actions/core"
  dependency-version: 1.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: artifact-minor-patch
- dependency-name: "@azure/storage-blob"
  dependency-version: 12.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: artifact-minor-patch
- dependency-name: "@protobuf-ts/plugin"
  dependency-version: 2.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: artifact-minor-patch
- dependency-name: typedoc
  dependency-version: 0.28.13
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: artifact-minor-patch
- dependency-name: typescript
  dependency-version: 5.9.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: artifact-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-15 16:25:54 +01:00
Daniel Kennedy ad4afeeff1 Update the group names 2025-10-15 16:24:47 +01:00
Daniel Kennedy f9bdf6a054 Dependabot: add support for /packages/artifact and `/packages/cache 2025-10-15 16:24:47 +01:00
Bassem Dghaidi 59c7ebde79 Prepapre cache v4.1.0 release 2025-10-15 16:24:47 +01:00
Ryan Ghadimi 0c907a43d3 no need to resolve 2025-10-15 16:23:02 +01:00
Ryan Ghadimi d1c1fc4108 lint 2025-10-15 16:23:02 +01:00
Ryan Ghadimi 36f30e6d37 new error state, tests to cover 2025-10-15 16:23:02 +01:00
Ryan Ghadimi 308e05bc50 remove cache size limit 2025-10-15 16:23:02 +01:00
Salman Muin Kayser Chishti 33a9b6c09c update with dist updates 2025-10-15 16:22:51 +01:00
Daniel Kennedy ddc5fa4ae8 Merge pull request #2133 from actions/danwkennedy/test-blob-stream-timeout
Test: add a timeout test for downloading chunks from the stream
2025-09-25 10:54:19 -04:00
Daniel Kennedy 9b08f07cd3 Fix linting 2025-09-25 09:26:13 -04:00
Daniel Kennedy d26e9423f4 Test: add a timeout test for downloading chunks from the stream 2025-09-25 09:11:38 -04:00
Daniel Kennedy 714f93aedc Merge pull request #2124 from akashchi/reject-on-download-failure
[ARTIFACT] Reject download promise if timeout was reached
2025-09-25 09:06:20 -04:00
Andrei Kashchikhin 844423665b lint 2025-09-25 10:53:34 +02:00
Daniel Kennedy f2ba502b92 Merge pull request #2136 from actions/dependabot/npm_and_yarn/packages/artifact/artifact-minor-patch-612b72ffd4
Bump the artifact-minor-patch group in /packages/artifact with 5 updates
2025-09-24 20:21:48 -04:00
Daniel Kennedy 1db3130eb3 fix: only mock the cpus() function on the os module instead of the whole module 2025-09-24 20:01:53 -04:00
Daniel Kennedy ca8a35d78f Take a direct dependency on @azure/core-http 2025-09-24 16:46:53 -04:00
dependabot[bot] f7f057193f Bump the artifact-minor-patch group in /packages/artifact with 5 updates
Bumps the artifact-minor-patch group in /packages/artifact with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) | `1.10.0` | `1.11.1` |
| [@azure/storage-blob](https://github.com/Azure/azure-sdk-for-js) | `12.15.0` | `12.28.0` |
| [@protobuf-ts/plugin](https://github.com/timostamm/protobuf-ts/tree/HEAD/packages/plugin) | `2.9.1` | `2.11.1` |
| [typedoc](https://github.com/TypeStrong/TypeDoc) | `0.25.4` | `0.28.13` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.2.2` | `5.9.2` |


Updates `@actions/core` from 1.10.0 to 1.11.1
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core)

Updates `@azure/storage-blob` from 12.15.0 to 12.28.0
- [Release notes](https://github.com/Azure/azure-sdk-for-js/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-js/blob/main/documentation/Changelog-for-next-generation.md)
- [Commits](https://github.com/Azure/azure-sdk-for-js/compare/@azure/storage-blob_12.15.0...@azure/storage-blob_12.28.0)

Updates `@protobuf-ts/plugin` from 2.9.1 to 2.11.1
- [Release notes](https://github.com/timostamm/protobuf-ts/releases)
- [Commits](https://github.com/timostamm/protobuf-ts/commits/v2.11.1/packages/plugin)

Updates `typedoc` from 0.25.4 to 0.28.13
- [Release notes](https://github.com/TypeStrong/TypeDoc/releases)
- [Changelog](https://github.com/TypeStrong/typedoc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.25.4...v0.28.13)

Updates `typescript` from 5.2.2 to 5.9.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.2.2...v5.9.2)

---
updated-dependencies:
- dependency-name: "@actions/core"
  dependency-version: 1.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: artifact-minor-patch
- dependency-name: "@azure/storage-blob"
  dependency-version: 12.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: artifact-minor-patch
- dependency-name: "@protobuf-ts/plugin"
  dependency-version: 2.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: artifact-minor-patch
- dependency-name: typedoc
  dependency-version: 0.28.13
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: artifact-minor-patch
- dependency-name: typescript
  dependency-version: 5.9.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: artifact-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-24 20:07:58 +00:00
Daniel Kennedy 8e146e124e Merge pull request #2134 from actions/danwkennedy/dependabot-artifact-cache
Dependabot: add support for `/packages/artifact` and `/packages/cache`
2025-09-24 16:06:29 -04:00
Daniel Kennedy 1ea77a84d7 Update the group names 2025-09-24 16:04:16 -04:00
Daniel Kennedy 7da95b182e Dependabot: add support for /packages/artifact and `/packages/cache 2025-09-24 16:00:44 -04:00
Andrei Kashchikhin 7c689a5156 use error in both reject and destroy 2025-09-24 17:05:25 +02:00
Andrei Kashchikhin 8c6c662cda Merge remote-tracking branch 'upstream/main' into reject-on-download-failure 2025-09-24 17:03:35 +02:00
Bassem Dghaidi 3898ed70c4 Merge pull request #2132 from actions/Link-/cache-4.1.0
Prepare cache `v4.1.0` release
2025-09-24 14:35:45 +02:00
Bassem Dghaidi 9a41b33065 Prepapre cache v4.1.0 release 2025-09-24 05:23:58 -07:00
Salman Muin Kayser Chishti 7aea3e735f changes 2025-09-08 15:37:51 +01:00
Salman Muin Kayser Chishti b1eb18b224 http 2025-09-08 15:36:39 +01:00
Salman Muin Kayser Chishti 48e42b1fdd linting 2025-09-04 15:24:57 +01:00
Salman Muin Kayser Chishti b738f10ef3 package updates 2025-09-04 15:15:02 +01:00
Salman Muin Kayser Chishti 8f32f385e0 Bump package versions, and fix issues 2025-09-04 14:16:27 +01:00
Salman Muin Kayser Chishti 011f07d1dc package changes 2025-09-04 12:58:54 +01:00
Salman Muin Kayser Chishti aa7077acfb Override to fix npm audit stuff 2025-09-04 12:49:31 +01:00
Salman Muin Kayser Chishti 86207b5042 remove engines 24 reuqirement from toolkit and fix test 2025-09-04 12:41:43 +01:00
Andrei Kashchikhin 523ce8ccda add reject 2025-09-01 11:52:11 +02:00
Ryan Ghadimi f58042f9cc Merge pull request #2118 from actions/ghadimir/cache_size_restriction
Remove 10GB Cache Size Limit for Cache Service V2
2025-08-21 15:14:38 +01:00
Ryan Ghadimi 091616a0b8 no need to resolve 2025-08-13 13:38:51 +00:00
Ryan Ghadimi 8da1e670b6 lint 2025-08-13 13:37:36 +00:00
Ryan Ghadimi 06f7fd9df1 new error state, tests to cover 2025-08-13 13:00:46 +00:00
Ryan Ghadimi 0fe20e9d56 remove cache size limit 2025-08-13 10:14:18 +00:00
Salman Muin Kayser Chishti f82db4c00b audit fix 2025-08-08 12:26:34 +01:00
Salman Muin Kayser Chishti b8cca0c71f fix lint errors 2025-08-08 04:02:29 +01:00
Salman Muin Kayser Chishti 6f0cb0c45e Merge branch 'main' into salmanmkc/node24 2025-08-08 03:54:30 +01:00
Salman Muin Kayser Chishti 944ede4d09 custom readlink implementation for Windows compatibility with trailing backslashes 2025-08-08 03:46:42 +01:00
Bassem Dghaidi 227b1ce741 Merge pull request #2115 from actions/Link-/release-4.0.5
Prepare release `4.0.5`
2025-08-07 13:08:34 +02:00
Bassem Dghaidi 447ee85f36 Prepare release 4.0.5 2025-08-07 04:00:47 -07:00
Bassem Dghaidi a6be3de743 Merge pull request #2114 from actions/Link-/fix-cache-tests
Update cache package compilation step to only install runtime dependencies
2025-08-07 12:58:32 +02:00
Bassem Dghaidi 26b94036cb Merge branch 'Link-/fix-cache-tests' of github.com:actions/toolkit into Link-/fix-cache-tests 2025-08-07 03:50:04 -07:00
Bassem Dghaidi f3e6fb165e Fix linter complaints 2025-08-07 03:49:51 -07:00
Bassem Dghaidi 3a607d0f00 Update .github/workflows/cache-tests.yml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-07 12:40:20 +02:00
Bassem Dghaidi c9316bb4a7 Update cache package compilation step to install only runtime dependencies 2025-08-07 03:38:19 -07:00
Bassem Dghaidi ec43e5810d Merge pull request #2113 from actions/Link-/fix-runtime-deps
Reintroduce `@protobuf-ts/runtime-rpc` as a runtime dependency
2025-08-07 12:31:57 +02:00
Bassem Dghaidi 01715621b0 Replace @protobuf-ts/runtime with higher level dep @protobuf-ts/runtime-rpc 2025-08-07 03:21:24 -07:00
Bassem Dghaidi 6c64260c6d Reintroduce @protobuf-ts/runtime as a runtime dependency v2.11.1 2025-08-07 03:15:33 -07:00
Bassem Dghaidi bf3fc9226a Merge pull request #2111 from actions/Link-/cache-4.0.4
Prepare `@actions/cache` 4.0.4
2025-08-06 21:11:24 +02:00
Bassem Dghaidi c6723084aa Prepare release 4.0.4 2025-08-06 11:37:53 -07:00
Salman Muin Kayser Chishti bcb928642f format 2025-08-06 12:57:10 +01:00
Salman Muin Kayser Chishti 8c3fc9ed99 Update test to use mock 2025-08-06 12:49:50 +01:00
Salman Muin Kayser Chishti 1ef3214cee update for types 2025-08-01 11:50:25 +01:00
Salman Muin Kayser Chishti ece2273b24 updates 2025-07-31 23:48:44 +01:00
Salman Muin Kayser Chishti 717b895584 support node 24 2025-07-31 23:37:22 +01:00
Bassem Dghaidi 8ff772deb1 Merge pull request #2106 from actions/Link-/optimise-cache-deps
Move `@protobuf-ts/plugin` to dev dependencies
2025-07-31 14:03:14 +02:00
Bassem Dghaidi 8a3652e16d Optimise cache dependencies 2025-07-31 04:20:29 -07:00
Salman Chishti eb6226501b Merge pull request #2076 from esainane/what-the-word-is-is
Fix typo in `core/README.md`
2025-07-31 12:03:28 +01:00
Bassem Dghaidi d65ee66d9b Move @protobuf-ts/plugin to dev dependencies 2025-07-28 07:58:41 -07:00
Bassem Dghaidi 6d3feab2bf Merge pull request #2100 from actions/copilot/fix-2099
Improve cache service availability determination and implement conditional error logging
2025-07-28 16:49:10 +02:00
Bassem Dghaidi 79e1d8bb74 Merge pull request #2101 from actions/Link-/clarify-cache-hit-log
Explicit logging of cache key and restore key matches
2025-07-18 15:20:45 +02:00
copilot-swe-agent[bot] a0907ed2e2 Remove .nx/ from .gitignore as requested
Co-authored-by: Link- <568794+Link-@users.noreply.github.com>
2025-07-14 13:49:49 +00:00
copilot-swe-agent[bot] bd54a2413a Fix v1 cache service to only check ACTIONS_CACHE_URL
Co-authored-by: Link- <568794+Link-@users.noreply.github.com>
2025-07-14 13:39:13 +00:00
copilot-swe-agent[bot] 89397db14b Restore server error test and confirm logCacheError function removal
Co-authored-by: Link- <568794+Link-@users.noreply.github.com>
2025-07-14 13:01:02 +00:00
copilot-swe-agent[bot] d48d6b62a4 Remove logCacheError function and implement inline 5xx error detection as requested
Co-authored-by: Link- <568794+Link-@users.noreply.github.com>
2025-07-14 12:42:37 +00:00
copilot-swe-agent[bot] bab3dcf7f3 Complete PR feedback implementation: all cache tests passing
Co-authored-by: Link- <568794+Link-@users.noreply.github.com>
2025-07-14 12:26:40 +00:00
copilot-swe-agent[bot] c51178a15e Implement 5xx server error detection and fix most cache tests
Co-authored-by: Link- <568794+Link-@users.noreply.github.com>
2025-07-14 12:23:44 +00:00
copilot-swe-agent[bot] bbc6082700 Add .nx/ to .gitignore to exclude build cache files 2025-07-14 12:09:41 +00:00
copilot-swe-agent[bot] cf3aaeb491 Update tests to expect warnings instead of errors for non-5xx cache failures
Co-authored-by: Link- <568794+Link-@users.noreply.github.com>
2025-07-14 12:07:37 +00:00
Bassem Dghaidi cf4886cccb Fix linting issues 2025-07-14 03:49:28 -07:00
Bassem Dghaidi 0c5da92b52 Fix logging of cache key and restore key matches 2025-07-14 03:45:17 -07:00
copilot-swe-agent[bot] 513216f1dd Fix tests to expect errors instead of warnings for cache failures
- Update restoreCacheV2.test.ts, restoreCache.test.ts, saveCacheV2.test.ts, and saveCache.test.ts
- Change test expectations from core.warning to core.error for cache operation failures
- All tests now pass successfully

Co-authored-by: Link- <568794+Link-@users.noreply.github.com>
2025-07-14 10:38:47 +00:00
copilot-swe-agent[bot] 3c90578c30 Improve cache service availability determination and change warnings to errors
- Update isFeatureAvailable() to leverage ACTIONS_CACHE_SERVICE_V2 feature flag
- For v2: check ACTIONS_RESULTS_URL availability
- For v1: check either ACTIONS_CACHE_URL or ACTIONS_RESULTS_URL availability
- Change warning logs to error logs for cache failures
- Add comprehensive tests covering all scenarios

Co-authored-by: Link- <568794+Link-@users.noreply.github.com>
2025-07-14 10:32:34 +00:00
copilot-swe-agent[bot] be5a2ce677 Initial plan 2025-07-14 10:19:51 +00:00
Ben De St Paer-Gotch 683703c114 Merge pull request #2086 from actions/nebuk89-patch-1
Update README.md
2025-06-16 10:06:48 +01:00
Sai Nane dbb1ea35ff Fix typo in core/README.md 2025-05-27 04:27:17 +00:00
193 changed files with 13247 additions and 10655 deletions
+5
View File
@@ -57,3 +57,8 @@ This will ask you some questions about the new package. Start with `0.0.0` as th
```
3. Start developing 😄.
## Releasing Packages
For information on how packages are published to npm, including workflow inputs, dist-tags, and safety guards, see the [release documentation](../docs/release.md).
+27
View File
@@ -0,0 +1,27 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm"
directory: "/packages/artifact"
schedule:
interval: "daily"
groups:
# Group minor and patch updates together but keep major separate
artifact-minor-patch:
update-types:
- "minor"
- "patch"
- package-ecosystem: "npm"
directory: "/packages/cache"
schedule:
interval: "daily"
groups:
# Group minor and patch updates together but keep major separate
cache-minor-patch:
update-types:
- "minor"
- "patch"
+220 -14
View File
@@ -22,12 +22,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set Node.js 20.x
uses: actions/setup-node@v4
- name: Set Node.js 24.x
uses: actions/setup-node@v5
with:
node-version: 20.x
node-version: 24.x
# Need root node_modules because certain npm packages like jest are configured for the entire repository and it won't be possible
# without these to just compile the artifacts package
@@ -47,7 +47,7 @@ jobs:
echo -n 'hello from file 2' > artifact-path/second.txt
- name: Upload Artifacts
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const {default: artifact} = require('./packages/artifact/lib/artifact')
@@ -71,18 +71,151 @@ jobs:
} catch (err) {
console.log('Successfully blocked second artifact upload')
}
upload-single-file:
name: Upload Single File (no zip)
strategy:
matrix:
runs-on: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: false
runs-on: ${{ matrix.runs-on }}
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set Node.js 24.x
uses: actions/setup-node@v5
with:
node-version: 24.x
- name: Install root npm packages
run: npm ci
- name: Compile artifact package
run: |
npm ci
npm run tsc
working-directory: packages/artifact
- name: Create file that will be uploaded
run: |
echo -n 'hello from single file upload' > single-file-${{ matrix.runs-on }}.txt
- name: Upload Single File Artifact (skipArchive)
uses: actions/github-script@v8
with:
script: |
const {default: artifact} = require('./packages/artifact/lib/artifact')
const artifactName = 'my-single-file-${{ matrix.runs-on }}'
console.log('artifactName: ' + artifactName)
const uploadResult = await artifact.uploadArtifact(
artifactName,
['single-file-${{ matrix.runs-on }}.txt'],
'./',
{skipArchive: true}
)
console.log(uploadResult)
const size = uploadResult.size
const id = uploadResult.id
if (!id) {
throw new Error('Artifact ID is missing from upload result')
}
console.log(`Successfully uploaded single file artifact ${id}`)
upload-html-file:
name: Upload HTML File (no zip)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set Node.js 24.x
uses: actions/setup-node@v5
with:
node-version: 24.x
- name: Install root npm packages
run: npm ci
- name: Compile artifact package
run: |
npm ci
npm run tsc
working-directory: packages/artifact
- name: Create HTML file
run: |
cat > report.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Artifact Upload Test Report</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; color: #24292f; }
h1 { border-bottom: 1px solid #d0d7de; padding-bottom: 8px; }
.success { color: #1a7f37; }
.info { background: #ddf4ff; border: 1px solid #54aeff; border-radius: 6px; padding: 12px 16px; margin: 16px 0; }
table { border-collapse: collapse; width: 100%; margin: 16px 0; }
th, td { border: 1px solid #d0d7de; padding: 8px 12px; text-align: left; }
th { background: #f6f8fa; }
</style>
</head>
<body>
<h1>Artifact Upload Test Report</h1>
<div class="info">
<strong>This HTML file was uploaded as a single un-zipped artifact.</strong>
If you can see this in the browser, the feature is working correctly!
</div>
<table>
<tr><th>Property</th><th>Value</th></tr>
<tr><td>Upload method</td><td><code>skipArchive: true</code></td></tr>
<tr><td>Content-Type</td><td><code>text/html</code></td></tr>
<tr><td>File</td><td><code>report.html</code></td></tr>
</table>
<p class="success">&#10004; Single file upload is working!</p>
</body>
</html>
EOF
- name: Upload HTML Artifact (skipArchive)
uses: actions/github-script@v8
with:
script: |
const {default: artifact} = require('./packages/artifact/lib/artifact')
const uploadResult = await artifact.uploadArtifact(
'test-report',
['report.html'],
'./',
{skipArchive: true}
)
console.log(uploadResult)
console.log(`Successfully uploaded HTML artifact ${uploadResult.id}`)
console.log('This artifact is intentionally kept for manual browser verification')
verify:
name: Verify and Delete
runs-on: ubuntu-latest
needs: [upload]
needs: [upload, upload-single-file, upload-html-file]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set Node.js 20.x
uses: actions/setup-node@v4
- name: Set Node.js 24.x
uses: actions/setup-node@v5
with:
node-version: 20.x
node-version: 24.x
# Need root node_modules because certain npm packages like jest are configured for the entire repository and it won't be possible
# without these to just compile the artifacts package
@@ -96,7 +229,7 @@ jobs:
working-directory: packages/artifact
- name: List and Download Artifacts
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const {default: artifactClient} = require('./packages/artifact/lib/artifact')
@@ -164,8 +297,73 @@ jobs:
}
}
}
- name: Download and Verify Single File Artifacts
uses: actions/github-script@v8
with:
script: |
const {default: artifactClient} = require('./packages/artifact/lib/artifact')
const {readFile} = require('fs/promises')
const path = require('path')
const findBy = {
repositoryOwner: process.env.GITHUB_REPOSITORY.split('/')[0],
repositoryName: process.env.GITHUB_REPOSITORY.split('/')[1],
token: '${{ secrets.GITHUB_TOKEN }}',
workflowRunId: process.env.GITHUB_RUN_ID
}
const listResult = await artifactClient.listArtifacts({latest: true, findBy})
const expectedSingleFiles = [
'single-file-ubuntu-latest.txt',
'single-file-windows-latest.txt',
'single-file-macos-latest.txt'
]
// Single file artifacts are named after the file basename
const singleFileArtifacts = listResult.artifacts.filter(a =>
expectedSingleFiles.includes(a.name)
)
console.log('Found single file artifacts:', singleFileArtifacts.length)
if (singleFileArtifacts.length !== 3) {
console.log('Unexpected single file artifacts:', singleFileArtifacts)
throw new Error(
`Expected 3 single-file artifacts but found ${singleFileArtifacts.length}`
)
}
for (const artifact of singleFileArtifacts) {
const downloadDir = `single-file-download-${artifact.id}`
const {downloadPath} = await artifactClient.downloadArtifact(artifact.id, {
path: downloadDir,
findBy
})
console.log('Downloaded single file artifact to:', downloadPath)
const filePath = path.join(
process.env.GITHUB_WORKSPACE,
downloadPath,
artifact.name
)
console.log('Checking file:', filePath)
const content = await readFile(filePath, 'utf8')
if (content.trim() !== 'hello from single file upload') {
throw new Error(
`Expected single file to contain 'hello from single file upload' but found '${content}'`
)
}
console.log(`Successfully verified single file artifact ${artifact.id}`)
}
- name: Delete Artifacts
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const {default: artifactClient} = require('./packages/artifact/lib/artifact')
@@ -173,11 +371,19 @@ jobs:
const artifactsToDelete = [
'my-artifact-ubuntu-latest',
'my-artifact-windows-latest',
'my-artifact-macos-latest'
'my-artifact-macos-latest',
'single-file-ubuntu-latest.txt',
'single-file-windows-latest.txt',
'single-file-macos-latest.txt'
]
for (const artifactName of artifactsToDelete) {
const {id} = await artifactClient.deleteArtifact(artifactName)
try {
const {id} = await artifactClient.deleteArtifact(artifactName)
console.log(`Deleted artifact '${artifactName}' (ID: ${id})`)
} catch (err) {
console.log(`Could not delete artifact '${artifactName}': ${err.message}`)
}
}
const {artifacts} = await artifactClient.listArtifacts({latest: true})
+4 -4
View File
@@ -18,12 +18,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set Node.js 20.x
uses: actions/setup-node@v4
- name: Set Node.js 24.x
uses: actions/setup-node@v5
with:
node-version: 20.x
node-version: 24.x
- name: npm install
run: npm install
+10 -12
View File
@@ -22,12 +22,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set Node.js 20.x
uses: actions/setup-node@v4
- name: Set Node.js 24.x
uses: actions/setup-node@v5
with:
node-version: 20.x
node-version: 24.x
# In order to save & restore cache from a shell script, certain env variables need to be set that are only available in the
# node context. This runs a local action that gets and sets the necessary env variables that are needed
@@ -39,9 +39,11 @@ jobs:
- name: Install root npm packages
run: npm ci
# We need to install only runtime dependencies (omit dev dependencies) to verify that what we're shipping is all
# that is needed
- name: Compile cache package
run: |
npm ci
npm ci --omit=dev
npm run tsc
working-directory: packages/cache
@@ -53,10 +55,8 @@ jobs:
shell: bash
run: packages/cache/__tests__/create-cache-files.sh ${{ runner.os }} ~/test-cache
# We're using node -e to call the functions directly available in the @actions/cache package
- name: Save cache using saveCache()
run: |
node -e "Promise.resolve(require('./packages/cache/lib/cache').saveCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}'))"
run: node packages/cache/__tests__/save-cache.mjs ${{ runner.os }} ${{ github.run_id }}
- name: Delete cache folders before restoring
shell: bash
@@ -65,8 +65,7 @@ jobs:
rm -rf ~/test-cache
- name: Restore cache using restoreCache() with http-client
run: |
node -e "Promise.resolve(require('./packages/cache/lib/cache').restoreCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}',[],{useAzureSdk: false}))"
run: node packages/cache/__tests__/restore-cache.mjs ${{ runner.os }} ${{ github.run_id }} false
- name: Verify cache restored with http-client
shell: bash
@@ -81,8 +80,7 @@ jobs:
rm -rf ~/test-cache
- name: Restore cache using restoreCache() with Azure SDK
run: |
node -e "Promise.resolve(require('./packages/cache/lib/cache').restoreCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}'))"
run: node packages/cache/__tests__/restore-cache.mjs ${{ runner.os }} ${{ github.run_id }} true
- name: Verify cache restored with Azure SDK
shell: bash
+7 -11
View File
@@ -17,16 +17,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v5
- shell: bash
run: |
rm "C:\Program Files\Git\usr\bin\tar.exe"
- name: Set Node.js 20.x
uses: actions/setup-node@v1
- name: Set Node.js 24.x
uses: actions/setup-node@v5
with:
node-version: 20.x
node-version: 24.x
# In order to save & restore cache from a shell script, certain env variables need to be set that are only available in the
# node context. This runs a local action that gets and sets the necessary env variables that are needed
@@ -52,10 +52,8 @@ jobs:
shell: bash
run: packages/cache/__tests__/create-cache-files.sh ${{ runner.os }} ~/test-cache
# We're using node -e to call the functions directly available in the @actions/cache package
- name: Save cache using saveCache()
run: |
node -e "Promise.resolve(require('./packages/cache/lib/cache').saveCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}'))"
run: node packages/cache/__tests__/save-cache.mjs ${{ runner.os }} ${{ github.run_id }}
- name: Delete cache folders before restoring
shell: bash
@@ -64,8 +62,7 @@ jobs:
rm -rf ~/test-cache
- name: Restore cache using restoreCache() with http-client
run: |
node -e "Promise.resolve(require('./packages/cache/lib/cache').restoreCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}',[],{useAzureSdk: false}))"
run: node packages/cache/__tests__/restore-cache.mjs ${{ runner.os }} ${{ github.run_id }} false
- name: Verify cache restored with http-client
shell: bash
@@ -80,8 +77,7 @@ jobs:
rm -rf ~/test-cache
- name: Restore cache using restoreCache() with Azure SDK
run: |
node -e "Promise.resolve(require('./packages/cache/lib/cache').restoreCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}'))"
run: node packages/cache/__tests__/restore-cache.mjs ${{ runner.os }} ${{ github.run_id }} true
- name: Verify cache restored with Azure SDK
shell: bash
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+47 -20
View File
@@ -1,6 +1,6 @@
name: Publish NPM
run-name: Publish NPM - ${{ github.event.inputs.package }}
run-name: Publish NPM - ${{ inputs.package }} from ${{ inputs.branch }}
on:
workflow_dispatch:
@@ -20,23 +20,39 @@ on:
- http-client
- io
- tool-cache
branch:
type: string
required: false
default: 'main'
description: 'Branch to release from'
npm-tag:
type: string
required: false
default: 'latest'
description: 'npm dist-tag for the release. Use "latest" for main branch releases. For non-main branches, use a non-semver tag like "v1-longlived". Semver values (e.g. "5.0.0") are not valid dist-tags and will be rejected by npm.'
test-all:
type: boolean
required: false
default: false
description: 'Run tests for all packages instead of only the package being published'
jobs:
test:
runs-on: macos-latest-large
runs-on: ubuntu-latest
steps:
- name: setup repo
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch }}
- name: verify package exists
run: ls packages/${{ github.event.inputs.package }}
- name: Set Node.js 20.x
uses: actions/setup-node@v4
- name: Set Node.js 24.x
uses: actions/setup-node@v6
with:
node-version: 20.x
node-version: 24.x
- name: npm install
run: npm install
@@ -48,20 +64,25 @@ jobs:
run: npm run build
- name: test
run: npm run test
run: |
if [ "${{ inputs.test-all }}" = "true" ]; then
npm run test
else
npm run test -- --testPathPattern="packages/${{ inputs.package }}"
fi
- name: pack
run: npm pack
working-directory: packages/${{ github.event.inputs.package }}
- name: upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ github.event.inputs.package }}
path: packages/${{ github.event.inputs.package }}/*.tgz
publish:
runs-on: macos-latest-large
runs-on: ubuntu-slim
needs: test
environment: npm-publish
permissions:
@@ -69,30 +90,36 @@ jobs:
id-token: write
steps:
- name: download artifact
uses: actions/download-artifact@v4
- name: Set Node.js 24.x
uses: actions/setup-node@v6
with:
name: ${{ github.event.inputs.package }}
node-version: 24.x
- name: setup authentication
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
env:
NPM_TOKEN: ${{ secrets.TOKEN }}
- name: download artifact
uses: actions/download-artifact@v8
with:
name: ${{ inputs.package }}
- name: guard against publishing latest from non-main branch
if: inputs.branch != 'main' && inputs.npm-tag == 'latest'
run: |
echo "::error::Publishing with the 'latest' dist-tag from a non-main branch ('${{ inputs.branch }}') is not allowed. Use the package major version as the tag (e.g. v5)."
exit 1
- name: publish
run: npm publish --provenance *.tgz
run: npm publish --provenance --tag "${{ inputs.npm-tag }}" *.tgz
- name: notify slack on failure
if: failure()
run: |
curl -X POST -H 'Content-type: application/json' --data '{"text":":pb__failed: Failed to publish a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
curl -X POST -H 'Content-type: application/json' --data '{"text":":pb__failed: Failed to publish a new version of ${{ inputs.package }} from ${{ inputs.branch }} (tag: ${{ inputs.npm-tag }})"}' $SLACK_WEBHOOK
env:
SLACK_WEBHOOK: ${{ secrets.SLACK }}
- name: notify slack on success
if: success()
run: |
curl -X POST -H 'Content-type: application/json' --data '{"text":":dance: Successfully published a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
curl -X POST -H 'Content-type: application/json' --data '{"text":":dance: Successfully published a new version of ${{ inputs.package }} from ${{ inputs.branch }} (tag: ${{ inputs.npm-tag }})"}' $SLACK_WEBHOOK
env:
SLACK_WEBHOOK: ${{ secrets.SLACK }}
+5 -5
View File
@@ -18,19 +18,19 @@ jobs:
matrix:
runs-on: [ubuntu-latest, macos-latest-large, windows-latest]
# Node 18 is the current default Node version in hosted runners, so users may still use the toolkit with it when running tests (see https://github.com/actions/toolkit/issues/1841)
# Node 20 is the currently support Node version for actions - https://docs.github.com/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#runsusing-for-javascript-actions
node-version: [18.x, 20.x]
# Node 20 is the currently supported stable Node version for actions - https://docs.github.com/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#runsusing-for-javascript-actions
# Node 24 is the new version being added with support in actions runners
node-version: [20.x, 24.x]
fail-fast: false
runs-on: ${{ matrix.runs-on }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set up Node ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node-version }}
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
if: ${{ github.repository_owner == 'actions' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Update Octokit
working-directory: packages/github
run: |
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
os: [ubuntu-16.04, windows-2019]
runs-on: ${{matrix.os}}
actions:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
version: ${{matrix.node}}
- run: |
Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

+1 -1
View File
@@ -18,7 +18,7 @@ e.g. To use https://github.com/actions/setup-node, users will author:
```yaml
steps:
using: actions/setup-node@v4
using: actions/setup-node@v5
```
# Define Metadata
+82
View File
@@ -0,0 +1,82 @@
# Releasing Packages
Packages are published to npm via the **Publish NPM** workflow ([`.github/workflows/releases.yml`](../.github/workflows/releases.yml)). The workflow is triggered manually through `workflow_dispatch` from the GitHub Actions UI.
## How it works
The workflow has two jobs:
1. **test** — Checks out the specified branch, installs dependencies, bootstraps the monorepo, builds all packages, runs tests, then packs the target package into a `.tgz` archive and uploads it as a workflow artifact.
2. **publish** — Downloads the packed artifact and publishes it to npm with `--provenance` (OIDC-based). Sends a Slack notification on success or failure. Requires the `npm-publish` environment.
## Inputs
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
| `package` | choice | **yes** | — | Which package to release. One of: `artifact`, `attest`, `cache`, `core`, `exec`, `github`, `glob`, `http-client`, `io`, `tool-cache`. |
| `branch` | string | no | `main` | The branch to check out and release from. |
| `npm-tag` | string | no | `latest` | The npm dist-tag to publish under. See [Dist-tags](#dist-tags) below. |
| `test-all` | boolean | no | `false` | When `false`, only tests for the selected package are run. Set to `true` to run the full test suite across all packages. |
## Dist-tags
npm dist-tags control which version users get when they `npm install @actions/<package>` (or `@<tag>`).
> **Important:** npm dist-tags **cannot** be valid semver strings. Values like `5.0.0` or `1.2.3` will be rejected by npm. Use a descriptive name instead.
- **`latest`** — The default tag. This is what users get with a plain `npm install`. Should only be used for releases from `main`.
- **Custom tags** (e.g. `v1-longlived`) — Used for releases from long-lived or experimental branches.
Examples of **valid** dist-tags: `latest`, `next`, `beta`, `v1-longlived`
Examples of **invalid** dist-tags: `5.0.0`, `1.2.3`, `6.0.0-rc.1` (these are semver and will be rejected)
| ![Screenshot showcasing the npm distribution tags](assets/npm-dist-tags.png) |
|---|
| npm distribution tags |
### Safety guard
The workflow **blocks** publishing with the `latest` dist-tag from any branch other than `main`. This prevents accidentally overwriting `latest` with a version from an older or experimental branch. If you're releasing from a non-main branch, use the package's major version as the tag (e.g. `v5`).
## Examples
### Standard release from main
Use default inputs — just pick the package:
| Input | Value |
|---|---|
| `package` | `core` |
| `branch` | `main` (default) |
| `npm-tag` | `latest` (default) |
| `test-all` | `false` (default) |
This publishes the version in `packages/core/package.json` on `main` as `@actions/core@latest`.
### Patch release from a long-lived branch
| Input | Value |
|---|---|
| `package` | `artifact` |
| `branch` | `releases/v5` |
| `npm-tag` | `v5` |
| `test-all` | `false` (default) |
This publishes the version in `packages/artifact/package.json` on the `releases/v5` branch under the `v5` dist-tag. The `latest` tag remains untouched.
### Release with full test suite
| Input | Value |
|---|---|
| `package` | `cache` |
| `branch` | `main` (default) |
| `npm-tag` | `latest` (default) |
| `test-all` | `true` |
Same as a standard release, but runs tests for all packages before publishing.
## Prerequisites
- You must have permission to trigger workflows on this repository.
- The `npm-publish` environment must be configured with npm credentials and OIDC.
- The `SLACK` secret must be set for Slack notifications to work.
+29 -2
View File
@@ -4,8 +4,35 @@ module.exports = {
roots: ['<rootDir>/packages'],
testEnvironment: 'node',
testMatch: ['**/__tests__/*.test.ts'],
transform: {
'^.+\\.ts$': 'ts-jest'
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
'^@actions/core$': '<rootDir>/packages/core/lib/core.js',
'^@actions/exec$': '<rootDir>/packages/exec/lib/exec.js',
'^@actions/io$': '<rootDir>/packages/io/lib/io.js',
'^@actions/io/lib/io-util$': '<rootDir>/packages/io/lib/io-util.js',
'^@actions/http-client$': '<rootDir>/packages/http-client/lib/index.js',
'^@actions/http-client/lib/auth$': '<rootDir>/packages/http-client/lib/auth.js',
'^@actions/http-client/lib/interfaces$': '<rootDir>/packages/http-client/lib/interfaces.js',
'^@actions/github$': '<rootDir>/packages/github/lib/github.js',
'^@actions/github/lib/utils$': '<rootDir>/packages/github/lib/utils.js',
'^@actions/glob$': '<rootDir>/packages/glob/lib/glob.js',
'^@actions/tool-cache$': '<rootDir>/packages/tool-cache/lib/tool-cache.js',
'^@actions/cache$': '<rootDir>/packages/cache/lib/cache.js',
'^@actions/attest$': '<rootDir>/packages/attest/lib/index.js'
},
transform: {
'^.+\\.(ts|js)$': ['ts-jest', {
diagnostics: {warnOnly: true},
tsconfig: {
allowJs: true,
esModuleInterop: true,
module: 'commonjs',
moduleResolution: 'node'
}
}]
},
transformIgnorePatterns: [
'/node_modules/(?!(@octokit|@actions/github|@actions/http-client|@actions/io|@actions/exec|@actions/core|@actions/glob|@actions/tool-cache|@actions/cache|@actions/attest|universal-user-agent|before-after-hook)/)'
],
verbose: true
}
+6694 -4922
View File
File diff suppressed because it is too large Load Diff
+16 -2
View File
@@ -1,6 +1,6 @@
{
"name": "root",
"private": true,
"private": true,
"scripts": {
"audit-all": "lerna run audit-moderate",
"bootstrap": "lerna exec -- npm install",
@@ -17,7 +17,7 @@
},
"devDependencies": {
"@types/jest": "^29.5.4",
"@types/node": "^20.5.7",
"@types/node": "^24.1.0",
"@types/signale": "^1.4.1",
"concurrently": "^6.1.0",
"eslint": "^8.0.1",
@@ -32,5 +32,19 @@
"prettier": "^3.0.0",
"ts-jest": "^29.1.1",
"typescript": "^5.2.2"
},
"overrides": {
"semver": "^7.6.0",
"tar": "^6.2.1",
"@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/request": "^10.0.7",
"@octokit/request-error": "^7.1.0",
"@octokit/core": "^7.0.6",
"tmp": "^0.2.4",
"@types/node": "^24.1.0",
"brace-expansion": "^2.0.2",
"form-data": "^4.0.4",
"uri-js": "npm:uri-js-replace@^1.0.1",
"node-fetch": "^3.3.2"
}
}
-1
View File
@@ -4,7 +4,6 @@ Interact programmatically with [Actions Artifacts](https://docs.github.com/en/ac
This is the core library that powers the [`@actions/upload-artifact`](https://github.com/actions/upload-artifact) and [`@actions/download-artifact`](https://github.com/actions/download-artifact) actions.
- [`@actions/artifact`](#actionsartifact)
- [v2 - What's New](#v2---whats-new)
- [Improvements](#improvements)
+83 -44
View File
@@ -1,122 +1,161 @@
# @actions/artifact Releases
### 2.3.3
## 6.2.1
- Support the RFC 5987 `filename*` field in the `content-disposition` header. This allows us to correctly download files and artifacts with Chinese/Japanese/Korean (among other) characters in their name.
## 6.2.0
- Support uploading single un-archived files (not zipped). Direct uploads are only supported for artifacts version 7+ (based on the major version of `actions/upload-artifact`). Callers must pass the `skipArchive` option to `uploadArtifact`. Only single files can be uploaded at a time right now. Default behavior should remain unchanged if `skipArchive = false`. When `skipArchive = true`, the name of the file is used as the name of the artifact for consistency with the downloads: you upload `artifact.txt`, you download `artifact.txt`.
## 6.1.0
- Support downloading non-zip artifacts. Zipped artifacts will be decompressed automatically (with an optional override). Un-zipped artifacts will be downloaded as-is.
## 6.0.0
- **Breaking change**: Package is now ESM-only
- CommonJS consumers must use dynamic `import()` instead of `require()`
## 5.0.3
- Bump `@actions/http-client` to `3.0.2`
## 5.0.1
- Fix Node.js 24 punycode deprecation warning by updating `@azure/storage-blob` from `^12.15.0` to `^12.29.1` [#2211](https://github.com/actions/toolkit/pull/2211)
- Removed direct `@azure/core-http` dependency (now uses `@azure/core-rest-pipeline` via storage-blob)
## 5.0.0
- Dependency updates for Node.js 24 runtime support
- Update `@actions/core` to v2
- Update `@actions/http-client` to v3
## 4.0.0
- Add support for Node 24 [#2110](https://github.com/actions/toolkit/pull/2110)
- Fix: artifact pagination bugs and configurable artifact count limits [#2165](https://github.com/actions/toolkit/pull/2165)
- Fix: reject the promise on timeout [#2124](https://github.com/actions/toolkit/pull/2124)
- Update dependency versions
## 2.3.3
- Dependency updates [#2049](https://github.com/actions/toolkit/pull/2049)
### 2.3.2
## 2.3.2
- Added masking for Shared Access Signature (SAS) artifact URLs [#1982](https://github.com/actions/toolkit/pull/1982)
- Change hash to digest for consistent terminology across runner logs [#1991](https://github.com/actions/toolkit/pull/1991)
- Change hash to digest for consistent terminology across runner logs [#1991](https://github.com/actions/toolkit/pull/1991)
### 2.3.1
## 2.3.1
- Fix comment typo on expectedHash. [#1986](https://github.com/actions/toolkit/pull/1986)
### 2.3.0
## 2.3.0
- Allow ArtifactClient to perform digest comparisons, if supplied. [#1975](https://github.com/actions/toolkit/pull/1975)
### 2.2.2
## 2.2.2
- Default concurrency to 5 for uploading artifacts [#1962](https://github.com/actions/toolkit/pull/1962)
### 2.2.1
## 2.2.1
- Add `ACTIONS_ARTIFACT_UPLOAD_CONCURRENCY` and `ACTIONS_ARTIFACT_UPLOAD_TIMEOUT_MS` environment variables [#1928](https://github.com/actions/toolkit/pull/1928)
### 2.2.0
## 2.2.0
- Return artifact digest on upload [#1896](https://github.com/actions/toolkit/pull/1896)
### 2.1.11
## 2.1.11
- Fixed a bug with relative symlinks resolution [#1844](https://github.com/actions/toolkit/pull/1844)
- Use native `crypto` [#1815](https://github.com/actions/toolkit/pull/1815)
### 2.1.10
## 2.1.10
- Fixed a regression with symlinks not being automatically resolved [#1830](https://github.com/actions/toolkit/pull/1830)
- Fixed a regression with chunk timeout [#1786](https://github.com/actions/toolkit/pull/1786)
### 2.1.9
## 2.1.9
- Fixed artifact upload chunk timeout logic [#1774](https://github.com/actions/toolkit/pull/1774)
- Use lazy stream to prevent issues with open file limits [#1771](https://github.com/actions/toolkit/pull/1771)
### 2.1.8
## 2.1.8
- Allows `*.localhost` domains for hostname checks for local development.
### 2.1.7
## 2.1.7
- Update unzip-stream dependency and reverted to using `unzip.Extract()`
### 2.1.6
## 2.1.6
- Will retry on invalid request responses.
### 2.1.5
## 2.1.5
- Bumped `archiver` dependency to 7.0.1
### 2.1.4
## 2.1.4
- Adds info-level logging for zip extraction
### 2.1.3
## 2.1.3
- Fixes a bug in the extract logic updated in 2.1.2
### 2.1.2
## 2.1.2
- Updated the stream extract functionality to use `unzip.Parse()` instead of `unzip.Extract()` for greater control of unzipping artifacts
### 2.1.1
## 2.1.1
- Updated `isGhes` check to include `.ghe.com` and `.ghe.localhost` as accepted hosts
### 2.1.0
## 2.1.0
- Added `ArtifactClient#deleteArtifact` to delete artifacts by name [#1626](https://github.com/actions/toolkit/pull/1626)
- Update error messaging to be more useful [#1628](https://github.com/actions/toolkit/pull/1628)
### 2.0.1
## 2.0.1
- Patch to fix transient request timeouts https://github.com/actions/download-artifact/issues/249
- Patch to fix transient request timeouts <https://github.com/actions/download-artifact/issues/249>
### 2.0.0
## 2.0.0
- Major release. Supports new Artifact backend for improved speed, reliability and behavior.
- Numerous API changes, [some breaking](./README.md#breaking-changes).
- [Blog post with more info](https://github.blog/2024-02-12-get-started-with-v4-of-github-actions-artifacts/)
### 1.1.1
## 1.1.1
- Fixed a bug in Node16 where if an HTTP download finished too quickly (<1ms, e.g. when it's mocked) we attempt to delete a temp file that has not been created yet [#1278](https://github.com/actions/toolkit/pull/1278/commits/b9de68a590daf37c6747e38d3cb4f1dd2cfb791c)
### 1.1.0
## 1.1.0
- Add `x-actions-results-crc64` and `x-actions-results-md5` checksum headers on upload [#1063](https://github.com/actions/toolkit/pull/1063)
### 1.0.2
## 1.0.2
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
### 1.0.1
## 1.0.1
- Update to v2.0.0 of `@actions/http-client`
### 1.0.0
## 1.0.0
- Update `lockfileVersion` to `v2` in `package-lock.json` [#1009](https://github.com/actions/toolkit/pull/1009)
### 0.6.1
## 0.6.1
- Fix for failing 0 byte file uploads on Windows [#962](https://github.com/actions/toolkit/pull/962)
### 0.6.0
## 0.6.0
- Support upload from named pipes [#748](https://github.com/actions/toolkit/pull/748)
- Fixes to percentage values being greater than 100% when downloading all artifacts [#889](https://github.com/actions/toolkit/pull/889)
@@ -125,49 +164,49 @@
- Faster upload speeds for certain types of large files by exempting gzip compression [#956](https://github.com/actions/toolkit/pull/956)
- More detailed logging when dealing with chunked uploads [#957](https://github.com/actions/toolkit/pull/957)
### 0.5.2
## 0.5.2
- Add HTTP 500 as a retryable status code for artifact upload and download.
### 0.5.1
## 0.5.1
- Bump @actions/http-client to version 1.0.11 to fix proxy related issues during artifact upload and download
### 0.5.0
## 0.5.0
- Improved retry-ability for all http calls during artifact upload and download if an error is encountered
### 0.4.2
## 0.4.2
- Improved retry-ability when a partial artifact download is encountered
### 0.4.1
## 0.4.1
- Update to latest @actions/core version
### 0.4.0
## 0.4.0
- Add option to specify custom retentions on artifacts
-
### 0.3.5
## 0.3.5
- Retry in the event of a 413 response
### 0.3.3
## 0.3.3
- Increase chunk size during upload from 4MB to 8MB
- Improve user-agent strings during API calls to help internally diagnose issues
### 0.3.2
## 0.3.2
- Fix to ensure readstreams get correctly reset in the event of a retry
### 0.3.1
## 0.3.1
- Fix to ensure temporary gzip files get correctly deleted during artifact upload
- Remove spaces as a forbidden character during upload
### 0.3.0
## 0.3.0
- Fixes to gzip decompression when downloading artifacts
- Support handling 429 response codes
@@ -176,13 +215,13 @@
- Clearer error message if storage quota has been reached
- Improved logging and output during artifact download
### 0.2.0
## 0.2.0
- Fixes to TCP connections not closing
- GZip file compression to speed up downloads
- Improved logging and output
- Extra documentation
### 0.1.0
## 0.1.0
- Initial release
@@ -1,10 +1,10 @@
import * as http from 'http'
import * as net from 'net'
import {HttpClient} from '@actions/http-client'
import * as config from '../src/internal/shared/config'
import {internalArtifactTwirpClient} from '../src/internal/shared/artifact-twirp-client'
import {noopLogs} from './common'
import {NetworkError, UsageError} from '../src/internal/shared/errors'
import * as config from '../src/internal/shared/config.js'
import {internalArtifactTwirpClient} from '../src/internal/shared/artifact-twirp-client.js'
import {noopLogs} from './common.js'
import {NetworkError, UsageError} from '../src/internal/shared/errors.js'
jest.mock('@actions/http-client')
+51 -5
View File
@@ -1,10 +1,14 @@
import * as config from '../src/internal/shared/config'
import * as config from '../src/internal/shared/config.js'
import os from 'os'
// Mock the 'os' module
jest.mock('os', () => ({
cpus: jest.fn()
}))
// Mock the `cpus()` function in the `os` module
jest.mock('os', () => {
const osActual = jest.requireActual('os')
return {
...osActual,
cpus: jest.fn()
}
})
beforeEach(() => {
jest.resetModules()
@@ -101,3 +105,45 @@ describe('uploadConcurrencyEnv', () => {
}).toThrow()
})
})
describe('getMaxArtifactListCount', () => {
beforeEach(() => {
delete process.env.ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT
})
it('should return default 1000 when no env set', () => {
expect(config.getMaxArtifactListCount()).toBe(1000)
})
it('should return value set in ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT', () => {
process.env.ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT = '2000'
expect(config.getMaxArtifactListCount()).toBe(2000)
})
it('should throw if value set in ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT is invalid', () => {
process.env.ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT = 'abc'
expect(() => {
config.getMaxArtifactListCount()
}).toThrow(
'Invalid value set for ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT env variable'
)
})
it('should throw if ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT is < 1', () => {
process.env.ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT = '0'
expect(() => {
config.getMaxArtifactListCount()
}).toThrow(
'Invalid value set for ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT env variable'
)
})
it('should throw if ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT is negative', () => {
process.env.ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT = '-100'
expect(() => {
config.getMaxArtifactListCount()
}).toThrow(
'Invalid value set for ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT env variable'
)
})
})
@@ -4,11 +4,11 @@ import type {RequestInterface} from '@octokit/types'
import {
deleteArtifactInternal,
deleteArtifactPublic
} from '../src/internal/delete/delete-artifact'
import * as config from '../src/internal/shared/config'
import {ArtifactServiceClientJSON, Timestamp} from '../src/generated'
import * as util from '../src/internal/shared/util'
import {noopLogs} from './common'
} from '../src/internal/delete/delete-artifact.js'
import * as config from '../src/internal/shared/config.js'
import {ArtifactServiceClientJSON, Timestamp} from '../src/generated/index.js'
import * as util from '../src/internal/shared/util.js'
import {noopLogs} from './common.js'
type MockedRequest = jest.MockedFunction<RequestInterface<object>>
@@ -11,12 +11,12 @@ import {
downloadArtifactInternal,
downloadArtifactPublic,
streamExtractExternal
} from '../src/internal/download/download-artifact'
import {getUserAgentString} from '../src/internal/shared/user-agent'
import {noopLogs} from './common'
import * as config from '../src/internal/shared/config'
import {ArtifactServiceClientJSON} from '../src/generated'
import * as util from '../src/internal/shared/util'
} from '../src/internal/download/download-artifact.js'
import {getUserAgentString} from '../src/internal/shared/user-agent.js'
import {noopLogs} from './common.js'
import * as config from '../src/internal/shared/config.js'
import {ArtifactServiceClientJSON} from '../src/generated/index.js'
import * as util from '../src/internal/shared/util.js'
type MockedDownloadArtifact = jest.MockedFunction<
RestEndpointMethods['actions']['downloadArtifact']
@@ -104,6 +104,7 @@ const cleanup = async (): Promise<void> => {
const mockGetArtifactSuccess = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200
message.headers['content-type'] = 'application/zip'
message.push(fs.readFileSync(fixtures.exampleArtifact.path))
message.push(null)
return {
@@ -111,6 +112,17 @@ const mockGetArtifactSuccess = jest.fn(() => {
}
})
const mockGetArtifactHung = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200
message.headers['content-type'] = 'application/zip'
// Don't push any data or call push(null) to end the stream
// This creates a stream that hangs and never completes
return {
message
}
})
const mockGetArtifactFailure = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 500
@@ -124,6 +136,7 @@ const mockGetArtifactFailure = jest.fn(() => {
const mockGetArtifactMalicious = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200
message.headers['content-type'] = 'application/zip'
message.push(fs.readFileSync(path.join(__dirname, 'fixtures', 'evil.zip'))) // evil.zip contains files that are formatted x/../../etc/hosts
message.push(null)
return {
@@ -609,6 +622,480 @@ describe('download-artifact', () => {
...fixtures.backendIds,
name: fixtures.artifactName
})
}, 38000)
})
describe('streamExtractExternal', () => {
beforeEach(async () => {
await setup()
// Create workspace directory for streamExtractExternal tests
await fs.promises.mkdir(fixtures.workspaceDir, {recursive: true})
})
afterEach(cleanup)
it('should fail if the timeout is exceeded', async () => {
const mockSlowGetArtifact = jest.fn(mockGetArtifactHung)
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockSlowGetArtifact
}
}
)
try {
await streamExtractExternal(
fixtures.blobStorageUrl,
fixtures.workspaceDir,
{timeout: 2}
)
expect(true).toBe(false) // should not be called
} catch (error: unknown) {
const e = error as Error
expect(e).toBeInstanceOf(Error)
expect(e.message).toContain('did not respond in 2ms')
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
expect(mockSlowGetArtifact).toHaveBeenCalledTimes(1)
}
})
it('should extract zip file when content-type is application/zip', async () => {
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetArtifactSuccess
}
}
)
await streamExtractExternal(
fixtures.blobStorageUrl,
fixtures.workspaceDir
)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
// Verify files were extracted (not saved as a single file)
await expectExtractedArchive(fixtures.workspaceDir)
})
it('should save raw file without extracting when content-type is not a zip', async () => {
const rawFileContent = 'This is a raw text file, not a zip'
const rawFileName = 'my-artifact.txt'
const mockGetRawFile = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200
message.headers['content-type'] = 'text/plain'
message.headers['content-disposition'] =
`attachment; filename="${rawFileName}"`
message.push(Buffer.from(rawFileContent))
message.push(null)
return {
message
}
})
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetRawFile
}
}
)
await streamExtractExternal(
fixtures.blobStorageUrl,
fixtures.workspaceDir
)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
// Verify file was saved as-is, not extracted
const savedFilePath = path.join(fixtures.workspaceDir, rawFileName)
expect(fs.existsSync(savedFilePath)).toBe(true)
expect(fs.readFileSync(savedFilePath, 'utf8')).toBe(rawFileContent)
})
it('should save raw file with default name when content-disposition is missing', async () => {
const rawFileContent = 'Binary content here'
const mockGetRawFileNoDisposition = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200
message.headers['content-type'] = 'application/octet-stream'
// No content-disposition header
message.push(Buffer.from(rawFileContent))
message.push(null)
return {
message
}
})
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetRawFileNoDisposition
}
}
)
await streamExtractExternal(
fixtures.blobStorageUrl,
fixtures.workspaceDir
)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
// Verify file was saved with default name 'artifact'
const savedFilePath = path.join(fixtures.workspaceDir, 'artifact')
expect(fs.existsSync(savedFilePath)).toBe(true)
expect(fs.readFileSync(savedFilePath, 'utf8')).toBe(rawFileContent)
})
it('should not attempt to unzip when content-type is image/png', async () => {
const pngFileName = 'screenshot.png'
// Simple PNG header bytes for testing
const pngContent = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
])
const mockGetPngFile = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200
message.headers['content-type'] = 'image/png'
message.headers['content-disposition'] =
`attachment; filename="${pngFileName}"`
message.push(pngContent)
message.push(null)
return {
message
}
})
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetPngFile
}
}
)
await streamExtractExternal(
fixtures.blobStorageUrl,
fixtures.workspaceDir
)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
// Verify PNG was saved as-is
const savedFilePath = path.join(fixtures.workspaceDir, pngFileName)
expect(fs.existsSync(savedFilePath)).toBe(true)
expect(fs.readFileSync(savedFilePath)).toEqual(pngContent)
})
it('should extract when content-type is application/x-zip-compressed', async () => {
const mockGetZipCompressed = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200
message.headers['content-type'] = 'application/x-zip-compressed'
message.push(fs.readFileSync(fixtures.exampleArtifact.path))
message.push(null)
return {
message
}
})
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetZipCompressed
}
}
)
await streamExtractExternal(
fixtures.blobStorageUrl,
fixtures.workspaceDir
)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
// Verify files were extracted
await expectExtractedArchive(fixtures.workspaceDir)
})
it('should extract zip when URL ends with .zip even if content-type is not application/zip', async () => {
const blobUrlWithZipExtension =
'https://blob-storage.local/artifact.zip?sig=abc123'
const mockGetZipByUrl = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200
// Azure Blob Storage may return a generic content-type
message.headers['content-type'] = 'application/octet-stream'
message.push(fs.readFileSync(fixtures.exampleArtifact.path))
message.push(null)
return {
message
}
})
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetZipByUrl
}
}
)
await streamExtractExternal(
blobUrlWithZipExtension,
fixtures.workspaceDir
)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
// Verify files were extracted based on URL .zip extension
await expectExtractedArchive(fixtures.workspaceDir)
})
it('should skip decompression when skipDecompress option is true even for zip content-type', async () => {
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetArtifactSuccess
}
}
)
await streamExtractExternal(
fixtures.blobStorageUrl,
fixtures.workspaceDir,
{skipDecompress: true}
)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
// Verify zip was saved as-is, not extracted
// When skipDecompress is true, the file should be saved with default name 'artifact'
const savedFilePath = path.join(fixtures.workspaceDir, 'artifact')
expect(fs.existsSync(savedFilePath)).toBe(true)
// The saved file should be the raw zip content
const savedContent = fs.readFileSync(savedFilePath)
const originalZipContent = fs.readFileSync(fixtures.exampleArtifact.path)
expect(savedContent).toEqual(originalZipContent)
})
it('should sanitize path traversal attempts in Content-Disposition filename', async () => {
const rawFileContent = 'malicious content'
const maliciousFileName = '../../../etc/passwd'
const mockGetMaliciousFile = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200
message.headers['content-type'] = 'text/plain'
message.headers['content-disposition'] =
`attachment; filename="${maliciousFileName}"`
message.push(Buffer.from(rawFileContent))
message.push(null)
return {
message
}
})
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetMaliciousFile
}
}
)
await streamExtractExternal(
fixtures.blobStorageUrl,
fixtures.workspaceDir
)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
// Verify file was saved with sanitized name (just 'passwd', not the full path)
const sanitizedFileName = 'passwd'
const savedFilePath = path.join(fixtures.workspaceDir, sanitizedFileName)
expect(fs.existsSync(savedFilePath)).toBe(true)
expect(fs.readFileSync(savedFilePath, 'utf8')).toBe(rawFileContent)
// Verify the file was NOT written outside the workspace directory
const maliciousPath = path.resolve(
fixtures.workspaceDir,
maliciousFileName
)
expect(fs.existsSync(maliciousPath)).toBe(false)
})
it('should handle encoded path traversal attempts in Content-Disposition filename', async () => {
const rawFileContent = 'encoded malicious content'
// URL encoded version of ../../../etc/passwd
const encodedMaliciousFileName = '..%2F..%2F..%2Fetc%2Fpasswd'
const mockGetEncodedMaliciousFile = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200
message.headers['content-type'] = 'application/octet-stream'
message.headers['content-disposition'] =
`attachment; filename="${encodedMaliciousFileName}"`
message.push(Buffer.from(rawFileContent))
message.push(null)
return {
message
}
})
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetEncodedMaliciousFile
}
}
)
await streamExtractExternal(
fixtures.blobStorageUrl,
fixtures.workspaceDir
)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
// After decoding and sanitizing, should just be 'passwd'
const sanitizedFileName = 'passwd'
const savedFilePath = path.join(fixtures.workspaceDir, sanitizedFileName)
expect(fs.existsSync(savedFilePath)).toBe(true)
expect(fs.readFileSync(savedFilePath, 'utf8')).toBe(rawFileContent)
// Verify the file was NOT written outside the workspace directory
const maliciousPathEncoded = path.resolve(
fixtures.workspaceDir,
encodedMaliciousFileName
)
expect(fs.existsSync(maliciousPathEncoded)).toBe(false)
const maliciousPath = path.resolve(
fixtures.workspaceDir,
'../../../etc/passwd'
)
expect(fs.existsSync(maliciousPath)).toBe(false)
})
it('should correctly handle Content-Disposition with filename* parameter (RFC 5987)', async () => {
const rawFileContent = 'content with rfc5987 encoding'
const expectedFileName = '报告-土-x.txt'
const asciiFileName = '__-_-x.txt'
const mockGetRfc5987File = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200
message.headers['content-type'] = 'text/plain'
// Server sends both: filename with _ fallbacks, filename* with UTF-8 encoding
message.headers['content-disposition'] =
`attachment; filename="${asciiFileName}"; filename*=UTF-8''${encodeURIComponent(expectedFileName)}`
message.push(Buffer.from(rawFileContent, 'utf8'))
message.push(null)
return {
message
}
})
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetRfc5987File
}
}
)
await streamExtractExternal(
fixtures.blobStorageUrl,
fixtures.workspaceDir
)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
const savedFilePath = path.join(fixtures.workspaceDir, expectedFileName)
expect(fs.existsSync(savedFilePath)).toBe(true)
expect(fs.readFileSync(savedFilePath, 'utf8')).toBe(rawFileContent)
})
it('should handle zip artifacts with Chinese characters in the artifact name', async () => {
// Simulate Azure Blob Storage URL with rscd containing Chinese filename
const chineseArtifactName = 'probe-土-x'
const asciiArtifactName = 'probe-_-x'
const blobUrlWithChineseName = `https://blob-storage.local/artifact.zip?rscd=${encodeURIComponent(`attachment; filename="${asciiArtifactName}.zip"; filename*=UTF-8''${encodeURIComponent(`${chineseArtifactName}.zip`)}`)}&rsct=application%2Fzip&sig=abc123`
const mockGetZip = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200
message.headers['content-type'] = 'application/zip'
message.headers['content-disposition'] =
`attachment; filename="${asciiArtifactName}.zip"; filename*=UTF-8''${encodeURIComponent(`${chineseArtifactName}.zip`)}`
message.push(fs.readFileSync(fixtures.exampleArtifact.path))
message.push(null)
return {
message
}
})
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetZip
}
}
)
await streamExtractExternal(blobUrlWithChineseName, fixtures.workspaceDir)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
// Zip should be extracted normally regardless of Chinese artifact name
await expectExtractedArchive(fixtures.workspaceDir)
})
it.each([
['土', '_'], // U+571F - known to cause 400 errors
['日', '_'], // U+65E5 - reported to work fine
['中文测试', '____'], // multiple Chinese characters
['文件-2026年', '__-2026_'], // mixed Chinese and numbers
['データ', '___'], // Japanese katakana
['테스트', '___'] // Korean characters
])(
'should prefer filename* over filename for non-ASCII character %s (%s)',
async (chars, asciiReplacement) => {
const rawFileContent = `content for ${chars}`
const expectedFileName = `artifact-${chars}.txt`
const asciiFileName = `artifact-${asciiReplacement}.txt`
const mockGetFile = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200
message.headers['content-type'] = 'text/plain'
// Server sends filename with _ replacing non-ASCII, filename* with proper encoding
message.headers['content-disposition'] =
`attachment; filename="${asciiFileName}"; filename*=UTF-8''${encodeURIComponent(expectedFileName)}`
message.push(Buffer.from(rawFileContent, 'utf8'))
message.push(null)
return {
message
}
})
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetFile
}
}
)
await streamExtractExternal(
fixtures.blobStorageUrl,
fixtures.workspaceDir
)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
const savedFilePath = path.join(fixtures.workspaceDir, expectedFileName)
expect(fs.existsSync(savedFilePath)).toBe(true)
expect(fs.readFileSync(savedFilePath, 'utf8')).toBe(rawFileContent)
}
)
})
})
@@ -3,15 +3,15 @@ import type {RequestInterface} from '@octokit/types'
import {
getArtifactInternal,
getArtifactPublic
} from '../src/internal/find/get-artifact'
import * as config from '../src/internal/shared/config'
import {ArtifactServiceClientJSON, Timestamp} from '../src/generated'
import * as util from '../src/internal/shared/util'
import {noopLogs} from './common'
} from '../src/internal/find/get-artifact.js'
import * as config from '../src/internal/shared/config.js'
import {ArtifactServiceClientJSON, Timestamp} from '../src/generated/index.js'
import * as util from '../src/internal/shared/util.js'
import {noopLogs} from './common.js'
import {
ArtifactNotFoundError,
InvalidResponseError
} from '../src/internal/shared/errors'
} from '../src/internal/shared/errors.js'
type MockedRequest = jest.MockedFunction<RequestInterface<object>>
@@ -3,12 +3,12 @@ import type {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-method
import {
listArtifactsInternal,
listArtifactsPublic
} from '../src/internal/find/list-artifacts'
import * as config from '../src/internal/shared/config'
import {ArtifactServiceClientJSON, Timestamp} from '../src/generated'
import * as util from '../src/internal/shared/util'
import {noopLogs} from './common'
import {Artifact} from '../src/internal/shared/interfaces'
} from '../src/internal/find/list-artifacts.js'
import * as config from '../src/internal/shared/config.js'
import {ArtifactServiceClientJSON, Timestamp} from '../src/generated/index.js'
import * as util from '../src/internal/shared/util.js'
import {noopLogs} from './common.js'
import {Artifact} from '../src/internal/shared/interfaces.js'
import {RequestInterface} from '@octokit/types'
type MockedRequest = jest.MockedFunction<RequestInterface<object>>
@@ -170,6 +170,126 @@ describe('list-artifact', () => {
)
).rejects.toThrow('boom')
})
it('should handle pagination correctly when fetching multiple pages', async () => {
const mockRequest = github.getOctokit(fixtures.token)
.request as MockedRequest
const manyArtifacts = Array.from({length: 150}, (_, i) => ({
id: i + 1,
name: `artifact-${i + 1}`,
size: 100,
createdAt: new Date('2023-12-01')
}))
mockRequest
.mockResolvedValueOnce({
status: 200,
headers: {},
url: '',
data: {
...artifactsToListResponse(manyArtifacts.slice(0, 100)),
total_count: 150
}
})
.mockResolvedValueOnce({
status: 200,
headers: {},
url: '',
data: {
...artifactsToListResponse(manyArtifacts.slice(100, 150)),
total_count: 150
}
})
const response = await listArtifactsPublic(
fixtures.runId,
fixtures.owner,
fixtures.repo,
fixtures.token,
false
)
// Verify that both API calls were made
expect(mockRequest).toHaveBeenCalledTimes(2)
// Should return all 150 artifacts across both pages
expect(response.artifacts).toHaveLength(150)
// Verify we got artifacts from both pages
expect(response.artifacts[0].name).toBe('artifact-1')
expect(response.artifacts[99].name).toBe('artifact-100')
expect(response.artifacts[100].name).toBe('artifact-101')
expect(response.artifacts[149].name).toBe('artifact-150')
})
it('should respect ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT environment variable', async () => {
const originalEnv = process.env.ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT
process.env.ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT = '150'
jest.resetModules()
try {
const {listArtifactsPublic: listArtifactsPublicReloaded} = await import(
'../src/internal/find/list-artifacts'
)
const githubReloaded = await import('@actions/github')
const mockRequest = (githubReloaded.getOctokit as jest.Mock)(
fixtures.token
).request as MockedRequest
const manyArtifacts = Array.from({length: 200}, (_, i) => ({
id: i + 1,
name: `artifact-${i + 1}`,
size: 100,
createdAt: new Date('2023-12-01')
}))
mockRequest
.mockResolvedValueOnce({
status: 200,
headers: {},
url: '',
data: {
...artifactsToListResponse(manyArtifacts.slice(0, 100)),
total_count: 200
}
})
.mockResolvedValueOnce({
status: 200,
headers: {},
url: '',
data: {
...artifactsToListResponse(manyArtifacts.slice(100, 150)),
total_count: 200
}
})
const response = await listArtifactsPublicReloaded(
fixtures.runId,
fixtures.owner,
fixtures.repo,
fixtures.token,
false
)
// Should only return 150 artifacts due to the limit
expect(response.artifacts).toHaveLength(150)
expect(response.artifacts[0].name).toBe('artifact-1')
expect(response.artifacts[149].name).toBe('artifact-150')
} finally {
// Restore original environment variable
if (originalEnv !== undefined) {
process.env.ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT = originalEnv
} else {
delete process.env.ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT
}
// Reset modules again to restore original state
jest.resetModules()
}
})
})
describe('internal', () => {
@@ -1,9 +1,9 @@
import {
validateArtifactName,
validateFilePath
} from '../src/internal/upload/path-and-artifact-name-validation'
} from '../src/internal/upload/path-and-artifact-name-validation.js'
import {noopLogs} from './common'
import {noopLogs} from './common.js'
describe('Path and artifact name validation', () => {
beforeAll(() => {
@@ -1,5 +1,5 @@
import {Timestamp} from '../src/generated'
import * as retention from '../src/internal/upload/retention'
import {Timestamp} from '../src/generated/index.js'
import * as retention from '../src/internal/upload/retention.js'
describe('retention', () => {
beforeEach(() => {
@@ -0,0 +1,59 @@
import * as fs from 'fs'
import * as path from 'path'
import {createRawFileUploadStream} from '../src/internal/upload/stream.js'
import {noopLogs} from './common.js'
const fixtures = {
testDirectory: path.join(__dirname, '_temp', 'stream-test'),
testFile: path.join(__dirname, '_temp', 'stream-test', 'test-file.txt'),
testContent: 'hello stream test'
}
describe('createRawFileUploadStream', () => {
beforeAll(() => {
fs.mkdirSync(fixtures.testDirectory, {recursive: true})
fs.writeFileSync(fixtures.testFile, fixtures.testContent)
})
beforeEach(() => {
noopLogs()
})
afterEach(() => {
jest.restoreAllMocks()
})
it('should stream file contents through the upload stream', async () => {
const uploadStream = await createRawFileUploadStream(fixtures.testFile)
const chunks: Buffer[] = []
const result = await new Promise<string>((resolve, reject) => {
uploadStream.on('data', chunk => chunks.push(Buffer.from(chunk)))
uploadStream.on('end', () =>
resolve(Buffer.concat(chunks).toString('utf-8'))
)
uploadStream.on('error', reject)
})
expect(result).toBe(fixtures.testContent)
})
it('should propagate file read errors through the upload stream', async () => {
// Use a directory path — createReadStream on a directory fails cross-platform
// Mock lstat to return a non-symlink result so we reach createReadStream
const dirPath = fixtures.testDirectory
jest
.spyOn(fs.promises, 'lstat')
.mockResolvedValue({isSymbolicLink: () => false} as fs.Stats)
const uploadStream = await createRawFileUploadStream(dirPath)
await expect(
new Promise((resolve, reject) => {
uploadStream.on('data', resolve)
uploadStream.on('end', resolve)
uploadStream.on('error', reject)
})
).rejects.toThrow('An error has occurred during file read for the artifact')
})
})
@@ -1,12 +1,13 @@
import * as uploadZipSpecification from '../src/internal/upload/upload-zip-specification'
import * as zip from '../src/internal/upload/zip'
import * as util from '../src/internal/shared/util'
import * as config from '../src/internal/shared/config'
import {ArtifactServiceClientJSON} from '../src/generated'
import * as blobUpload from '../src/internal/upload/blob-upload'
import {uploadArtifact} from '../src/internal/upload/upload-artifact'
import {noopLogs} from './common'
import {FilesNotFoundError} from '../src/internal/shared/errors'
import * as uploadZipSpecification from '../src/internal/upload/upload-zip-specification.js'
import * as zip from '../src/internal/upload/zip.js'
import * as util from '../src/internal/shared/util.js'
import * as config from '../src/internal/shared/config.js'
import {ArtifactServiceClientJSON} from '../src/generated/index.js'
import * as blobUpload from '../src/internal/upload/blob-upload.js'
import {uploadArtifact} from '../src/internal/upload/upload-artifact.js'
import {noopLogs} from './common.js'
import {FilesNotFoundError} from '../src/internal/shared/errors.js'
import * as stream from '../src/internal/upload/stream.js'
import {BlockBlobUploadStreamOptions} from '@azure/storage-blob'
import * as fs from 'fs'
import * as path from 'path'
@@ -108,7 +109,7 @@ describe('upload-artifact', () => {
fixtures.files.map(file => ({
sourcePath: path.join(fixtures.uploadDirectory, file.name),
destinationPath: file.name,
stats: new fs.Stats()
stats: fs.statSync(path.join(fixtures.uploadDirectory, file.name))
}))
)
jest.spyOn(config, 'getRuntimeToken').mockReturnValue(fixtures.runtimeToken)
@@ -150,7 +151,7 @@ describe('upload-artifact', () => {
it('should return false if the creation request fails', async () => {
jest
.spyOn(zip, 'createZipUploadStream')
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
.mockReturnValue(Promise.resolve(new stream.WaterMarkedUploadStream(1)))
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(Promise.resolve({ok: false, signedUploadUrl: ''}))
@@ -167,7 +168,7 @@ describe('upload-artifact', () => {
it('should return false if blob storage upload is unsuccessful', async () => {
jest
.spyOn(zip, 'createZipUploadStream')
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
.mockReturnValue(Promise.resolve(new stream.WaterMarkedUploadStream(1)))
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(
@@ -177,7 +178,7 @@ describe('upload-artifact', () => {
})
)
jest
.spyOn(blobUpload, 'uploadZipToBlobStorage')
.spyOn(blobUpload, 'uploadToBlobStorage')
.mockReturnValue(Promise.reject(new Error('boom')))
const uploadResp = uploadArtifact(
@@ -192,7 +193,7 @@ describe('upload-artifact', () => {
it('should reject if finalize artifact fails', async () => {
jest
.spyOn(zip, 'createZipUploadStream')
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
.mockReturnValue(Promise.resolve(new stream.WaterMarkedUploadStream(1)))
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(
@@ -201,7 +202,7 @@ describe('upload-artifact', () => {
signedUploadUrl: 'https://signed-upload-url.com'
})
)
jest.spyOn(blobUpload, 'uploadZipToBlobStorage').mockReturnValue(
jest.spyOn(blobUpload, 'uploadToBlobStorage').mockReturnValue(
Promise.resolve({
uploadSize: 1234,
sha256Hash: 'test-sha256-hash'
@@ -370,4 +371,284 @@ describe('upload-artifact', () => {
await expect(uploadResp).rejects.toThrow('Upload progress stalled.')
})
describe('skipArchive option', () => {
it('should throw an error if skipArchive is true and files array is empty', async () => {
const uploadResp = uploadArtifact(
fixtures.inputs.artifactName,
[],
fixtures.inputs.rootDirectory,
{skipArchive: true}
)
await expect(uploadResp).rejects.toThrow(FilesNotFoundError)
})
it('should throw an error if skipArchive is true and multiple files are provided', async () => {
const uploadResp = uploadArtifact(
fixtures.inputs.artifactName,
fixtures.inputs.files,
fixtures.inputs.rootDirectory,
{skipArchive: true}
)
await expect(uploadResp).rejects.toThrow(
'skipArchive option is only supported when uploading a single file'
)
})
it('should upload a single file without archiving when skipArchive is true', async () => {
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockRestore()
const singleFile = path.join(fixtures.uploadDirectory, 'file1.txt')
const expectedContent = 'test 1 file content'
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
signedUploadUrl: 'https://signed-upload-url.local'
})
)
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
artifactId: '1'
})
)
let uploadedContent = ''
let loadedBytes = 0
uploadStreamMock.mockImplementation(
async (
stream: NodeJS.ReadableStream,
bufferSize?: number,
maxConcurrency?: number,
options?: BlockBlobUploadStreamOptions
) => {
const {onProgress} = options || {}
onProgress?.({loadedBytes: 0})
return new Promise((resolve, reject) => {
stream.on('data', chunk => {
loadedBytes += chunk.length
uploadedContent += chunk.toString()
onProgress?.({loadedBytes})
})
stream.on('end', () => {
onProgress?.({loadedBytes})
resolve({})
})
stream.on('error', err => {
reject(err)
})
})
}
)
const {id, size, digest} = await uploadArtifact(
fixtures.inputs.artifactName,
[singleFile],
fixtures.uploadDirectory,
{skipArchive: true}
)
expect(id).toBe(1)
expect(size).toBe(loadedBytes)
expect(digest).toBeDefined()
expect(digest).toHaveLength(64)
// Verify the uploaded content is the raw file, not a zip
expect(uploadedContent).toBe(expectedContent)
})
it('should use the correct MIME type when skipArchive is true', async () => {
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockRestore()
const singleFile = path.join(fixtures.uploadDirectory, 'file1.txt')
const createArtifactSpy = jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
signedUploadUrl: 'https://signed-upload-url.local'
})
)
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
artifactId: '1'
})
)
uploadStreamMock.mockImplementation(
async (
stream: NodeJS.ReadableStream,
bufferSize?: number,
maxConcurrency?: number,
options?: BlockBlobUploadStreamOptions
) => {
const {onProgress} = options || {}
onProgress?.({loadedBytes: 0})
return new Promise((resolve, reject) => {
stream.on('data', chunk => {
onProgress?.({loadedBytes: chunk.length})
})
stream.on('end', () => {
resolve({})
})
stream.on('error', err => {
reject(err)
})
})
}
)
await uploadArtifact(
fixtures.inputs.artifactName,
[singleFile],
fixtures.uploadDirectory,
{skipArchive: true}
)
// Verify CreateArtifact was called with the correct MIME type for .txt file
expect(createArtifactSpy).toHaveBeenCalledWith(
expect.objectContaining({
mimeType: expect.objectContaining({value: 'text/plain'})
})
)
})
it('should use application/zip MIME type when skipArchive is false', async () => {
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockRestore()
const createArtifactSpy = jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
signedUploadUrl: 'https://signed-upload-url.local'
})
)
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
artifactId: '1'
})
)
uploadStreamMock.mockImplementation(
async (
stream: NodeJS.ReadableStream,
bufferSize?: number,
maxConcurrency?: number,
options?: BlockBlobUploadStreamOptions
) => {
const {onProgress} = options || {}
onProgress?.({loadedBytes: 0})
return new Promise((resolve, reject) => {
stream.on('data', chunk => {
onProgress?.({loadedBytes: chunk.length})
})
stream.on('end', () => {
resolve({})
})
stream.on('error', err => {
reject(err)
})
})
}
)
await uploadArtifact(
fixtures.inputs.artifactName,
fixtures.files.map(file =>
path.join(fixtures.uploadDirectory, file.name)
),
fixtures.uploadDirectory
)
// Verify CreateArtifact was called with application/zip MIME type
expect(createArtifactSpy).toHaveBeenCalledWith(
expect.objectContaining({
mimeType: expect.objectContaining({value: 'application/zip'})
})
)
})
it('should use the file basename as artifact name when skipArchive is true', async () => {
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockRestore()
const singleFile = path.join(fixtures.uploadDirectory, 'file1.txt')
const createArtifactSpy = jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
signedUploadUrl: 'https://signed-upload-url.local'
})
)
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
artifactId: '1'
})
)
uploadStreamMock.mockImplementation(
async (
stream: NodeJS.ReadableStream,
bufferSize?: number,
maxConcurrency?: number,
options?: BlockBlobUploadStreamOptions
) => {
const {onProgress} = options || {}
onProgress?.({loadedBytes: 0})
return new Promise((resolve, reject) => {
stream.on('data', chunk => {
onProgress?.({loadedBytes: chunk.length})
})
stream.on('end', () => {
resolve({})
})
stream.on('error', err => {
reject(err)
})
})
}
)
await uploadArtifact(
'original-name',
[singleFile],
fixtures.uploadDirectory,
{skipArchive: true}
)
// Verify CreateArtifact was called with the file basename, not the original name
expect(createArtifactSpy).toHaveBeenCalledWith(
expect.objectContaining({
name: 'file1.txt'
})
)
})
})
})
@@ -4,8 +4,8 @@ import {promises as fs} from 'fs'
import {
getUploadZipSpecification,
validateRootDirectory
} from '../src/internal/upload/upload-zip-specification'
import {noopLogs} from './common'
} from '../src/internal/upload/upload-zip-specification.js'
import {noopLogs} from './common.js'
const root = path.join(__dirname, '_temp', 'upload-specification')
const goodItem1Path = path.join(
+3 -3
View File
@@ -1,6 +1,6 @@
import * as config from '../src/internal/shared/config'
import * as util from '../src/internal/shared/util'
import {maskSigUrl, maskSecretUrls} from '../src/internal/shared/util'
import * as config from '../src/internal/shared/config.js'
import * as util from '../src/internal/shared/util.js'
import {maskSigUrl, maskSecretUrls} from '../src/internal/shared/util.js'
import {setSecret, debug} from '@actions/core'
export const testRuntimeToken =
+917 -789
View File
File diff suppressed because it is too large Load Diff
+28 -16
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/artifact",
"version": "2.3.3",
"version": "6.2.1",
"preview": true,
"description": "Actions artifact lib",
"keywords": [
@@ -10,8 +10,15 @@
],
"homepage": "https://github.com/actions/toolkit/tree/main/packages/artifact",
"license": "MIT",
"type": "module",
"main": "lib/artifact.js",
"types": "lib/artifact.d.ts",
"exports": {
".": {
"types": "./lib/artifact.d.ts",
"import": "./lib/artifact.js"
}
},
"directories": {
"lib": "lib",
"test": "__tests__"
@@ -32,7 +39,7 @@
"audit-moderate": "npm install && npm audit --json --audit-level=moderate > audit.json",
"test": "cd ../../ && npm run test ./packages/artifact",
"bootstrap": "cd ../../ && npm run bootstrap",
"tsc-run": "tsc",
"tsc-run": "tsc && cp src/internal/shared/package-version.cjs lib/internal/shared/",
"tsc": "npm run bootstrap && npm run tsc-run",
"gen:docs": "typedoc --plugin typedoc-plugin-markdown --out docs/generated src/artifact.ts --githubPages false --readme none"
},
@@ -40,25 +47,30 @@
"url": "https://github.com/actions/toolkit/issues"
},
"dependencies": {
"@actions/core": "^1.10.0",
"@actions/github": "^6.0.1",
"@actions/http-client": "^2.1.0",
"@azure/storage-blob": "^12.15.0",
"@octokit/core": "^5.2.1",
"@octokit/plugin-request-log": "^1.0.4",
"@octokit/plugin-retry": "^3.0.9",
"@octokit/request": "^8.4.1",
"@octokit/request-error": "^5.1.1",
"@actions/core": "^3.0.0",
"@actions/github": "^9.0.0",
"@actions/http-client": "^4.0.0",
"@azure/storage-blob": "^12.30.0",
"@octokit/core": "^7.0.6",
"@octokit/plugin-request-log": "^6.0.0",
"@octokit/plugin-retry": "^8.0.0",
"@octokit/request": "^10.0.7",
"@octokit/request-error": "^7.1.0",
"@protobuf-ts/plugin": "^2.2.3-alpha.1",
"@protobuf-ts/runtime": "^2.9.4",
"archiver": "^7.0.1",
"jwt-decode": "^3.1.2",
"jwt-decode": "^4.0.0",
"unzip-stream": "^0.3.1"
},
"devDependencies": {
"@types/archiver": "^5.3.2",
"@types/archiver": "^7.0.0",
"@types/unzip-stream": "^0.3.4",
"typedoc": "^0.25.4",
"typedoc-plugin-markdown": "^3.17.1",
"typescript": "^5.2.2"
"typedoc": "^0.28.16",
"typedoc-plugin-markdown": "^4.9.0",
"typescript": "^5.9.3"
},
"overrides": {
"uri-js": "npm:uri-js-replace@^1.0.1",
"node-fetch": "^3.3.2"
}
}
+4 -4
View File
@@ -1,8 +1,8 @@
import {ArtifactClient, DefaultArtifactClient} from './internal/client'
import {ArtifactClient, DefaultArtifactClient} from './internal/client.js'
export * from './internal/shared/interfaces'
export * from './internal/shared/errors'
export * from './internal/client'
export * from './internal/shared/interfaces.js'
export * from './internal/shared/errors.js'
export * from './internal/client.js'
const client: ArtifactClient = new DefaultArtifactClient()
export default client
+4 -4
View File
@@ -1,4 +1,4 @@
export * from './google/protobuf/timestamp'
export * from './google/protobuf/wrappers'
export * from './results/api/v1/artifact'
export * from './results/api/v1/artifact.twirp-client'
export * from './google/protobuf/timestamp.js'
export * from './google/protobuf/wrappers.js'
export * from './results/api/v1/artifact.js'
export * from './results/api/v1/artifact.twirp-client.js'
@@ -12,69 +12,9 @@ import type { PartialMessage } from "@protobuf-ts/runtime";
import { reflectionMergePartial } from "@protobuf-ts/runtime";
import { MESSAGE_TYPE } from "@protobuf-ts/runtime";
import { MessageType } from "@protobuf-ts/runtime";
import { Int64Value } from "../../../google/protobuf/wrappers";
import { StringValue } from "../../../google/protobuf/wrappers";
import { Timestamp } from "../../../google/protobuf/timestamp";
/**
* @generated from protobuf message github.actions.results.api.v1.MigrateArtifactRequest
*/
export interface MigrateArtifactRequest {
/**
* @generated from protobuf field: string workflow_run_backend_id = 1;
*/
workflowRunBackendId: string;
/**
* @generated from protobuf field: string name = 2;
*/
name: string;
/**
* @generated from protobuf field: google.protobuf.Timestamp expires_at = 3;
*/
expiresAt?: Timestamp;
}
/**
* @generated from protobuf message github.actions.results.api.v1.MigrateArtifactResponse
*/
export interface MigrateArtifactResponse {
/**
* @generated from protobuf field: bool ok = 1;
*/
ok: boolean;
/**
* @generated from protobuf field: string signed_upload_url = 2;
*/
signedUploadUrl: string;
}
/**
* @generated from protobuf message github.actions.results.api.v1.FinalizeMigratedArtifactRequest
*/
export interface FinalizeMigratedArtifactRequest {
/**
* @generated from protobuf field: string workflow_run_backend_id = 1;
*/
workflowRunBackendId: string;
/**
* @generated from protobuf field: string name = 2;
*/
name: string;
/**
* @generated from protobuf field: int64 size = 3;
*/
size: string;
}
/**
* @generated from protobuf message github.actions.results.api.v1.FinalizeMigratedArtifactResponse
*/
export interface FinalizeMigratedArtifactResponse {
/**
* @generated from protobuf field: bool ok = 1;
*/
ok: boolean;
/**
* @generated from protobuf field: int64 artifact_id = 2;
*/
artifactId: string;
}
import { Int64Value } from "../../../google/protobuf/wrappers.js";
import { StringValue } from "../../../google/protobuf/wrappers.js";
import { Timestamp } from "../../../google/protobuf/timestamp.js";
/**
* @generated from protobuf message github.actions.results.api.v1.CreateArtifactRequest
*/
@@ -99,6 +39,10 @@ export interface CreateArtifactRequest {
* @generated from protobuf field: int32 version = 5;
*/
version: number;
/**
* @generated from protobuf field: google.protobuf.StringValue mime_type = 6;
*/
mimeType?: StringValue; // optional
}
/**
* @generated from protobuf message github.actions.results.api.v1.CreateArtifactResponse
@@ -293,236 +237,6 @@ export interface DeleteArtifactResponse {
artifactId: string;
}
// @generated message type with reflection information, may provide speed optimized methods
class MigrateArtifactRequest$Type extends MessageType<MigrateArtifactRequest> {
constructor() {
super("github.actions.results.api.v1.MigrateArtifactRequest", [
{ no: 1, name: "workflow_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "expires_at", kind: "message", T: () => Timestamp }
]);
}
create(value?: PartialMessage<MigrateArtifactRequest>): MigrateArtifactRequest {
const message = { workflowRunBackendId: "", name: "" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<MigrateArtifactRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: MigrateArtifactRequest): MigrateArtifactRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string workflow_run_backend_id */ 1:
message.workflowRunBackendId = reader.string();
break;
case /* string name */ 2:
message.name = reader.string();
break;
case /* google.protobuf.Timestamp expires_at */ 3:
message.expiresAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.expiresAt);
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: MigrateArtifactRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string workflow_run_backend_id = 1; */
if (message.workflowRunBackendId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.workflowRunBackendId);
/* string name = 2; */
if (message.name !== "")
writer.tag(2, WireType.LengthDelimited).string(message.name);
/* google.protobuf.Timestamp expires_at = 3; */
if (message.expiresAt)
Timestamp.internalBinaryWrite(message.expiresAt, writer.tag(3, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message github.actions.results.api.v1.MigrateArtifactRequest
*/
export const MigrateArtifactRequest = new MigrateArtifactRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class MigrateArtifactResponse$Type extends MessageType<MigrateArtifactResponse> {
constructor() {
super("github.actions.results.api.v1.MigrateArtifactResponse", [
{ no: 1, name: "ok", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 2, name: "signed_upload_url", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<MigrateArtifactResponse>): MigrateArtifactResponse {
const message = { ok: false, signedUploadUrl: "" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<MigrateArtifactResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: MigrateArtifactResponse): MigrateArtifactResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* bool ok */ 1:
message.ok = reader.bool();
break;
case /* string signed_upload_url */ 2:
message.signedUploadUrl = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: MigrateArtifactResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bool ok = 1; */
if (message.ok !== false)
writer.tag(1, WireType.Varint).bool(message.ok);
/* string signed_upload_url = 2; */
if (message.signedUploadUrl !== "")
writer.tag(2, WireType.LengthDelimited).string(message.signedUploadUrl);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message github.actions.results.api.v1.MigrateArtifactResponse
*/
export const MigrateArtifactResponse = new MigrateArtifactResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class FinalizeMigratedArtifactRequest$Type extends MessageType<FinalizeMigratedArtifactRequest> {
constructor() {
super("github.actions.results.api.v1.FinalizeMigratedArtifactRequest", [
{ no: 1, name: "workflow_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "size", kind: "scalar", T: 3 /*ScalarType.INT64*/ }
]);
}
create(value?: PartialMessage<FinalizeMigratedArtifactRequest>): FinalizeMigratedArtifactRequest {
const message = { workflowRunBackendId: "", name: "", size: "0" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<FinalizeMigratedArtifactRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FinalizeMigratedArtifactRequest): FinalizeMigratedArtifactRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string workflow_run_backend_id */ 1:
message.workflowRunBackendId = reader.string();
break;
case /* string name */ 2:
message.name = reader.string();
break;
case /* int64 size */ 3:
message.size = reader.int64().toString();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: FinalizeMigratedArtifactRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string workflow_run_backend_id = 1; */
if (message.workflowRunBackendId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.workflowRunBackendId);
/* string name = 2; */
if (message.name !== "")
writer.tag(2, WireType.LengthDelimited).string(message.name);
/* int64 size = 3; */
if (message.size !== "0")
writer.tag(3, WireType.Varint).int64(message.size);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message github.actions.results.api.v1.FinalizeMigratedArtifactRequest
*/
export const FinalizeMigratedArtifactRequest = new FinalizeMigratedArtifactRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class FinalizeMigratedArtifactResponse$Type extends MessageType<FinalizeMigratedArtifactResponse> {
constructor() {
super("github.actions.results.api.v1.FinalizeMigratedArtifactResponse", [
{ no: 1, name: "ok", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 2, name: "artifact_id", kind: "scalar", T: 3 /*ScalarType.INT64*/ }
]);
}
create(value?: PartialMessage<FinalizeMigratedArtifactResponse>): FinalizeMigratedArtifactResponse {
const message = { ok: false, artifactId: "0" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<FinalizeMigratedArtifactResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FinalizeMigratedArtifactResponse): FinalizeMigratedArtifactResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* bool ok */ 1:
message.ok = reader.bool();
break;
case /* int64 artifact_id */ 2:
message.artifactId = reader.int64().toString();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: FinalizeMigratedArtifactResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bool ok = 1; */
if (message.ok !== false)
writer.tag(1, WireType.Varint).bool(message.ok);
/* int64 artifact_id = 2; */
if (message.artifactId !== "0")
writer.tag(2, WireType.Varint).int64(message.artifactId);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message github.actions.results.api.v1.FinalizeMigratedArtifactResponse
*/
export const FinalizeMigratedArtifactResponse = new FinalizeMigratedArtifactResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class CreateArtifactRequest$Type extends MessageType<CreateArtifactRequest> {
constructor() {
super("github.actions.results.api.v1.CreateArtifactRequest", [
@@ -530,7 +244,8 @@ class CreateArtifactRequest$Type extends MessageType<CreateArtifactRequest> {
{ no: 2, name: "workflow_job_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 4, name: "expires_at", kind: "message", T: () => Timestamp },
{ no: 5, name: "version", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
{ no: 5, name: "version", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 6, name: "mime_type", kind: "message", T: () => StringValue }
]);
}
create(value?: PartialMessage<CreateArtifactRequest>): CreateArtifactRequest {
@@ -560,6 +275,9 @@ class CreateArtifactRequest$Type extends MessageType<CreateArtifactRequest> {
case /* int32 version */ 5:
message.version = reader.int32();
break;
case /* google.protobuf.StringValue mime_type */ 6:
message.mimeType = StringValue.internalBinaryRead(reader, reader.uint32(), options, message.mimeType);
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -587,6 +305,9 @@ class CreateArtifactRequest$Type extends MessageType<CreateArtifactRequest> {
/* int32 version = 5; */
if (message.version !== 0)
writer.tag(5, WireType.Varint).int32(message.version);
/* google.protobuf.StringValue mime_type = 6; */
if (message.mimeType)
StringValue.internalBinaryWrite(message.mimeType, writer.tag(6, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -852,7 +573,7 @@ export const ListArtifactsRequest = new ListArtifactsRequest$Type();
class ListArtifactsResponse$Type extends MessageType<ListArtifactsResponse> {
constructor() {
super("github.actions.results.api.v1.ListArtifactsResponse", [
{ no: 1, name: "artifacts", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => ListArtifactsResponse_MonolithArtifact }
{ no: 1, name: "artifacts", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => ListArtifactsResponse_MonolithArtifact }
]);
}
create(value?: PartialMessage<ListArtifactsResponse>): ListArtifactsResponse {
@@ -1215,7 +936,5 @@ export const ArtifactService = new ServiceType("github.actions.results.api.v1.Ar
{ name: "FinalizeArtifact", options: {}, I: FinalizeArtifactRequest, O: FinalizeArtifactResponse },
{ name: "ListArtifacts", options: {}, I: ListArtifactsRequest, O: ListArtifactsResponse },
{ name: "GetSignedArtifactURL", options: {}, I: GetSignedArtifactURLRequest, O: GetSignedArtifactURLResponse },
{ name: "DeleteArtifact", options: {}, I: DeleteArtifactRequest, O: DeleteArtifactResponse },
{ name: "MigrateArtifact", options: {}, I: MigrateArtifactRequest, O: MigrateArtifactResponse },
{ name: "FinalizeMigratedArtifact", options: {}, I: FinalizeMigratedArtifactRequest, O: FinalizeMigratedArtifactResponse }
{ name: "DeleteArtifact", options: {}, I: DeleteArtifactRequest, O: DeleteArtifactResponse }
]);
@@ -9,7 +9,7 @@ import {
GetSignedArtifactURLResponse,
DeleteArtifactRequest,
DeleteArtifactResponse,
} from "./artifact";
} from "./artifact.js";
//==================================//
// Client Code //
@@ -229,4 +229,4 @@ export class ArtifactServiceClientProtobuf implements ArtifactServiceClient {
DeleteArtifactResponse.fromBinary(data as Uint8Array)
);
}
}
}
+11 -8
View File
@@ -1,5 +1,5 @@
import {warning} from '@actions/core'
import {isGhes} from './shared/config'
import {isGhes} from './shared/config.js'
import {
UploadArtifactOptions,
UploadArtifactResponse,
@@ -10,19 +10,22 @@ import {
DownloadArtifactResponse,
FindOptions,
DeleteArtifactResponse
} from './shared/interfaces'
import {uploadArtifact} from './upload/upload-artifact'
} from './shared/interfaces.js'
import {uploadArtifact} from './upload/upload-artifact.js'
import {
downloadArtifactPublic,
downloadArtifactInternal
} from './download/download-artifact'
} from './download/download-artifact.js'
import {
deleteArtifactPublic,
deleteArtifactInternal
} from './delete/delete-artifact'
import {getArtifactPublic, getArtifactInternal} from './find/get-artifact'
import {listArtifactsPublic, listArtifactsInternal} from './find/list-artifacts'
import {GHESNotSupportedError} from './shared/errors'
} from './delete/delete-artifact.js'
import {getArtifactPublic, getArtifactInternal} from './find/get-artifact.js'
import {
listArtifactsPublic,
listArtifactsInternal
} from './find/list-artifacts.js'
import {GHESNotSupportedError} from './shared/errors.js'
/**
* Generic interface for the artifact client.
@@ -1,21 +1,21 @@
import {info, debug} from '@actions/core'
import {getOctokit} from '@actions/github'
import {DeleteArtifactResponse} from '../shared/interfaces'
import {getUserAgentString} from '../shared/user-agent'
import {getRetryOptions} from '../find/retry-options'
import {DeleteArtifactResponse} from '../shared/interfaces.js'
import {getUserAgentString} from '../shared/user-agent.js'
import {getRetryOptions} from '../find/retry-options.js'
import {defaults as defaultGitHubOptions} from '@actions/github/lib/utils'
import {requestLog} from '@octokit/plugin-request-log'
import {retry} from '@octokit/plugin-retry'
import {OctokitOptions} from '@octokit/core/dist-types/types'
import {internalArtifactTwirpClient} from '../shared/artifact-twirp-client'
import {getBackendIdsFromToken} from '../shared/util'
import type {OctokitOptions} from '@octokit/core/types'
import {internalArtifactTwirpClient} from '../shared/artifact-twirp-client.js'
import {getBackendIdsFromToken} from '../shared/util.js'
import {
DeleteArtifactRequest,
ListArtifactsRequest,
StringValue
} from '../../generated'
import {getArtifactPublic} from '../find/get-artifact'
import {ArtifactNotFoundError, InvalidResponseError} from '../shared/errors'
} from '../../generated/index.js'
import {getArtifactPublic} from '../find/get-artifact.js'
import {ArtifactNotFoundError, InvalidResponseError} from '../shared/errors.js'
export async function deleteArtifactPublic(
artifactName: string,
@@ -1,6 +1,8 @@
import fs from 'fs/promises'
import * as fsSync from 'fs'
import * as crypto from 'crypto'
import * as stream from 'stream'
import * as path from 'path'
import * as github from '@actions/github'
import * as core from '@actions/core'
@@ -10,17 +12,17 @@ import {
DownloadArtifactOptions,
DownloadArtifactResponse,
StreamExtractResponse
} from '../shared/interfaces'
import {getUserAgentString} from '../shared/user-agent'
import {getGitHubWorkspaceDir} from '../shared/config'
import {internalArtifactTwirpClient} from '../shared/artifact-twirp-client'
} from '../shared/interfaces.js'
import {getUserAgentString} from '../shared/user-agent.js'
import {getGitHubWorkspaceDir} from '../shared/config.js'
import {internalArtifactTwirpClient} from '../shared/artifact-twirp-client.js'
import {
GetSignedArtifactURLRequest,
Int64Value,
ListArtifactsRequest
} from '../../generated'
import {getBackendIdsFromToken} from '../shared/util'
import {ArtifactNotFoundError} from '../shared/errors'
} from '../../generated/index.js'
import {getBackendIdsFromToken} from '../shared/util.js'
import {ArtifactNotFoundError} from '../shared/errors.js'
const scrubQueryParameters = (url: string): string => {
const parsed = new URL(url)
@@ -43,12 +45,13 @@ async function exists(path: string): Promise<boolean> {
async function streamExtract(
url: string,
directory: string
directory: string,
skipDecompress?: boolean
): Promise<StreamExtractResponse> {
let retryCount = 0
while (retryCount < 5) {
try {
return await streamExtractExternal(url, directory)
return await streamExtractExternal(url, directory, {skipDecompress})
} catch (error) {
retryCount++
core.debug(
@@ -64,8 +67,10 @@ async function streamExtract(
export async function streamExtractExternal(
url: string,
directory: string
directory: string,
opts: {timeout?: number; skipDecompress?: boolean} = {}
): Promise<StreamExtractResponse> {
const {timeout = 30 * 1000, skipDecompress = false} = opts
const client = new httpClient.HttpClient(getUserAgentString())
const response = await client.get(url)
if (response.message.statusCode !== 200) {
@@ -74,48 +79,97 @@ export async function streamExtractExternal(
)
}
const timeout = 30 * 1000 // 30 seconds
const contentType = response.message.headers['content-type'] || ''
const mimeType = contentType.split(';', 1)[0].trim().toLowerCase()
// Check if the URL path ends with .zip (ignoring query parameters)
const urlPath = new URL(url).pathname.toLowerCase()
const urlEndsWithZip = urlPath.endsWith('.zip')
const isZip =
mimeType === 'application/zip' ||
mimeType === 'application/x-zip-compressed' ||
mimeType === 'application/zip-compressed' ||
urlEndsWithZip
// Extract filename from Content-Disposition header
// Prefer filename* (RFC 5987) which supports UTF-8 encoded filenames,
// fall back to filename which may contain ASCII-only replacements
const contentDisposition =
response.message.headers['content-disposition'] || ''
let fileName = 'artifact'
const filenameStar = contentDisposition.match(
/filename\*\s*=\s*UTF-8''([^;\r\n]*)/i
)
const filenamePlain = contentDisposition.match(
/(?<!\*)filename\s*=\s*['"]?([^;\r\n"']*)['"]?/i
)
const rawName = filenameStar?.[1] || filenamePlain?.[1]
if (rawName) {
// Sanitize fileName to prevent path traversal attacks
// Use path.basename to extract only the filename component
fileName = path.basename(decodeURIComponent(rawName.trim()))
}
core.debug(
`Content-Type: ${contentType}, mimeType: ${mimeType}, urlEndsWithZip: ${urlEndsWithZip}, isZip: ${isZip}, skipDecompress: ${skipDecompress}`
)
core.debug(
`Content-Disposition: ${contentDisposition}, fileName: ${fileName}`
)
let sha256Digest: string | undefined = undefined
return new Promise((resolve, reject) => {
const timerFn = (): void => {
response.message.destroy(
new Error(`Blob storage chunk did not respond in ${timeout}ms`)
const timeoutError = new Error(
`Blob storage chunk did not respond in ${timeout}ms`
)
response.message.destroy(timeoutError)
reject(timeoutError)
}
const timer = setTimeout(timerFn, timeout)
const onError = (error: Error): void => {
core.debug(`response.message: Artifact download failed: ${error.message}`)
clearTimeout(timer)
reject(error)
}
const hashStream = crypto.createHash('sha256').setEncoding('hex')
const passThrough = new stream.PassThrough()
response.message.pipe(passThrough)
passThrough.pipe(hashStream)
const extractStream = passThrough
extractStream
.on('data', () => {
timer.refresh()
})
.on('error', (error: Error) => {
core.debug(
`response.message: Artifact download failed: ${error.message}`
)
clearTimeout(timer)
reject(error)
})
.pipe(unzip.Extract({path: directory}))
.on('close', () => {
clearTimeout(timer)
if (hashStream) {
hashStream.end()
sha256Digest = hashStream.read() as string
core.info(`SHA256 digest of downloaded artifact is ${sha256Digest}`)
}
resolve({sha256Digest: `sha256:${sha256Digest}`})
})
.on('error', (error: Error) => {
reject(error)
})
.on('error', onError)
response.message.pipe(passThrough)
passThrough.pipe(hashStream)
const onClose = (): void => {
clearTimeout(timer)
if (hashStream) {
hashStream.end()
sha256Digest = hashStream.read() as string
core.info(`SHA256 digest of downloaded artifact is ${sha256Digest}`)
}
resolve({sha256Digest: `sha256:${sha256Digest}`})
}
if (isZip && !skipDecompress) {
// Extract zip file
passThrough
.pipe(unzip.Extract({path: directory}))
.on('close', onClose)
.on('error', onError)
} else {
// Save raw file without extracting
const filePath = path.join(directory, fileName)
const writeStream = fsSync.createWriteStream(filePath)
core.info(`Downloading raw file (non-zip) to: ${filePath}`)
passThrough.pipe(writeStream).on('close', onClose).on('error', onError)
}
})
}
@@ -161,7 +215,11 @@ export async function downloadArtifactPublic(
try {
core.info(`Starting download of artifact to: ${downloadPath}`)
const extractResponse = await streamExtract(location, downloadPath)
const extractResponse = await streamExtract(
location,
downloadPath,
options?.skipDecompress
)
core.info(`Artifact download completed successfully.`)
if (options?.expectedHash) {
if (options?.expectedHash !== extractResponse.sha256Digest) {
@@ -222,7 +280,11 @@ export async function downloadArtifactInternal(
try {
core.info(`Starting download of artifact to: ${downloadPath}`)
const extractResponse = await streamExtract(signedUrl, downloadPath)
const extractResponse = await streamExtract(
signedUrl,
downloadPath,
options?.skipDecompress
)
core.info(`Artifact download completed successfully.`)
if (options?.expectedHash) {
if (options?.expectedHash !== extractResponse.sha256Digest) {
@@ -1,16 +1,20 @@
import {getOctokit} from '@actions/github'
import {retry} from '@octokit/plugin-retry'
import * as core from '@actions/core'
import {OctokitOptions} from '@octokit/core/dist-types/types'
import type {OctokitOptions} from '@octokit/core/types'
import {defaults as defaultGitHubOptions} from '@actions/github/lib/utils'
import {getRetryOptions} from './retry-options'
import {getRetryOptions} from './retry-options.js'
import {requestLog} from '@octokit/plugin-request-log'
import {GetArtifactResponse} from '../shared/interfaces'
import {getBackendIdsFromToken} from '../shared/util'
import {getUserAgentString} from '../shared/user-agent'
import {internalArtifactTwirpClient} from '../shared/artifact-twirp-client'
import {ListArtifactsRequest, StringValue, Timestamp} from '../../generated'
import {ArtifactNotFoundError, InvalidResponseError} from '../shared/errors'
import {GetArtifactResponse} from '../shared/interfaces.js'
import {getBackendIdsFromToken} from '../shared/util.js'
import {getUserAgentString} from '../shared/user-agent.js'
import {internalArtifactTwirpClient} from '../shared/artifact-twirp-client.js'
import {
ListArtifactsRequest,
StringValue,
Timestamp
} from '../../generated/index.js'
import {ArtifactNotFoundError, InvalidResponseError} from '../shared/errors.js'
export async function getArtifactPublic(
artifactName: string,
@@ -1,20 +1,20 @@
import {info, warning, debug} from '@actions/core'
import {getOctokit} from '@actions/github'
import {ListArtifactsResponse, Artifact} from '../shared/interfaces'
import {getUserAgentString} from '../shared/user-agent'
import {getRetryOptions} from './retry-options'
import {ListArtifactsResponse, Artifact} from '../shared/interfaces.js'
import {getUserAgentString} from '../shared/user-agent.js'
import {getRetryOptions} from './retry-options.js'
import {defaults as defaultGitHubOptions} from '@actions/github/lib/utils'
import {requestLog} from '@octokit/plugin-request-log'
import {retry} from '@octokit/plugin-retry'
import {OctokitOptions} from '@octokit/core/dist-types/types'
import {internalArtifactTwirpClient} from '../shared/artifact-twirp-client'
import {getBackendIdsFromToken} from '../shared/util'
import {ListArtifactsRequest, Timestamp} from '../../generated'
import type {OctokitOptions} from '@octokit/core/types'
import {internalArtifactTwirpClient} from '../shared/artifact-twirp-client.js'
import {getBackendIdsFromToken} from '../shared/util.js'
import {getMaxArtifactListCount} from '../shared/config.js'
import {ListArtifactsRequest, Timestamp} from '../../generated/index.js'
// Limiting to 1000 for perf reasons
const maximumArtifactCount = 1000
const maximumArtifactCount = getMaxArtifactListCount()
const paginationCount = 100
const maxNumberOfPages = maximumArtifactCount / paginationCount
const maxNumberOfPages = Math.ceil(maximumArtifactCount / paginationCount)
export async function listArtifactsPublic(
workflowRunId: number,
@@ -59,7 +59,7 @@ export async function listArtifactsPublic(
const totalArtifactCount = listArtifactResponse.total_count
if (totalArtifactCount > maximumArtifactCount) {
warning(
`Workflow run ${workflowRunId} has more than 1000 artifacts. Results will be incomplete as only the first ${maximumArtifactCount} artifacts will be returned`
`Workflow run ${workflowRunId} has ${totalArtifactCount} artifacts, exceeding the limit of ${maximumArtifactCount}. Results will be incomplete as only the first ${maximumArtifactCount} artifacts will be returned`
)
numberOfPages = maxNumberOfPages
}
@@ -81,7 +81,7 @@ export async function listArtifactsPublic(
// Iterate over any remaining pages
for (
currentPageNumber;
currentPageNumber < numberOfPages;
currentPageNumber <= numberOfPages;
currentPageNumber++
) {
debug(`Fetching page ${currentPageNumber} of artifact list`)
@@ -1,5 +1,5 @@
import * as core from '@actions/core'
import {OctokitOptions} from '@octokit/core/dist-types/types'
import type {OctokitOptions} from '@octokit/core/types'
import {RequestRequestOptions} from '@octokit/types'
export type RetryOptions = {
@@ -1,11 +1,11 @@
import {HttpClient, HttpClientResponse, HttpCodes} from '@actions/http-client'
import {BearerCredentialHandler} from '@actions/http-client/lib/auth'
import {info, debug} from '@actions/core'
import {ArtifactServiceClientJSON} from '../../generated'
import {getResultsServiceUrl, getRuntimeToken} from './config'
import {getUserAgentString} from './user-agent'
import {NetworkError, UsageError} from './errors'
import {maskSecretUrls} from './util'
import {ArtifactServiceClientJSON} from '../../generated/index.js'
import {getResultsServiceUrl, getRuntimeToken} from './config.js'
import {getUserAgentString} from './user-agent.js'
import {NetworkError, UsageError} from './errors.js'
import {maskSecretUrls} from './util.js'
// The twirp http client must implement this interface
interface Rpc {
@@ -97,3 +97,19 @@ export function getUploadChunkTimeout(): number {
return timeout
}
// This value can be changed with ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT variable.
// Defaults to 1000 as a safeguard for rate limiting.
export function getMaxArtifactListCount(): number {
const maxCountVar =
process.env['ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT'] || '1000'
const maxCount = parseInt(maxCountVar)
if (isNaN(maxCount) || maxCount < 1) {
throw new Error(
'Invalid value set for ACTIONS_ARTIFACT_MAX_ARTIFACT_COUNT env variable'
)
}
return maxCount
}
@@ -60,7 +60,7 @@ export class NetworkError extends Error {
export class UsageError extends Error {
constructor() {
const message = `Artifact storage quota has been hit. Unable to upload any new artifacts. Usage is recalculated every 6-12 hours.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`
const message = `Artifact storage quota has been hit. Unable to upload any new artifacts.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`
super(message)
this.name = 'UsageError'
}
@@ -50,6 +50,13 @@ export interface UploadArtifactOptions {
* For large files that are not easily compressed, a value of 0 is recommended for significantly faster uploads.
*/
compressionLevel?: number
/**
* If true, the artifact will be uploaded without being archived (zipped).
* This is only supported when uploading a single file.
* When using this option, the artifact will not be compressed.
* When using this option, the name parameter passed to the upload is ignored. Instead, the name of the file is used as the name of the artifact.
*/
skipArchive?: boolean
}
/**
@@ -113,6 +120,12 @@ export interface DownloadArtifactOptions {
* matches the expected hash.
*/
expectedHash?: string
/**
* If true, the downloaded artifact will not be automatically extracted/decompressed.
* The artifact will be saved as-is to the destination path.
*/
skipDecompress?: boolean
}
export interface StreamExtractResponse {
@@ -0,0 +1,7 @@
// This file exists as a CommonJS module to read the version from package.json.
// In an ESM package, using `require()` directly in .ts files requires disabling
// ESLint rules and doesn't work reliably across all Node.js versions.
// By keeping this as a .cjs file, we can use require() naturally and export
// the version for the ESM modules to import.
const packageJson = require('../../../package.json')
module.exports = { version: packageJson.version }
@@ -1,9 +1,8 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const packageJson = require('../../../package.json')
import {version} from './package-version.cjs'
/**
* Ensure that this User Agent String is used in all HTTP calls so that we can monitor telemetry between different versions of this package
*/
export function getUserAgentString(): string {
return `@actions/artifact-${packageJson.version}`
return `@actions/artifact-${version}`
}
@@ -1,6 +1,6 @@
import * as core from '@actions/core'
import {getRuntimeToken} from './config'
import jwt_decode from 'jwt-decode'
import {getRuntimeToken} from './config.js'
import {jwtDecode} from 'jwt-decode'
import {debug, setSecret} from '@actions/core'
export interface BackendIds {
@@ -20,7 +20,7 @@ const InvalidJwtError = new Error(
// workflow run and workflow job run backend ids
export function getBackendIdsFromToken(): BackendIds {
const token = getRuntimeToken()
const decoded = jwt_decode<ActionsToken>(token)
const decoded = jwtDecode<ActionsToken>(token)
if (!decoded.scp) {
throw InvalidJwtError
}
@@ -1,15 +1,15 @@
import {BlobClient, BlockBlobUploadStreamOptions} from '@azure/storage-blob'
import {TransferProgressEvent} from '@azure/core-http'
import {ZipUploadStream} from './zip'
import {TransferProgressEvent} from '@azure/core-http-compat'
import {WaterMarkedUploadStream} from './stream.js'
import {
getUploadChunkSize,
getConcurrency,
getUploadChunkTimeout
} from '../shared/config'
} from '../shared/config.js'
import * as core from '@actions/core'
import * as crypto from 'crypto'
import * as stream from 'stream'
import {NetworkError} from '../shared/errors'
import {NetworkError} from '../shared/errors.js'
export interface BlobUploadResponse {
/**
@@ -23,9 +23,10 @@ export interface BlobUploadResponse {
sha256Hash?: string
}
export async function uploadZipToBlobStorage(
export async function uploadToBlobStorage(
authenticatedUploadURL: string,
zipUploadStream: ZipUploadStream
uploadStream: WaterMarkedUploadStream,
contentType: string
): Promise<BlobUploadResponse> {
let uploadByteCount = 0
let lastProgressTime = Date.now()
@@ -51,7 +52,7 @@ export async function uploadZipToBlobStorage(
const blockBlobClient = blobClient.getBlockBlobClient()
core.debug(
`Uploading artifact zip to blob storage with maxConcurrency: ${maxConcurrency}, bufferSize: ${bufferSize}`
`Uploading artifact to blob storage with maxConcurrency: ${maxConcurrency}, bufferSize: ${bufferSize}, contentType: ${contentType}`
)
const uploadCallback = (progress: TransferProgressEvent): void => {
@@ -61,24 +62,24 @@ export async function uploadZipToBlobStorage(
}
const options: BlockBlobUploadStreamOptions = {
blobHTTPHeaders: {blobContentType: 'zip'},
blobHTTPHeaders: {blobContentType: contentType},
onProgress: uploadCallback,
abortSignal: abortController.signal
}
let sha256Hash: string | undefined = undefined
const uploadStream = new stream.PassThrough()
const blobUploadStream = new stream.PassThrough()
const hashStream = crypto.createHash('sha256')
zipUploadStream.pipe(uploadStream) // This stream is used for the upload
zipUploadStream.pipe(hashStream).setEncoding('hex') // This stream is used to compute a hash of the zip content that gets used. Integrity check
uploadStream.pipe(blobUploadStream) // This stream is used for the upload
uploadStream.pipe(hashStream).setEncoding('hex') // This stream is used to compute a hash of the content for integrity check
core.info('Beginning upload of artifact content to blob storage')
try {
await Promise.race([
blockBlobClient.uploadStream(
uploadStream,
blobUploadStream,
bufferSize,
maxConcurrency,
options
@@ -98,7 +99,7 @@ export async function uploadZipToBlobStorage(
hashStream.end()
sha256Hash = hashStream.read() as string
core.info(`SHA256 digest of uploaded artifact zip is ${sha256Hash}`)
core.info(`SHA256 digest of uploaded artifact is ${sha256Hash}`)
if (uploadByteCount === 0) {
core.warning(
@@ -1,4 +1,4 @@
import {Timestamp} from '../../generated'
import {Timestamp} from '../../generated/index.js'
import * as core from '@actions/core'
export function getExpiration(retentionDays?: number): Timestamp | undefined {
@@ -0,0 +1,53 @@
import * as stream from 'stream'
import * as fs from 'fs'
import {realpath} from 'fs/promises'
import * as core from '@actions/core'
import {getUploadChunkSize} from '../shared/config.js'
// Custom stream transformer so we can set the highWaterMark property
// See https://github.com/nodejs/node/issues/8855
export class WaterMarkedUploadStream extends stream.Transform {
constructor(bufferSize: number) {
super({
highWaterMark: bufferSize
})
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_transform(chunk: any, enc: any, cb: any): void {
cb(null, chunk)
}
}
export async function createRawFileUploadStream(
filePath: string
): Promise<WaterMarkedUploadStream> {
core.debug(`Creating raw file upload stream for: ${filePath}`)
const bufferSize = getUploadChunkSize()
const uploadStream = new WaterMarkedUploadStream(bufferSize)
// Check if symlink and resolve the source path
let sourcePath = filePath
const stats = await fs.promises.lstat(filePath)
if (stats.isSymbolicLink()) {
sourcePath = await realpath(filePath)
}
// Create a read stream from the file and pipe it to the upload stream
const fileStream = fs.createReadStream(sourcePath, {
highWaterMark: bufferSize
})
fileStream.on('error', error => {
core.error('An error has occurred while reading the file for upload')
core.error(String(error))
uploadStream.destroy(
new Error('An error has occurred during file read for the artifact')
)
})
fileStream.pipe(uploadStream)
return uploadStream
}
@@ -0,0 +1,82 @@
import * as path from 'path'
/**
* Maps file extensions to MIME types
*/
const mimeTypes: Record<string, string> = {
// Text
'.txt': 'text/plain',
'.html': 'text/html',
'.htm': 'text/html',
'.css': 'text/css',
'.csv': 'text/csv',
'.xml': 'text/xml',
'.md': 'text/markdown',
// JavaScript/JSON
'.js': 'application/javascript',
'.mjs': 'application/javascript',
'.json': 'application/json',
// Images
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.webp': 'image/webp',
'.ico': 'image/x-icon',
'.bmp': 'image/bmp',
'.tiff': 'image/tiff',
'.tif': 'image/tiff',
// Audio
'.mp3': 'audio/mpeg',
'.wav': 'audio/wav',
'.ogg': 'audio/ogg',
'.flac': 'audio/flac',
// Video
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.avi': 'video/x-msvideo',
'.mov': 'video/quicktime',
// Documents
'.pdf': 'application/pdf',
'.doc': 'application/msword',
'.docx':
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.xls': 'application/vnd.ms-excel',
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'.ppt': 'application/vnd.ms-powerpoint',
'.pptx':
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
// Archives
'.zip': 'application/zip',
'.tar': 'application/x-tar',
'.gz': 'application/gzip',
'.rar': 'application/vnd.rar',
'.7z': 'application/x-7z-compressed',
// Code/Data
'.wasm': 'application/wasm',
'.yaml': 'application/x-yaml',
'.yml': 'application/x-yaml',
// Fonts
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.otf': 'font/otf',
'.eot': 'application/vnd.ms-fontobject'
}
/**
* Gets the MIME type for a file based on its extension
*/
export function getMimeType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase()
return mimeTypes[ext] || 'application/octet-stream'
}
@@ -1,25 +1,29 @@
import * as core from '@actions/core'
import * as fs from 'fs'
import * as path from 'path'
import {
UploadArtifactOptions,
UploadArtifactResponse
} from '../shared/interfaces'
import {getExpiration} from './retention'
import {validateArtifactName} from './path-and-artifact-name-validation'
import {internalArtifactTwirpClient} from '../shared/artifact-twirp-client'
} from '../shared/interfaces.js'
import {getExpiration} from './retention.js'
import {validateArtifactName} from './path-and-artifact-name-validation.js'
import {internalArtifactTwirpClient} from '../shared/artifact-twirp-client.js'
import {
UploadZipSpecification,
getUploadZipSpecification,
validateRootDirectory
} from './upload-zip-specification'
import {getBackendIdsFromToken} from '../shared/util'
import {uploadZipToBlobStorage} from './blob-upload'
import {createZipUploadStream} from './zip'
} from './upload-zip-specification.js'
import {getBackendIdsFromToken} from '../shared/util.js'
import {uploadToBlobStorage} from './blob-upload.js'
import {createZipUploadStream} from './zip.js'
import {createRawFileUploadStream, WaterMarkedUploadStream} from './stream.js'
import {
CreateArtifactRequest,
FinalizeArtifactRequest,
StringValue
} from '../../generated'
import {FilesNotFoundError, InvalidResponseError} from '../shared/errors'
} from '../../generated/index.js'
import {FilesNotFoundError, InvalidResponseError} from '../shared/errors.js'
import {getMimeType} from './types.js'
export async function uploadArtifact(
name: string,
@@ -27,19 +31,43 @@ export async function uploadArtifact(
rootDirectory: string,
options?: UploadArtifactOptions | undefined
): Promise<UploadArtifactResponse> {
let artifactFileName = `${name}.zip`
if (options?.skipArchive) {
if (files.length === 0) {
throw new FilesNotFoundError([])
}
if (files.length > 1) {
throw new Error(
'skipArchive option is only supported when uploading a single file'
)
}
if (!fs.existsSync(files[0])) {
throw new FilesNotFoundError(files)
}
artifactFileName = path.basename(files[0])
name = artifactFileName
}
validateArtifactName(name)
validateRootDirectory(rootDirectory)
const zipSpecification: UploadZipSpecification[] = getUploadZipSpecification(
files,
rootDirectory
)
if (zipSpecification.length === 0) {
throw new FilesNotFoundError(
zipSpecification.flatMap(s => (s.sourcePath ? [s.sourcePath] : []))
)
let zipSpecification: UploadZipSpecification[] = []
if (!options?.skipArchive) {
zipSpecification = getUploadZipSpecification(files, rootDirectory)
if (zipSpecification.length === 0) {
throw new FilesNotFoundError(
zipSpecification.flatMap(s => (s.sourcePath ? [s.sourcePath] : []))
)
}
}
const contentType = getMimeType(artifactFileName)
// get the IDs needed for the artifact creation
const backendIds = getBackendIdsFromToken()
@@ -51,7 +79,8 @@ export async function uploadArtifact(
workflowRunBackendId: backendIds.workflowRunBackendId,
workflowJobRunBackendId: backendIds.workflowJobRunBackendId,
name,
version: 4
mimeType: StringValue.create({value: contentType}),
version: 7
}
// if there is a retention period, add it to the request
@@ -68,15 +97,24 @@ export async function uploadArtifact(
)
}
const zipUploadStream = await createZipUploadStream(
zipSpecification,
options?.compressionLevel
)
let stream: WaterMarkedUploadStream
// Upload zip to blob storage
const uploadResult = await uploadZipToBlobStorage(
if (options?.skipArchive) {
// Upload raw file without archiving
stream = await createRawFileUploadStream(files[0])
} else {
// Create and upload zip archive
stream = await createZipUploadStream(
zipSpecification,
options?.compressionLevel
)
}
core.info(`Uploading artifact: ${artifactFileName}`)
const uploadResult = await uploadToBlobStorage(
createArtifactResp.signedUploadUrl,
zipUploadStream
stream,
contentType
)
// finalize the artifact
@@ -105,7 +143,7 @@ export async function uploadArtifact(
const artifactId = BigInt(finalizeArtifactResp.artifactId)
core.info(
`Artifact ${name}.zip successfully finalized. Artifact ID ${artifactId}`
`Artifact ${name} successfully finalized. Artifact ID ${artifactId}`
)
return {
@@ -1,7 +1,7 @@
import * as fs from 'fs'
import {info} from '@actions/core'
import {normalize, resolve} from 'path'
import {validateFilePath} from './path-and-artifact-name-validation'
import {validateFilePath} from './path-and-artifact-name-validation.js'
export interface UploadZipSpecification {
/**
+6 -21
View File
@@ -1,31 +1,16 @@
import * as stream from 'stream'
import {realpath} from 'fs/promises'
import * as archiver from 'archiver'
import archiver from 'archiver'
import * as core from '@actions/core'
import {UploadZipSpecification} from './upload-zip-specification'
import {getUploadChunkSize} from '../shared/config'
import {UploadZipSpecification} from './upload-zip-specification.js'
import {getUploadChunkSize} from '../shared/config.js'
import {WaterMarkedUploadStream} from './stream.js'
export const DEFAULT_COMPRESSION_LEVEL = 6
// Custom stream transformer so we can set the highWaterMark property
// See https://github.com/nodejs/node/issues/8855
export class ZipUploadStream extends stream.Transform {
constructor(bufferSize: number) {
super({
highWaterMark: bufferSize
})
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_transform(chunk: any, enc: any, cb: any): void {
cb(null, chunk)
}
}
export async function createZipUploadStream(
uploadSpecification: UploadZipSpecification[],
compressionLevel: number = DEFAULT_COMPRESSION_LEVEL
): Promise<ZipUploadStream> {
): Promise<WaterMarkedUploadStream> {
core.debug(
`Creating Artifact archive with compressionLevel: ${compressionLevel}`
)
@@ -60,7 +45,7 @@ export async function createZipUploadStream(
}
const bufferSize = getUploadChunkSize()
const zipUploadStream = new ZipUploadStream(bufferSize)
const zipUploadStream = new WaterMarkedUploadStream(bufferSize)
core.debug(
`Zip write high watermark value ${zipUploadStream.writableHighWaterMark}`
+2
View File
@@ -4,6 +4,8 @@
"baseUrl": "./",
"outDir": "./lib",
"rootDir": "./src",
"module": "node16",
"moduleResolution": "node16",
"paths": {
"@actions/core": [
"../core"
+76
View File
@@ -15,6 +15,14 @@ initiated.
See [Using artifact attestations to establish provenance for builds](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)
for more information on artifact attestations.
## Table of Contents
- [Usage](#usage)
- [attest](#attest)
- [attestProvenance](#attestprovenance)
- [Attestation](#attestation)
- [Sigstore Instance](#sigstore-instance)
- [Storage](#storage)
## Usage
### `attest`
@@ -165,6 +173,74 @@ export type Attestation = {
For details about the Sigstore bundle format, see the [Bundle protobuf
specification](https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto).
### createStorageRecord
The `createStorageRecord` function creates an
[artifact metadata storage record](https://docs.github.com/en/rest/orgs/artifact-metadata?apiVersion=2022-11-28#create-artifact-metadata-storage-record)
on behalf of an attested artifact. It accepts parameters defining artifact
and package registry details. The storage record contains metadata about where the artifact is stored on a given package registry.
```js
const { createStorageRecord } = require('@actions/attest');
const core = require('@actions/core');
async function run() {
// In order to persist attestations to the repo, this should be a token with
// repository write permissions.
const ghToken = core.getInput('gh-token');
const record = await createStorageRecord(
artifactOptions: {
name: 'my-artifact-name',
digest: { 'sha256': '36ab4667...'},
version: "v1.0.0"
},
packageRegistryOptions: {
registryUrl: "https://my-fave-pkg-registry.com"
},
token: ghToken
);
console.log(record);
}
run();
```
The `createStorageRecord` function supports the following options:
```typescript
// Artifact details to associate the record with
export type ArtifactOptions = {
// The name of the artifact
name: string
// The digest of the artifact
digest: string
// The version of the artifact
version?: string
// The status of the artifact
status?: string
}
// Includes details about the package registry the artifact was published to
export type PackageRegistryOptions = {
// The URL of the package registry
registryUrl: string
// The URL of the artifact in the package registry
artifactUrl?: string
// The package registry repository the artifact was published to.
repo?: string
// The path of the artifact in the package registry repository.
path?: string
}
// GitHub token for writing attestations.
token: string
// Optional parameters for the write operation.
// The number of times to retry the request.
retryAttempts?: number
// HTTP headers to include in request to Artifact Metadata API.
headers?: RequestHeaders
```
## Sigstore Instance
When generating the signed attestation there are two different Sigstore
+45 -11
View File
@@ -1,10 +1,43 @@
# @actions/attest Releases
### 1.6.0
## 3.2.0
- Add custom user-agent for more API calls [#2321](https://github.com/actions/toolkit/pull/2321)
## 3.1.0
- Add support for `ACTIONS_ORCHESTRATION_ID` in user-agent [#2320](https://github.com/actions/toolkit/pull/2320)
## 3.0.0
- **Breaking change**: Package is now ESM-only
- CommonJS consumers must use dynamic `import()` instead of `require()`
- Bump `@actions/core` to `^3.0.0`
- Bump `@actions/http-client` to `^4.0.0`
## 2.2.1
- Bump `@actions/http-client` to `3.0.2`
- Bump `undici` to `6.23.0`
## 2.2.0
- Bump @actions/core from 1.11.1 to 2.0.2
- Bump @actions/github from 6.0.0 to 7.0.0
- Bump @actions/http-client from 2.2.3 to 3.0.1
## 2.0.0
- Add support for Node 24 [#2110](https://github.com/actions/toolkit/pull/2110)
- Bump @sigstore/bundle from 3.0.0 to 3.1.0
- Bump @sigstore/sign from 3.0.0 to 3.1.0
- Bump jose from 5.2.3 to 5.10.0
## 1.6.0
- Update `buildSLSAProvenancePredicate` to populate `workflow.ref` field from the `ref` claim in the OIDC token [#1969](https://github.com/actions/toolkit/pull/1969)
### 1.5.0
## 1.5.0
- Bump @actions/core from 1.10.1 to 1.11.1 [#1847](https://github.com/actions/toolkit/pull/1847)
- Bump @sigstore/bundle from 2.3.2 to 3.0.0 [#1846](https://github.com/actions/toolkit/pull/1846)
@@ -12,23 +45,24 @@
- Support for generating multi-subject attestations [#1864](https://github.com/actions/toolkit/pull/1865)
- Fix bug in `buildSLSAProvenancePredicate` related to `workflow_ref` OIDC token claims containing the "@" symbol in the tag name [#1863](https://github.com/actions/toolkit/pull/1863)
### 1.4.2
## 1.4.2
- Fix bug in `buildSLSAProvenancePredicate`/`attestProvenance` when generating provenance statement for enterprise account using customized OIDC issuer value [#1823](https://github.com/actions/toolkit/pull/1823)
### 1.4.1
## 1.4.1
- Bump @actions/http-client from 2.2.1 to 2.2.3 [#1805](https://github.com/actions/toolkit/pull/1805)
### 1.4.0
## 1.4.0
- Add new `headers` parameter to the `attest` and `attestProvenance` functions [#1790](https://github.com/actions/toolkit/pull/1790)
- Update `buildSLSAProvenancePredicate`/`attestProvenance` to automatically derive default OIDC issuer URL from current execution context [#1796](https://github.com/actions/toolkit/pull/1796)
### 1.3.1
## 1.3.1
- Fix bug with proxy support when retrieving JWKS for OIDC issuer [#1776](https://github.com/actions/toolkit/pull/1776)
### 1.3.0
## 1.3.0
- Dynamic construction of Sigstore API URLs [#1735](https://github.com/actions/toolkit/pull/1735)
- Switch to new GH provenance build type [#1745](https://github.com/actions/toolkit/pull/1745)
@@ -36,21 +70,21 @@
- Bump @sigstore/bundle from 2.3.0 to 2.3.2 [#1738](https://github.com/actions/toolkit/pull/1738)
- Bump @sigstore/sign from 2.3.0 to 2.3.2 [#1738](https://github.com/actions/toolkit/pull/1738)
### 1.2.1
## 1.2.1
- Retry request on attestation persistence failure [#1725](https://github.com/actions/toolkit/pull/1725)
### 1.2.0
## 1.2.0
- Generate attestations using the v0.3 Sigstore bundle format [#1701](https://github.com/actions/toolkit/pull/1701)
- Bump @sigstore/bundle from 2.2.0 to 2.3.0 [#1701](https://github.com/actions/toolkit/pull/1701)
- Bump @sigstore/sign from 2.2.3 to 2.3.0 [#1701](https://github.com/actions/toolkit/pull/1701)
- Remove dependency on make-fetch-happen [#1714](https://github.com/actions/toolkit/pull/1714)
### 1.1.0
## 1.1.0
- Updates the `attestProvenance` function to retrieve a token from the GitHub OIDC provider and use the token claims to populate the provenance statement [#1693](https://github.com/actions/toolkit/pull/1693)
### 1.0.0
## 1.0.0
- Initial release
@@ -0,0 +1,137 @@
import {MockAgent, setGlobalDispatcher} from 'undici'
import {createStorageRecord} from '../src/artifactMetadata'
describe('createStorageRecord', () => {
const originalEnv = process.env
const token = 'token'
const headers = {'X-GitHub-Foo': 'true'}
const artifactOptions = {
name: 'my-lib',
version: '1.0.0',
digest: `sha256:${'a'.repeat(64)}`
}
const packageRegistryOptions = {
registryUrl: 'https://my-registry.org'
}
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
beforeEach(() => {
process.env = {
...originalEnv,
GITHUB_REPOSITORY: 'foo/bar'
}
})
afterEach(() => {
process.env = originalEnv
})
describe('when the api call is successful', () => {
beforeEach(() => {
mockAgent
.get('https://api.github.com')
.intercept({
path: '/orgs/foo/artifacts/metadata/storage-record',
method: 'POST',
headers: {authorization: `token ${token}`, ...headers},
body: JSON.stringify({
name: 'my-lib',
version: '1.0.0',
digest: `sha256:${'a'.repeat(64)}`,
registry_url: 'https://my-registry.org'
})
})
.reply(200, {storage_records: [{id: 123}, {id: 456}]})
})
it('persists the storage record', async () => {
await expect(
createStorageRecord(
artifactOptions,
packageRegistryOptions,
token,
undefined,
headers
)
).resolves.toEqual([123, 456])
})
})
describe('when the api call fails', () => {
beforeEach(() => {
mockAgent
.get('https://api.github.com')
.intercept({
path: '/orgs/foo/artifacts/metadata/storage-record',
method: 'POST',
headers: {authorization: `token ${token}`},
body: JSON.stringify({
name: 'my-lib',
version: '1.0.0',
digest: `sha256:${'a'.repeat(64)}`,
registry_url: 'https://my-registry.org'
})
})
.reply(500, 'oops')
})
it('throws an error', async () => {
await expect(
createStorageRecord(
artifactOptions,
packageRegistryOptions,
token,
0,
headers
)
).rejects.toThrow(/oops/)
})
})
describe('when the api call fails but succeeds on retry', () => {
beforeEach(() => {
const pool = mockAgent.get('https://api.github.com')
pool
.intercept({
path: '/orgs/foo/artifacts/metadata/storage-record',
method: 'POST',
headers: {authorization: `token ${token}`},
body: JSON.stringify({
...artifactOptions,
registry_url: packageRegistryOptions.registryUrl
})
})
.reply(500, 'oops')
.times(1)
pool
.intercept({
path: '/orgs/foo/artifacts/metadata/storage-record',
method: 'POST',
headers: {authorization: `token ${token}`},
body: JSON.stringify({
...artifactOptions,
registry_url: packageRegistryOptions.registryUrl
})
})
.reply(200, {storage_records: [{id: 123}, {id: 456}]})
.times(1)
})
it('persists the storage record', async () => {
await expect(
createStorageRecord(
artifactOptions,
packageRegistryOptions,
token,
undefined,
headers
)
).resolves.toEqual([123, 456])
})
})
})
+411 -1829
View File
File diff suppressed because it is too large Load Diff
+18 -16
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/attest",
"version": "1.6.0",
"version": "3.2.0",
"description": "Actions attestation lib",
"keywords": [
"github",
@@ -9,8 +9,15 @@
],
"homepage": "https://github.com/actions/toolkit/tree/main/packages/attest",
"license": "MIT",
"type": "module",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": {
"types": "./lib/index.d.ts",
"import": "./lib/index.js"
}
},
"directories": {
"lib": "lib",
"test": "__tests__"
@@ -29,30 +36,25 @@
},
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
"tsc": "tsc"
"tsc": "tsc && cp src/internal/package-version.cjs lib/internal/"
},
"bugs": {
"url": "https://github.com/actions/toolkit/issues"
},
"devDependencies": {
"@sigstore/mock": "^0.8.0",
"@sigstore/mock": "^0.10.0",
"@sigstore/rekor-types": "^3.0.0",
"@types/jsonwebtoken": "^9.0.6",
"nock": "^13.5.1",
"undici": "^5.28.5"
"undici": "^6.23.0"
},
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.0",
"@actions/http-client": "^2.2.3",
"@octokit/plugin-retry": "^6.0.1",
"@sigstore/bundle": "^3.0.0",
"@sigstore/sign": "^3.0.0",
"jose": "^5.2.3"
},
"overrides": {
"@octokit/plugin-retry": {
"@octokit/core": "^5.2.0"
}
"@actions/core": "^3.0.0",
"@actions/github": "^9.0.0",
"@actions/http-client": "^4.0.0",
"@octokit/plugin-retry": "^8.0.3",
"@sigstore/bundle": "^3.1.0",
"@sigstore/sign": "^3.1.0",
"jose": "^5.10.0"
}
}
+92
View File
@@ -0,0 +1,92 @@
import * as github from '@actions/github'
import {retry} from '@octokit/plugin-retry'
import {RequestHeaders} from '@octokit/types'
import {getUserAgent} from './internal/utils.js'
const CREATE_STORAGE_RECORD_REQUEST =
'POST /orgs/{owner}/artifacts/metadata/storage-record'
const DEFAULT_RETRY_COUNT = 5
/**
* Options for creating a storage record for an attested artifact.
*/
export type ArtifactOptions = {
// Includes details about the attested artifact
// The name of the artifact
name: string
// The digest of the artifact
digest: string
// The version of the artifact
version?: string
// The status of the artifact
status?: string
}
// Includes details about the package registry the artifact was published to
export type PackageRegistryOptions = {
// The URL of the package registry
registryUrl: string
// The URL of the artifact in the package registry
artifactUrl?: string
// The package registry repository the artifact was published to.
repo?: string
// The path of the artifact in the package registry repository.
path?: string
}
/**
* Writes a storage record on behalf of an artifact that has been attested
* @param artifactOptions - parameters for the storage record API request.
* @param packageRegistryOptions - parameters for the package registry API request.
* @param token - GitHub token used to authenticate the request.
* @param retryAttempts - The number of retries to attempt if the request fails.
* @param headers - Additional headers to include in the request.
*
* @returns The ID of the storage record.
* @throws Error if the storage record fails to persist.
*/
export async function createStorageRecord(
artifactOptions: ArtifactOptions,
packageRegistryOptions: PackageRegistryOptions,
token: string,
retryAttempts?: number,
headers?: RequestHeaders
): Promise<number[]> {
const retries = retryAttempts ?? DEFAULT_RETRY_COUNT
const octokit = github.getOctokit(token, {retry: {retries}}, retry)
const headersWithUserAgent = {
'User-Agent': getUserAgent(),
...headers
}
try {
const response = await octokit.request(CREATE_STORAGE_RECORD_REQUEST, {
owner: github.context.repo.owner,
headers: headersWithUserAgent,
...buildRequestParams(artifactOptions, packageRegistryOptions)
})
const data =
typeof response.data == 'string'
? JSON.parse(response.data)
: response.data
return data?.storage_records.map((r: {id: number}) => r.id)
} catch (err) {
const message = err instanceof Error ? err.message : err
throw new Error(`Failed to persist storage record: ${message}`)
}
}
function buildRequestParams(
artifactOptions: ArtifactOptions,
packageRegistryOptions: PackageRegistryOptions
): Record<string, unknown> {
const {registryUrl, artifactUrl, ...rest} = packageRegistryOptions
return {
...artifactOptions,
registry_url: registryUrl,
artifact_url: artifactUrl,
...rest
}
}
+5 -5
View File
@@ -1,12 +1,12 @@
import {bundleToJSON} from '@sigstore/bundle'
import {X509Certificate} from 'crypto'
import {SigstoreInstance, signingEndpoints} from './endpoints'
import {buildIntotoStatement} from './intoto'
import {Payload, signPayload} from './sign'
import {writeAttestation} from './store'
import {SigstoreInstance, signingEndpoints} from './endpoints.js'
import {buildIntotoStatement} from './intoto.js'
import {Payload, signPayload} from './sign.js'
import {writeAttestation} from './store.js'
import type {Bundle} from '@sigstore/sign'
import type {Attestation, Predicate, Subject} from './shared.types'
import type {Attestation, Predicate, Subject} from './shared.types.js'
const INTOTO_PAYLOAD_TYPE = 'application/vnd.in-toto+json'
+9 -3
View File
@@ -1,9 +1,15 @@
export {AttestOptions, attest} from './attest'
export {
createStorageRecord,
ArtifactOptions,
PackageRegistryOptions
} from './artifactMetadata.js'
export {AttestOptions, attest} from './attest.js'
export {
AttestProvenanceOptions,
attestProvenance,
buildSLSAProvenancePredicate
} from './provenance'
} from './provenance.js'
export type {SerializedBundle} from '@sigstore/bundle'
export type {Attestation, Predicate, Subject} from './shared.types'
export type {Attestation, Predicate, Subject} from './shared.types.js'
export type {SigstoreInstance} from './endpoints.js'
@@ -0,0 +1,7 @@
// This file exists as a CommonJS module to read the version from package.json.
// In an ESM package, using `require()` directly in .ts files requires disabling
// ESLint rules and doesn't work reliably across all Node.js versions.
// By keeping this as a .cjs file, we can use require() naturally and export
// the version for the ESM modules to import.
const packageJson = require('../../package.json')
module.exports = {version: packageJson.version}
+15
View File
@@ -0,0 +1,15 @@
import {version} from './package-version.cjs'
export const getUserAgent = (): string => {
const baseUserAgent = `@actions/attest-${version}`
const orchId = process.env['ACTIONS_ORCHESTRATION_ID']
if (orchId) {
// Sanitize the orchestration ID to ensure it contains only valid characters
// Valid characters: 0-9, a-z, _, -, .
const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_')
return `${baseUserAgent} actions_orchestration_id/${sanitizedId}`
}
return baseUserAgent
}
+1 -1
View File
@@ -1,4 +1,4 @@
import {Predicate, Subject} from './shared.types'
import {Predicate, Subject} from './shared.types.js'
const INTOTO_STATEMENT_V1_TYPE = 'https://in-toto.io/Statement/v1'
+3 -3
View File
@@ -1,6 +1,6 @@
import {attest, AttestOptions} from './attest'
import {getIDTokenClaims} from './oidc'
import type {Attestation, Predicate} from './shared.types'
import {attest, AttestOptions} from './attest.js'
import {getIDTokenClaims} from './oidc.js'
import type {Attestation, Predicate} from './shared.types.js'
const SLSA_PREDICATE_V1_TYPE = 'https://slsa.dev/provenance/v1'
const GITHUB_BUILD_TYPE = 'https://actions.github.io/buildtypes/workflow/v1'
+12 -2
View File
@@ -1,6 +1,7 @@
import * as github from '@actions/github'
import {retry} from '@octokit/plugin-retry'
import {RequestHeaders} from '@octokit/types'
import {getUserAgent} from './internal/utils.js'
const CREATE_ATTESTATION_REQUEST = 'POST /repos/{owner}/{repo}/attestations'
const DEFAULT_RETRY_COUNT = 5
@@ -24,12 +25,21 @@ export const writeAttestation = async (
const retries = options.retry ?? DEFAULT_RETRY_COUNT
const octokit = github.getOctokit(token, {retry: {retries}}, retry)
const headers = {
'User-Agent': getUserAgent(),
...options.headers
}
try {
const response = await octokit.request(CREATE_ATTESTATION_REQUEST, {
owner: github.context.repo.owner,
repo: github.context.repo.repo,
headers: options.headers,
data: {bundle: attestation}
headers,
bundle: attestation as {
mediaType?: string
verificationMaterial?: {[key: string]: unknown}
dsseEnvelope?: {[key: string]: unknown}
}
})
const data =
+3 -1
View File
@@ -4,7 +4,9 @@
"baseUrl": "./",
"outDir": "./lib",
"declaration": true,
"rootDir": "./src"
"rootDir": "./src",
"module": "node16",
"moduleResolution": "node16"
},
"include": [
"./src"
+97 -46
View File
@@ -1,22 +1,73 @@
# @actions/cache Releases
### 4.0.3
## 6.0.0
- **Breaking change**: Package is now ESM-only
- CommonJS consumers must use dynamic `import()` instead of `require()`
## 5.0.5
- Bump `@actions/glob` to `0.5.1`
## 5.0.4
- Bump `@actions/http-client` to `3.0.2`
## 5.0.3
Prevent retries for rate limited cache operations [2243](https://github.com/actions/toolkit/pull/2243).
## 5.0.1
- Fix Node.js 24 punycode deprecation warning by updating `@azure/storage-blob` from `^12.13.0` to `^12.29.1` [#2213](https://github.com/actions/toolkit/pull/2213)
- Newer storage-blob uses `@azure/core-rest-pipeline` instead of deprecated `@azure/core-http`, which eliminates the transitive dependency on `node-fetch@2``whatwg-url@5``tr46@0.0.3` that used the deprecated punycode module
## 5.0.0
- Remove `@azure/ms-rest-js` dependency [#2197](https://github.com/actions/toolkit/pull/2197)
- The `TransferProgressEvent` type is now imported from `@azure/core-rest-pipeline` instead of `@azure/ms-rest-js`
- Bump `@actions/core` from `^1.11.1` to `^2.0.0` [#2198](https://github.com/actions/toolkit/pull/2198)
- Bump `@actions/exec` from `^1.0.1` to `^2.0.0` [#2198](https://github.com/actions/toolkit/pull/2198)
- Bump `@actions/glob` from `^0.1.0` to `^0.5.0` [#2198](https://github.com/actions/toolkit/pull/2198)
- Bump `@actions/http-client` from `^2.1.1` to `^3.0.0` [#2198](https://github.com/actions/toolkit/pull/2198)
- Bump `@actions/io` from `^1.0.1` to `^2.0.0` [#2198](https://github.com/actions/toolkit/pull/2198)
- Add support for Node.js 24 [#2110](https://github.com/actions/toolkit/pull/2110)
- Add `node-fetch` override to resolve audit vulnerabilities [#2110](https://github.com/actions/toolkit/pull/2110)
## 4.1.0
- Remove client side 10GiB cache size limit check & update twirp client [#2118](https://github.com/actions/toolkit/pull/2118)
## 4.0.5
- Reintroduce @protobuf-ts/runtime-rpc as a runtime dependency [#2113](https://github.com/actions/toolkit/pull/2113)
## 4.0.4
⚠️ Faulty patch release. Upgrade to 4.0.5 instead.
- Optimized cache dependencies by moving `@protobuf-ts/plugin` to dev dependencies [#2106](https://github.com/actions/toolkit/pull/2106)
- Improved cache service availability determination for different cache service versions (v1 and v2) [#2100](https://github.com/actions/toolkit/pull/2100)
- Enhanced server error handling: 5xx HTTP errors are now logged as errors instead of warnings [#2099](https://github.com/actions/toolkit/pull/2099)
- Fixed cache hit logging to properly distinguish between exact key matches and restore key matches [#2101](https://github.com/actions/toolkit/pull/2101)
## 4.0.3
- Added masking for Shared Access Signature (SAS) cache entry URLs [#1982](https://github.com/actions/toolkit/pull/1982)
- Improved debugging by logging both the cache version alongside the keys requested when a cache restore fails [#1994](https://github.com/actions/toolkit/pull/1994)
### 4.0.2
## 4.0.2
- Wrap create failures in ReserveCacheError [#1966](https://github.com/actions/toolkit/pull/1966)
### 4.0.1
## 4.0.1
- Remove runtime dependency on `twirp-ts` [#1947](https://github.com/actions/toolkit/pull/1947)
- Cache miss as debug, not warning annotation [#1954](https://github.com/actions/toolkit/pull/1954)
### 4.0.0
## 4.0.0
#### Important changes
### Important changes
The cache backend service has been rewritten from the ground up for improved performance and reliability. The [@actions/cache](https://github.com/actions/toolkit/tree/main/packages/cache) package now integrates with the new cache service (v2) APIs.
@@ -30,182 +81,182 @@ Upgrading to the recommended version should not break or require any changes to
Read more about the change & access the migration guide: [reference to the announcement](https://github.com/actions/toolkit/discussions/1890).
#### Minor changes
### Minor changes
- Update `@actions/core` to `1.11.0`
- Update `semver` `6.3.1`
- Add `twirp-ts` `2.5.0` to dependencies
### 3.3.0
## 3.3.0
- Update `@actions/core` to `1.11.1`
- Remove dependency on `uuid` package [#1824](https://github.com/actions/toolkit/pull/1824), [#1842](https://github.com/actions/toolkit/pull/1842)
### 3.2.4
## 3.2.4
- Updated `isGhes` check to include `.ghe.com` and `.ghe.localhost` as accepted hosts
### 3.2.3
## 3.2.3
- Fixed a bug that mutated path arguments to `getCacheVersion` [#1378](https://github.com/actions/toolkit/pull/1378)
### 3.2.2
## 3.2.2
- Add new default cache download method to improve performance and reduce hangs [#1484](https://github.com/actions/toolkit/pull/1484)
### 3.2.1
## 3.2.1
- Updated @azure/storage-blob to `v12.13.0`
### 3.2.0
## 3.2.0
- Add `lookupOnly` to cache restore `DownloadOptions`.
### 3.1.4
## 3.1.4
- Fix zstd not being used due to `zstd --version` output change in zstd 1.5.4 release. See [#1353](https://github.com/actions/toolkit/pull/1353).
### 3.1.3
## 3.1.3
- Fix to prevent from setting MYSYS environement variable globally [#1329](https://github.com/actions/toolkit/pull/1329).
### 3.1.2
## 3.1.2
- Fix issue with symlink restoration on windows.
### 3.1.1
## 3.1.1
- Reverted changes in 3.1.0 to fix issue with symlink restoration on windows.
- Added support for verbose logging about cache version during cache miss.
### 3.1.0
## 3.1.0
- Update actions/cache on windows to use gnu tar and zstd by default
- Update actions/cache on windows to fallback to bsdtar and zstd if gnu tar is not available.
- Added support for fallback to gzip to restore old caches on windows.
### 3.1.0-beta.3
## 3.1.0-beta.3
- Bug Fixes for fallback to gzip to restore old caches on windows and bsdtar if gnutar is not available.
### 3.1.0-beta.2
## 3.1.0-beta.2
- Added support for fallback to gzip to restore old caches on windows.
### 3.0.6
## 3.0.6
- Added `@azure/abort-controller` to dependencies to fix compatibility issue with ESM [#1208](https://github.com/actions/toolkit/issues/1208)
### 3.0.5
## 3.0.5
- Update `@actions/cache` to use `@actions/core@^1.10.0`
### 3.0.4
## 3.0.4
- Fix zstd not working for windows on gnu tar in issues [#888](https://github.com/actions/cache/issues/888) and [#891](https://github.com/actions/cache/issues/891).
- Allowing users to provide a custom timeout as input for aborting download of a cache segment using an environment variable `SEGMENT_DOWNLOAD_TIMEOUT_MINS`. Default is 60 minutes.
### 3.0.3
## 3.0.3
- Bug fixes for download stuck issue [#810](https://github.com/actions/cache/issues/810).
### 3.0.2
## 3.0.2
- Added 1 hour timeout for the download stuck issue [#810](https://github.com/actions/cache/issues/810).
### 3.0.1
## 3.0.1
- Fix [#833](https://github.com/actions/cache/issues/833) - cache doesn't work with github workspace directory.
- Fix [#809](https://github.com/actions/cache/issues/809) `zstd -d: no such file or directory` error on AWS self-hosted runners.
### 3.0.0
## 3.0.0
- Updated actions/cache to suppress Actions cache server error and log warning for those error [#1122](https://github.com/actions/toolkit/pull/1122)
### 2.0.6
## 2.0.6
- Fix `Tar failed with error: The process '/usr/bin/tar' failed with exit code 1` issue when temp directory where tar is getting created is actually the subdirectory of the path mentioned by the user for caching. ([issue](https://github.com/actions/cache/issues/689))
### 2.0.5
## 2.0.5
- Fix to avoid saving empty cache when no files are available for caching. ([issue](https://github.com/actions/cache/issues/624))
### 2.0.4
## 2.0.4
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
### 2.0.3
## 2.0.3
- Update to v2.0.0 of `@actions/http-client`
### 2.0.0
## 2.0.0
- Added support to check if Actions cache service feature is available or not [#1028](https://github.com/actions/toolkit/pull/1028)
### 1.0.11
## 1.0.11
- Fix file downloads > 2GB([issue](https://github.com/actions/cache/issues/773))
### 1.0.10
## 1.0.10
- Update `lockfileVersion` to `v2` in `package-lock.json [#1022](https://github.com/actions/toolkit/pull/1022)
### 1.0.9
## 1.0.9
- Use @azure/ms-rest-js v2.6.0
- Use @azure/storage-blob v12.8.0
### 1.0.8
## 1.0.8
- Increase the allowed artifact cache size from 5GB to 10GB ([issue](https://github.com/actions/cache/discussions/497))
### 1.0.7
## 1.0.7
- Fixes permissions issue extracting archives with GNU tar on macOS ([issue](https://github.com/actions/cache/issues/527))
### 1.0.6
## 1.0.6
- Make caching more verbose [#650](https://github.com/actions/toolkit/pull/650)
- Use GNU tar on macOS if available [#701](https://github.com/actions/toolkit/pull/701)
### 1.0.5
## 1.0.5
- Fix to ensure Windows cache paths get resolved correctly
### 1.0.4
## 1.0.4
- Use @actions/core v1.2.6
- Fixes uploadChunk to throw an error if any unsuccessful response code is received
### 1.0.3
## 1.0.3
- Use http-client v1.0.9
- Fixes error handling so retries are not attempted on non-retryable errors (409 Conflict, for example)
- Adds 5 second delay between retry attempts
### 1.0.2
## 1.0.2
- Use posix archive format to add support for some tools
### 1.0.1
## 1.0.1
- Fix bug in downloading large files (> 2 GBs) with the Azure SDK
### 1.0.0
## 1.0.0
- Downloads Azure-hosted caches using the Azure SDK for speed and reliability
- Displays download progress
- Includes changes that break compatibility with earlier versions, including:
- `retry`, `retryTypedResponse`, and `retryHttpClientResponse` moved from `cacheHttpClient` to `requestUtils`
### 0.2.1
## 0.2.1
- Fix to await async function getCompressionMethod
### 0.2.0
## 0.2.0
- Fixes issues with the zstd compression algorithm on Windows and Ubuntu 16.04 [#469](https://github.com/actions/toolkit/pull/469)
### 0.1.0
## 0.1.0
- Initial release
+1 -1
View File
@@ -1,5 +1,5 @@
name: 'Set env variables'
description: 'Sets certain env variables so that e2e restore and save cache can be tested in a shell'
runs:
using: 'node12'
using: 'node20'
main: 'index.js'
+4 -3
View File
@@ -1,8 +1,9 @@
// Certain env variables are not set by default in a shell context and are only available in a node context from a running action
// In order to be able to restore and save cache e2e in a shell when running CI tests, we need these env variables set
const fs = require('fs');
const os = require('os');
const filePath = process.env[`GITHUB_ENV`]
import fs from 'fs'
import os from 'os'
const filePath = process.env['GITHUB_ENV']
fs.appendFileSync(filePath, `ACTIONS_CACHE_SERVICE_V2=true${os.EOL}`, {
encoding: 'utf8'
})
+63 -8
View File
@@ -1,14 +1,69 @@
import * as cache from '../src/cache'
test('isFeatureAvailable returns true if server url is set', () => {
try {
describe('isFeatureAvailable', () => {
const originalEnv = process.env
beforeEach(() => {
jest.resetModules()
process.env = {...originalEnv}
// Clean cache-related environment variables
delete process.env['ACTIONS_CACHE_URL']
delete process.env['ACTIONS_RESULTS_URL']
delete process.env['ACTIONS_CACHE_SERVICE_V2']
delete process.env['GITHUB_SERVER_URL']
})
afterAll(() => {
process.env = originalEnv
})
test('returns true for cache service v1 when ACTIONS_CACHE_URL is set', () => {
process.env['ACTIONS_CACHE_URL'] = 'http://cache.com'
expect(cache.isFeatureAvailable()).toBe(true)
} finally {
delete process.env['ACTIONS_CACHE_URL']
}
})
})
test('isFeatureAvailable returns false if server url is not set', () => {
expect(cache.isFeatureAvailable()).toBe(false)
test('returns false for cache service v1 when only ACTIONS_RESULTS_URL is set', () => {
process.env['ACTIONS_RESULTS_URL'] = 'http://results.com'
expect(cache.isFeatureAvailable()).toBe(false)
})
test('returns true for cache service v1 when both URLs are set', () => {
process.env['ACTIONS_CACHE_URL'] = 'http://cache.com'
process.env['ACTIONS_RESULTS_URL'] = 'http://results.com'
expect(cache.isFeatureAvailable()).toBe(true)
})
test('returns true for cache service v2 when ACTIONS_RESULTS_URL is set', () => {
process.env['ACTIONS_CACHE_SERVICE_V2'] = 'true'
process.env['ACTIONS_RESULTS_URL'] = 'http://results.com'
expect(cache.isFeatureAvailable()).toBe(true)
})
test('returns false for cache service v2 when only ACTIONS_CACHE_URL is set', () => {
process.env['ACTIONS_CACHE_SERVICE_V2'] = 'true'
process.env['ACTIONS_CACHE_URL'] = 'http://cache.com'
expect(cache.isFeatureAvailable()).toBe(false)
})
test('returns false when no cache URLs are set', () => {
expect(cache.isFeatureAvailable()).toBe(false)
})
test('returns false for cache service v2 when no URLs are set', () => {
process.env['ACTIONS_CACHE_SERVICE_V2'] = 'true'
expect(cache.isFeatureAvailable()).toBe(false)
})
test('returns true for GHES with v1 even when v2 flag is set', () => {
process.env['GITHUB_SERVER_URL'] = 'https://my-enterprise.github.com'
process.env['ACTIONS_CACHE_SERVICE_V2'] = 'true'
process.env['ACTIONS_CACHE_URL'] = 'http://cache.com'
expect(cache.isFeatureAvailable()).toBe(true)
})
test('returns false for GHES with only ACTIONS_RESULTS_URL', () => {
process.env['GITHUB_SERVER_URL'] = 'https://my-enterprise.github.com'
process.env['ACTIONS_RESULTS_URL'] = 'http://results.com'
expect(cache.isFeatureAvailable()).toBe(false)
})
})
+174
View File
@@ -0,0 +1,174 @@
import * as http from 'http'
import * as net from 'net'
import {HttpClient} from '@actions/http-client'
import * as core from '@actions/core'
import * as config from '../src/internal/config'
import * as cacheUtils from '../src/internal/cacheUtils'
import {internalCacheTwirpClient} from '../src/internal/shared/cacheTwirpClient'
jest.mock('@actions/http-client')
const clientOptions = {
maxAttempts: 5,
retryIntervalMs: 1,
retryMultiplier: 1.5
}
// noopLogs mocks the console.log and core.* functions to prevent output in the console while testing
const noopLogs = (): void => {
jest.spyOn(console, 'log').mockImplementation(() => {})
jest.spyOn(core, 'debug').mockImplementation(() => {})
jest.spyOn(core, 'info').mockImplementation(() => {})
jest.spyOn(core, 'warning').mockImplementation(() => {})
}
describe('cacheTwirpClient', () => {
beforeAll(() => {
noopLogs()
jest
.spyOn(config, 'getCacheServiceURL')
.mockReturnValue('http://localhost:8080')
jest.spyOn(cacheUtils, 'getRuntimeToken').mockReturnValue('token')
})
beforeEach(() => {
jest.clearAllMocks()
})
it('should fail immediately on 429 rate limit without retrying', async () => {
const mockPost = jest.fn(() => {
const msg = new http.IncomingMessage(new net.Socket())
msg.statusCode = 429
msg.statusMessage = 'Too Many Requests'
return {
message: msg,
readBody: async () => {
return Promise.resolve(`{"ok": false}`)
}
}
})
;(HttpClient as unknown as jest.Mock).mockImplementation(() => {
return {
post: mockPost
}
})
const client = internalCacheTwirpClient(clientOptions)
await expect(
client.CreateCacheEntry({
key: 'test-key',
version: 'test-version'
})
).rejects.toThrow(
'Failed to CreateCacheEntry: Rate limited: Failed request: (429) Too Many Requests'
)
// Should only be called once - no retries for 429
expect(mockPost).toHaveBeenCalledTimes(1)
})
it('should log warning with retry-after header on 429', async () => {
const warningSpy = jest.spyOn(core, 'warning')
const mockPost = jest.fn(() => {
const msg = new http.IncomingMessage(new net.Socket())
msg.statusCode = 429
msg.statusMessage = 'Too Many Requests'
msg.headers = {'retry-after': '60'}
return {
message: msg,
readBody: async () => {
return Promise.resolve(`{"ok": false}`)
}
}
})
;(HttpClient as unknown as jest.Mock).mockImplementation(() => {
return {
post: mockPost
}
})
const client = internalCacheTwirpClient(clientOptions)
await expect(
client.CreateCacheEntry({
key: 'test-key',
version: 'test-version'
})
).rejects.toThrow('Rate limited')
expect(mockPost).toHaveBeenCalledTimes(1)
expect(warningSpy).toHaveBeenCalledWith(
"You've hit a rate limit, your rate limit will reset in 60 seconds"
)
})
it('should not log warning if retry-after header is missing on 429', async () => {
const warningSpy = jest.spyOn(core, 'warning')
const mockPost = jest.fn(() => {
const msg = new http.IncomingMessage(new net.Socket())
msg.statusCode = 429
msg.statusMessage = 'Too Many Requests'
// No retry-after header
return {
message: msg,
readBody: async () => {
return Promise.resolve(`{"ok": false}`)
}
}
})
;(HttpClient as unknown as jest.Mock).mockImplementation(() => {
return {
post: mockPost
}
})
const client = internalCacheTwirpClient(clientOptions)
await expect(
client.CreateCacheEntry({
key: 'test-key',
version: 'test-version'
})
).rejects.toThrow('Rate limited')
expect(mockPost).toHaveBeenCalledTimes(1)
expect(warningSpy).not.toHaveBeenCalled()
})
it('should not log warning if retry-after header is invalid on 429', async () => {
const warningSpy = jest.spyOn(core, 'warning')
const mockPost = jest.fn(() => {
const msg = new http.IncomingMessage(new net.Socket())
msg.statusCode = 429
msg.statusMessage = 'Too Many Requests'
msg.headers = {'retry-after': 'invalid'}
return {
message: msg,
readBody: async () => {
return Promise.resolve(`{"ok": false}`)
}
}
})
;(HttpClient as unknown as jest.Mock).mockImplementation(() => {
return {
post: mockPost
}
})
const client = internalCacheTwirpClient(clientOptions)
await expect(
client.CreateCacheEntry({
key: 'test-key',
version: 'test-version'
})
).rejects.toThrow('Rate limited')
expect(mockPost).toHaveBeenCalledTimes(1)
expect(warningSpy).not.toHaveBeenCalled()
})
})
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env node
// Helper script to restore cache for e2e testing
import * as cache from '../lib/cache.js'
const [prefix, runId, useAzureSdk] = process.argv.slice(2)
if (!prefix || !runId) {
console.error('Usage: restore-cache.mjs <prefix> <runId> [useAzureSdk]')
process.exit(1)
}
const key = `test-${prefix}-${runId}`
const paths = ['test-cache', '~/test-cache']
const options = {useAzureSdk: useAzureSdk !== 'false'}
console.log(`Restoring cache with key: ${key}`)
console.log(`Paths: ${paths.join(', ')}`)
console.log(`Using Azure SDK: ${options.useAzureSdk}`)
const maxRetries = 3
const retryDelayMs = 5000
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`Attempt ${attempt} of ${maxRetries}`)
const restoredKey = await cache.restoreCache(paths, key, [], options)
if (restoredKey) {
console.log(`Cache restored with key: ${restoredKey}`)
process.exit(0)
} else {
console.log('Cache not found on this attempt')
}
} catch (error) {
console.error(`Error on attempt ${attempt}:`, error.message)
}
if (attempt < maxRetries) {
console.log(`Waiting ${retryDelayMs / 1000}s before retry...`)
await new Promise(resolve => setTimeout(resolve, retryDelayMs))
}
}
console.error(`Failed to restore cache after ${maxRetries} attempts`)
process.exit(1)
+18 -6
View File
@@ -6,6 +6,8 @@ import * as cacheUtils from '../src/internal/cacheUtils'
import {CacheFilename, CompressionMethod} from '../src/internal/constants'
import {ArtifactCacheEntry} from '../src/internal/contracts'
import * as tar from '../src/internal/tar'
import {HttpClientError} from '@actions/http-client'
import {CacheServiceClientJSON} from '../src/generated/results/api/v1/cache.twirp-client'
jest.mock('../src/internal/cacheHttpClient')
jest.mock('../src/internal/cacheUtils')
@@ -73,18 +75,28 @@ test('restore with no cache found', async () => {
test('restore with server error should fail', async () => {
const paths = ['node_modules']
const key = 'node-test'
const logWarningMock = jest.spyOn(core, 'warning')
const logErrorMock = jest.spyOn(core, 'error')
jest.spyOn(cacheHttpClient, 'getCacheEntry').mockImplementation(() => {
throw new Error('HTTP Error Occurred')
})
// Set cache service to V2 to test error logging for server errors
process.env['ACTIONS_CACHE_SERVICE_V2'] = 'true'
process.env['ACTIONS_RESULTS_URL'] = 'https://results.local/'
jest
.spyOn(CacheServiceClientJSON.prototype, 'GetCacheEntryDownloadURL')
.mockImplementation(() => {
throw new HttpClientError('HTTP Error Occurred', 500)
})
const cacheKey = await restoreCache(paths, key)
expect(cacheKey).toBe(undefined)
expect(logWarningMock).toHaveBeenCalledTimes(1)
expect(logWarningMock).toHaveBeenCalledWith(
expect(logErrorMock).toHaveBeenCalledTimes(1)
expect(logErrorMock).toHaveBeenCalledWith(
'Failed to restore: HTTP Error Occurred'
)
// Clean up environment
delete process.env['ACTIONS_CACHE_SERVICE_V2']
delete process.env['ACTIONS_RESULTS_URL']
})
test('restore with restore keys and no cache found', async () => {
+9 -4
View File
@@ -8,6 +8,7 @@ import {restoreCache} from '../src/cache'
import {CacheFilename, CompressionMethod} from '../src/internal/constants'
import {CacheServiceClientJSON} from '../src/generated/results/api/v1/cache.twirp-client'
import {DownloadOptions} from '../src/options'
import {HttpClientError} from '@actions/http-client'
jest.mock('../src/internal/cacheHttpClient')
jest.mock('../src/internal/cacheUtils')
@@ -95,18 +96,18 @@ test('restore with no cache found', async () => {
test('restore with server error should fail', async () => {
const paths = ['node_modules']
const key = 'node-test'
const logWarningMock = jest.spyOn(core, 'warning')
const logErrorMock = jest.spyOn(core, 'error')
jest
.spyOn(CacheServiceClientJSON.prototype, 'GetCacheEntryDownloadURL')
.mockImplementation(() => {
throw new Error('HTTP Error Occurred')
throw new HttpClientError('HTTP Error Occurred', 500)
})
const cacheKey = await restoreCache(paths, key)
expect(cacheKey).toBe(undefined)
expect(logWarningMock).toHaveBeenCalledTimes(1)
expect(logWarningMock).toHaveBeenCalledWith(
expect(logErrorMock).toHaveBeenCalledTimes(1)
expect(logErrorMock).toHaveBeenCalledWith(
'Failed to restore: HTTP Error Occurred'
)
})
@@ -265,6 +266,7 @@ test('restore with zstd compressed cache found', async () => {
const cacheKey = await restoreCache(paths, key, [], options)
expect(cacheKey).toBe(key)
expect(logInfoMock).toHaveBeenCalledWith(`Cache hit for: ${key}`)
expect(getCacheVersionMock).toHaveBeenCalledWith(
paths,
compressionMethod,
@@ -342,6 +344,9 @@ test('restore with cache found for restore key', async () => {
const cacheKey = await restoreCache(paths, key, restoreKeys, options)
expect(cacheKey).toBe(restoreKeys[0])
expect(logInfoMock).toHaveBeenCalledWith(
`Cache hit for restore-key: ${restoreKeys[0]}`
)
expect(getCacheVersionMock).toHaveBeenCalledWith(
paths,
compressionMethod,
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env node
// Helper script to save cache for e2e testing
import * as cache from '../lib/cache.js'
const [prefix, runId] = process.argv.slice(2)
if (!prefix || !runId) {
console.error('Usage: save-cache.mjs <prefix> <runId>')
process.exit(1)
}
const key = `test-${prefix}-${runId}`
const paths = ['test-cache', '~/test-cache']
console.log(`Saving cache with key: ${key}`)
console.log(`Paths: ${paths.join(', ')}`)
try {
const cacheId = await cache.saveCache(paths, key)
console.log(`Cache saved with ID: ${cacheId}`)
} catch (error) {
console.error('Error saving cache:', error)
process.exit(1)
}
+37 -30
View File
@@ -7,11 +7,12 @@ import * as config from '../src/internal/config'
import {CacheFilename, CompressionMethod} from '../src/internal/constants'
import * as tar from '../src/internal/tar'
import {TypedResponse} from '@actions/http-client/lib/interfaces'
import {HttpClientError} from '@actions/http-client'
import {
ReserveCacheResponse,
ITypedResponseWithError
} from '../src/internal/contracts'
import {HttpClientError} from '@actions/http-client'
import {CacheServiceClientJSON} from '../src/generated/results/api/v1/cache.twirp-client'
jest.mock('../src/internal/cacheHttpClient')
jest.mock('../src/internal/cacheUtils')
@@ -223,46 +224,55 @@ test('save with reserve cache failure should fail', async () => {
test('save with server error should fail', async () => {
const filePath = 'node_modules'
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
const cachePaths = [path.resolve(filePath)]
const logWarningMock = jest.spyOn(core, 'warning')
const cacheId = 4
const reserveCacheMock = jest
.spyOn(cacheHttpClient, 'reserveCache')
.mockImplementation(async () => {
const response: TypedResponse<ReserveCacheResponse> = {
statusCode: 500,
result: {cacheId},
headers: {}
}
return response
})
const logErrorMock = jest.spyOn(core, 'error')
// Mock cache service version to V2
const getCacheServiceVersionMock = jest
.spyOn(config, 'getCacheServiceVersion')
.mockReturnValue('v2')
// Mock V2 CreateCacheEntry to succeed
const createCacheEntryMock = jest
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
.mockReturnValue(
Promise.resolve({
ok: true,
signedUploadUrl: 'https://blob-storage.local?signed=true',
message: ''
})
)
// Mock the FinalizeCacheEntryUpload to succeed (since the error should happen in saveCache)
jest
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
.mockReturnValue(
Promise.resolve({ok: true, entryId: '4', message: 'Success'})
)
const createTarMock = jest.spyOn(tar, 'createTar')
// Mock the saveCache call to throw a server error
const saveCacheMock = jest
.spyOn(cacheHttpClient, 'saveCache')
.mockImplementationOnce(() => {
throw new Error('HTTP Error Occurred')
throw new HttpClientError('HTTP Error Occurred', 500)
})
const compression = CompressionMethod.Zstd
const getCompressionMock = jest
.spyOn(cacheUtils, 'getCompressionMethod')
.mockReturnValueOnce(Promise.resolve(compression))
await saveCache([filePath], primaryKey)
expect(logWarningMock).toHaveBeenCalledTimes(1)
expect(logWarningMock).toHaveBeenCalledWith(
expect(logErrorMock).toHaveBeenCalledTimes(1)
expect(logErrorMock).toHaveBeenCalledWith(
'Failed to save: HTTP Error Occurred'
)
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], {
cacheSize: undefined,
compressionMethod: compression,
enableCrossOsArchive: false
})
expect(createCacheEntryMock).toHaveBeenCalledTimes(1)
const archiveFolder = '/foo/bar'
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
const cachePaths = [path.resolve(filePath)]
expect(createTarMock).toHaveBeenCalledTimes(1)
expect(createTarMock).toHaveBeenCalledWith(
archiveFolder,
@@ -270,13 +280,10 @@ test('save with server error should fail', async () => {
compression
)
expect(saveCacheMock).toHaveBeenCalledTimes(1)
expect(saveCacheMock).toHaveBeenCalledWith(
cacheId,
archiveFile,
'',
undefined
)
expect(getCompressionMock).toHaveBeenCalledTimes(1)
// Restore the getCacheServiceVersion mock to its original state
getCacheServiceVersionMock.mockRestore()
})
test('save with valid inputs uploads a cache', async () => {
+254 -39
View File
@@ -59,39 +59,6 @@ test('save with missing input should fail', async () => {
)
})
test('save with large cache outputs should fail using', async () => {
const paths = 'node_modules'
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
const cachePaths = [path.resolve(paths)]
const createTarMock = jest.spyOn(tar, 'createTar')
const logWarningMock = jest.spyOn(core, 'warning')
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
jest
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
.mockReturnValueOnce(cacheSize)
const compression = CompressionMethod.Gzip
const getCompressionMock = jest
.spyOn(cacheUtils, 'getCompressionMethod')
.mockReturnValueOnce(Promise.resolve(compression))
const cacheId = await saveCache([paths], key)
expect(cacheId).toBe(-1)
expect(logWarningMock).toHaveBeenCalledWith(
'Failed to save: Cache size of ~11264 MB (11811160064 B) is over the 10GB limit, not saving cache.'
)
const archiveFolder = '/foo/bar'
expect(createTarMock).toHaveBeenCalledWith(
archiveFolder,
cachePaths,
compression
)
expect(getCompressionMock).toHaveBeenCalledTimes(1)
})
test('create cache entry failure on non-ok response', async () => {
const paths = ['node_modules']
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
@@ -99,7 +66,7 @@ test('create cache entry failure on non-ok response', async () => {
const createCacheEntryMock = jest
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
.mockResolvedValue({ok: false, signedUploadUrl: ''})
.mockResolvedValue({ok: false, signedUploadUrl: '', message: ''})
const createTarMock = jest.spyOn(tar, 'createTar')
const finalizeCacheEntryMock = jest.spyOn(
@@ -182,7 +149,7 @@ test('save cache fails if a signedUploadURL was not passed', async () => {
const createCacheEntryMock = jest
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
.mockReturnValue(
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL})
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL, message: ''})
)
const createTarMock = jest.spyOn(tar, 'createTar')
@@ -240,7 +207,7 @@ test('finalize save cache failure', async () => {
const createCacheEntryMock = jest
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
.mockReturnValue(
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL})
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL, message: ''})
)
const createTarMock = jest.spyOn(tar, 'createTar')
@@ -260,7 +227,7 @@ test('finalize save cache failure', async () => {
const finalizeCacheEntryMock = jest
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
.mockReturnValue(Promise.resolve({ok: false, entryId: ''}))
.mockReturnValue(Promise.resolve({ok: false, entryId: '', message: ''}))
const cacheId = await saveCache([paths], key, options)
@@ -319,7 +286,7 @@ test('save with valid inputs uploads a cache', async () => {
jest
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
.mockReturnValue(
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL})
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL, message: ''})
)
const saveCacheMock = jest.spyOn(cacheHttpClient, 'saveCache')
@@ -332,7 +299,9 @@ test('save with valid inputs uploads a cache', async () => {
const finalizeCacheEntryMock = jest
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
.mockReturnValue(Promise.resolve({ok: true, entryId: cacheId.toString()}))
.mockReturnValue(
Promise.resolve({ok: true, entryId: cacheId.toString(), message: ''})
)
const expectedCacheId = await saveCache([paths], key)
@@ -360,6 +329,252 @@ test('save with valid inputs uploads a cache', async () => {
expect(expectedCacheId).toBe(cacheId)
})
test('save with extremely large cache should succeed in v2 (no size limit)', async () => {
const paths = 'node_modules'
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
const cachePaths = [path.resolve(paths)]
const signedUploadURL = 'https://blob-storage.local?signed=true'
const createTarMock = jest.spyOn(tar, 'createTar')
// Simulate a very large cache (20GB)
const archiveFileSize = 20 * 1024 * 1024 * 1024 // 20GB
const options: UploadOptions = {
archiveSizeBytes: archiveFileSize,
useAzureSdk: true,
uploadChunkSize: 64 * 1024 * 1024,
uploadConcurrency: 8
}
jest
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
.mockReturnValueOnce(archiveFileSize)
const cacheId = 4
jest
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
.mockReturnValue(
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL, message: ''})
)
const saveCacheMock = jest.spyOn(cacheHttpClient, 'saveCache')
const compression = CompressionMethod.Zstd
const getCompressionMock = jest
.spyOn(cacheUtils, 'getCompressionMethod')
.mockReturnValue(Promise.resolve(compression))
const cacheVersion = cacheUtils.getCacheVersion([paths], compression)
const finalizeCacheEntryMock = jest
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
.mockReturnValue(
Promise.resolve({ok: true, entryId: cacheId.toString(), message: ''})
)
const expectedCacheId = await saveCache([paths], key)
const archiveFolder = '/foo/bar'
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
expect(saveCacheMock).toHaveBeenCalledWith(
-1,
archiveFile,
signedUploadURL,
options
)
expect(createTarMock).toHaveBeenCalledWith(
archiveFolder,
cachePaths,
compression
)
expect(finalizeCacheEntryMock).toHaveBeenCalledWith({
key,
version: cacheVersion,
sizeBytes: archiveFileSize.toString()
})
expect(getCompressionMock).toHaveBeenCalledTimes(1)
expect(expectedCacheId).toBe(cacheId)
})
test('save with create cache entry failure and specific error message', async () => {
const paths = ['node_modules']
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
const infoLogMock = jest.spyOn(core, 'info')
const warningLogMock = jest.spyOn(core, 'warning')
const errorMessage = 'Cache storage quota exceeded for repository'
const createCacheEntryMock = jest
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
.mockResolvedValue({ok: false, signedUploadUrl: '', message: errorMessage})
const createTarMock = jest.spyOn(tar, 'createTar')
const compression = CompressionMethod.Zstd
const getCompressionMock = jest
.spyOn(cacheUtils, 'getCompressionMethod')
.mockResolvedValueOnce(compression)
const archiveFileSize = 1024
jest
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
.mockReturnValueOnce(archiveFileSize)
const cacheId = await saveCache(paths, key)
expect(cacheId).toBe(-1)
expect(warningLogMock).toHaveBeenCalledWith(
`Cache reservation failed: ${errorMessage}`
)
expect(infoLogMock).toHaveBeenCalledWith(
`Failed to save: Unable to reserve cache with key ${key}, another job may be creating this cache.`
)
expect(createCacheEntryMock).toHaveBeenCalledWith({
key,
version: cacheUtils.getCacheVersion(paths, compression)
})
expect(createTarMock).toHaveBeenCalledTimes(1)
expect(getCompressionMock).toHaveBeenCalledTimes(1)
})
test('save with finalize cache entry failure and specific error message', async () => {
const paths = 'node_modules'
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
const cachePaths = [path.resolve(paths)]
const logWarningMock = jest.spyOn(core, 'warning')
const signedUploadURL = 'https://blob-storage.local?signed=true'
const archiveFileSize = 1024
const errorMessage =
'Cache entry finalization failed due to concurrent access'
const options: UploadOptions = {
archiveSizeBytes: archiveFileSize,
useAzureSdk: true,
uploadChunkSize: 64 * 1024 * 1024,
uploadConcurrency: 8
}
const createCacheEntryMock = jest
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
.mockReturnValue(
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL, message: ''})
)
const createTarMock = jest.spyOn(tar, 'createTar')
const saveCacheMock = jest
.spyOn(cacheHttpClient, 'saveCache')
.mockResolvedValue()
const compression = CompressionMethod.Zstd
const getCompressionMock = jest
.spyOn(cacheUtils, 'getCompressionMethod')
.mockReturnValueOnce(Promise.resolve(compression))
const cacheVersion = cacheUtils.getCacheVersion([paths], compression)
jest
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
.mockReturnValueOnce(archiveFileSize)
const finalizeCacheEntryMock = jest
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
.mockReturnValue(
Promise.resolve({ok: false, entryId: '', message: errorMessage})
)
const cacheId = await saveCache([paths], key, options)
expect(createCacheEntryMock).toHaveBeenCalledWith({
key,
version: cacheVersion
})
const archiveFolder = '/foo/bar'
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
expect(createTarMock).toHaveBeenCalledWith(
archiveFolder,
cachePaths,
compression
)
expect(saveCacheMock).toHaveBeenCalledWith(
-1,
archiveFile,
signedUploadURL,
options
)
expect(getCompressionMock).toHaveBeenCalledTimes(1)
expect(finalizeCacheEntryMock).toHaveBeenCalledWith({
key,
version: cacheVersion,
sizeBytes: archiveFileSize.toString()
})
expect(cacheId).toBe(-1)
expect(logWarningMock).toHaveBeenCalledWith(errorMessage)
})
test('save with multiple large caches should succeed in v2 (testing 50GB)', async () => {
const paths = ['large-dataset', 'node_modules', 'build-artifacts']
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
const cachePaths = paths.map(p => path.resolve(p))
const signedUploadURL = 'https://blob-storage.local?signed=true'
const createTarMock = jest.spyOn(tar, 'createTar')
// Simulate an extremely large cache (50GB)
const archiveFileSize = 50 * 1024 * 1024 * 1024 // 50GB
const options: UploadOptions = {
archiveSizeBytes: archiveFileSize,
useAzureSdk: true,
uploadChunkSize: 64 * 1024 * 1024,
uploadConcurrency: 8
}
jest
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
.mockReturnValueOnce(archiveFileSize)
const cacheId = 7
jest
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
.mockReturnValue(
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL, message: ''})
)
const saveCacheMock = jest.spyOn(cacheHttpClient, 'saveCache')
const compression = CompressionMethod.Zstd
const getCompressionMock = jest
.spyOn(cacheUtils, 'getCompressionMethod')
.mockReturnValue(Promise.resolve(compression))
const cacheVersion = cacheUtils.getCacheVersion(paths, compression)
const finalizeCacheEntryMock = jest
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
.mockReturnValue(
Promise.resolve({ok: true, entryId: cacheId.toString(), message: ''})
)
const expectedCacheId = await saveCache(paths, key)
const archiveFolder = '/foo/bar'
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
expect(saveCacheMock).toHaveBeenCalledWith(
-1,
archiveFile,
signedUploadURL,
options
)
expect(createTarMock).toHaveBeenCalledWith(
archiveFolder,
cachePaths,
compression
)
expect(finalizeCacheEntryMock).toHaveBeenCalledWith({
key,
version: cacheVersion,
sizeBytes: archiveFileSize.toString()
})
expect(getCompressionMock).toHaveBeenCalledTimes(1)
expect(expectedCacheId).toBe(cacheId)
})
test('save with non existing path should not save cache using v2 saveCache', async () => {
const path = 'node_modules'
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
+1 -1
View File
@@ -1,5 +1,5 @@
import * as uploadUtils from '../src/internal/uploadUtils'
import {TransferProgressEvent} from '@azure/ms-rest-js'
import {TransferProgressEvent} from '@azure/core-rest-pipeline'
test('upload progress tracked correctly', () => {
const progress = new uploadUtils.UploadProgress(1000)
+371 -817
View File
File diff suppressed because it is too large Load Diff
+25 -15
View File
@@ -1,7 +1,6 @@
{
"name": "@actions/cache",
"version": "4.0.3",
"preview": true,
"version": "6.0.0",
"description": "Actions cache lib",
"keywords": [
"github",
@@ -10,8 +9,15 @@
],
"homepage": "https://github.com/actions/toolkit/tree/main/packages/cache",
"license": "MIT",
"type": "module",
"main": "lib/cache.js",
"types": "lib/cache.d.ts",
"exports": {
".": {
"types": "./lib/cache.d.ts",
"import": "./lib/cache.js"
}
},
"directories": {
"lib": "lib",
"test": "__tests__"
@@ -31,26 +37,30 @@
"scripts": {
"audit-moderate": "npm install && npm audit --json --audit-level=moderate > audit.json",
"test": "echo \"Error: run tests from root\" && exit 1",
"tsc": "tsc"
"tsc": "tsc && cp src/internal/shared/package-version.cjs lib/internal/shared/"
},
"bugs": {
"url": "https://github.com/actions/toolkit/issues"
},
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/exec": "^1.0.1",
"@actions/glob": "^0.1.0",
"@actions/http-client": "^2.1.1",
"@actions/io": "^1.0.1",
"@azure/abort-controller": "^1.1.0",
"@azure/ms-rest-js": "^2.6.0",
"@azure/storage-blob": "^12.13.0",
"@protobuf-ts/plugin": "^2.9.4",
"semver": "^6.3.1"
"@actions/core": "^3.0.0",
"@actions/exec": "^3.0.0",
"@actions/glob": "^0.6.1",
"@actions/http-client": "^4.0.0",
"@actions/io": "^3.0.0",
"@azure/core-rest-pipeline": "^1.22.0",
"@azure/storage-blob": "^12.30.0",
"@protobuf-ts/runtime-rpc": "^2.11.1",
"semver": "^7.7.3"
},
"devDependencies": {
"@types/node": "^22.13.9",
"@types/semver": "^6.0.0",
"@protobuf-ts/plugin": "^2.9.4",
"@types/node": "^25.1.0",
"@types/semver": "^7.7.1",
"typescript": "^5.2.2"
},
"overrides": {
"uri-js": "npm:uri-js-replace@^1.0.1",
"node-fetch": "^3.3.2"
}
}
+86 -25
View File
@@ -1,18 +1,20 @@
import * as core from '@actions/core'
import * as path from 'path'
import * as utils from './internal/cacheUtils'
import * as cacheHttpClient from './internal/cacheHttpClient'
import * as cacheTwirpClient from './internal/shared/cacheTwirpClient'
import {getCacheServiceVersion, isGhes} from './internal/config'
import {DownloadOptions, UploadOptions} from './options'
import {createTar, extractTar, listTar} from './internal/tar'
import * as utils from './internal/cacheUtils.js'
import * as cacheHttpClient from './internal/cacheHttpClient.js'
import * as cacheTwirpClient from './internal/shared/cacheTwirpClient.js'
import {getCacheServiceVersion, isGhes} from './internal/config.js'
import {DownloadOptions, UploadOptions} from './options.js'
import {createTar, extractTar, listTar} from './internal/tar.js'
import {
CreateCacheEntryRequest,
FinalizeCacheEntryUploadRequest,
FinalizeCacheEntryUploadResponse,
GetCacheEntryDownloadURLRequest
} from './generated/results/api/v1/cache'
import {CacheFileSizeLimit} from './internal/constants'
} from './generated/results/api/v1/cache.js'
import {HttpClientError} from '@actions/http-client'
export type {DownloadOptions, UploadOptions}
export class ValidationError extends Error {
constructor(message: string) {
super(message)
@@ -29,6 +31,14 @@ export class ReserveCacheError extends Error {
}
}
export class FinalizeCacheError extends Error {
constructor(message: string) {
super(message)
this.name = 'FinalizeCacheError'
Object.setPrototypeOf(this, FinalizeCacheError.prototype)
}
}
function checkPaths(paths: string[]): void {
if (!paths || paths.length === 0) {
throw new ValidationError(
@@ -57,7 +67,18 @@ function checkKey(key: string): void {
* @returns boolean return true if Actions cache service feature is available, otherwise false
*/
export function isFeatureAvailable(): boolean {
return !!process.env['ACTIONS_CACHE_URL']
const cacheServiceVersion = getCacheServiceVersion()
// Check availability based on cache service version
switch (cacheServiceVersion) {
case 'v2':
// For v2, we need ACTIONS_RESULTS_URL
return !!process.env['ACTIONS_RESULTS_URL']
case 'v1':
default:
// For v1, we only need ACTIONS_CACHE_URL
return !!process.env['ACTIONS_CACHE_URL']
}
}
/**
@@ -186,8 +207,17 @@ async function restoreCacheV1(
if (typedError.name === ValidationError.name) {
throw error
} else {
// Supress all non-validation cache related errors because caching should be optional
core.warning(`Failed to restore: ${(error as Error).message}`)
// warn on cache restore failure and continue build
// Log server errors (5xx) as errors, all other errors as warnings
if (
typedError instanceof HttpClientError &&
typeof typedError.statusCode === 'number' &&
typedError.statusCode >= 500
) {
core.error(`Failed to restore: ${(error as Error).message}`)
} else {
core.warning(`Failed to restore: ${(error as Error).message}`)
}
}
} finally {
// Try to delete the archive to save space
@@ -264,7 +294,12 @@ async function restoreCacheV2(
return undefined
}
core.info(`Cache hit for: ${request.key}`)
const isRestoreKeyMatch = request.key !== response.matchedKey
if (isRestoreKeyMatch) {
core.info(`Cache hit for restore-key: ${response.matchedKey}`)
} else {
core.info(`Cache hit for: ${response.matchedKey}`)
}
if (options?.lookupOnly) {
core.info('Lookup only - skipping download')
@@ -305,7 +340,16 @@ async function restoreCacheV2(
throw error
} else {
// Supress all non-validation cache related errors because caching should be optional
core.warning(`Failed to restore: ${(error as Error).message}`)
// Log server errors (5xx) as errors, all other errors as warnings
if (
typedError instanceof HttpClientError &&
typeof typedError.statusCode === 'number' &&
typedError.statusCode >= 500
) {
core.error(`Failed to restore: ${(error as Error).message}`)
} else {
core.warning(`Failed to restore: ${(error as Error).message}`)
}
}
} finally {
try {
@@ -437,7 +481,16 @@ async function saveCacheV1(
} else if (typedError.name === ReserveCacheError.name) {
core.info(`Failed to save: ${typedError.message}`)
} else {
core.warning(`Failed to save: ${typedError.message}`)
// Log server errors (5xx) as errors, all other errors as warnings
if (
typedError instanceof HttpClientError &&
typeof typedError.statusCode === 'number' &&
typedError.statusCode >= 500
) {
core.error(`Failed to save: ${typedError.message}`)
} else {
core.warning(`Failed to save: ${typedError.message}`)
}
}
} finally {
// Try to delete the archive to save space
@@ -506,15 +559,6 @@ async function saveCacheV2(
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
core.debug(`File Size: ${archiveFileSize}`)
// For GHES, this check will take place in ReserveCache API with enterprise file size limit
if (archiveFileSize > CacheFileSizeLimit && !isGhes()) {
throw new Error(
`Cache size of ~${Math.round(
archiveFileSize / (1024 * 1024)
)} MB (${archiveFileSize} B) is over the 10GB limit, not saving cache.`
)
}
// Set the archive size in the options, will be used to display the upload progress
options.archiveSizeBytes = archiveFileSize
@@ -534,7 +578,10 @@ async function saveCacheV2(
try {
const response = await twirpClient.CreateCacheEntry(request)
if (!response.ok) {
throw new Error('Response was not ok')
if (response.message) {
core.warning(`Cache reservation failed: ${response.message}`)
}
throw new Error(response.message || 'Response was not ok')
}
signedUploadUrl = response.signedUploadUrl
} catch (error) {
@@ -563,6 +610,9 @@ async function saveCacheV2(
core.debug(`FinalizeCacheEntryUploadResponse: ${finalizeResponse.ok}`)
if (!finalizeResponse.ok) {
if (finalizeResponse.message) {
throw new FinalizeCacheError(finalizeResponse.message)
}
throw new Error(
`Unable to finalize cache with key ${key}, another job may be finalizing this cache.`
)
@@ -575,8 +625,19 @@ async function saveCacheV2(
throw error
} else if (typedError.name === ReserveCacheError.name) {
core.info(`Failed to save: ${typedError.message}`)
} else if (typedError.name === FinalizeCacheError.name) {
core.warning(typedError.message)
} else {
core.warning(`Failed to save: ${typedError.message}`)
// Log server errors (5xx) as errors, all other errors as warnings
if (
typedError instanceof HttpClientError &&
typeof typedError.statusCode === 'number' &&
typedError.statusCode >= 500
) {
core.error(`Failed to save: ${typedError.message}`)
} else {
core.warning(`Failed to save: ${typedError.message}`)
}
}
} finally {
// Try to delete the archive to save space
+31 -5
View File
@@ -12,7 +12,7 @@ import type { PartialMessage } from "@protobuf-ts/runtime";
import { reflectionMergePartial } from "@protobuf-ts/runtime";
import { MESSAGE_TYPE } from "@protobuf-ts/runtime";
import { MessageType } from "@protobuf-ts/runtime";
import { CacheMetadata } from "../../entities/v1/cachemetadata";
import { CacheMetadata } from "../../entities/v1/cachemetadata.js";
/**
* @generated from protobuf message github.actions.results.api.v1.CreateCacheEntryRequest
*/
@@ -50,6 +50,12 @@ export interface CreateCacheEntryResponse {
* @generated from protobuf field: string signed_upload_url = 2;
*/
signedUploadUrl: string;
/**
* When !ok, this field may contain a human-readable error message used to create an annotation
*
* @generated from protobuf field: string message = 3;
*/
message: string;
}
/**
* @generated from protobuf message github.actions.results.api.v1.FinalizeCacheEntryUploadRequest
@@ -94,6 +100,12 @@ export interface FinalizeCacheEntryUploadResponse {
* @generated from protobuf field: int64 entry_id = 2;
*/
entryId: string;
/**
* When !ok, this field may contain a human-readable error message used to create an annotation
*
* @generated from protobuf field: string message = 3;
*/
message: string;
}
/**
* @generated from protobuf message github.actions.results.api.v1.GetCacheEntryDownloadURLRequest
@@ -211,11 +223,12 @@ class CreateCacheEntryResponse$Type extends MessageType<CreateCacheEntryResponse
constructor() {
super("github.actions.results.api.v1.CreateCacheEntryResponse", [
{ no: 1, name: "ok", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 2, name: "signed_upload_url", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
{ no: 2, name: "signed_upload_url", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "message", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<CreateCacheEntryResponse>): CreateCacheEntryResponse {
const message = { ok: false, signedUploadUrl: "" };
const message = { ok: false, signedUploadUrl: "", message: "" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<CreateCacheEntryResponse>(this, message, value);
@@ -232,6 +245,9 @@ class CreateCacheEntryResponse$Type extends MessageType<CreateCacheEntryResponse
case /* string signed_upload_url */ 2:
message.signedUploadUrl = reader.string();
break;
case /* string message */ 3:
message.message = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -250,6 +266,9 @@ class CreateCacheEntryResponse$Type extends MessageType<CreateCacheEntryResponse
/* string signed_upload_url = 2; */
if (message.signedUploadUrl !== "")
writer.tag(2, WireType.LengthDelimited).string(message.signedUploadUrl);
/* string message = 3; */
if (message.message !== "")
writer.tag(3, WireType.LengthDelimited).string(message.message);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -333,11 +352,12 @@ class FinalizeCacheEntryUploadResponse$Type extends MessageType<FinalizeCacheEnt
constructor() {
super("github.actions.results.api.v1.FinalizeCacheEntryUploadResponse", [
{ no: 1, name: "ok", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 2, name: "entry_id", kind: "scalar", T: 3 /*ScalarType.INT64*/ }
{ no: 2, name: "entry_id", kind: "scalar", T: 3 /*ScalarType.INT64*/ },
{ no: 3, name: "message", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<FinalizeCacheEntryUploadResponse>): FinalizeCacheEntryUploadResponse {
const message = { ok: false, entryId: "0" };
const message = { ok: false, entryId: "0", message: "" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<FinalizeCacheEntryUploadResponse>(this, message, value);
@@ -354,6 +374,9 @@ class FinalizeCacheEntryUploadResponse$Type extends MessageType<FinalizeCacheEnt
case /* int64 entry_id */ 2:
message.entryId = reader.int64().toString();
break;
case /* string message */ 3:
message.message = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -372,6 +395,9 @@ class FinalizeCacheEntryUploadResponse$Type extends MessageType<FinalizeCacheEnt
/* int64 entry_id = 2; */
if (message.entryId !== "0")
writer.tag(2, WireType.Varint).int64(message.entryId);
/* string message = 3; */
if (message.message !== "")
writer.tag(3, WireType.LengthDelimited).string(message.message);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -5,7 +5,7 @@ import {
FinalizeCacheEntryUploadResponse,
GetCacheEntryDownloadURLRequest,
GetCacheEntryDownloadURLResponse,
} from "./cache";
} from "./cache.js";
//==================================//
// Client Code //
@@ -11,7 +11,7 @@ import type { PartialMessage } from "@protobuf-ts/runtime";
import { reflectionMergePartial } from "@protobuf-ts/runtime";
import { MESSAGE_TYPE } from "@protobuf-ts/runtime";
import { MessageType } from "@protobuf-ts/runtime";
import { CacheScope } from "./cachescope";
import { CacheScope } from "./cachescope.js";
/**
* @generated from protobuf message github.actions.results.entities.v1.CacheMetadata
*/
+8 -8
View File
@@ -7,8 +7,8 @@ import {
} from '@actions/http-client/lib/interfaces'
import * as fs from 'fs'
import {URL} from 'url'
import * as utils from './cacheUtils'
import {uploadCacheArchiveSDK} from './uploadUtils'
import * as utils from './cacheUtils.js'
import {uploadCacheArchiveSDK} from './uploadUtils.js'
import {
ArtifactCacheEntry,
InternalCacheOptions,
@@ -17,25 +17,25 @@ import {
ReserveCacheResponse,
ITypedResponseWithError,
ArtifactCacheList
} from './contracts'
} from './contracts.js'
import {
downloadCacheHttpClient,
downloadCacheHttpClientConcurrent,
downloadCacheStorageSDK
} from './downloadUtils'
} from './downloadUtils.js'
import {
DownloadOptions,
UploadOptions,
getDownloadOptions,
getUploadOptions
} from '../options'
} from '../options.js'
import {
isSuccessStatusCode,
retryHttpClientResponse,
retryTypedResponse
} from './requestUtils'
import {getCacheServiceURL} from './config'
import {getUserAgentString} from './shared/user-agent'
} from './requestUtils.js'
import {getCacheServiceURL} from './config.js'
import {getUserAgentString} from './shared/user-agent.js'
function getCacheApiUrl(resource: string): string {
const baseUrl: string = getCacheServiceURL()
+1 -1
View File
@@ -11,7 +11,7 @@ import {
CacheFilename,
CompressionMethod,
GnuTarPathOnWindows
} from './constants'
} from './constants.js'
const versionSalt = '1.0'
@@ -1,4 +1,4 @@
import {CompressionMethod} from './constants'
import {CompressionMethod} from './constants.js'
import {TypedResponse} from '@actions/http-client/lib/interfaces'
import {HttpClientError} from '@actions/http-client'
+5 -7
View File
@@ -1,18 +1,16 @@
import * as core from '@actions/core'
import {HttpClient, HttpClientResponse} from '@actions/http-client'
import {BlockBlobClient} from '@azure/storage-blob'
import {TransferProgressEvent} from '@azure/ms-rest-js'
import {TransferProgressEvent} from '@azure/core-rest-pipeline'
import * as buffer from 'buffer'
import * as fs from 'fs'
import * as stream from 'stream'
import * as util from 'util'
import * as utils from './cacheUtils'
import {SocketTimeout} from './constants'
import {DownloadOptions} from '../options'
import {retryHttpClientResponse} from './requestUtils'
import {AbortController} from '@azure/abort-controller'
import * as utils from './cacheUtils.js'
import {SocketTimeout} from './constants.js'
import {DownloadOptions} from '../options.js'
import {retryHttpClientResponse} from './requestUtils.js'
/**
* Pipes the body of a HTTP response to a stream
+2 -2
View File
@@ -4,8 +4,8 @@ import {
HttpClientError,
HttpClientResponse
} from '@actions/http-client'
import {DefaultRetryDelay, DefaultRetryAttempts} from './constants'
import {ITypedResponseWithError} from './contracts'
import {DefaultRetryDelay, DefaultRetryAttempts} from './constants.js'
import {ITypedResponseWithError} from './contracts.js'
export function isSuccessStatusCode(statusCode?: number): boolean {
if (!statusCode) {
+27 -9
View File
@@ -1,12 +1,12 @@
import {info, debug} from '@actions/core'
import {getUserAgentString} from './user-agent'
import {NetworkError, UsageError} from './errors'
import {getCacheServiceURL} from '../config'
import {getRuntimeToken} from '../cacheUtils'
import {info, debug, warning} from '@actions/core'
import {getUserAgentString} from './user-agent.js'
import {NetworkError, RateLimitError, UsageError} from './errors.js'
import {getCacheServiceURL} from '../config.js'
import {getRuntimeToken} from '../cacheUtils.js'
import {BearerCredentialHandler} from '@actions/http-client/lib/auth'
import {HttpClient, HttpClientResponse, HttpCodes} from '@actions/http-client'
import {CacheServiceClientJSON} from '../../generated/results/api/v1/cache.twirp-client'
import {maskSecretUrls} from './util'
import {CacheServiceClientJSON} from '../../generated/results/api/v1/cache.twirp-client.js'
import {maskSecretUrls} from './util.js'
// The twirp http client must implement this interface
interface Rpc {
@@ -109,6 +109,21 @@ class CacheServiceClient implements Rpc {
errorMessage = `${errorMessage}: ${body.msg}`
}
// Handle rate limiting - don't retry, just warn and exit
// For more info, see https://docs.github.com/en/actions/reference/limits
if (statusCode === HttpCodes.TooManyRequests) {
const retryAfterHeader = response.message.headers['retry-after']
if (retryAfterHeader) {
const parsedSeconds = parseInt(retryAfterHeader, 10)
if (!isNaN(parsedSeconds) && parsedSeconds > 0) {
warning(
`You've hit a rate limit, your rate limit will reset in ${parsedSeconds} seconds`
)
}
}
throw new RateLimitError(`Rate limited: ${errorMessage}`)
}
} catch (error) {
if (error instanceof SyntaxError) {
debug(`Raw Body: ${rawBody}`)
@@ -118,6 +133,10 @@ class CacheServiceClient implements Rpc {
throw error
}
if (error instanceof RateLimitError) {
throw error
}
if (NetworkError.isNetworkErrorCode(error?.code)) {
throw new NetworkError(error?.code)
}
@@ -162,8 +181,7 @@ class CacheServiceClient implements Rpc {
HttpCodes.BadGateway,
HttpCodes.GatewayTimeout,
HttpCodes.InternalServerError,
HttpCodes.ServiceUnavailable,
HttpCodes.TooManyRequests
HttpCodes.ServiceUnavailable
]
return retryableStatusCodes.includes(statusCode)
+8 -1
View File
@@ -60,7 +60,7 @@ export class NetworkError extends Error {
export class UsageError extends Error {
constructor() {
const message = `Cache storage quota has been hit. Unable to upload any new cache entries. Usage is recalculated every 6-12 hours.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`
const message = `Cache storage quota has been hit. Unable to upload any new cache entries.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`
super(message)
this.name = 'UsageError'
}
@@ -70,3 +70,10 @@ export class UsageError extends Error {
return msg.includes('insufficient usage')
}
}
export class RateLimitError extends Error {
constructor(message: string) {
super(message)
this.name = 'RateLimitError'
}
}
@@ -0,0 +1,7 @@
// This file exists as a CommonJS module to read the version from package.json.
// In an ESM package, using `require()` directly in .ts files requires disabling
// ESLint rules and doesn't work reliably across all Node.js versions.
// By keeping this as a .cjs file, we can use require() naturally and export
// the version for the ESM modules to import.
const packageJson = require('../../../package.json')
module.exports = { version: packageJson.version }
+2 -3
View File
@@ -1,9 +1,8 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const packageJson = require('../../../package.json')
import {version} from './package-version.cjs'
/**
* Ensure that this User Agent String is used in all HTTP calls so that we can monitor telemetry between different versions of this package
*/
export function getUserAgentString(): string {
return `@actions/cache-${packageJson.version}`
return `@actions/cache-${version}`
}
+3 -3
View File
@@ -2,15 +2,15 @@ import {exec} from '@actions/exec'
import * as io from '@actions/io'
import {existsSync, writeFileSync} from 'fs'
import * as path from 'path'
import * as utils from './cacheUtils'
import {ArchiveTool} from './contracts'
import * as utils from './cacheUtils.js'
import {ArchiveTool} from './contracts.js'
import {
CompressionMethod,
SystemTarPathOnWindows,
ArchiveToolType,
TarFilename,
ManifestFilename
} from './constants'
} from './constants.js'
const IS_WINDOWS = process.platform === 'win32'

Some files were not shown because too many files have changed in this diff Show More