Compare commits

...

40 Commits

Author SHA1 Message Date
Joshua Brooks
5a912e8b4a Fix lint and jest issues 2026-05-20 20:18:29 +00:00
Joshua Brooks
b9bf592b98 Update documentation for v6 release 2026-05-20 19:47:09 +00:00
Joshua Brooks
80f777761d Update packages, migrate to ESM 2026-05-20 19:31:39 +00:00
Yang Cao
27d5ce7f10 Merge pull request #1747 from actions/yacaovsnc/update-dependency
Update ts-http-runtime dependency
2026-04-13 11:36:02 -04:00
Yang Cao
f280785d7b licensed changes 2026-04-13 11:30:08 -04:00
Yang Cao
619aeb1606 npm run build generated dist files 2026-04-13 11:17:26 -04:00
Yang Cao
bcf16c2893 Update ts-http-runtime to 0.3.5 2026-04-13 11:14:40 -04:00
Bassem Dghaidi
668228422a Merge pull request #1738 from actions/prepare-v5.0.4
Update dependencies & patch security vulnerabilities
2026-03-18 16:03:49 +01:00
Bassem Dghaidi
e34039626f Update RELEASES 2026-03-18 07:03:52 -07:00
Bassem Dghaidi
8a67110529 Add licenses 2026-03-18 06:34:30 -07:00
Bassem Dghaidi
1865903e1b Update dependencies & patch security vulnerabilities 2026-03-18 06:29:24 -07:00
Ryan Ghadimi
5656298164 Merge pull request #1722 from RyPeck/patch-1
Fix cache key in examples.md for bun.lock
2026-02-24 14:21:04 +00:00
Ryan Peck
4e380d19e1 Fix cache key in examples.md for bun.lock
Updated cache key to use 'bun.lock' instead of 'bun.lockb' for consistency.
2026-02-24 09:11:36 -05:00
Bassem Dghaidi
b7e8d49f17 Merge pull request #1701 from actions/Link-/fix-proxy-integration-tests
Fix proxy integration tests
2026-01-30 16:37:01 +01:00
Bassem Dghaidi
984a21b1cb Add traffic sanity check step 2026-01-30 02:05:51 -08:00
Bassem Dghaidi
acf2f1f76a Fix resolution 2026-01-30 02:03:12 -08:00
Bassem Dghaidi
95a07c5132 Add wait for proxy 2026-01-30 02:00:09 -08:00
Bassem Dghaidi
90e4fae240 Rewrite and simplify 2026-01-30 01:56:07 -08:00
Bassem Dghaidi
b45623637f Merge pull request #1690 from XZTDean/docs/v5-example-update
docs: Update examples to use the latest version
2026-01-29 11:36:35 +01:00
Bassem Dghaidi
bdb94ce451 Merge branch 'main' into docs/v5-example-update 2026-01-29 11:32:26 +01:00
Bassem Dghaidi
cb6ea63971 Merge pull request #1699 from actions/Link-/fix-workflow-permissions
Fix workflow permissions and cleanup workflow names / formatting
2026-01-29 11:31:19 +01:00
Bassem Dghaidi
49bc452f55 Merge branch 'Link-/fix-workflow-permissions' of github.com:actions/cache into Link-/fix-workflow-permissions 2026-01-29 02:27:40 -08:00
Bassem Dghaidi
1e06c934a1 Merge branch 'main' of github.com:actions/cache into Link-/fix-workflow-permissions 2026-01-29 02:27:32 -08:00
Bassem Dghaidi
a838313d22 Merge pull request #1697 from actions/alert-autofix-52
Potential fix for code scanning alert no. 52: Workflow does not contain permissions
2026-01-29 11:26:02 +01:00
Bassem Dghaidi
6e48a73476 Update .github/workflows/pr-opened-workflow.yml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 11:25:11 +01:00
Bassem Dghaidi
9848277c90 Cleanup workflow file names 2026-01-29 02:19:45 -08:00
Bassem Dghaidi
94056f1871 Fix workflow permissions and cleanup 2026-01-29 02:17:35 -08:00
Bassem Dghaidi
b56bb3a202 Fix permissions for workflows/workflow.yml 2026-01-29 02:11:09 -08:00
Bassem Dghaidi
4a6586c009 Merge branch 'main' into alert-autofix-52 2026-01-29 11:04:16 +01:00
Bassem Dghaidi
0de23fd4eb Merge pull request #1696 from actions/Link-/release-docs
Add release instructions and update maintainer docs
2026-01-29 11:03:40 +01:00
Bassem Dghaidi
4e38c8636e Potential fix for code scanning alert no. 52: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-01-29 11:02:45 +01:00
Bassem Dghaidi
24e8b877a8 Add note 2026-01-29 01:58:41 -08:00
Bassem Dghaidi
3baa5f1c7b Update contribution docs 2026-01-29 01:58:05 -08:00
Bassem Dghaidi
cdf6c1fa76 Merge pull request #1695 from actions/Link-/prepare-5.0.3
Prepare v5.0.3 release
2026-01-29 10:43:43 +01:00
Bassem Dghaidi
a1bee22673 Add review for the @actions/http-client license 2026-01-29 01:39:24 -08:00
Bassem Dghaidi
46957638dc Add licensed output 2026-01-29 01:31:38 -08:00
Bassem Dghaidi
dc73bb9f7b Upgrade dependencies and address security warnings
- Bump `@actions/cache` to v5.0.5
- Bump `@actions/core` to v2.0.3
2026-01-29 01:25:30 -08:00
Bassem Dghaidi
345d5c2f76 Add 5.0.3 builds 2026-01-29 01:24:44 -08:00
XZTDean
5b7a9f8ae5 docs: Update other actions in examples to the latest version 2025-12-15 06:34:21 -08:00
XZTDean
de99935c0c docs: Update examples to cache@v5 2025-12-15 06:15:33 -08:00
53 changed files with 82431 additions and 81426 deletions

View File

@@ -1,4 +1,4 @@
name: Check dist/
name: Check dist content
on:
push:
@@ -11,6 +11,9 @@ on:
- '**.md'
workflow_dispatch:
permissions:
contents: read
jobs:
call-check-dist:
name: Check dist/

View File

@@ -1,4 +1,5 @@
name: Close inactive issues
on:
schedule:
- cron: "30 8 * * *"

View File

@@ -1,4 +1,4 @@
name: "Code scanning - action"
name: Code scanning
on:
push:
@@ -6,15 +6,14 @@ on:
schedule:
- cron: '0 19 * * 0'
permissions:
contents: read
security-events: write
jobs:
CodeQL-Build:
# CodeQL runs on ubuntu-latest, windows-latest, and macos-latest
runs-on: ubuntu-latest
permissions:
# required for all workflows
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v5

View File

@@ -1,16 +1,21 @@
name: Assign issue
on:
issues:
types: [opened]
permissions:
issues: write
jobs:
run-action:
runs-on: ubuntu-latest
steps:
- name: Get current oncall
id: oncall
run: |
echo "CURRENT=$(curl --request GET 'https://api.pagerduty.com/oncalls?include[]=users&schedule_ids[]=P5VG2BX&earliest=true' --header 'Authorization: Token token=${{ secrets.PAGERDUTY_TOKEN }}' --header 'Accept: application/vnd.pagerduty+json;version=2' --header 'Content-Type: application/json' | jq -r '.oncalls[].user.name')" >> $GITHUB_OUTPUT
- name: add_assignees
run: |
curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN}}" https://api.github.com/repos/${{github.repository}}/issues/${{ github.event.issue.number}}/assignees -d '{"assignees":["${{steps.oncall.outputs.CURRENT}}"]}'
- name: Get current oncall
id: oncall
run: |
echo "CURRENT=$(curl --request GET 'https://api.pagerduty.com/oncalls?include[]=users&schedule_ids[]=P5VG2BX&earliest=true' --header 'Authorization: Token token=${{ secrets.PAGERDUTY_TOKEN }}' --header 'Accept: application/vnd.pagerduty+json;version=2' --header 'Content-Type: application/json' | jq -r '.oncalls[].user.name')" >> $GITHUB_OUTPUT
- name: add_assignees
run: |
curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN}}" https://api.github.com/repos/${{github.repository}}/issues/${{ github.event.issue.number}}/assignees -d '{"assignees":["${{steps.oncall.outputs.CURRENT}}"]}'

View File

@@ -1,4 +1,4 @@
name: Licensed
name: License check
on:
push:
@@ -9,6 +9,9 @@ on:
- main
workflow_dispatch:
permissions:
contents: read
jobs:
validate-cached-dependency-records:
runs-on: ubuntu-latest

View File

@@ -1,20 +1,25 @@
name: Add Reviewer PR
name: Assign pull request reviewer
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: write
jobs:
run-action:
runs-on: ubuntu-latest
steps:
- name: Get current oncall
id: oncall
run: |
echo "CURRENT=$(curl --request GET 'https://api.pagerduty.com/oncalls?include[]=users&schedule_ids[]=P5VG2BX&earliest=true' --header 'Authorization: Token token=${{ secrets.PAGERDUTY_TOKEN }}' --header 'Accept: application/vnd.pagerduty+json;version=2' --header 'Content-Type: application/json' | jq -r '.oncalls[].user.name')" >> $GITHUB_OUTPUT
- name: Request Review
run: |
curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN}}" https://api.github.com/repos/${{github.repository}}/pulls/${{ github.event.pull_request.number}}/requested_reviewers -d '{"reviewers":["${{steps.oncall.outputs.CURRENT}}"]}'
- name: Add Assignee
run: |
curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN}}" https://api.github.com/repos/${{github.repository}}/issues/${{ github.event.pull_request.number}}/assignees -d '{"assignees":["${{steps.oncall.outputs.CURRENT}}"]}'
- name: Get current oncall
id: oncall
run: |
echo "CURRENT=$(curl --request GET 'https://api.pagerduty.com/oncalls?include[]=users&schedule_ids[]=P5VG2BX&earliest=true' --header 'Authorization: Token token=${{ secrets.PAGERDUTY_TOKEN }}' --header 'Accept: application/vnd.pagerduty+json;version=2' --header 'Content-Type: application/json' | jq -r '.oncalls[].user.name')" >> $GITHUB_OUTPUT
- name: Request Review
run: |
curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN}}" https://api.github.com/repos/${{github.repository}}/pulls/${{ github.event.pull_request.number}}/requested_reviewers -d '{"reviewers":["${{steps.oncall.outputs.CURRENT}}"]}'
- name: Add Assignee
run: |
curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN}}" https://api.github.com/repos/${{github.repository}}/issues/${{ github.event.pull_request.number}}/assignees -d '{"assignees":["${{steps.oncall.outputs.CURRENT}}"]}'

View File

@@ -1,17 +1,17 @@
name: 'Publish Immutable Action Version'
name: Publish immutable action
on:
release:
types: [released]
permissions:
contents: read
id-token: write
packages: write
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
packages: write
steps:
- name: Checking out
uses: actions/checkout@v5

View File

@@ -1,4 +1,5 @@
name: Release new action version
on:
release:
types: [released]
@@ -10,6 +11,7 @@ on:
env:
TAG_NAME: ${{ github.event.inputs.TAG_NAME || github.event.release.tag_name }}
permissions:
contents: write

View File

@@ -10,6 +10,9 @@ on:
- main
- releases/**
permissions:
contents: read
jobs:
# Build and unit test
build:
@@ -57,6 +60,7 @@ jobs:
path: |
test-cache
~/test-cache
test-restore:
needs: test-save
strategy:
@@ -86,15 +90,86 @@ jobs:
runs-on: ubuntu-latest
container:
image: ubuntu:latest
options: --dns 127.0.0.1
options: --cap-add=NET_ADMIN
services:
squid-proxy:
image: ubuntu/squid:latest
ports:
- 3128:3128
env:
http_proxy: http://squid-proxy:3128
https_proxy: http://squid-proxy:3128
steps:
- name: Wait for proxy to be ready
shell: bash
run: |
echo "Waiting for squid proxy to be ready..."
echo "Resolving squid-proxy hostname:"
getent hosts squid-proxy || echo "DNS resolution failed"
for i in $(seq 1 30); do
if (echo > /dev/tcp/squid-proxy/3128) 2>/dev/null; then
echo "Proxy is ready!"
exit 0
fi
echo "Attempt $i: Proxy not ready, waiting..."
sleep 2
done
echo "Proxy failed to become ready"
exit 1
env:
http_proxy: ""
https_proxy: ""
- name: Install dependencies
run: |
apt-get update
apt-get install -y iptables curl
- name: Verify proxy is working
run: |
echo "Testing proxy connectivity..."
curl -s -o /dev/null -w "%{http_code}" --proxy http://squid-proxy:3128 http://github.com || true
echo "Proxy verification complete"
- name: Block direct traffic (enforce proxy usage)
run: |
# Get the squid-proxy container IP
PROXY_IP=$(getent hosts squid-proxy | awk '{ print $1 }')
echo "Proxy IP: $PROXY_IP"
# Allow loopback traffic
iptables -A OUTPUT -o lo -j ACCEPT
# Allow traffic to the proxy container
iptables -A OUTPUT -d $PROXY_IP -j ACCEPT
# Allow established connections
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow DNS (needed for initial resolution)
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
# Block all other outbound traffic (HTTP/HTTPS)
iptables -A OUTPUT -p tcp --dport 80 -j REJECT
iptables -A OUTPUT -p tcp --dport 443 -j REJECT
# Log the iptables rules for debugging
iptables -L -v -n
- name: Verify direct HTTPS is blocked
run: |
echo "Testing that direct HTTPS requests fail..."
if curl --noproxy '*' -s --connect-timeout 5 https://github.com > /dev/null 2>&1; then
echo "ERROR: Direct HTTPS request succeeded - blocking is not working!"
exit 1
else
echo "SUCCESS: Direct HTTPS request was blocked as expected"
fi
echo "Testing that HTTPS through proxy succeeds..."
if curl --proxy http://squid-proxy:3128 -s --connect-timeout 10 https://github.com > /dev/null 2>&1; then
echo "SUCCESS: HTTPS request through proxy succeeded"
else
echo "ERROR: HTTPS request through proxy failed!"
exit 1
fi
- name: Checkout
uses: actions/checkout@v5
- name: Generate files
@@ -104,20 +179,92 @@ jobs:
with:
key: test-proxy-${{ github.run_id }}
path: test-cache
test-proxy-restore:
needs: test-proxy-save
runs-on: ubuntu-latest
container:
image: ubuntu:latest
options: --dns 127.0.0.1
options: --cap-add=NET_ADMIN
services:
squid-proxy:
image: ubuntu/squid:latest
ports:
- 3128:3128
env:
http_proxy: http://squid-proxy:3128
https_proxy: http://squid-proxy:3128
steps:
- name: Wait for proxy to be ready
shell: bash
run: |
echo "Waiting for squid proxy to be ready..."
echo "Resolving squid-proxy hostname:"
getent hosts squid-proxy || echo "DNS resolution failed"
for i in $(seq 1 30); do
if (echo > /dev/tcp/squid-proxy/3128) 2>/dev/null; then
echo "Proxy is ready!"
exit 0
fi
echo "Attempt $i: Proxy not ready, waiting..."
sleep 2
done
echo "Proxy failed to become ready"
exit 1
env:
http_proxy: ""
https_proxy: ""
- name: Install dependencies
run: |
apt-get update
apt-get install -y iptables curl
- name: Verify proxy is working
run: |
echo "Testing proxy connectivity..."
curl -s -o /dev/null -w "%{http_code}" --proxy http://squid-proxy:3128 http://github.com || true
echo "Proxy verification complete"
- name: Block direct traffic (enforce proxy usage)
run: |
# Get the squid-proxy container IP
PROXY_IP=$(getent hosts squid-proxy | awk '{ print $1 }')
echo "Proxy IP: $PROXY_IP"
# Allow loopback traffic
iptables -A OUTPUT -o lo -j ACCEPT
# Allow traffic to the proxy container
iptables -A OUTPUT -d $PROXY_IP -j ACCEPT
# Allow established connections
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow DNS (needed for initial resolution)
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
# Block all other outbound traffic (HTTP/HTTPS)
iptables -A OUTPUT -p tcp --dport 80 -j REJECT
iptables -A OUTPUT -p tcp --dport 443 -j REJECT
# Log the iptables rules for debugging
iptables -L -v -n
- name: Verify direct HTTPS is blocked
run: |
echo "Testing that direct HTTPS requests fail..."
if curl --noproxy '*' -s --connect-timeout 5 https://github.com > /dev/null 2>&1; then
echo "ERROR: Direct HTTPS request succeeded - blocking is not working!"
exit 1
else
echo "SUCCESS: Direct HTTPS request was blocked as expected"
fi
echo "Testing that HTTPS through proxy succeeds..."
if curl --proxy http://squid-proxy:3128 -s --connect-timeout 10 https://github.com > /dev/null 2>&1; then
echo "SUCCESS: HTTPS request through proxy succeeded"
else
echo "ERROR: HTTPS request through proxy failed!"
exit 1
fi
- name: Checkout
uses: actions/checkout@v5
- name: Restore cache

View File

@@ -27,4 +27,5 @@ reviewed:
- fs.realpath # ISC
- glob # ISC
- prettier # MIT
- lodash # MIT
- lodash # MIT
- "@actions/http-client" # MIT

View File

@@ -1,6 +1,6 @@
---
name: "@actions/cache"
version: 5.0.3
version: 5.0.5
type: npm
summary: Actions cache lib
homepage: https://github.com/actions/toolkit/tree/main/packages/cache

View File

@@ -1,20 +0,0 @@
---
name: "@actions/core"
version: 1.11.1
type: npm
summary: Actions core lib
homepage: https://github.com/actions/toolkit/tree/main/packages/core
license: mit
licenses:
- sources: LICENSE.md
text: |-
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
---
name: "@actions/core"
version: 2.0.1
version: 2.0.3
type: npm
summary: Actions core lib
homepage: https://github.com/actions/toolkit/tree/main/packages/core

View File

@@ -1,20 +0,0 @@
---
name: "@actions/exec"
version: 1.1.1
type: npm
summary: Actions exec lib
homepage: https://github.com/actions/toolkit/tree/main/packages/exec
license: mit
licenses:
- sources: LICENSE.md
text: |-
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
---
name: "@actions/glob"
version: 0.5.0
version: 0.5.1
type: npm
summary: Actions glob lib
homepage: https://github.com/actions/toolkit/tree/main/packages/glob

View File

@@ -1,32 +0,0 @@
---
name: "@actions/http-client"
version: 2.2.3
type: npm
summary: Actions Http Client
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client
license: mit
licenses:
- sources: LICENSE
text: |
Actions Http Client for Node.js
Copyright (c) GitHub, Inc.
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
notices: []

View File

@@ -1,10 +1,10 @@
---
name: "@actions/http-client"
version: 3.0.1
version: 3.0.2
type: npm
summary: Actions Http Client
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client
license: mit
license: other
licenses:
- sources: LICENSE
text: |

View File

@@ -1,20 +0,0 @@
---
name: "@actions/io"
version: 1.1.3
type: npm
summary: Actions io lib
homepage: https://github.com/actions/toolkit/tree/main/packages/io
license: mit
licenses:
- sources: LICENSE.md
text: |-
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
---
name: "@azure/storage-blob"
version: 12.29.1
version: 12.30.0
type: npm
summary: Microsoft Azure Storage SDK for JavaScript - Blob
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/storage/storage-blob/

View File

@@ -1,6 +1,6 @@
---
name: "@azure/storage-common"
version: 12.1.1
version: 12.2.0
type: npm
summary: Azure Storage Common Client Library for JavaScript
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/storage/storage-internal-avro/

View File

@@ -1,6 +1,6 @@
---
name: "@typespec/ts-http-runtime"
version: 0.3.2
version: 0.3.5
type: npm
summary: Isomorphic client library for making HTTP requests in node.js and browser.
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/ts-http-runtime/

View File

@@ -1,30 +1,32 @@
---
name: "@fastify/busboy"
version: 2.1.1
name: fast-xml-builder
version: 1.1.4
type: npm
summary: A streaming parser for HTML form data for node.js
summary: Build XML from JSON without C/C++ based libraries
homepage:
license: mit
licenses:
- sources: LICENSE
text: |-
Copyright Brian White. All rights reserved.
text: |
MIT License
Copyright (c) 2026 Natural Intelligence
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
---
name: fast-xml-parser
version: 5.3.3
version: 5.5.6
type: npm
summary: Validate XML, Parse XML, Build XML without C/C++ based libraries
homepage:

View File

@@ -1,9 +1,9 @@
---
name: minimatch
version: 3.1.2
version: 3.1.5
type: npm
summary: a glob matcher in javascript
homepage:
homepage:
license: isc
licenses:
- sources: LICENSE

View File

@@ -0,0 +1,32 @@
---
name: path-expression-matcher
version: 1.1.3
type: npm
summary: Efficient path tracking and pattern matching for XML/JSON parsers
homepage: https://github.com/NaturalIntelligence/path-expression-matcher#readme
license: mit
licenses:
- sources: LICENSE
text: |
MIT License
Copyright (c) 2024
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
---
name: undici
version: 5.29.0
version: 6.24.1
type: npm
summary: An HTTP/1.1 client, written from scratch for Node.js
homepage: https://undici.nodejs.org

View File

@@ -16,7 +16,7 @@ Please note that this project is released with a [Contributor Code of Conduct][c
1. [Fork][fork] and clone the repository
2. Configure and install the dependencies: `npm install`
3. Make sure the tests pass on your machine: `npm run test`
4. Create a new branch: `git checkout -b my-branch-name`
4. Create a new branch: `git switch -c my-branch-name`
5. Make your change, add tests, and make sure the tests still pass
6. Push to your fork and [submit a pull request][pr]
7. Pat your self on the back and wait for your pull request to be reviewed and merged.

View File

@@ -18,7 +18,7 @@ See ["Caching dependencies to speed up workflows"](https://docs.github.com/en/ac
### ⚠️ Important changes
> [!IMPORTANT]
> `actions/cache@v5` runs on the Node.js 24 runtime and requires a minimum Actions Runner version of `2.327.1`.
> `actions/cache@v6` runs on the Node.js 24 runtime and requires a minimum Actions Runner version of `2.327.1`.
> If you are using self-hosted runners, ensure they are updated before upgrading.
The cache backend service has been rewritten from the ground up for improved performance and reliability. [actions/cache](https://github.com/actions/cache) now integrates with the new cache service (v2) APIs.
@@ -38,6 +38,11 @@ Upgrading to the recommended versions will not break your workflows.
Read more about the change & access the migration guide: [reference to the announcement](https://github.com/actions/cache/discussions/1510).
### v6
* Updated `@actions/cache`, `@actions/core`, `@actions/exec` to latest major versions
* Migrated to ESM module system
### v5
* Updated to node 24
@@ -78,7 +83,7 @@ Create a workflow `.yml` file in your repository's `.github/workflows` directory
If you are using this inside a container, a POSIX-compliant `tar` needs to be included and accessible from the execution path.
Note: `actions/cache@v5` runs on Node.js 24 and requires a minimum Actions Runner version of `2.327.1`.
Note: `actions/cache@v6` runs on Node.js 24 and requires a minimum Actions Runner version of `2.327.1`.
If you are using a `self-hosted` Windows runner, `GNU tar` and `zstd` are required for [Cross-OS caching](https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cross-os-cache) to work. They are also recommended to be installed in general so the performance is on par with `hosted` Windows runners.
@@ -123,11 +128,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Primes
id: cache-primes
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: prime-numbers
key: ${{ runner.os }}-primes
@@ -154,11 +159,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Restore cached Primes
id: cache-primes-restore
uses: actions/cache/restore@v4
uses: actions/cache/restore@v6
with:
path: |
path/to/dependencies
@@ -169,7 +174,7 @@ jobs:
.
- name: Save Primes
id: cache-primes-save
uses: actions/cache/save@v4
uses: actions/cache/save@v6
with:
path: |
path/to/dependencies
@@ -224,7 +229,7 @@ A cache key can include any of the contexts, functions, literals, and operators
For example, using the [`hashFiles`](https://docs.github.com/en/actions/learn-github-actions/expressions#hashfiles) function allows you to create a new cache when dependencies change.
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
path/to/dependencies
@@ -242,7 +247,7 @@ Additionally, you can use arbitrary command output in a cache key, such as a dat
echo "date=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_OUTPUT
shell: bash
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: path/to/dependencies
key: ${{ runner.os }}-${{ steps.get-date.outputs.date }}-${{ hashFiles('**/lockfiles') }}
@@ -262,9 +267,9 @@ Example:
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/cache@v4
- uses: actions/cache@v6
id: cache
with:
path: path/to/dependencies
@@ -292,11 +297,11 @@ jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Primes
id: cache-primes
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: prime-numbers
key: primes
@@ -307,7 +312,7 @@ jobs:
- name: Cache Numbers
id: cache-numbers
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: numbers
key: primes
@@ -319,11 +324,11 @@ jobs:
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Primes
id: cache-primes
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: prime-numbers
key: primes

View File

@@ -1,7 +1,47 @@
# Releases
## How to prepare a release
> [!NOTE]
> Relevant for maintainers with write access only.
1. Switch to a new branch from `main`.
1. Run `npm test` to ensure all tests are passing.
1. Update the version in [`package.json`](package.json).
1. Run `npm run build` to update the compiled files.
1. Update this [`RELEASES.md`](RELEASES.md) with the new version and changes in the `## Changelog` section.
1. Run `licensed cache` to update the license report.
1. Run `licensed status` and resolve any warnings by updating the [`.licensed.yml`](.licensed.yml) file with the exceptions.
1. Commit your changes and push your branch upstream.
1. Open a pull request against `main` and get it reviewed and merged.
1. Draft a new release https://github.com/actions/cache/releases use the same version number used in `package.json`
1. Create a new tag with the version number.
1. Auto generate release notes and update them to match the changes you made in `RELEASES.md`.
1. Toggle the set as the latest release option.
1. Publish the release.
1. Navigate to https://github.com/actions/cache/actions/workflows/release-new-action-version.yml
1. There should be a workflow run queued with the same version number.
1. Approve the run to publish the new version and update the major tags for this action.
## Changelog
### 6.0.0
- Updated `@actions/cache` to ^6.0.1, `@actions/core` to ^3.0.1, `@actions/exec` to ^3.0.0, `@actions/io` to ^3.0.2
- Migrated to ESM module system
- Upgraded Jest to v30 and test infrastructure to be ESM compatible
### 5.0.4
- Bump `minimatch` to v3.1.5 (fixes ReDoS via globstar patterns)
- Bump `undici` to v6.24.1 (WebSocket decompression bomb protection, header validation fixes)
- Bump `fast-xml-parser` to v5.5.6
### 5.0.3
- Bump `@actions/cache` to v5.0.5 (Resolves: https://github.com/actions/cache/security/dependabot/33)
- Bump `@actions/core` to v2.0.3
### 5.0.2
- Bump `@actions/cache` to v5.0.3 [#1692](https://github.com/actions/cache/pull/1692)

View File

@@ -1,25 +1,66 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { afterAll, beforeEach, expect, jest, test } from "@jest/globals";
import { Events, RefKey } from "../src/constants";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
getInput: jest.fn((name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.mock("@actions/core");
jest.mock("@actions/cache");
// Mock @actions/cache
jest.unstable_mockModule("@actions/cache", () => ({
restoreCache: jest.fn(),
saveCache: jest.fn(),
isFeatureAvailable: jest.fn(() => true),
ReserveCacheError: class ReserveCacheError extends Error {
constructor(message: string) {
super(message);
this.name = "ReserveCacheError";
}
}
}));
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, RefKey } = await import("../src/constants");
const actionUtils = await import("../src/utils/actionUtils");
const testUtils = await import("../src/utils/testUtils");
let pristineEnv: NodeJS.ProcessEnv;
beforeAll(() => {
pristineEnv = process.env;
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
return jest.requireActual("@actions/core").getInput(name, options);
});
});
beforeEach(() => {
jest.resetModules();
process.env = pristineEnv;
pristineEnv = { ...process.env };
jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] ||
"";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
return val.trim();
}
);
delete process.env[Events.Key];
delete process.env[RefKey];
});
@@ -47,74 +88,46 @@ test("isGhes returns false when server url is github.com", () => {
});
test("isExactKeyMatch with undefined cache key returns false", () => {
const key = "linux-rust";
const cacheKey = undefined;
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
expect(actionUtils.isExactKeyMatch("linux-rust", undefined)).toBe(false);
});
test("isExactKeyMatch with empty cache key returns false", () => {
const key = "linux-rust";
const cacheKey = "";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
expect(actionUtils.isExactKeyMatch("linux-rust", "")).toBe(false);
});
test("isExactKeyMatch with different keys returns false", () => {
const key = "linux-rust";
const cacheKey = "linux-";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
expect(actionUtils.isExactKeyMatch("linux-rust", "linux-")).toBe(false);
});
test("isExactKeyMatch with different key accents returns false", () => {
const key = "linux-áccent";
const cacheKey = "linux-accent";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
expect(actionUtils.isExactKeyMatch("linux-áccent", "linux-accent")).toBe(
false
);
});
test("isExactKeyMatch with same key returns true", () => {
const key = "linux-rust";
const cacheKey = "linux-rust";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(true);
expect(actionUtils.isExactKeyMatch("linux-rust", "linux-rust")).toBe(true);
});
test("isExactKeyMatch with same key and different casing returns true", () => {
const key = "linux-rust";
const cacheKey = "LINUX-RUST";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(true);
expect(actionUtils.isExactKeyMatch("linux-rust", "LINUX-RUST")).toBe(true);
});
test("logWarning logs a message with a warning prefix", () => {
const message = "A warning occurred.";
const infoMock = jest.spyOn(core, "info");
actionUtils.logWarning(message);
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`);
expect(core.info).toHaveBeenCalledWith(`[warning]${message}`);
});
test("isValidEvent returns false for event that does not have a branch or tag", () => {
const event = "foo";
process.env[Events.Key] = event;
const isValidEvent = actionUtils.isValidEvent();
expect(isValidEvent).toBe(false);
process.env[Events.Key] = "foo";
expect(actionUtils.isValidEvent()).toBe(false);
});
test("isValidEvent returns true for event that has a ref", () => {
const event = Events.Push;
process.env[Events.Key] = event;
process.env[Events.Key] = Events.Push;
process.env[RefKey] = "ref/heads/feature";
const isValidEvent = actionUtils.isValidEvent();
expect(isValidEvent).toBe(true);
expect(actionUtils.isValidEvent()).toBe(true);
});
test("getInputAsArray returns empty array if not required and missing", () => {
@@ -124,7 +137,7 @@ test("getInputAsArray returns empty array if not required and missing", () => {
test("getInputAsArray throws error if required and missing", () => {
expect(() =>
actionUtils.getInputAsArray("foo", { required: true })
).toThrowError();
).toThrow();
});
test("getInputAsArray handles single line correctly", () => {
@@ -180,7 +193,7 @@ test("getInputAsInt returns undefined if input is invalid or NaN", () => {
test("getInputAsInt throws if required and value missing", () => {
expect(() =>
actionUtils.getInputAsInt("undefined", { required: true })
).toThrowError();
).toThrow();
});
test("getInputAsBool returns false if input not set", () => {
@@ -200,68 +213,65 @@ test("getInputAsBool returns false if input is invalid or NaN", () => {
test("getInputAsBool throws if required and value missing", () => {
expect(() =>
actionUtils.getInputAsBool("undefined2", { required: true })
).toThrowError();
).toThrow();
});
test("isCacheFeatureAvailable for ac enabled", () => {
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => true);
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
expect(actionUtils.isCacheFeatureAvailable()).toBe(true);
});
test("isCacheFeatureAvailable for ac disabled on GHES", () => {
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => false);
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
const message = `Cache action is only supported on GHES version >= 3.5. If you are on version >=3.5 Please check with GHES admin if Actions cache service is enabled or not.
Otherwise please upgrade to GHES version >= 3.5 and If you are also using Github Connect, please unretire the actions/cache namespace before upgrade (see https://docs.github.com/en/enterprise-server@3.5/admin/github-actions/managing-access-to-actions-from-githubcom/enabling-automatic-access-to-githubcom-actions-using-github-connect#automatic-retirement-of-namespaces-for-actions-accessed-on-githubcom)`;
const infoMock = jest.spyOn(core, "info");
try {
process.env["GITHUB_SERVER_URL"] = "http://example.com";
expect(actionUtils.isCacheFeatureAvailable()).toBe(false);
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`);
expect(core.info).toHaveBeenCalledWith(`[warning]${message}`);
} finally {
delete process.env["GITHUB_SERVER_URL"];
}
});
test("isCacheFeatureAvailable for ac disabled on dotcom", () => {
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => false);
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
const message =
"An internal error has occurred in cache backend. Please check https://www.githubstatus.com/ for any ongoing issue in actions.";
const infoMock = jest.spyOn(core, "info");
try {
process.env["GITHUB_SERVER_URL"] = "http://github.com";
expect(actionUtils.isCacheFeatureAvailable()).toBe(false);
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`);
expect(core.info).toHaveBeenCalledWith(`[warning]${message}`);
} finally {
delete process.env["GITHUB_SERVER_URL"];
}
});
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is not defined", async () => {
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is not defined", () => {
delete process.env["GITHUB_SERVER_URL"];
expect(actionUtils.isGhes()).toBeFalsy();
});
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is set to github.com", async () => {
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is set to github.com", () => {
process.env["GITHUB_SERVER_URL"] = "https://github.com";
expect(actionUtils.isGhes()).toBeFalsy();
});
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL", async () => {
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL", () => {
process.env["GITHUB_SERVER_URL"] = "https://contoso.ghe.com";
expect(actionUtils.isGhes()).toBeFalsy();
});
test("isGhes returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix", async () => {
test("isGhes returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix", () => {
process.env["GITHUB_SERVER_URL"] = "https://mock-github.localhost";
expect(actionUtils.isGhes()).toBeFalsy();
});
test("isGhes returns true when the GITHUB_SERVER_URL environment variable is set to some other URL", async () => {
test("isGhes returns true when the GITHUB_SERVER_URL environment variable is set to some other URL", () => {
process.env["GITHUB_SERVER_URL"] = "https://src.onpremise.fabrikam.com";
expect(actionUtils.isGhes()).toBeTruthy();
});

View File

@@ -1,50 +1,66 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { afterEach, beforeEach, expect, jest, test } from "@jest/globals";
import { Events, RefKey } from "../src/constants";
import { restoreRun } from "../src/restoreImpl";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isExactKeyMatch(key, cacheResult);
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
getInput: jest.fn((name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
);
return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
(name, options) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.getInputAsArray(name, options);
// Mock @actions/cache
jest.unstable_mockModule("@actions/cache", () => ({
restoreCache: jest.fn(),
saveCache: jest.fn(),
isFeatureAvailable: jest.fn(() => true),
ReserveCacheError: class ReserveCacheError extends Error {
constructor(message: string) {
super(message);
this.name = "ReserveCacheError";
}
);
}
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
(name, options) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.getInputAsBool(name, options);
}
);
});
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, RefKey } = await import("../src/constants");
const { restoreRun } = await import("../src/restoreImpl");
const testUtils = await import("../src/utils/testUtils");
beforeEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] ||
"";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
return val.trim();
}
);
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env[Events.Key] = Events.Push;
process.env[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
});
afterEach(() => {
@@ -62,19 +78,12 @@ test("restore with no cache found", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(cache.restoreCache).toHaveBeenCalledWith(
[path],
key,
[],
@@ -84,12 +93,10 @@ test("restore with no cache found", async () => {
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}`
);
});
@@ -105,19 +112,12 @@ test("restore with restore keys and no cache found", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(cache.restoreCache).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
@@ -127,12 +127,10 @@ test("restore with restore keys and no cache found", async () => {
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}, ${restoreKey}`
);
});
@@ -146,20 +144,12 @@ test("restore with cache found for key", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(key);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(key);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(cache.restoreCache).toHaveBeenCalledWith(
[path],
key,
[],
@@ -169,15 +159,13 @@ test("restore with cache found for key", async () => {
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", key);
expect(stateMock).toHaveBeenCalledTimes(2);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledWith("CACHE_RESULT", key);
expect(core.saveState).toHaveBeenCalledTimes(2);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
expect(core.info).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("restore with cache found for restore key", async () => {
@@ -191,39 +179,20 @@ test("restore with cache found for restore key", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(restoreKey);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(restoreKey);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", restoreKey);
expect(stateMock).toHaveBeenCalledTimes(2);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(infoMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledWith("CACHE_RESULT", restoreKey);
expect(core.saveState).toHaveBeenCalledTimes(2);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
expect(core.info).toHaveBeenCalledWith(
`Cache restored from key: ${restoreKey}`
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("Fail restore when fail on cache miss is enabled and primary + restore keys not found", async () => {
@@ -237,35 +206,17 @@ test("Fail restore when fail on cache miss is enabled and primary + restore keys
failOnCacheMiss: true
});
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.setOutput).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledWith(
`Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${key}`
);
expect(failedMock).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledTimes(1);
});
test("restore when fail on cache miss is enabled and primary key doesn't match restored key", async () => {
@@ -279,40 +230,20 @@ test("restore when fail on cache miss is enabled and primary key doesn't match r
failOnCacheMiss: true
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(restoreKey);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(restoreKey);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", restoreKey);
expect(stateMock).toHaveBeenCalledTimes(2);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(infoMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledWith("CACHE_RESULT", restoreKey);
expect(core.saveState).toHaveBeenCalledTimes(2);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
expect(core.info).toHaveBeenCalledWith(
`Cache restored from key: ${restoreKey}`
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("restore with fail on cache miss disabled and no cache found", async () => {
@@ -326,33 +257,15 @@ test("restore with fail on cache miss disabled and no cache found", async () =>
failOnCacheMiss: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledTimes(1);
expect(infoMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledTimes(1);
expect(core.info).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}, ${restoreKey}`
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});

View File

@@ -1,51 +1,68 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { afterEach, beforeEach, expect, jest, test } from "@jest/globals";
import { Events, Inputs, RefKey } from "../src/constants";
import { restoreImpl } from "../src/restoreImpl";
import { StateProvider } from "../src/stateProvider";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isExactKeyMatch(key, cacheResult);
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
getInput: jest.fn((name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
);
return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
(name, options) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.getInputAsArray(name, options);
// Mock @actions/cache
jest.unstable_mockModule("@actions/cache", () => ({
restoreCache: jest.fn(),
saveCache: jest.fn(),
isFeatureAvailable: jest.fn(() => true),
ReserveCacheError: class ReserveCacheError extends Error {
constructor(message: string) {
super(message);
this.name = "ReserveCacheError";
}
);
}
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
(name, options) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.getInputAsBool(name, options);
}
);
});
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, Inputs, RefKey } = await import("../src/constants");
const { restoreImpl } = await import("../src/restoreImpl");
const { StateProvider } = await import("../src/stateProvider");
const testUtils = await import("../src/utils/testUtils");
beforeEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] ||
"";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
return val.trim();
}
);
(core.getState as jest.Mock).mockReturnValue("");
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env[Events.Key] = Events.Push;
process.env[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
});
afterEach(() => {
@@ -55,52 +72,37 @@ afterEach(() => {
});
test("restore with invalid event outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const invalidEvent = "commit_comment";
process.env[Events.Key] = invalidEvent;
delete process.env[RefKey];
await restoreImpl(new StateProvider());
expect(logWarningMock).toHaveBeenCalledWith(
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
expect(core.info).toHaveBeenCalledWith(
`[warning]Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("restore without AC available should no-op", async () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => false
);
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
});
test("restore on GHES without AC available should no-op", async () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => false
);
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
});
test("restore on GHES with AC available ", async () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
const path = "node_modules";
const key = "node-test";
testUtils.setInputs({
@@ -109,20 +111,12 @@ test("restore on GHES with AC available ", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(key);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(key);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(cache.restoreCache).toHaveBeenCalledWith(
[path],
key,
[],
@@ -132,32 +126,26 @@ test("restore on GHES with AC available ", async () => {
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
expect(core.info).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("restore with no path should fail", async () => {
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
// this input isn't necessary for restore b/c tarball contains entries relative to workspace
expect(failedMock).not.toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(core.setFailed).not.toHaveBeenCalledWith(
"Input required and not supplied: path"
);
});
test("restore with no key", async () => {
testUtils.setInput(Inputs.Path, "node_modules");
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledWith(
"Input required and not supplied: key"
);
});
@@ -172,46 +160,32 @@ test("restore with too many keys should fail", async () => {
restoreKeys,
enableCrossOsArchive: false
});
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
restoreKeys,
{
lookupOnly: false
},
false
(cache.restoreCache as jest.Mock).mockRejectedValue(
new Error("Key Validation Error: Keys are limited to a maximum of 10.")
);
expect(failedMock).toHaveBeenCalledWith(
await restoreImpl(new StateProvider());
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledWith(
`Key Validation Error: Keys are limited to a maximum of 10.`
);
});
test("restore with large key should fail", async () => {
const path = "node_modules";
const key = "foo".repeat(512); // Over the 512 character limit
const key = "foo".repeat(512);
testUtils.setInputs({
path: path,
key,
enableCrossOsArchive: false
});
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[],
{
lookupOnly: false
},
false
(cache.restoreCache as jest.Mock).mockRejectedValue(
new Error(
`Key Validation Error: ${key} cannot be larger than 512 characters.`
)
);
expect(failedMock).toHaveBeenCalledWith(
await restoreImpl(new StateProvider());
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledWith(
`Key Validation Error: ${key} cannot be larger than 512 characters.`
);
});
@@ -224,20 +198,12 @@ test("restore with invalid key should fail", async () => {
key,
enableCrossOsArchive: false
});
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[],
{
lookupOnly: false
},
false
(cache.restoreCache as jest.Mock).mockRejectedValue(
new Error(`Key Validation Error: ${key} cannot contain commas.`)
);
expect(failedMock).toHaveBeenCalledWith(
await restoreImpl(new StateProvider());
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledWith(
`Key Validation Error: ${key} cannot contain commas.`
);
});
@@ -251,32 +217,14 @@ test("restore with no cache found", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}`
);
});
@@ -292,32 +240,14 @@ test("restore with restore keys and no cache found", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}, ${restoreKey}`
);
});
@@ -331,35 +261,16 @@ test("restore with cache found for key", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(key);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(key);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
expect(core.info).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("restore with cache found for restore key", async () => {
@@ -373,36 +284,18 @@ test("restore with cache found for restore key", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(restoreKey);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(restoreKey);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(infoMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
expect(core.info).toHaveBeenCalledWith(
`Cache restored from key: ${restoreKey}`
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("restore with lookup-only set", async () => {
@@ -414,20 +307,12 @@ test("restore with lookup-only set", async () => {
lookupOnly: true
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(key);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(key);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(cache.restoreCache).toHaveBeenCalledWith(
[path],
key,
[],
@@ -437,31 +322,30 @@ test("restore with lookup-only set", async () => {
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", key);
expect(stateMock).toHaveBeenCalledTimes(2);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
expect(infoMock).toHaveBeenCalledWith(
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledWith("CACHE_RESULT", key);
expect(core.saveState).toHaveBeenCalledTimes(2);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
expect(core.info).toHaveBeenCalledWith(
`Cache found and can be restored from key: ${key}`
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("restore failure with earlyExit should call process exit", async () => {
testUtils.setInput(Inputs.Path, "node_modules");
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
const processExitMock = jest.spyOn(process, "exit").mockImplementation();
const processExitMock = jest
.spyOn(process, "exit")
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.mockImplementation((() => {}) as any);
// call restoreImpl with `earlyExit` set to true
await restoreImpl(new StateProvider(), true);
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledWith(
"Input required and not supplied: key"
);
expect(processExitMock).toHaveBeenCalledWith(1);
processExitMock.mockRestore();
});

View File

@@ -1,51 +1,66 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { afterEach, beforeEach, expect, jest, test } from "@jest/globals";
import { Events, RefKey } from "../src/constants";
import { restoreOnlyRun } from "../src/restoreImpl";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isExactKeyMatch(key, cacheResult);
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
getInput: jest.fn((name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
);
return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
(name, options) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.getInputAsArray(name, options);
// Mock @actions/cache
jest.unstable_mockModule("@actions/cache", () => ({
restoreCache: jest.fn(),
saveCache: jest.fn(),
isFeatureAvailable: jest.fn(() => true),
ReserveCacheError: class ReserveCacheError extends Error {
constructor(message: string) {
super(message);
this.name = "ReserveCacheError";
}
);
}
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsBool(name, options);
}
);
});
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, RefKey } = await import("../src/constants");
const { restoreOnlyRun } = await import("../src/restoreImpl");
const testUtils = await import("../src/utils/testUtils");
beforeEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] ||
"";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
return val.trim();
}
);
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env[Events.Key] = Events.Push;
process.env[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
});
afterEach(() => {
@@ -63,19 +78,12 @@ test("restore with no cache found", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreOnlyRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(cache.restoreCache).toHaveBeenCalledWith(
[path],
key,
[],
@@ -85,11 +93,10 @@ test("restore with no cache found", async () => {
false
);
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
expect(outputMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(core.setOutput).toHaveBeenCalledWith("cache-primary-key", key);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}`
);
});
@@ -105,32 +112,14 @@ test("restore with restore keys and no cache found", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreOnlyRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
false
);
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-primary-key", key);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}, ${restoreKey}`
);
});
@@ -144,36 +133,17 @@ test("restore with cache found for key", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(key);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(key);
await restoreOnlyRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[],
{
lookupOnly: false
},
false
);
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
expect(outputMock).toHaveBeenCalledWith("cache-hit", "true");
expect(outputMock).toHaveBeenCalledWith("cache-matched-key", key);
expect(outputMock).toHaveBeenCalledTimes(3);
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-primary-key", key);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
expect(core.setOutput).toHaveBeenCalledWith("cache-matched-key", key);
expect(core.setOutput).toHaveBeenCalledTimes(3);
expect(core.info).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("restore with cache found for restore key", async () => {
@@ -187,36 +157,20 @@ test("restore with cache found for restore key", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(restoreKey);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(restoreKey);
await restoreOnlyRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
false
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-primary-key", key);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
expect(core.setOutput).toHaveBeenCalledWith(
"cache-matched-key",
restoreKey
);
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
expect(outputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(outputMock).toHaveBeenCalledWith("cache-matched-key", restoreKey);
expect(outputMock).toHaveBeenCalledTimes(3);
expect(infoMock).toHaveBeenCalledWith(
expect(core.setOutput).toHaveBeenCalledTimes(3);
expect(core.info).toHaveBeenCalledWith(
`Cache restored from key: ${restoreKey}`
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});

View File

@@ -1,70 +1,66 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { afterEach, beforeEach, expect, jest, test } from "@jest/globals";
import { Events, Inputs, RefKey } from "../src/constants";
import { saveRun } from "../src/saveImpl";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
jest.mock("@actions/core");
jest.mock("@actions/cache");
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
return jest.requireActual("@actions/core").getInput(name, options);
});
jest.spyOn(core, "getState").mockImplementation(name => {
return jest.requireActual("@actions/core").getState(name);
});
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsArray(name, options);
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
getInput: jest.fn((name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
);
return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.spyOn(actionUtils, "getInputAsInt").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsInt(name, options);
// Mock @actions/cache
jest.unstable_mockModule("@actions/cache", () => ({
restoreCache: jest.fn(),
saveCache: jest.fn(),
isFeatureAvailable: jest.fn(() => true),
ReserveCacheError: class ReserveCacheError extends Error {
constructor(message: string) {
super(message);
this.name = "ReserveCacheError";
}
);
}
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsBool(name, options);
}
);
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
return jest
.requireActual("../src/utils/actionUtils")
.isExactKeyMatch(key, cacheResult);
}
);
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
});
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, Inputs, RefKey } = await import("../src/constants");
const { saveRun } = await import("../src/saveImpl");
const testUtils = await import("../src/utils/testUtils");
beforeEach(() => {
jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] ||
"";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
return val.trim();
}
);
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env[Events.Key] = Events.Push;
process.env[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
});
afterEach(() => {
@@ -74,36 +70,24 @@ afterEach(() => {
});
test("save with valid inputs uploads a cache", async () => {
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return primaryKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return savedCacheKey;
});
(core.getState as jest.Mock)
.mockReturnValueOnce(primaryKey)
.mockReturnValueOnce(savedCacheKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = 4;
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
(cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
await saveRun();
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath],
primaryKey,
{
@@ -112,5 +96,5 @@ test("save with valid inputs uploads a cache", async () => {
false
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});

View File

@@ -1,68 +1,68 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { afterEach, beforeEach, expect, jest, test } from "@jest/globals";
import { Events, Inputs, RefKey } from "../src/constants";
import { saveImpl } from "../src/saveImpl";
import { StateProvider } from "../src/stateProvider";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
jest.mock("@actions/core");
jest.mock("@actions/cache");
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
return jest.requireActual("@actions/core").getInput(name, options);
});
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsArray(name, options);
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
getInput: jest.fn((name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
);
return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.spyOn(actionUtils, "getInputAsInt").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsInt(name, options);
// Mock @actions/cache
jest.unstable_mockModule("@actions/cache", () => ({
restoreCache: jest.fn(),
saveCache: jest.fn(),
isFeatureAvailable: jest.fn(() => true),
ReserveCacheError: class ReserveCacheError extends Error {
constructor(message: string) {
super(message);
this.name = "ReserveCacheError";
}
);
}
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsBool(name, options);
}
);
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
return jest
.requireActual("../src/utils/actionUtils")
.isExactKeyMatch(key, cacheResult);
}
);
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
});
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, Inputs, RefKey } = await import("../src/constants");
const { saveImpl } = await import("../src/saveImpl");
const { StateProvider } = await import("../src/stateProvider");
const testUtils = await import("../src/utils/testUtils");
beforeEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] ||
"";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
return val.trim();
}
);
(core.getState as jest.Mock).mockReturnValue("");
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env[Events.Key] = Events.Push;
process.env[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
});
afterEach(() => {
@@ -72,99 +72,61 @@ afterEach(() => {
});
test("save with invalid event outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const invalidEvent = "commit_comment";
process.env[Events.Key] = invalidEvent;
delete process.env[RefKey];
await saveImpl(new StateProvider());
expect(logWarningMock).toHaveBeenCalledWith(
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
expect(core.info).toHaveBeenCalledWith(
`[warning]Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("save with no primary key in state outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const savedCacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return "";
})
// Cache Key State
.mockImplementationOnce(() => {
return savedCacheKey;
});
const saveCacheMock = jest.spyOn(cache, "saveCache");
(core.getState as jest.Mock).mockReturnValue("");
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0);
expect(logWarningMock).toHaveBeenCalledWith(`Key is not specified.`);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(cache.saveCache).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(`[warning]Key is not specified.`);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("save without AC available should no-op", async () => {
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => false
);
const saveCacheMock = jest.spyOn(cache, "saveCache");
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0);
expect(cache.saveCache).toHaveBeenCalledTimes(0);
});
test("save on ghes without AC available should no-op", async () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => false
);
const saveCacheMock = jest.spyOn(cache, "saveCache");
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0);
expect(cache.saveCache).toHaveBeenCalledTimes(0);
});
test("save on GHES with AC available", async () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
(core.getState as jest.Mock)
.mockReturnValueOnce(primaryKey)
.mockReturnValueOnce(savedCacheKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = 4;
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
(cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath],
primaryKey,
{
@@ -173,229 +135,135 @@ test("save on GHES with AC available", async () => {
false
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("save with exact match returns early", async () => {
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = primaryKey;
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
const saveCacheMock = jest.spyOn(cache, "saveCache");
(core.getState as jest.Mock)
.mockReturnValueOnce(primaryKey)
.mockReturnValueOnce(primaryKey);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(cache.saveCache).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("save with missing input outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
const saveCacheMock = jest.spyOn(cache, "saveCache");
(core.getState as jest.Mock)
.mockReturnValueOnce(savedCacheKey)
.mockReturnValueOnce(primaryKey);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0);
expect(logWarningMock).toHaveBeenCalledWith(
"Input required and not supplied: path"
expect(cache.saveCache).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
"[warning]Input required and not supplied: path"
);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("save with large cache outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
(core.getState as jest.Mock)
.mockReturnValueOnce(savedCacheKey)
.mockReturnValueOnce(primaryKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
throw new Error(
"Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
);
});
(cache.saveCache as jest.Mock).mockRejectedValue(
new Error(
"Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
)
);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
[inputPath],
primaryKey,
expect.anything(),
false
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(core.info).toHaveBeenCalledWith(
"[warning]Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).toHaveBeenCalledWith(
"Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("save with reserve cache failure outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
(core.getState as jest.Mock)
.mockReturnValueOnce(savedCacheKey)
.mockReturnValueOnce(primaryKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
const actualCache = jest.requireActual("@actions/cache");
const error = new actualCache.ReserveCacheError(
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
);
throw error;
});
(cache.saveCache as jest.Mock).mockRejectedValue(
new Error(
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
)
);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
[inputPath],
primaryKey,
expect.anything(),
false
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(core.info).toHaveBeenCalledWith(
`[warning]Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
);
expect(logWarningMock).toHaveBeenCalledWith(
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("save with server error outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
(core.getState as jest.Mock)
.mockReturnValueOnce(savedCacheKey)
.mockReturnValueOnce(primaryKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
throw new Error("HTTP Error Occurred");
});
(cache.saveCache as jest.Mock).mockRejectedValue(
new Error("HTTP Error Occurred")
);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
[inputPath],
primaryKey,
expect.anything(),
false
);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
expect(failedMock).toHaveBeenCalledTimes(0);
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(core.info).toHaveBeenCalledWith("[warning]HTTP Error Occurred");
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("save with valid inputs uploads a cache", async () => {
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
(core.getState as jest.Mock)
.mockReturnValueOnce(primaryKey)
.mockReturnValueOnce(savedCacheKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = 4;
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
(cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath],
primaryKey,
{
@@ -404,5 +272,5 @@ test("save with valid inputs uploads a cache", async () => {
false
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});

View File

@@ -1,70 +1,66 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { afterEach, beforeEach, expect, jest, test } from "@jest/globals";
import { Events, Inputs, RefKey } from "../src/constants";
import { saveOnlyRun } from "../src/saveImpl";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
jest.mock("@actions/core");
jest.mock("@actions/cache");
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
return jest.requireActual("@actions/core").getInput(name, options);
});
jest.spyOn(core, "setOutput").mockImplementation((key, value) => {
return jest.requireActual("@actions/core").getInput(key, value);
});
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsArray(name, options);
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
getInput: jest.fn((name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
);
return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.spyOn(actionUtils, "getInputAsInt").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsInt(name, options);
// Mock @actions/cache
jest.unstable_mockModule("@actions/cache", () => ({
restoreCache: jest.fn(),
saveCache: jest.fn(),
isFeatureAvailable: jest.fn(() => true),
ReserveCacheError: class ReserveCacheError extends Error {
constructor(message: string) {
super(message);
this.name = "ReserveCacheError";
}
);
}
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsBool(name, options);
}
);
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
return jest
.requireActual("../src/utils/actionUtils")
.isExactKeyMatch(key, cacheResult);
}
);
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
});
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, Inputs, RefKey } = await import("../src/constants");
const { saveOnlyRun } = await import("../src/saveImpl");
const testUtils = await import("../src/utils/testUtils");
beforeEach(() => {
jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] ||
"";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
return val.trim();
}
);
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env[Events.Key] = Events.Push;
process.env[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
});
afterEach(() => {
@@ -74,8 +70,6 @@ afterEach(() => {
});
test("save with valid inputs uploads a cache", async () => {
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const inputPath = "node_modules";
@@ -84,16 +78,12 @@ test("save with valid inputs uploads a cache", async () => {
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = 4;
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
(cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
await saveOnlyRun();
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath],
primaryKey,
{
@@ -102,12 +92,10 @@ test("save with valid inputs uploads a cache", async () => {
false
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("save failing logs the warning message", async () => {
const warningMock = jest.spyOn(core, "warning");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const inputPath = "node_modules";
@@ -116,16 +104,12 @@ test("save failing logs the warning message", async () => {
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = -1;
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
(cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
await saveOnlyRun();
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath],
primaryKey,
{
@@ -134,6 +118,6 @@ test("save failing logs the warning message", async () => {
false
);
expect(warningMock).toHaveBeenCalledTimes(1);
expect(warningMock).toHaveBeenCalledWith("Cache save failed.");
expect(core.warning).toHaveBeenCalledTimes(1);
expect(core.warning).toHaveBeenCalledWith("Cache save failed.");
});

View File

@@ -1,22 +1,41 @@
import * as core from "@actions/core";
import { afterEach, beforeEach, expect, jest, test } from "@jest/globals";
import { Events, RefKey, State } from "../src/constants";
import {
IStateProvider,
NullStateProvider,
StateProvider
} from "../src/stateProvider";
import type { IStateProvider } from "../src/stateProvider";
jest.mock("@actions/core");
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
getInput: jest.fn((name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
beforeAll(() => {
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
return jest.requireActual("@actions/core").getInput(name, options);
});
const core = await import("@actions/core");
const { Events, RefKey, State } = await import("../src/constants");
const { NullStateProvider, StateProvider } =
await import("../src/stateProvider");
jest.spyOn(core, "setOutput").mockImplementation((key, value) => {
return jest.requireActual("@actions/core").setOutput(key, value);
});
beforeEach(() => {
jest.clearAllMocks();
(core.getState as jest.Mock).mockReturnValue("");
});
afterEach(() => {
@@ -26,21 +45,14 @@ afterEach(() => {
test("StateProvider saves states", async () => {
const states = new Map<string, string>();
const getStateMock = jest
.spyOn(core, "getState")
.mockImplementation(key => states.get(key) || "");
const saveStateMock = jest
.spyOn(core, "saveState")
.mockImplementation((key, value) => {
(core.getState as jest.Mock).mockImplementation(
(key: string) => states.get(key) || ""
);
(core.saveState as jest.Mock).mockImplementation(
(key: string, value: string) => {
states.set(key, value);
});
const setOutputMock = jest
.spyOn(core, "setOutput")
.mockImplementation((key, value) => {
return jest.requireActual("@actions/core").setOutput(key, value);
});
}
);
const cacheMatchedKey = "node-cache";
@@ -52,38 +64,19 @@ test("StateProvider saves states", async () => {
expect(stateValue).toBe("stateValue");
expect(cacheStateValue).toBe(cacheMatchedKey);
expect(getStateMock).toHaveBeenCalledTimes(2);
expect(saveStateMock).toHaveBeenCalledTimes(2);
expect(setOutputMock).toHaveBeenCalledTimes(0);
expect(core.getState).toHaveBeenCalledTimes(2);
expect(core.saveState).toHaveBeenCalledTimes(2);
expect(core.setOutput).toHaveBeenCalledTimes(0);
});
test("NullStateProvider saves outputs", async () => {
const getStateMock = jest
.spyOn(core, "getState")
.mockImplementation(name =>
jest.requireActual("@actions/core").getState(name)
);
const setOutputMock = jest
.spyOn(core, "setOutput")
.mockImplementation((key, value) => {
return jest.requireActual("@actions/core").setOutput(key, value);
});
const saveStateMock = jest
.spyOn(core, "saveState")
.mockImplementation((key, value) => {
return jest.requireActual("@actions/core").saveState(key, value);
});
const cacheMatchedKey = "node-cache";
const nullStateProvider: IStateProvider = new NullStateProvider();
nullStateProvider.setState(State.CacheMatchedKey, "outputValue");
nullStateProvider.setState(State.CachePrimaryKey, cacheMatchedKey);
nullStateProvider.setState(State.CachePrimaryKey, "node-cache");
nullStateProvider.getState("outputKey");
nullStateProvider.getCacheState();
expect(getStateMock).toHaveBeenCalledTimes(0);
expect(setOutputMock).toHaveBeenCalledTimes(2);
expect(saveStateMock).toHaveBeenCalledTimes(0);
expect(core.getState).toHaveBeenCalledTimes(0);
expect(core.setOutput).toHaveBeenCalledTimes(2);
expect(core.saveState).toHaveBeenCalledTimes(0);
});

View File

@@ -12,7 +12,7 @@ This document lists some of the strategies (and example workflows if possible) w
jobs:
build:
runs-on: ubuntu-latest
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
key: ${{ some-metadata }}-cache
```
@@ -24,7 +24,7 @@ In your workflows, you can use different strategies to name your key depending o
One of the most common use case is to use hash for lockfile as key. This way, same cache will be restored for a lockfile until there's a change in dependencies listed in lockfile.
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
path/to/dependencies
@@ -37,7 +37,7 @@ One of the most common use case is to use hash for lockfile as key. This way, sa
If cache is not found matching the primary key, restore keys can be used to download the closest matching cache that was recently created. This ensures that the build/install step will need to additionally fetch just a handful of newer dependencies, and hence saving build time.
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
path/to/dependencies
@@ -54,7 +54,7 @@ The restore keys can be provided as a complete name, or a prefix, read more [her
In case of workflows with matrix running for multiple Operating Systems, the caches can be stored separately for each of them. This can be used in combination with hashfiles in case multiple caches are being generated per OS.
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
path/to/dependencies
@@ -73,7 +73,7 @@ Caches scoped to the particular workflow run id or run attempt can be stored and
On similar lines, commit sha can be used to create a very specialized and short lived cache.
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
path/to/dependencies
@@ -86,7 +86,7 @@ On similar lines, commit sha can be used to create a very specialized and short
Cache key can be formed by combination of more than one metadata, evaluated info.
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
path/to/dependencies
@@ -146,9 +146,9 @@ In case you are using a centralized job to create and save your cache that can b
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/cache/restore@v4
- uses: actions/cache/restore@v6
id: cache
with:
path: path/to/dependencies
@@ -171,9 +171,9 @@ You can use the output of this action to exit the workflow on cache miss. This w
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/cache/restore@v4
- uses: actions/cache/restore@v6
id: cache
with:
path: path/to/dependencies
@@ -194,7 +194,7 @@ steps:
If you want to avoid re-computing the cache key again in `save` action, the outputs from `restore` action can be used as input to the `save` action.
```yaml
- uses: actions/cache/restore@v4
- uses: actions/cache/restore@v6
id: restore-cache
with:
path: |
@@ -204,7 +204,7 @@ If you want to avoid re-computing the cache key again in `save` action, the outp
.
.
.
- uses: actions/cache/save@v4
- uses: actions/cache/save@v6
with:
path: |
path/to/dependencies
@@ -219,7 +219,7 @@ On the other hand, the key can also be explicitly re-computed while executing th
Let's say we have a restore step that computes key at runtime
```yaml
uses: actions/cache/restore@v4
uses: actions/cache/restore@v6
id: restore-cache
with:
key: cache-${{ hashFiles('**/lockfiles') }}
@@ -228,7 +228,7 @@ with:
Case 1: Where an user would want to reuse the key as it is
```yaml
uses: actions/cache/save@v4
uses: actions/cache/save@v6
with:
key: ${{ steps.restore-cache.outputs.cache-primary-key }}
```
@@ -236,7 +236,7 @@ with:
Case 2: Where the user would want to re-evaluate the key
```yaml
uses: actions/cache/save@v4
uses: actions/cache/save@v6
with:
key: npm-cache-${{hashfiles(package-lock.json)}}
```
@@ -253,12 +253,12 @@ In case of multi-module projects, where the built artifact of one project needs
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Build
run: ./build-parent-module.sh
- uses: actions/cache/save@v4
- uses: actions/cache/save@v6
id: cache
with:
path: path/to/dependencies
@@ -269,9 +269,9 @@ steps:
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/cache/restore@v4
- uses: actions/cache/restore@v6
id: cache
with:
path: path/to/dependencies
@@ -280,7 +280,7 @@ steps:
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: ./install.sh
- name: Build
run: ./build-child-module.sh

File diff suppressed because one or more lines are too long

39581
dist/restore/index.js vendored

File diff suppressed because one or more lines are too long

39581
dist/save-only/index.js vendored

File diff suppressed because one or more lines are too long

39581
dist/save/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -45,21 +45,21 @@
## Bun
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
```
### Windows
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
~\.bun
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
```
## C# - NuGet
@@ -67,7 +67,7 @@
Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies):
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
@@ -76,10 +76,10 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa
```
Depending on the environment, huge packages might be pre-installed in the global cache folder.
With `actions/cache@v4` you can now exclude unwanted packages with [exclude pattern](https://github.com/actions/toolkit/tree/main/packages/glob#exclude-patterns)
From `actions/cache@v3` onwards, you can now exclude unwanted packages with [exclude pattern](https://github.com/actions/toolkit/tree/main/packages/glob#exclude-patterns)
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
~/.nuget/packages
@@ -96,7 +96,7 @@ Or you could move the cache folder like below.
env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: ${{ github.workspace }}/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
@@ -108,7 +108,7 @@ steps:
```yaml
- name: Cache lein project dependencies
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-clojure-${{ hashFiles('**/project.clj') }}
@@ -122,7 +122,7 @@ steps:
### POSIX
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: ~/.dub
key: ${{ runner.os }}-dub-${{ hashFiles('**/dub.selections.json') }}
@@ -133,7 +133,7 @@ steps:
### Windows
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: ~\AppData\Local\dub
key: ${{ runner.os }}-dub-${{ hashFiles('**/dub.selections.json') }}
@@ -146,7 +146,7 @@ steps:
### Linux
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
~/.deno
@@ -157,7 +157,7 @@ steps:
### macOS
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
~/.deno
@@ -168,7 +168,7 @@ steps:
### Windows
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
~\.deno
@@ -179,7 +179,7 @@ steps:
## Elixir - Mix
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
deps
@@ -191,7 +191,7 @@ steps:
## Erlang - Rebar3
```yaml
- uses: actions/cache@v2
- uses: actions/cache@v6
with:
path: |
~/.cache/rebar3
@@ -206,7 +206,7 @@ steps:
### Linux
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
~/.cache/go-build
@@ -219,7 +219,7 @@ steps:
### macOS
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
~/Library/Caches/go-build
@@ -232,7 +232,7 @@ steps:
### Windows
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
~\AppData\Local\go-build
@@ -248,7 +248,7 @@ We cache the elements of the Cabal store separately, as the entirety of `~/.caba
```yaml
- name: Cache ~/.cabal/packages, ~/.cabal/store and dist-newstyle
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cabal/packages
@@ -263,14 +263,14 @@ We cache the elements of the Cabal store separately, as the entirety of `~/.caba
### Linux or macOS
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
name: Cache ~/.stack
with:
path: ~/.stack
key: ${{ runner.os }}-stack-global-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}
restore-keys: |
${{ runner.os }}-stack-global-
- uses: actions/cache@v4
- uses: actions/cache@v6
name: Cache .stack-work
with:
path: .stack-work
@@ -282,16 +282,16 @@ We cache the elements of the Cabal store separately, as the entirety of `~/.caba
### Windows
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
name: Cache %APPDATA%\stack %LOCALAPPDATA%\Programs\stack
with:
path: |
~\AppData\Roaming\stack
~\AppData\Local\Programs\stack
~\AppData\Local\Programs\stack
key: ${{ runner.os }}-stack-global-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}
restore-keys: |
${{ runner.os }}-stack-global-
- uses: actions/cache@v4
- uses: actions/cache@v6
name: Cache .stack-work
with:
path: .stack-work
@@ -305,7 +305,7 @@ We cache the elements of the Cabal store separately, as the entirety of `~/.caba
> **Note** Ensure no Gradle daemons are running anymore when your workflow completes. Creating the cache package might fail due to locks being held by Gradle. Refer to the [Gradle Daemon documentation](https://docs.gradle.org/current/userguide/gradle_daemon.html) on how to disable or stop the Gradle Daemons.
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
~/.gradle/caches
@@ -319,7 +319,7 @@ We cache the elements of the Cabal store separately, as the entirety of `~/.caba
```yaml
- name: Cache local Maven repository
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -355,7 +355,7 @@ After [deprecation](https://github.blog/changelog/2022-10-11-github-actions-depr
`Get npm cache directory` step can then be used with `actions/cache` as shown below
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true'
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
@@ -368,7 +368,7 @@ After [deprecation](https://github.blog/changelog/2022-10-11-github-actions-depr
```yaml
- name: restore lerna
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
@@ -382,7 +382,7 @@ The yarn cache directory will depend on your operating system and version of `ya
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
- uses: actions/cache@v6
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -400,7 +400,7 @@ The yarn 2 cache directory will depend on your config. See https://yarnpkg.com/c
id: yarn-cache-dir-path
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
- uses: actions/cache@v6
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -415,7 +415,7 @@ Esy allows you to export built dependencies and import pre-built dependencies.
```yaml
- name: Restore Cache
id: restore-cache
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: _export
key: ${{ runner.os }}-esy-${{ hashFiles('esy.lock/index.json') }}
@@ -444,7 +444,7 @@ Esy allows you to export built dependencies and import pre-built dependencies.
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -465,7 +465,7 @@ Locations:
### Simple example
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@@ -478,7 +478,7 @@ Replace `~/.cache/pip` with the correct `path` if not using Ubuntu.
### Multiple OS's in a workflow
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
if: startsWith(runner.os, 'Linux')
with:
path: ~/.cache/pip
@@ -486,7 +486,7 @@ Replace `~/.cache/pip` with the correct `path` if not using Ubuntu.
restore-keys: |
${{ runner.os }}-pip-
- uses: actions/cache@v4
- uses: actions/cache@v6
if: startsWith(runner.os, 'macOS')
with:
path: ~/Library/Caches/pip
@@ -494,7 +494,7 @@ Replace `~/.cache/pip` with the correct `path` if not using Ubuntu.
restore-keys: |
${{ runner.os }}-pip-
- uses: actions/cache@v4
- uses: actions/cache@v6
if: startsWith(runner.os, 'Windows')
with:
path: ~\AppData\Local\pip\Cache
@@ -520,7 +520,7 @@ jobs:
- os: windows-latest
path: ~\AppData\Local\pip\Cache
steps:
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: ${{ matrix.path }}
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@@ -539,7 +539,7 @@ jobs:
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: pip cache
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@@ -553,11 +553,11 @@ jobs:
- name: Set up Python
# The actions/cache step below uses this id to get the exact python version
id: setup-python
uses: actions/setup-python@v2
uses: actions/setup-python@v6
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-python-${{ steps.setup-python.outputs.python-version }}-pipenv-${{ hashFiles('Pipfile.lock') }}
@@ -584,7 +584,7 @@ For renv, the cache directory will vary by OS. The `RENV_PATHS_ROOT` environment
cat("##[set-output name=r-version;]", R.Version()$version.string, sep = "")
shell: Rscript {0}
- name: Restore Renv package cache
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ${{ env.RENV_PATHS_ROOT }}
key: ${{ steps.get-version.outputs.os-version }}-${{ steps.get-version.outputs.r-version }}-${{ inputs.cache-version }}-${{ hashFiles('renv.lock') }}
@@ -610,7 +610,7 @@ whenever possible:
## Rust - Cargo
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: |
~/.cargo/bin/
@@ -625,7 +625,7 @@ whenever possible:
```yaml
- name: Cache SBT
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.ivy2/cache
@@ -636,7 +636,7 @@ whenever possible:
## Swift, Objective-C - Carthage
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: Carthage
key: ${{ runner.os }}-carthage-${{ hashFiles('**/Cartfile.resolved') }}
@@ -647,7 +647,7 @@ whenever possible:
## Swift, Objective-C - CocoaPods
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
@@ -658,7 +658,7 @@ whenever possible:
## Swift - Swift Package Manager
```yaml
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: .build
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
@@ -673,7 +673,7 @@ env:
MINT_PATH: .mint/lib
MINT_LINK_PATH: .mint/bin
steps:
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: .mint
key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }}
@@ -689,7 +689,7 @@ steps:
```yaml
- name: Cache Bazel
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
~/.cache/bazel
@@ -703,7 +703,7 @@ steps:
```yaml
- name: Cache Bazel
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: |
/private/var/tmp/_bazel_runner/

View File

@@ -1,23 +0,0 @@
require("nock").disableNetConnect();
module.exports = {
clearMocks: true,
moduleFileExtensions: ["js", "ts"],
testEnvironment: "node",
testMatch: ["**/*.test.ts"],
testRunner: "jest-circus/runner",
transform: {
"^.+\\.ts$": "ts-jest"
},
verbose: true
};
const processStdoutWrite = process.stdout.write.bind(process.stdout);
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
process.stdout.write = (str, encoding, cb) => {
// Core library will directly call process.stdout.write for commands
// We don't want :: commands to be executed by the runner during tests
if (!String(str).match(/^::/)) {
return processStdoutWrite(str, encoding, cb);
}
};

21
jest.config.ts Normal file
View File

@@ -0,0 +1,21 @@
export default {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
roots: ['<rootDir>/__tests__'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
useESM: true,
diagnostics: {
ignoreCodes: [151002]
}
}
]
},
extensionsToTreatAsEsm: ['.ts'],
transformIgnorePatterns: ['node_modules/(?!(@actions)/)'],
verbose: true
}

2893
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,13 @@
{
"name": "cache",
"version": "5.0.2",
"version": "6.0.0",
"private": true,
"description": "Cache dependencies and build outputs",
"type": "module",
"main": "dist/restore/index.js",
"scripts": {
"build": "tsc && ncc build -o dist/restore src/restore.ts && ncc build -o dist/save src/save.ts && ncc build -o dist/restore-only src/restoreOnly.ts && ncc build -o dist/save-only src/saveOnly.ts",
"test": "tsc --noEmit && jest --coverage",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
"lint": "eslint **/*.ts --cache",
"format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts"
@@ -23,13 +24,13 @@
"author": "GitHub",
"license": "MIT",
"dependencies": {
"@actions/cache": "^5.0.3",
"@actions/core": "^2.0.0",
"@actions/exec": "^2.0.0",
"@actions/io": "^2.0.0"
"@actions/cache": "^6.0.1",
"@actions/core": "^3.0.1",
"@actions/exec": "^3.0.0",
"@actions/io": "^3.0.2"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/jest": "^30.0.0",
"@types/nock": "^11.1.0",
"@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^7.2.0",
@@ -41,11 +42,11 @@
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-simple-import-sort": "^12.1.1",
"jest": "^29.7.0",
"jest-circus": "^29.7.0",
"jest": "^30.2.0",
"nock": "^13.2.9",
"prettier": "^3.6.2",
"ts-jest": "^29.4.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
},
"engines": {

View File

@@ -35,9 +35,9 @@ If you are using separate jobs to create and save your cache(s) to be reused by
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/cache/restore@v4
- uses: actions/cache/restore@v5
id: cache
with:
path: path/to/dependencies
@@ -64,12 +64,12 @@ In case of multi-module projects, where the built artifact of one project needs
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Build
run: /build-parent-module.sh
- uses: actions/cache/save@v4
- uses: actions/cache/save@v5
id: cache
with:
path: path/to/dependencies
@@ -80,9 +80,9 @@ steps:
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/cache/restore@v4
- uses: actions/cache/restore@v5
id: cache
with:
path: path/to/dependencies
@@ -107,9 +107,9 @@ To fail if there is no cache hit for the primary key, leave `restore-keys` empty
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/cache/restore@v4
- uses: actions/cache/restore@v5
id: cache
with:
path: path/to/dependencies

View File

@@ -23,7 +23,7 @@ If you are using separate jobs for generating common artifacts and sharing them
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Dependencies
run: /install.sh
@@ -31,7 +31,7 @@ steps:
- name: Build artifacts
run: /build.sh
- uses: actions/cache/save@v4
- uses: actions/cache/save@v5
id: cache
with:
path: path/to/dependencies
@@ -47,7 +47,7 @@ Let's say we have a restore step that computes a key at runtime.
#### Restore a cache
```yaml
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
id: restore-cache
with:
key: cache-${{ hashFiles('**/lockfiles') }}
@@ -55,7 +55,7 @@ with:
#### Case 1 - Where a user would want to reuse the key as it is
```yaml
uses: actions/cache/save@v4
uses: actions/cache/save@v5
with:
key: ${{ steps.restore-cache.outputs.cache-primary-key }}
```
@@ -63,7 +63,7 @@ with:
#### Case 2 - Where the user would want to re-evaluate the key
```yaml
uses: actions/cache/save@v4
uses: actions/cache/save@v5
with:
key: npm-cache-${{hashfiles(package-lock.json)}}
```
@@ -91,11 +91,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Restore cached Prime Numbers
id: cache-prime-numbers-restore
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
key: ${{ runner.os }}-prime-numbers
path: |
@@ -107,7 +107,7 @@ jobs:
- name: Always Save Prime Numbers
id: cache-prime-numbers-save
if: always() && steps.cache-prime-numbers-restore.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
uses: actions/cache/save@v5
with:
key: ${{ steps.cache-prime-numbers-restore.outputs.cache-primary-key }}
path: |

View File

@@ -12,7 +12,7 @@ A cache today is immutable and cannot be updated. But some use cases require the
```yaml
- name: update cache on every commit
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: prime-numbers
key: primes-${{ runner.os }}-${{ github.run_id }} # Can use time based key as well
@@ -21,7 +21,7 @@ A cache today is immutable and cannot be updated. But some use cases require the
```
Please note that this will create a new cache on every run and hence will consume the cache [quota](./README.md#cache-limits).
## Use cache across feature branches
Reusing cache across feature branches is not allowed today to provide cache [isolation](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache). However if both feature branches are from the default branch, a good way to achieve this is to ensure that the default branch has a cache. This cache will then be consumable by both feature branches.

View File

@@ -1,63 +1,13 @@
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./lib", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"target": "ES2022",
"module": "ESNext",
"outDir": "./lib",
"rootDir": "./src",
"strict": true,
"noImplicitAny": false,
"moduleResolution": "bundler",
"esModuleInterop": true
},
"exclude": ["node_modules", "**/*.test.ts"]
"exclude": ["node_modules", "**/*.test.ts", "jest.config.ts", "__tests__"]
}