Compare commits
1 Commits
main
...
tiwarishub
Author | SHA1 | Date |
---|---|---|
Shubham Tiwari | 402b512df8 |
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Node.js & TypeScript",
|
|
||||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:16-bullseye",
|
|
||||||
// Features to add to the dev container. More info: https://containers.dev/implementors/features.
|
|
||||||
// "features": {},
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
||||||
// "forwardPorts": [],
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
"postCreateCommand": "npm install"
|
|
||||||
// Configure tool-specific properties.
|
|
||||||
// "customizations": {},
|
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
|
||||||
// "remoteUser": "root"
|
|
||||||
}
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Set to true to add reviewers to pull requests
|
||||||
|
addReviewers: true
|
||||||
|
|
||||||
|
# Set to true to add assignees to pull requests
|
||||||
|
addAssignees: false
|
||||||
|
|
||||||
|
# A list of reviewers to be added to pull requests (GitHub user name)
|
||||||
|
reviewers:
|
||||||
|
- phantsure
|
||||||
|
- kotewar
|
||||||
|
- aparna-ravindra
|
||||||
|
- tiwarishub
|
||||||
|
- vsvipul
|
||||||
|
- bishal-pdmsft
|
||||||
|
|
||||||
|
# A number of reviewers added to the pull request
|
||||||
|
# Set 0 to add all the reviewers (default: 0)
|
||||||
|
numberOfReviewers: 1
|
|
@ -1,32 +0,0 @@
|
||||||
<!--- Provide a general summary of your changes in the Title above -->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
<!--- Describe your changes in detail -->
|
|
||||||
|
|
||||||
## Motivation and Context
|
|
||||||
<!--- Why is this change required? What problem does it solve? -->
|
|
||||||
<!--- If it fixes an open issue, please link to the issue here. -->
|
|
||||||
|
|
||||||
## How Has This Been Tested?
|
|
||||||
<!--- Please describe in detail how you tested your changes. -->
|
|
||||||
<!--- Include details of your testing environment, and the tests you ran to -->
|
|
||||||
<!--- see how your change affects other areas of the code, etc. -->
|
|
||||||
|
|
||||||
## Screenshots (if appropriate):
|
|
||||||
|
|
||||||
## Types of changes
|
|
||||||
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
|
||||||
- [ ] Documentation (add or update README or docs)
|
|
||||||
|
|
||||||
## Checklist:
|
|
||||||
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
|
||||||
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
|
||||||
- [ ] My code follows the code style of this project.
|
|
||||||
- [ ] My change requires a change to the documentation.
|
|
||||||
- [ ] I have updated the documentation accordingly.
|
|
||||||
- [ ] I have read the **CONTRIBUTING** document.
|
|
||||||
- [ ] I have added tests to cover my changes.
|
|
||||||
- [ ] All new and existing tests passed.
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
name: Issue assignment
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-assign:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 'Auto-assign issue'
|
||||||
|
uses: pozil/auto-assign-issue@v1.4.0
|
||||||
|
with:
|
||||||
|
assignees: phantsure,kotewar,tiwarishub,aparna-ravindra,vsvipul,bishal-pdmsft
|
||||||
|
numOfAssignee: 1
|
|
@ -0,0 +1,10 @@
|
||||||
|
name: 'Auto Assign'
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add-reviews:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: kentaro-m/auto-assign-action@v1.2.1
|
|
@ -1,3 +1,9 @@
|
||||||
|
# `dist/index.js` is a special file in Actions.
|
||||||
|
# When you reference an action with `uses:` in a workflow,
|
||||||
|
# `index.js` is the code that will run.
|
||||||
|
# For our project, we generate this file through a build process
|
||||||
|
# from other source files.
|
||||||
|
# We need to make sure the checked-in `index.js` actually matches what we expect it to be.
|
||||||
name: Check dist/
|
name: Check dist/
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
@ -12,6 +18,32 @@ on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
call-check-dist:
|
check-dist:
|
||||||
name: Check dist/
|
runs-on: ubuntu-latest
|
||||||
uses: actions/reusable-workflows/.github/workflows/check-dist.yml@main
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Setup Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16.x
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Rebuild the dist/ directory
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Compare the expected and actual dist/ directories
|
||||||
|
run: |
|
||||||
|
if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then
|
||||||
|
echo "Detected uncommitted changes after build. See status below:"
|
||||||
|
git diff
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
id: diff
|
||||||
|
|
||||||
|
# If index.js was different than expected, upload the expected version as an artifact
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
|
||||||
|
with:
|
||||||
|
name: dist
|
||||||
|
path: dist/
|
||||||
|
|
|
@ -8,39 +8,45 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
CodeQL-Build:
|
CodeQL-Build:
|
||||||
# CodeQL runs on ubuntu-latest, windows-latest, and macos-latest
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
# CodeQL runs on ubuntu-latest and windows-latest
|
||||||
# required for all workflows
|
runs-on: ubuntu-latest
|
||||||
security-events: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# We must fetch at least the immediate parents so that if this is
|
||||||
|
# a pull request then we can checkout the head.
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
# If this run was triggered by a pull request event, then checkout
|
||||||
|
# the head of the pull request instead of the merge commit.
|
||||||
|
- run: git checkout HEAD^2
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v1
|
||||||
# Override language selection by uncommenting this and choosing your languages
|
# Override language selection by uncommenting this and choosing your languages
|
||||||
# with:
|
# with:
|
||||||
# languages: go, javascript, csharp, python, cpp, java, ruby
|
# languages: go, javascript, csharp, python, cpp, java
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below).
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
# three lines and modify them (or add more) to build your code if your
|
# and modify them (or add more) to build your code if your project
|
||||||
# project uses a compiled language
|
# uses a compiled language
|
||||||
|
|
||||||
#- run: |
|
#- run: |
|
||||||
# make bootstrap
|
# make bootstrap
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v1
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
name: Assign issue
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
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}}"]}'
|
|
|
@ -7,9 +7,18 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
call-licensed:
|
test:
|
||||||
name: Licensed
|
runs-on: ubuntu-latest
|
||||||
uses: actions/reusable-workflows/.github/workflows/licensed.yml@main
|
name: Check licenses
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- run: npm ci
|
||||||
|
- name: Install licensed
|
||||||
|
run: |
|
||||||
|
cd $RUNNER_TEMP
|
||||||
|
curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/2.12.2/licensed-2.12.2-linux-x64.tar.gz
|
||||||
|
sudo tar -xzf licensed.tar.gz
|
||||||
|
sudo mv licensed /usr/local/bin/licensed
|
||||||
|
- run: licensed status
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
name: Add Reviewer PR
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened]
|
|
||||||
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}}"]}'
|
|
|
@ -5,10 +5,14 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- releases/**
|
- releases/**
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- releases/**
|
- releases/**
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Build and unit test
|
# Build and unit test
|
||||||
|
@ -25,7 +29,17 @@ jobs:
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 16.x
|
||||||
cache: npm
|
- name: Determine npm cache directory
|
||||||
|
id: npm-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=dir::$(npm config get cache)"
|
||||||
|
- name: Restore npm cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ steps.npm-cache.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Prettier Format Check
|
- name: Prettier Format Check
|
||||||
run: npm run format-check
|
run: npm run format-check
|
||||||
|
@ -89,7 +103,7 @@ jobs:
|
||||||
options: --dns 127.0.0.1
|
options: --dns 127.0.0.1
|
||||||
services:
|
services:
|
||||||
squid-proxy:
|
squid-proxy:
|
||||||
image: ubuntu/squid:latest
|
image: datadog/squid:latest
|
||||||
ports:
|
ports:
|
||||||
- 3128:3128
|
- 3128:3128
|
||||||
env:
|
env:
|
||||||
|
@ -112,7 +126,7 @@ jobs:
|
||||||
options: --dns 127.0.0.1
|
options: --dns 127.0.0.1
|
||||||
services:
|
services:
|
||||||
squid-proxy:
|
squid-proxy:
|
||||||
image: ubuntu/squid:latest
|
image: datadog/squid:latest
|
||||||
ports:
|
ports:
|
||||||
- 3128:3128
|
- 3128:3128
|
||||||
env:
|
env:
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
188
README.md
188
README.md
|
@ -1,80 +1,49 @@
|
||||||
# Cache action
|
# cache
|
||||||
|
|
||||||
This action allows caching dependencies and build outputs to improve workflow execution time.
|
This action allows caching dependencies and build outputs to improve workflow execution time.
|
||||||
|
|
||||||
>Two other actions are available in addition to the primary `cache` action:
|
|
||||||
>* [Restore action](./restore/README.md)
|
|
||||||
>* [Save action](./save/README.md)
|
|
||||||
|
|
||||||
[![Tests](https://github.com/actions/cache/actions/workflows/workflow.yml/badge.svg)](https://github.com/actions/cache/actions/workflows/workflow.yml)
|
[![Tests](https://github.com/actions/cache/actions/workflows/workflow.yml/badge.svg)](https://github.com/actions/cache/actions/workflows/workflow.yml)
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
See ["Caching dependencies to speed up workflows"](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows).
|
See ["Caching dependencies to speed up workflows"](https://help.github.com/github/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows).
|
||||||
|
|
||||||
## What's New
|
## What's New
|
||||||
|
|
||||||
### v3
|
### v3
|
||||||
|
* Added support for caching from GHES 3.5.
|
||||||
* Added support for caching in GHES 3.5+.
|
|
||||||
* Fixed download issue for files > 2GB during restore.
|
* Fixed download issue for files > 2GB during restore.
|
||||||
* Updated the minimum runner version support from node 12 -> node 16.
|
* Updated the minimum runner version support from node 12 -> node 16.
|
||||||
* Fixed avoiding empty cache save when no files are available for caching.
|
* Fixed avoiding empty cache save when no files are available for caching.
|
||||||
* Fixed tar creation error while trying to create tar with path as `~/` home folder on `ubuntu-latest`.
|
* Fixed tar creation error while trying to create tar with path as `~/` home folder on `ubuntu-latest`.
|
||||||
* Fixed zstd failing on amazon linux 2.0 runners.
|
|
||||||
* Fixed cache not working with github workspace directory or current directory.
|
|
||||||
* Fixed the download stuck problem by introducing a timeout of 1 hour for cache downloads.
|
|
||||||
* Fix zstd not working for windows on gnu tar in issues.
|
|
||||||
* Allowing users to provide a custom timeout as input for aborting download of a cache segment using an environment variable `SEGMENT_DOWNLOAD_TIMEOUT_MINS`. Default is 10 minutes.
|
|
||||||
* New actions are available for granular control over caches - [restore](restore/action.yml) and [save](save/action.yml).
|
|
||||||
* Support cross-os caching as an opt-in feature. See [Cross OS caching](./tips-and-workarounds.md#cross-os-cache) for more info.
|
|
||||||
* Added option to fail job on cache miss. See [Exit workflow on cache miss](./restore/README.md#exit-workflow-on-cache-miss) for more info.
|
|
||||||
* Fix zstd not being used after zstd version upgrade to 1.5.4 on hosted runners
|
|
||||||
* Added option to lookup cache without downloading it.
|
|
||||||
* Reduced segment size to 128MB and segment timeout to 10 minutes to fail fast in case the cache download is stuck.
|
|
||||||
|
|
||||||
See the [v2 README.md](https://github.com/actions/cache/blob/v2/README.md) for older updates.
|
Refer [here](https://github.com/actions/cache/blob/v2/README.md) for previous versions
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Pre-requisites
|
### Pre-requisites
|
||||||
|
Create a workflow `.yml` file in your repositories `.github/workflows` directory. An [example workflow](#example-workflow) is available below. For more information, reference the GitHub Help Documentation for [Creating a workflow file](https://help.github.com/en/articles/configuring-a-workflow#creating-a-workflow-file).
|
||||||
|
|
||||||
Create a workflow `.yml` file in your repository's `.github/workflows` directory. An [example workflow](#example-cache-workflow) is available below. For more information, see the GitHub Help Documentation for [Creating a workflow file](https://help.github.com/en/articles/configuring-a-workflow#creating-a-workflow-file).
|
If you are using this inside a container, a POSIX-compliant `tar` needs to be included and accessible in the execution path.
|
||||||
|
|
||||||
If you are using this inside a container, a POSIX-compliant `tar` needs to be included and accessible from the execution path.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Inputs
|
### Inputs
|
||||||
|
|
||||||
* `key` - An explicit key for a cache entry. See [creating a cache key](#creating-a-cache-key).
|
|
||||||
* `path` - A list of files, directories, and wildcard patterns to cache and restore. See [`@actions/glob`](https://github.com/actions/toolkit/tree/main/packages/glob) for supported patterns.
|
* `path` - A list of files, directories, and wildcard patterns to cache and restore. See [`@actions/glob`](https://github.com/actions/toolkit/tree/main/packages/glob) for supported patterns.
|
||||||
* `restore-keys` - An ordered list of prefix-matched keys to use for restoring stale cache if no cache hit occurred for key.
|
* `key` - An explicit key for restoring and saving the cache
|
||||||
* `enableCrossOsArchive` - An optional boolean when enabled, allows Windows runners to save or restore caches that can be restored or saved respectively on other platforms. Default: `false`
|
* `restore-keys` - An ordered list of keys to use for restoring stale cache if no cache hit occurred for key. Note
|
||||||
* `fail-on-cache-miss` - Fail the workflow if cache entry is not found. Default: `false`
|
`cache-hit` returns false in this case.
|
||||||
* `lookup-only` - If true, only checks if cache entry exists and skips download. Does not change save cache behavior. Default: `false`
|
|
||||||
|
|
||||||
#### Environment Variables
|
|
||||||
|
|
||||||
* `SEGMENT_DOWNLOAD_TIMEOUT_MINS` - Segment download timeout (in minutes, default `10`) to abort download of the segment if not completed in the defined number of minutes. [Read more](https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout)
|
|
||||||
|
|
||||||
### Outputs
|
### Outputs
|
||||||
|
|
||||||
* `cache-hit` - A boolean value to indicate an exact match was found for the key.
|
* `cache-hit` - A boolean value to indicate an exact match was found for the key
|
||||||
|
|
||||||
> **Note** `cache-hit` will only be set to `true` when a cache hit occurs for the exact `key` match. For a partial key match via `restore-keys` or a cache miss, it will be set to `false`.
|
> See [Skipping steps based on cache-hit](#Skipping-steps-based-on-cache-hit) for info on using this output
|
||||||
|
|
||||||
See [Skipping steps based on cache-hit](#skipping-steps-based-on-cache-hit) for info on using this output
|
|
||||||
|
|
||||||
### Cache scopes
|
### Cache scopes
|
||||||
|
The cache is scoped to the key and branch. The default branch cache is available to other branches.
|
||||||
The cache is scoped to the key, [version](#cache-version), and branch. The default branch cache is available to other branches.
|
|
||||||
|
|
||||||
See [Matching a cache key](https://help.github.com/en/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows#matching-a-cache-key) for more info.
|
See [Matching a cache key](https://help.github.com/en/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows#matching-a-cache-key) for more info.
|
||||||
|
|
||||||
### Example cache workflow
|
### Example workflow
|
||||||
|
|
||||||
#### Restoring and saving cache using a single action
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: Caching Primes
|
name: Caching Primes
|
||||||
|
@ -103,49 +72,7 @@ jobs:
|
||||||
run: /primes.sh -d prime-numbers
|
run: /primes.sh -d prime-numbers
|
||||||
```
|
```
|
||||||
|
|
||||||
The `cache` action provides a `cache-hit` output which is set to `true` when the cache is restored using the primary `key` and `false` when the cache is restored using `restore-keys` or no cache is restored.
|
> Note: You must use the `cache` action in your workflow before you need to use the files that might be restored from the cache. If the provided `key` doesn't match an existing cache, a new cache is automatically created if the job completes successfully.
|
||||||
|
|
||||||
#### Using a combination of restore and save actions
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: Caching Primes
|
|
||||||
|
|
||||||
on: push
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Restore cached Primes
|
|
||||||
id: cache-primes-restore
|
|
||||||
uses: actions/cache/restore@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
path/to/dependencies
|
|
||||||
some/other/dependencies
|
|
||||||
key: ${{ runner.os }}-primes
|
|
||||||
.
|
|
||||||
. //intermediate workflow steps
|
|
||||||
.
|
|
||||||
- name: Save Primes
|
|
||||||
id: cache-primes-save
|
|
||||||
uses: actions/cache/save@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
path/to/dependencies
|
|
||||||
some/other/dependencies
|
|
||||||
key: ${{ steps.cache-primes-restore.outputs.cache-primary-key }}
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Note**
|
|
||||||
> You must use the `cache` or `restore` action in your workflow before you need to use the files that might be restored from the cache. If the provided `key` matches an existing cache, a new cache is not created and if the provided `key` doesn't match an existing cache, a new cache is automatically created provided the job completes successfully.
|
|
||||||
|
|
||||||
## Caching Strategies
|
|
||||||
|
|
||||||
With the introduction of the `restore` and `save` actions, a lot of caching use cases can now be achieved. Please see the [caching strategies](./caching-strategies.md) document for understanding how you can use the actions strategically to achieve the desired goal.
|
|
||||||
|
|
||||||
## Implementation Examples
|
## Implementation Examples
|
||||||
|
|
||||||
|
@ -153,37 +80,35 @@ Every programming language and framework has its own way of caching.
|
||||||
|
|
||||||
See [Examples](examples.md) for a list of `actions/cache` implementations for use with:
|
See [Examples](examples.md) for a list of `actions/cache` implementations for use with:
|
||||||
|
|
||||||
* [C# - NuGet](./examples.md#c---nuget)
|
- [C# - NuGet](./examples.md#c---nuget)
|
||||||
* [Clojure - Lein Deps](./examples.md#clojure---lein-deps)
|
- [D - DUB](./examples.md#d---dub)
|
||||||
* [D - DUB](./examples.md#d---dub)
|
- [Deno](./examples.md#deno)
|
||||||
* [Deno](./examples.md#deno)
|
- [Elixir - Mix](./examples.md#elixir---mix)
|
||||||
* [Elixir - Mix](./examples.md#elixir---mix)
|
- [Go - Modules](./examples.md#go---modules)
|
||||||
* [Go - Modules](./examples.md#go---modules)
|
- [Haskell - Cabal](./examples.md#haskell---cabal)
|
||||||
* [Haskell - Cabal](./examples.md#haskell---cabal)
|
- [Haskell - Stack](./examples.md#haskell---stack)
|
||||||
* [Haskell - Stack](./examples.md#haskell---stack)
|
- [Java - Gradle](./examples.md#java---gradle)
|
||||||
* [Java - Gradle](./examples.md#java---gradle)
|
- [Java - Maven](./examples.md#java---maven)
|
||||||
* [Java - Maven](./examples.md#java---maven)
|
- [Node - npm](./examples.md#node---npm)
|
||||||
* [Node - npm](./examples.md#node---npm)
|
- [Node - Lerna](./examples.md#node---lerna)
|
||||||
* [Node - Lerna](./examples.md#node---lerna)
|
- [Node - Yarn](./examples.md#node---yarn)
|
||||||
* [Node - Yarn](./examples.md#node---yarn)
|
- [OCaml/Reason - esy](./examples.md#ocamlreason---esy)
|
||||||
* [OCaml/Reason - esy](./examples.md#ocamlreason---esy)
|
- [PHP - Composer](./examples.md#php---composer)
|
||||||
* [PHP - Composer](./examples.md#php---composer)
|
- [Python - pip](./examples.md#python---pip)
|
||||||
* [Python - pip](./examples.md#python---pip)
|
- [Python - pipenv](./examples.md#python---pipenv)
|
||||||
* [Python - pipenv](./examples.md#python---pipenv)
|
- [R - renv](./examples.md#r---renv)
|
||||||
* [R - renv](./examples.md#r---renv)
|
- [Ruby - Bundler](./examples.md#ruby---bundler)
|
||||||
* [Ruby - Bundler](./examples.md#ruby---bundler)
|
- [Rust - Cargo](./examples.md#rust---cargo)
|
||||||
* [Rust - Cargo](./examples.md#rust---cargo)
|
- [Scala - SBT](./examples.md#scala---sbt)
|
||||||
* [Scala - SBT](./examples.md#scala---sbt)
|
- [Swift, Objective-C - Carthage](./examples.md#swift-objective-c---carthage)
|
||||||
* [Swift, Objective-C - Carthage](./examples.md#swift-objective-c---carthage)
|
- [Swift, Objective-C - CocoaPods](./examples.md#swift-objective-c---cocoapods)
|
||||||
* [Swift, Objective-C - CocoaPods](./examples.md#swift-objective-c---cocoapods)
|
- [Swift - Swift Package Manager](./examples.md#swift---swift-package-manager)
|
||||||
* [Swift - Swift Package Manager](./examples.md#swift---swift-package-manager)
|
|
||||||
* [Swift - Mint](./examples.md#swift---mint)
|
|
||||||
|
|
||||||
## Creating a cache key
|
## Creating a cache key
|
||||||
|
|
||||||
A cache key can include any of the contexts, functions, literals, and operators supported by GitHub Actions.
|
A cache key can include any of the contexts, functions, literals, and operators supported by GitHub Actions.
|
||||||
|
|
||||||
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.
|
For example, using the [`hashFiles`](https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#hashfiles) function allows you to create a new cache when dependencies change.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v3
|
||||||
|
@ -201,7 +126,7 @@ Additionally, you can use arbitrary command output in a cache key, such as a dat
|
||||||
- name: Get Date
|
- name: Get Date
|
||||||
id: get-date
|
id: get-date
|
||||||
run: |
|
run: |
|
||||||
echo "date=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_OUTPUT
|
echo "::set-output name=date::$(/bin/date -u "+%Y%m%d")"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v3
|
||||||
|
@ -218,10 +143,9 @@ A repository can have up to 10GB of caches. Once the 10GB limit is reached, olde
|
||||||
|
|
||||||
## Skipping steps based on cache-hit
|
## Skipping steps based on cache-hit
|
||||||
|
|
||||||
Using the `cache-hit` output, subsequent steps (such as install or build) can be skipped when a cache hit occurs on the key. It is recommended to install missing/updated dependencies in case of a partial key match when the key is dependent on the `hash` of the package file.
|
Using the `cache-hit` output, subsequent steps (such as install or build) can be skipped when a cache hit occurs on the key.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
@ -237,17 +161,13 @@ steps:
|
||||||
run: /install.sh
|
run: /install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note** The `id` defined in `actions/cache` must match the `id` in the `if` statement (i.e. `steps.[ID].outputs.cache-hit`)
|
> Note: The `id` defined in `actions/cache` must match the `id` in the `if` statement (i.e. `steps.[ID].outputs.cache-hit`)
|
||||||
|
|
||||||
|
|
||||||
## Cache Version
|
## Cache Version
|
||||||
|
Cache version is unique for a combination of compression tool used for compression of cache (Gzip, Zstd, etc based on runner OS) and the path of directories being cached. If two caches have different versions, they are identified as unique cache entries. This also means that a cache created on `windows-latest` runner can't be restored on `ubuntu-latest` as cache `Version`s are different.
|
||||||
|
|
||||||
Cache version is a hash [generated](https://github.com/actions/toolkit/blob/500d0b42fee2552ae9eeb5933091fe2fbf14e72d/packages/cache/src/internal/cacheHttpClient.ts#L73-L90) for a combination of compression tool used (Gzip, Zstd, etc. based on the runner OS) and the `path` of directories being cached. If two caches have different versions, they are identified as unique caches while matching. This, for example, means that a cache created on a `windows-latest` runner can't be restored on `ubuntu-latest` as cache `Version`s are different.
|
Example: Below example will create 3 unique caches with same keys. Ubuntu and windows runners will use different compression technique and hence create two different caches. And `build-linux` will create two different caches as the `paths` are different.
|
||||||
|
|
||||||
> Pro tip: The [list caches](https://docs.github.com/en/rest/actions/cache#list-github-actions-caches-for-a-repository) API can be used to get the version of a cache. This can be helpful to troubleshoot cache miss due to version.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example</summary>
|
|
||||||
The workflow will create 3 unique caches with same keys. Ubuntu and windows runners will use different compression technique and hence create two different caches. And `build-linux` will create two different caches as the `paths` are different.
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -295,26 +215,8 @@ jobs:
|
||||||
run: ./generate-primes -d prime-numbers
|
run: ./generate-primes -d prime-numbers
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Known practices and workarounds
|
|
||||||
|
|
||||||
There are a number of community practices/workarounds to fulfill specific requirements. You may choose to use them if they suit your use case. Note these are not necessarily the only solution or even a recommended solution.
|
|
||||||
|
|
||||||
* [Cache segment restore timeout](./tips-and-workarounds.md#cache-segment-restore-timeout)
|
|
||||||
* [Update a cache](./tips-and-workarounds.md#update-a-cache)
|
|
||||||
* [Use cache across feature branches](./tips-and-workarounds.md#use-cache-across-feature-branches)
|
|
||||||
* [Cross OS cache](./tips-and-workarounds.md#cross-os-cache)
|
|
||||||
* [Force deletion of caches overriding default cache eviction policy](./tips-and-workarounds.md#force-deletion-of-caches-overriding-default-cache-eviction-policy)
|
|
||||||
|
|
||||||
### Windows environment variables
|
|
||||||
|
|
||||||
Please note that Windows environment variables (like `%LocalAppData%`) will NOT be expanded by this action. Instead, prefer using `~` in your paths which will expand to the HOME directory. For example, instead of `%LocalAppData%`, use `~\AppData\Local`. For a list of supported default environment variables, see the [Learn GitHub Actions: Variables](https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables) page.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
We would love for you to contribute to `actions/cache`, pull requests are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) for more information.
|
||||||
We would love for you to contribute to `actions/cache`. Pull requests are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) for more information.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
The scripts and documentation in this project are released under the [MIT License](LICENSE)
|
The scripts and documentation in this project are released under the [MIT License](LICENSE)
|
||||||
|
|
93
RELEASES.md
93
RELEASES.md
|
@ -1,113 +1,20 @@
|
||||||
# Releases
|
# Releases
|
||||||
|
|
||||||
### 3.0.0
|
### 3.0.0
|
||||||
|
|
||||||
- Updated minimum runner version support from node 12 -> node 16
|
- Updated minimum runner version support from node 12 -> node 16
|
||||||
|
|
||||||
### 3.0.1
|
### 3.0.1
|
||||||
|
|
||||||
- Added support for caching from GHES 3.5.
|
- Added support for caching from GHES 3.5.
|
||||||
- Fixed download issue for files > 2GB during restore.
|
- Fixed download issue for files > 2GB during restore.
|
||||||
|
|
||||||
### 3.0.2
|
### 3.0.2
|
||||||
|
|
||||||
- Added support for dynamic cache size cap on GHES.
|
- Added support for dynamic cache size cap on GHES.
|
||||||
|
|
||||||
### 3.0.3
|
### 3.0.3
|
||||||
|
|
||||||
- Fixed avoiding empty cache save when no files are available for caching. ([issue](https://github.com/actions/cache/issues/624))
|
- Fixed avoiding empty cache save when no files are available for caching. ([issue](https://github.com/actions/cache/issues/624))
|
||||||
|
|
||||||
### 3.0.4
|
### 3.0.4
|
||||||
|
|
||||||
- Fixed tar creation error while trying to create tar with path as `~/` home folder on `ubuntu-latest`. ([issue](https://github.com/actions/cache/issues/689))
|
- Fixed tar creation error while trying to create tar with path as `~/` home folder on `ubuntu-latest`. ([issue](https://github.com/actions/cache/issues/689))
|
||||||
|
|
||||||
### 3.0.5
|
### 3.0.5
|
||||||
|
|
||||||
- Removed error handling by consuming actions/cache 3.0 toolkit, Now cache server error handling will be done by toolkit. ([PR](https://github.com/actions/cache/pull/834))
|
- Removed error handling by consuming actions/cache 3.0 toolkit, Now cache server error handling will be done by toolkit. ([PR](https://github.com/actions/cache/pull/834))
|
||||||
|
|
||||||
### 3.0.6
|
|
||||||
|
|
||||||
- Fixed [#809](https://github.com/actions/cache/issues/809) - zstd -d: no such file or directory error
|
|
||||||
- Fixed [#833](https://github.com/actions/cache/issues/833) - cache doesn't work with github workspace directory
|
|
||||||
|
|
||||||
### 3.0.7
|
|
||||||
|
|
||||||
- Fixed [#810](https://github.com/actions/cache/issues/810) - download stuck issue. A new timeout is introduced in the download process to abort the download if it gets stuck and doesn't finish within an hour.
|
|
||||||
|
|
||||||
### 3.0.8
|
|
||||||
|
|
||||||
- Fix zstd not working for windows on gnu tar in issues [#888](https://github.com/actions/cache/issues/888) and [#891](https://github.com/actions/cache/issues/891).
|
|
||||||
- Allowing users to provide a custom timeout as input for aborting download of a cache segment using an environment variable `SEGMENT_DOWNLOAD_TIMEOUT_MINS`. Default is 60 minutes.
|
|
||||||
|
|
||||||
### 3.0.9
|
|
||||||
|
|
||||||
- Enhanced the warning message for cache unavailablity in case of GHES.
|
|
||||||
|
|
||||||
### 3.0.10
|
|
||||||
|
|
||||||
- Fix a bug with sorting inputs.
|
|
||||||
- Update definition for restore-keys in README.md
|
|
||||||
|
|
||||||
### 3.0.11
|
|
||||||
|
|
||||||
- Update toolkit version to 3.0.5 to include `@actions/core@^1.10.0`
|
|
||||||
- Update `@actions/cache` to use updated `saveState` and `setOutput` functions from `@actions/core@^1.10.0`
|
|
||||||
|
|
||||||
### 3.1.0-beta.1
|
|
||||||
|
|
||||||
- Update `@actions/cache` on windows to use gnu tar and zstd by default and fallback to bsdtar and zstd if gnu tar is not available. ([issue](https://github.com/actions/cache/issues/984))
|
|
||||||
|
|
||||||
### 3.1.0-beta.2
|
|
||||||
|
|
||||||
- Added support for fallback to gzip to restore old caches on windows.
|
|
||||||
|
|
||||||
### 3.1.0-beta.3
|
|
||||||
|
|
||||||
- Bug fixes for bsdtar fallback if gnutar not available and gzip fallback if cache saved using old cache action on windows.
|
|
||||||
|
|
||||||
### 3.2.0-beta.1
|
|
||||||
|
|
||||||
- Added two new actions - [restore](restore/action.yml) and [save](save/action.yml) for granular control on cache.
|
|
||||||
|
|
||||||
### 3.2.0
|
|
||||||
|
|
||||||
- Released the two new actions - [restore](restore/action.yml) and [save](save/action.yml) for granular control on cache
|
|
||||||
|
|
||||||
### 3.2.1
|
|
||||||
|
|
||||||
- Update `@actions/cache` on windows to use gnu tar and zstd by default and fallback to bsdtar and zstd if gnu tar is not available. ([issue](https://github.com/actions/cache/issues/984))
|
|
||||||
- Added support for fallback to gzip to restore old caches on windows.
|
|
||||||
- Added logs for cache version in case of a cache miss.
|
|
||||||
|
|
||||||
### 3.2.2
|
|
||||||
|
|
||||||
- Reverted the changes made in 3.2.1 to use gnu tar and zstd by default on windows.
|
|
||||||
|
|
||||||
### 3.2.3
|
|
||||||
|
|
||||||
- Support cross os caching on Windows as an opt-in feature.
|
|
||||||
- Fix issue with symlink restoration on Windows for cross-os caches.
|
|
||||||
|
|
||||||
### 3.2.4
|
|
||||||
|
|
||||||
- Added option to fail job on cache miss.
|
|
||||||
|
|
||||||
### 3.2.5
|
|
||||||
|
|
||||||
- Added fix to prevent from setting MYSYS environment variable globally.
|
|
||||||
|
|
||||||
### 3.2.6
|
|
||||||
|
|
||||||
- Fix zstd not being used after zstd version upgrade to 1.5.4 on hosted runners.
|
|
||||||
|
|
||||||
### 3.3.0
|
|
||||||
|
|
||||||
- Added option to lookup cache without downloading it.
|
|
||||||
|
|
||||||
### 3.3.1
|
|
||||||
|
|
||||||
- Reduced segment size to 128MB and segment timeout to 10 minutes to fail fast in case the cache download is stuck.
|
|
||||||
|
|
||||||
### 3.3.2
|
|
||||||
|
|
||||||
- Fixes bug with Azure SDK causing blob downloads to get stuck.
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as cache from "@actions/cache";
|
import * as cache from "@actions/cache";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
import { Events, RefKey } from "../src/constants";
|
import { Events, Outputs, RefKey, State } from "../src/constants";
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
import * as testUtils from "../src/utils/testUtils";
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
|
|
||||||
|
@ -79,6 +79,83 @@ test("isExactKeyMatch with same key and different casing returns true", () => {
|
||||||
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(true);
|
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("setOutputAndState with undefined entry to set cache-hit output", () => {
|
||||||
|
const key = "linux-rust";
|
||||||
|
const cacheKey = undefined;
|
||||||
|
|
||||||
|
const setOutputMock = jest.spyOn(core, "setOutput");
|
||||||
|
const saveStateMock = jest.spyOn(core, "saveState");
|
||||||
|
|
||||||
|
actionUtils.setOutputAndState(key, cacheKey);
|
||||||
|
|
||||||
|
expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "false");
|
||||||
|
expect(setOutputMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(saveStateMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("setOutputAndState with exact match to set cache-hit output and state", () => {
|
||||||
|
const key = "linux-rust";
|
||||||
|
const cacheKey = "linux-rust";
|
||||||
|
|
||||||
|
const setOutputMock = jest.spyOn(core, "setOutput");
|
||||||
|
const saveStateMock = jest.spyOn(core, "saveState");
|
||||||
|
|
||||||
|
actionUtils.setOutputAndState(key, cacheKey);
|
||||||
|
|
||||||
|
expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "true");
|
||||||
|
expect(setOutputMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(saveStateMock).toHaveBeenCalledWith(State.CacheMatchedKey, cacheKey);
|
||||||
|
expect(saveStateMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("setOutputAndState with no exact match to set cache-hit output and state", () => {
|
||||||
|
const key = "linux-rust";
|
||||||
|
const cacheKey = "linux-rust-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||||
|
|
||||||
|
const setOutputMock = jest.spyOn(core, "setOutput");
|
||||||
|
const saveStateMock = jest.spyOn(core, "saveState");
|
||||||
|
|
||||||
|
actionUtils.setOutputAndState(key, cacheKey);
|
||||||
|
|
||||||
|
expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "false");
|
||||||
|
expect(setOutputMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(saveStateMock).toHaveBeenCalledWith(State.CacheMatchedKey, cacheKey);
|
||||||
|
expect(saveStateMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getCacheState with no state returns undefined", () => {
|
||||||
|
const getStateMock = jest.spyOn(core, "getState");
|
||||||
|
getStateMock.mockImplementation(() => {
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = actionUtils.getCacheState();
|
||||||
|
|
||||||
|
expect(state).toBe(undefined);
|
||||||
|
|
||||||
|
expect(getStateMock).toHaveBeenCalledWith(State.CacheMatchedKey);
|
||||||
|
expect(getStateMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getCacheState with valid state", () => {
|
||||||
|
const cacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||||
|
|
||||||
|
const getStateMock = jest.spyOn(core, "getState");
|
||||||
|
getStateMock.mockImplementation(() => {
|
||||||
|
return cacheKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = actionUtils.getCacheState();
|
||||||
|
|
||||||
|
expect(state).toEqual(cacheKey);
|
||||||
|
|
||||||
|
expect(getStateMock).toHaveBeenCalledWith(State.CacheMatchedKey);
|
||||||
|
expect(getStateMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
test("logWarning logs a message with a warning prefix", () => {
|
test("logWarning logs a message with a warning prefix", () => {
|
||||||
const message = "A warning occurred.";
|
const message = "A warning occurred.";
|
||||||
|
|
||||||
|
@ -138,22 +215,6 @@ test("getInputAsArray handles empty lines correctly", () => {
|
||||||
expect(actionUtils.getInputAsArray("foo")).toEqual(["bar", "baz"]);
|
expect(actionUtils.getInputAsArray("foo")).toEqual(["bar", "baz"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getInputAsArray removes spaces after ! at the beginning", () => {
|
|
||||||
testUtils.setInput(
|
|
||||||
"foo",
|
|
||||||
"! bar\n! baz\n! qux\n!quux\ncorge\ngrault! garply\n!\r\t waldo"
|
|
||||||
);
|
|
||||||
expect(actionUtils.getInputAsArray("foo")).toEqual([
|
|
||||||
"!bar",
|
|
||||||
"!baz",
|
|
||||||
"!qux",
|
|
||||||
"!quux",
|
|
||||||
"corge",
|
|
||||||
"grault! garply",
|
|
||||||
"!waldo"
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsInt returns undefined if input not set", () => {
|
test("getInputAsInt returns undefined if input not set", () => {
|
||||||
expect(actionUtils.getInputAsInt("undefined")).toBeUndefined();
|
expect(actionUtils.getInputAsInt("undefined")).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
@ -174,26 +235,6 @@ test("getInputAsInt throws if required and value missing", () => {
|
||||||
).toThrowError();
|
).toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getInputAsBool returns false if input not set", () => {
|
|
||||||
expect(actionUtils.getInputAsBool("undefined")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsBool returns value if input is valid", () => {
|
|
||||||
testUtils.setInput("foo", "true");
|
|
||||||
expect(actionUtils.getInputAsBool("foo")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsBool returns false if input is invalid or NaN", () => {
|
|
||||||
testUtils.setInput("foo", "bar");
|
|
||||||
expect(actionUtils.getInputAsBool("foo")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsBool throws if required and value missing", () => {
|
|
||||||
expect(() =>
|
|
||||||
actionUtils.getInputAsBool("undefined2", { required: true })
|
|
||||||
).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isCacheFeatureAvailable for ac enabled", () => {
|
test("isCacheFeatureAvailable for ac enabled", () => {
|
||||||
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => true);
|
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => true);
|
||||||
|
|
||||||
|
@ -203,8 +244,8 @@ test("isCacheFeatureAvailable for ac enabled", () => {
|
||||||
test("isCacheFeatureAvailable for ac disabled on GHES", () => {
|
test("isCacheFeatureAvailable for ac disabled on GHES", () => {
|
||||||
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => false);
|
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => 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.
|
const message =
|
||||||
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)`;
|
"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.";
|
||||||
const infoMock = jest.spyOn(core, "info");
|
const infoMock = jest.spyOn(core, "info");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as cache from "@actions/cache";
|
import * as cache from "@actions/cache";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
import { Events, RefKey } from "../src/constants";
|
import { Events, Inputs, RefKey } from "../src/constants";
|
||||||
import { restoreRun } from "../src/restoreImpl";
|
import run from "../src/restore";
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
import * as testUtils from "../src/utils/testUtils";
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
|
|
||||||
|
@ -27,17 +27,9 @@ beforeAll(() => {
|
||||||
return actualUtils.getInputAsArray(name, options);
|
return actualUtils.getInputAsArray(name, options);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
|
|
||||||
(name, options) => {
|
|
||||||
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
|
||||||
return actualUtils.getInputAsBool(name, options);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.restoreAllMocks();
|
|
||||||
process.env[Events.Key] = Events.Push;
|
process.env[Events.Key] = Events.Push;
|
||||||
process.env[RefKey] = "refs/heads/feature-branch";
|
process.env[RefKey] = "refs/heads/feature-branch";
|
||||||
|
|
||||||
|
@ -53,13 +45,164 @@ afterEach(() => {
|
||||||
delete process.env[RefKey];
|
delete process.env[RefKey];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 run();
|
||||||
|
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(failedMock).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(actionUtils, "setCacheHitOutput");
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith(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(actionUtils, "setCacheHitOutput");
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith(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({
|
||||||
|
path: path,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const infoMock = jest.spyOn(core, "info");
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
const stateMock = jest.spyOn(core, "saveState");
|
||||||
|
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
||||||
|
const restoreCacheMock = jest
|
||||||
|
.spyOn(cache, "restoreCache")
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||||
|
|
||||||
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
|
||||||
|
|
||||||
|
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore with no path should fail", async () => {
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||||
|
await run();
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
// this input isn't necessary for restore b/c tarball contains entries relative to workspace
|
||||||
|
expect(failedMock).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 run();
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(failedMock).toHaveBeenCalledWith(
|
||||||
|
"Input required and not supplied: key"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore with too many keys should fail", async () => {
|
||||||
|
const path = "node_modules";
|
||||||
|
const key = "node-test";
|
||||||
|
const restoreKeys = [...Array(20).keys()].map(x => x.toString());
|
||||||
|
testUtils.setInputs({
|
||||||
|
path: path,
|
||||||
|
key,
|
||||||
|
restoreKeys
|
||||||
|
});
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||||
|
await run();
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, restoreKeys);
|
||||||
|
expect(failedMock).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
|
||||||
|
testUtils.setInputs({
|
||||||
|
path: path,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||||
|
await run();
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||||
|
expect(failedMock).toHaveBeenCalledWith(
|
||||||
|
`Key Validation Error: ${key} cannot be larger than 512 characters.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore with invalid key should fail", async () => {
|
||||||
|
const path = "node_modules";
|
||||||
|
const key = "comma,comma";
|
||||||
|
testUtils.setInputs({
|
||||||
|
path: path,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||||
|
await run();
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||||
|
expect(failedMock).toHaveBeenCalledWith(
|
||||||
|
`Key Validation Error: ${key} cannot contain commas.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("restore with no cache found", async () => {
|
test("restore with no cache found", async () => {
|
||||||
const path = "node_modules";
|
const path = "node_modules";
|
||||||
const key = "node-test";
|
const key = "node-test";
|
||||||
testUtils.setInputs({
|
testUtils.setInputs({
|
||||||
path: path,
|
path: path,
|
||||||
key,
|
key
|
||||||
enableCrossOsArchive: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const infoMock = jest.spyOn(core, "info");
|
const infoMock = jest.spyOn(core, "info");
|
||||||
|
@ -71,22 +214,12 @@ test("restore with no cache found", async () => {
|
||||||
return Promise.resolve(undefined);
|
return Promise.resolve(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
await restoreRun();
|
await run();
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith(
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||||
[path],
|
|
||||||
key,
|
|
||||||
[],
|
|
||||||
{
|
|
||||||
lookupOnly: false
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||||
expect(stateMock).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
expect(infoMock).toHaveBeenCalledWith(
|
expect(infoMock).toHaveBeenCalledWith(
|
||||||
|
@ -101,8 +234,7 @@ test("restore with restore keys and no cache found", async () => {
|
||||||
testUtils.setInputs({
|
testUtils.setInputs({
|
||||||
path: path,
|
path: path,
|
||||||
key,
|
key,
|
||||||
restoreKeys: [restoreKey],
|
restoreKeys: [restoreKey]
|
||||||
enableCrossOsArchive: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const infoMock = jest.spyOn(core, "info");
|
const infoMock = jest.spyOn(core, "info");
|
||||||
|
@ -114,22 +246,12 @@ test("restore with restore keys and no cache found", async () => {
|
||||||
return Promise.resolve(undefined);
|
return Promise.resolve(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
await restoreRun();
|
await run();
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith(
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]);
|
||||||
[path],
|
|
||||||
key,
|
|
||||||
[restoreKey],
|
|
||||||
{
|
|
||||||
lookupOnly: false
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||||
expect(stateMock).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
expect(infoMock).toHaveBeenCalledWith(
|
expect(infoMock).toHaveBeenCalledWith(
|
||||||
|
@ -142,39 +264,27 @@ test("restore with cache found for key", async () => {
|
||||||
const key = "node-test";
|
const key = "node-test";
|
||||||
testUtils.setInputs({
|
testUtils.setInputs({
|
||||||
path: path,
|
path: path,
|
||||||
key,
|
key
|
||||||
enableCrossOsArchive: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const infoMock = jest.spyOn(core, "info");
|
const infoMock = jest.spyOn(core, "info");
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
const stateMock = jest.spyOn(core, "saveState");
|
const stateMock = jest.spyOn(core, "saveState");
|
||||||
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
|
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
||||||
const restoreCacheMock = jest
|
const restoreCacheMock = jest
|
||||||
.spyOn(cache, "restoreCache")
|
.spyOn(cache, "restoreCache")
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
return Promise.resolve(key);
|
return Promise.resolve(key);
|
||||||
});
|
});
|
||||||
|
|
||||||
await restoreRun();
|
await run();
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith(
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||||
[path],
|
|
||||||
key,
|
|
||||||
[],
|
|
||||||
{
|
|
||||||
lookupOnly: false
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||||
expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", key);
|
|
||||||
expect(stateMock).toHaveBeenCalledTimes(2);
|
|
||||||
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
|
||||||
|
|
||||||
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
|
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
@ -187,172 +297,30 @@ test("restore with cache found for restore key", async () => {
|
||||||
testUtils.setInputs({
|
testUtils.setInputs({
|
||||||
path: path,
|
path: path,
|
||||||
key,
|
key,
|
||||||
restoreKeys: [restoreKey],
|
restoreKeys: [restoreKey]
|
||||||
enableCrossOsArchive: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const infoMock = jest.spyOn(core, "info");
|
const infoMock = jest.spyOn(core, "info");
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
const stateMock = jest.spyOn(core, "saveState");
|
const stateMock = jest.spyOn(core, "saveState");
|
||||||
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
|
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
||||||
const restoreCacheMock = jest
|
const restoreCacheMock = jest
|
||||||
.spyOn(cache, "restoreCache")
|
.spyOn(cache, "restoreCache")
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
return Promise.resolve(restoreKey);
|
return Promise.resolve(restoreKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
await restoreRun();
|
await run();
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith(
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]);
|
||||||
[path],
|
|
||||||
key,
|
|
||||||
[restoreKey],
|
|
||||||
{
|
|
||||||
lookupOnly: false
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||||
expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", restoreKey);
|
|
||||||
expect(stateMock).toHaveBeenCalledTimes(2);
|
|
||||||
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
|
||||||
expect(infoMock).toHaveBeenCalledWith(
|
|
||||||
`Cache restored from key: ${restoreKey}`
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Fail restore when fail on cache miss is enabled and primary + restore keys not found", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
const restoreKey = "node-";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
restoreKeys: [restoreKey],
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
|
||||||
`Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${key}`
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore when fail on cache miss is enabled and primary key doesn't match restored key", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
const restoreKey = "node-";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
restoreKeys: [restoreKey],
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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(infoMock).toHaveBeenCalledWith(
|
||||||
`Cache restored from key: ${restoreKey}`
|
`Cache restored from key: ${restoreKey}`
|
||||||
);
|
);
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("restore with fail on cache miss disabled and no cache found", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
const restoreKey = "node-";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
restoreKeys: [restoreKey],
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
|
||||||
`Cache not found for input keys: ${key}, ${restoreKey}`
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,451 +0,0 @@
|
||||||
import * as cache from "@actions/cache";
|
|
||||||
import * as core from "@actions/core";
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
|
|
||||||
(name, options) => {
|
|
||||||
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
|
||||||
return actualUtils.getInputAsBool(name, options);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
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(() => {
|
|
||||||
testUtils.clearInputs();
|
|
||||||
delete process.env[Events.Key];
|
|
||||||
delete process.env[RefKey];
|
|
||||||
});
|
|
||||||
|
|
||||||
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(failedMock).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");
|
|
||||||
|
|
||||||
await restoreImpl(new StateProvider());
|
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(setCacheHitOutputMock).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");
|
|
||||||
|
|
||||||
await restoreImpl(new StateProvider());
|
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(setCacheHitOutputMock).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({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
|
||||||
"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(
|
|
||||||
"Input required and not supplied: key"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with too many keys should fail", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
const restoreKeys = [...Array(20).keys()].map(x => x.toString());
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
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
|
|
||||||
);
|
|
||||||
expect(failedMock).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
|
|
||||||
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
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledWith(
|
|
||||||
`Key Validation Error: ${key} cannot be larger than 512 characters.`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with invalid key should fail", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "comma,comma";
|
|
||||||
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
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledWith(
|
|
||||||
`Key Validation Error: ${key} cannot contain commas.`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with no cache found", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
|
||||||
`Cache not found for input keys: ${key}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with restore keys and no cache found", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
const restoreKey = "node-";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
restoreKeys: [restoreKey],
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
|
||||||
`Cache not found for input keys: ${key}, ${restoreKey}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with cache found for key", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with cache found for restore key", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
const restoreKey = "node-";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
restoreKeys: [restoreKey],
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
|
||||||
`Cache restored from key: ${restoreKey}`
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with lookup-only set", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
await restoreImpl(new StateProvider());
|
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
||||||
[path],
|
|
||||||
key,
|
|
||||||
[],
|
|
||||||
{
|
|
||||||
lookupOnly: true
|
|
||||||
},
|
|
||||||
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 found and can be restored from key: ${key}`
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
|
@ -1,222 +0,0 @@
|
||||||
import * as cache from "@actions/cache";
|
|
||||||
import * as core from "@actions/core";
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
|
|
||||||
(name, options) => {
|
|
||||||
return jest
|
|
||||||
.requireActual("../src/utils/actionUtils")
|
|
||||||
.getInputAsBool(name, options);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
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(() => {
|
|
||||||
testUtils.clearInputs();
|
|
||||||
delete process.env[Events.Key];
|
|
||||||
delete process.env[RefKey];
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with no cache found", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
await restoreOnlyRun();
|
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
||||||
[path],
|
|
||||||
key,
|
|
||||||
[],
|
|
||||||
{
|
|
||||||
lookupOnly: false
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
|
|
||||||
expect(outputMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
|
|
||||||
expect(infoMock).toHaveBeenCalledWith(
|
|
||||||
`Cache not found for input keys: ${key}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with restore keys and no cache found", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
const restoreKey = "node-";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
restoreKeys: [restoreKey],
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
|
||||||
`Cache not found for input keys: ${key}, ${restoreKey}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with cache found for key", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with cache found for restore key", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
const restoreKey = "node-";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
restoreKeys: [restoreKey],
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
await restoreOnlyRun();
|
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
||||||
[path],
|
|
||||||
key,
|
|
||||||
[restoreKey],
|
|
||||||
{
|
|
||||||
lookupOnly: false
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
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(
|
|
||||||
`Cache restored from key: ${restoreKey}`
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
|
@ -15,8 +15,8 @@ beforeAll(() => {
|
||||||
return jest.requireActual("@actions/core").getInput(name, options);
|
return jest.requireActual("@actions/core").getInput(name, options);
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.spyOn(core, "getState").mockImplementation(name => {
|
jest.spyOn(actionUtils, "getCacheState").mockImplementation(() => {
|
||||||
return jest.requireActual("@actions/core").getState(name);
|
return jest.requireActual("../src/utils/actionUtils").getCacheState();
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
|
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
|
||||||
|
@ -35,14 +35,6 @@ beforeAll(() => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
|
|
||||||
(name, options) => {
|
|
||||||
return jest
|
|
||||||
.requireActual("../src/utils/actionUtils")
|
|
||||||
.getInputAsBool(name, options);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
|
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
|
||||||
(key, cacheResult) => {
|
(key, cacheResult) => {
|
||||||
return jest
|
return jest
|
||||||
|
@ -73,7 +65,72 @@ afterEach(() => {
|
||||||
delete process.env[RefKey];
|
delete process.env[RefKey];
|
||||||
});
|
});
|
||||||
|
|
||||||
test("save with valid inputs uploads a cache", async () => {
|
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 run();
|
||||||
|
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(failedMock).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 savedCacheKey;
|
||||||
|
})
|
||||||
|
// Cache Key State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
|
`Error retrieving key from state.`
|
||||||
|
);
|
||||||
|
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save without AC available should no-op", async () => {
|
||||||
|
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
|
||||||
|
() => false
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(saveCacheMock).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");
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save on GHES with AC available", async () => {
|
||||||
|
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
|
||||||
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||||
|
@ -82,11 +139,11 @@ test("save with valid inputs uploads a cache", async () => {
|
||||||
jest.spyOn(core, "getState")
|
jest.spyOn(core, "getState")
|
||||||
// Cache Entry State
|
// Cache Entry State
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
return primaryKey;
|
return savedCacheKey;
|
||||||
})
|
})
|
||||||
// Cache Key State
|
// Cache Key State
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
return savedCacheKey;
|
return primaryKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputPath = "node_modules";
|
const inputPath = "node_modules";
|
||||||
|
@ -103,14 +160,232 @@ test("save with valid inputs uploads a cache", async () => {
|
||||||
await run();
|
await run();
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey, {
|
||||||
[inputPath],
|
|
||||||
primaryKey,
|
|
||||||
{
|
|
||||||
uploadChunkSize: 4000000
|
uploadChunkSize: 4000000
|
||||||
},
|
});
|
||||||
false
|
|
||||||
);
|
expect(failedMock).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");
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(infoMock).toHaveBeenCalledWith(
|
||||||
|
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
||||||
|
);
|
||||||
|
expect(failedMock).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");
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
|
"Input required and not supplied: path"
|
||||||
|
);
|
||||||
|
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(failedMock).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;
|
||||||
|
});
|
||||||
|
|
||||||
|
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."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
|
[inputPath],
|
||||||
|
primaryKey,
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
|
[inputPath],
|
||||||
|
primaryKey,
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputPath = "node_modules";
|
||||||
|
testUtils.setInput(Inputs.Path, inputPath);
|
||||||
|
|
||||||
|
const saveCacheMock = jest
|
||||||
|
.spyOn(cache, "saveCache")
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error("HTTP Error Occurred");
|
||||||
|
});
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
|
[inputPath],
|
||||||
|
primaryKey,
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
|
||||||
|
|
||||||
|
expect(failedMock).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;
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey, {
|
||||||
|
uploadChunkSize: 4000000
|
||||||
|
});
|
||||||
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,408 +0,0 @@
|
||||||
import * as cache from "@actions/cache";
|
|
||||||
import * as core from "@actions/core";
|
|
||||||
|
|
||||||
import { Events, Inputs, RefKey } from "../src/constants";
|
|
||||||
import run 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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
jest.spyOn(actionUtils, "getInputAsInt").mockImplementation(
|
|
||||||
(name, options) => {
|
|
||||||
return jest
|
|
||||||
.requireActual("../src/utils/actionUtils")
|
|
||||||
.getInputAsInt(name, options);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
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(() => {
|
|
||||||
testUtils.clearInputs();
|
|
||||||
delete process.env[Events.Key];
|
|
||||||
delete process.env[RefKey];
|
|
||||||
});
|
|
||||||
|
|
||||||
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 run(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(failedMock).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");
|
|
||||||
|
|
||||||
await run(new StateProvider());
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledWith(`Key is not specified.`);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("save without AC available should no-op", async () => {
|
|
||||||
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
|
|
||||||
() => false
|
|
||||||
);
|
|
||||||
|
|
||||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
|
||||||
|
|
||||||
await run(new StateProvider());
|
|
||||||
|
|
||||||
expect(saveCacheMock).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");
|
|
||||||
|
|
||||||
await run(new StateProvider());
|
|
||||||
|
|
||||||
expect(saveCacheMock).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;
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
await run(new StateProvider());
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
|
||||||
[inputPath],
|
|
||||||
primaryKey,
|
|
||||||
{
|
|
||||||
uploadChunkSize: 4000000
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(failedMock).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");
|
|
||||||
|
|
||||||
await run(new StateProvider());
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(infoMock).toHaveBeenCalledWith(
|
|
||||||
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
|
||||||
);
|
|
||||||
expect(failedMock).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");
|
|
||||||
|
|
||||||
await run(new StateProvider());
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledWith(
|
|
||||||
"Input required and not supplied: path"
|
|
||||||
);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(failedMock).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;
|
|
||||||
});
|
|
||||||
|
|
||||||
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."
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await run(new StateProvider());
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
|
||||||
[inputPath],
|
|
||||||
primaryKey,
|
|
||||||
expect.anything(),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
await run(new StateProvider());
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
|
||||||
[inputPath],
|
|
||||||
primaryKey,
|
|
||||||
expect.anything(),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputPath = "node_modules";
|
|
||||||
testUtils.setInput(Inputs.Path, inputPath);
|
|
||||||
|
|
||||||
const saveCacheMock = jest
|
|
||||||
.spyOn(cache, "saveCache")
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Error("HTTP Error Occurred");
|
|
||||||
});
|
|
||||||
|
|
||||||
await run(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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
await run(new StateProvider());
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
|
||||||
[inputPath],
|
|
||||||
primaryKey,
|
|
||||||
{
|
|
||||||
uploadChunkSize: 4000000
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
|
@ -1,139 +0,0 @@
|
||||||
import * as cache from "@actions/cache";
|
|
||||||
import * as core from "@actions/core";
|
|
||||||
|
|
||||||
import { Events, Inputs, RefKey } from "../src/constants";
|
|
||||||
import run from "../src/saveOnly";
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
jest.spyOn(actionUtils, "getInputAsInt").mockImplementation(
|
|
||||||
(name, options) => {
|
|
||||||
return jest
|
|
||||||
.requireActual("../src/utils/actionUtils")
|
|
||||||
.getInputAsInt(name, options);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
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(() => {
|
|
||||||
testUtils.clearInputs();
|
|
||||||
delete process.env[Events.Key];
|
|
||||||
delete process.env[RefKey];
|
|
||||||
});
|
|
||||||
|
|
||||||
test("save with valid inputs uploads a cache", async () => {
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
|
|
||||||
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
|
||||||
|
|
||||||
const inputPath = "node_modules";
|
|
||||||
testUtils.setInput(Inputs.Key, primaryKey);
|
|
||||||
testUtils.setInput(Inputs.Path, inputPath);
|
|
||||||
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
|
|
||||||
|
|
||||||
const cacheId = 4;
|
|
||||||
const saveCacheMock = jest
|
|
||||||
.spyOn(cache, "saveCache")
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return Promise.resolve(cacheId);
|
|
||||||
});
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
|
||||||
[inputPath],
|
|
||||||
primaryKey,
|
|
||||||
{
|
|
||||||
uploadChunkSize: 4000000
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(failedMock).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";
|
|
||||||
testUtils.setInput(Inputs.Key, primaryKey);
|
|
||||||
testUtils.setInput(Inputs.Path, inputPath);
|
|
||||||
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
|
|
||||||
|
|
||||||
const cacheId = -1;
|
|
||||||
const saveCacheMock = jest
|
|
||||||
.spyOn(cache, "saveCache")
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return Promise.resolve(cacheId);
|
|
||||||
});
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
|
||||||
[inputPath],
|
|
||||||
primaryKey,
|
|
||||||
{
|
|
||||||
uploadChunkSize: 4000000
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(warningMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(warningMock).toHaveBeenCalledWith("Cache save failed.");
|
|
||||||
});
|
|
|
@ -1,89 +0,0 @@
|
||||||
import * as core from "@actions/core";
|
|
||||||
|
|
||||||
import { Events, RefKey, State } from "../src/constants";
|
|
||||||
import {
|
|
||||||
IStateProvider,
|
|
||||||
NullStateProvider,
|
|
||||||
StateProvider
|
|
||||||
} from "../src/stateProvider";
|
|
||||||
|
|
||||||
jest.mock("@actions/core");
|
|
||||||
|
|
||||||
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").setOutput(key, value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
delete process.env[Events.Key];
|
|
||||||
delete process.env[RefKey];
|
|
||||||
});
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
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";
|
|
||||||
|
|
||||||
const stateProvider: IStateProvider = new StateProvider();
|
|
||||||
stateProvider.setState("stateKey", "stateValue");
|
|
||||||
stateProvider.setState(State.CacheMatchedKey, cacheMatchedKey);
|
|
||||||
const stateValue = stateProvider.getState("stateKey");
|
|
||||||
const cacheStateValue = stateProvider.getCacheState();
|
|
||||||
|
|
||||||
expect(stateValue).toBe("stateValue");
|
|
||||||
expect(cacheStateValue).toBe(cacheMatchedKey);
|
|
||||||
expect(getStateMock).toHaveBeenCalledTimes(2);
|
|
||||||
expect(saveStateMock).toHaveBeenCalledTimes(2);
|
|
||||||
expect(setOutputMock).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.getState("outputKey");
|
|
||||||
nullStateProvider.getCacheState();
|
|
||||||
|
|
||||||
expect(getStateMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(setOutputMock).toHaveBeenCalledTimes(2);
|
|
||||||
expect(saveStateMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
14
action.yml
14
action.yml
|
@ -14,18 +14,6 @@ inputs:
|
||||||
upload-chunk-size:
|
upload-chunk-size:
|
||||||
description: 'The chunk size used to split up large files during upload, in bytes'
|
description: 'The chunk size used to split up large files during upload, in bytes'
|
||||||
required: false
|
required: false
|
||||||
enableCrossOsArchive:
|
|
||||||
description: 'An optional boolean when enabled, allows windows runners to save or restore caches that can be restored or saved respectively on other platforms'
|
|
||||||
default: 'false'
|
|
||||||
required: false
|
|
||||||
fail-on-cache-miss:
|
|
||||||
description: 'Fail the workflow if cache entry is not found'
|
|
||||||
default: 'false'
|
|
||||||
required: false
|
|
||||||
lookup-only:
|
|
||||||
description: 'Check if a cache entry exists for the given input(s) (key, restore-keys) without downloading the cache'
|
|
||||||
default: 'false'
|
|
||||||
required: false
|
|
||||||
outputs:
|
outputs:
|
||||||
cache-hit:
|
cache-hit:
|
||||||
description: 'A boolean value to indicate an exact match was found for the primary key'
|
description: 'A boolean value to indicate an exact match was found for the primary key'
|
||||||
|
@ -33,7 +21,7 @@ runs:
|
||||||
using: 'node16'
|
using: 'node16'
|
||||||
main: 'dist/restore/index.js'
|
main: 'dist/restore/index.js'
|
||||||
post: 'dist/save/index.js'
|
post: 'dist/save/index.js'
|
||||||
post-if: success()
|
post-if: 'success()'
|
||||||
branding:
|
branding:
|
||||||
icon: 'archive'
|
icon: 'archive'
|
||||||
color: 'gray-dark'
|
color: 'gray-dark'
|
||||||
|
|
Binary file not shown.
|
@ -1,306 +0,0 @@
|
||||||
# Caching Strategies
|
|
||||||
|
|
||||||
This document lists some of the strategies (and example workflows if possible) which can be used to ...
|
|
||||||
|
|
||||||
- use an effective cache key and/or path
|
|
||||||
- solve some common use cases around saving and restoring caches
|
|
||||||
- leverage the step inputs and outputs more effectively
|
|
||||||
|
|
||||||
## Choosing the right key
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
key: ${{ some-metadata }}-cache
|
|
||||||
```
|
|
||||||
|
|
||||||
In your workflows, you can use different strategies to name your key depending on your use case so that the cache is scoped appropriately for your need. For example, you can have cache specific to OS, or based on the lockfile or commit SHA or even workflow run.
|
|
||||||
|
|
||||||
### Updating cache for any change in the dependencies
|
|
||||||
|
|
||||||
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@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
path/to/dependencies
|
|
||||||
some/other/dependencies
|
|
||||||
key: cache-${{ hashFiles('**/lockfiles') }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using restore keys to download the closest matching cache
|
|
||||||
|
|
||||||
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@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
path/to/dependencies
|
|
||||||
some/other/dependencies
|
|
||||||
key: cache-npm-${{ hashFiles('**/lockfiles') }}
|
|
||||||
restore-keys: |
|
|
||||||
cache-npm-
|
|
||||||
```
|
|
||||||
|
|
||||||
The restore keys can be provided as a complete name, or a prefix, read more [here](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#matching-a-cache-key) on how a cache key is matched using restore keys.
|
|
||||||
|
|
||||||
### Separate caches by Operating System
|
|
||||||
|
|
||||||
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@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
path/to/dependencies
|
|
||||||
some/other/dependencies
|
|
||||||
key: ${{ runner.os }}-cache
|
|
||||||
```
|
|
||||||
|
|
||||||
### Creating a short lived cache
|
|
||||||
|
|
||||||
Caches scoped to the particular workflow run id or run attempt can be stored and referred by using the run id/attempt. This is an effective way to have a short lived cache.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
key: cache-${{ github.run_id }}-${{ github.run_attempt }}
|
|
||||||
```
|
|
||||||
|
|
||||||
On similar lines, commit sha can be used to create a very specialized and short lived cache.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
path/to/dependencies
|
|
||||||
some/other/dependencies
|
|
||||||
key: cache-${{ github.sha }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using multiple factors while forming a key depening on the need
|
|
||||||
|
|
||||||
Cache key can be formed by combination of more than one metadata, evaluated info.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
path/to/dependencies
|
|
||||||
some/other/dependencies
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
|
||||||
```
|
|
||||||
|
|
||||||
The [GitHub Context](https://docs.github.com/en/actions/learn-github-actions/contexts#github-context) can be used to create keys using the workflows metadata.
|
|
||||||
|
|
||||||
## Restoring Cache
|
|
||||||
|
|
||||||
### Understanding how to choose path
|
|
||||||
|
|
||||||
While setting paths for caching dependencies it is important to give correct path depending on the hosted runner you are using or whether the action is running in a container job. Assigning different `path` for save and restore will result in cache miss.
|
|
||||||
|
|
||||||
Below are GiHub hosted runner specific paths one should take care of when writing a workflow which saves/restores caches across OS.
|
|
||||||
|
|
||||||
#### Ubuntu Paths
|
|
||||||
|
|
||||||
Home directory (`~/`) = `/home/runner`
|
|
||||||
|
|
||||||
`${{ github.workspace }}` = `/home/runner/work/repo/repo`
|
|
||||||
|
|
||||||
`process.env['RUNNER_TEMP']`=`/home/runner/work/_temp`
|
|
||||||
|
|
||||||
`process.cwd()` = `/home/runner/work/repo/repo`
|
|
||||||
|
|
||||||
#### Windows Paths
|
|
||||||
|
|
||||||
Home directory (`~/`) = `C:\Users\runneradmin`
|
|
||||||
|
|
||||||
`${{ github.workspace }}` = `D:\a\repo\repo`
|
|
||||||
|
|
||||||
`process.env['RUNNER_TEMP']` = `D:\a\_temp`
|
|
||||||
|
|
||||||
`process.cwd()` = `D:\a\repo\repo`
|
|
||||||
|
|
||||||
#### macOS Paths
|
|
||||||
|
|
||||||
Home directory (`~/`) = `/Users/runner`
|
|
||||||
|
|
||||||
`${{ github.workspace }}` = `/Users/runner/work/repo/repo`
|
|
||||||
|
|
||||||
`process.env['RUNNER_TEMP']` = `/Users/runner/work/_temp`
|
|
||||||
|
|
||||||
`process.cwd()` = `/Users/runner/work/repo/repo`
|
|
||||||
|
|
||||||
Where:
|
|
||||||
|
|
||||||
`cwd()` = Current working directory where the repository code resides.
|
|
||||||
|
|
||||||
`RUNNER_TEMP` = Environment variable defined for temporary storage location.
|
|
||||||
|
|
||||||
### Make cache read only / Reuse cache from centralized job
|
|
||||||
|
|
||||||
In case you are using a centralized job to create and save your cache that can be reused by other jobs in your repository, this action will take care of your restore only needs and make the cache read-only.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- uses: actions/cache/restore@v3
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: path/to/dependencies
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: /install.sh
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: /build.sh
|
|
||||||
|
|
||||||
- name: Publish package to public
|
|
||||||
run: /publish.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Failing/Exiting the workflow if cache with exact key is not found
|
|
||||||
|
|
||||||
You can use the output of this action to exit the workflow on cache miss. This way you can restrict your workflow to only initiate the build when `cache-hit` occurs, in other words, cache with exact key is found.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- uses: actions/cache/restore@v3
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: path/to/dependencies
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
|
||||||
|
|
||||||
- name: Check cache hit
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: exit 1
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: /build.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Saving cache
|
|
||||||
|
|
||||||
### Reusing primary key from restore cache as input to save action
|
|
||||||
|
|
||||||
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@v3
|
|
||||||
id: restore-cache
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
path/to/dependencies
|
|
||||||
some/other/dependencies
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
|
||||||
.
|
|
||||||
.
|
|
||||||
.
|
|
||||||
- uses: actions/cache/save@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
path/to/dependencies
|
|
||||||
some/other/dependencies
|
|
||||||
key: ${{ steps.restore-cache.outputs.cache-primary-key }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Re-evaluate cache key while saving cache
|
|
||||||
|
|
||||||
On the other hand, the key can also be explicitly re-computed while executing the save action. This helps in cases where the lockfiles are generated during the build.
|
|
||||||
|
|
||||||
Let's say we have a restore step that computes key at runtime
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
uses: actions/cache/restore@v3
|
|
||||||
id: restore-cache
|
|
||||||
with:
|
|
||||||
key: cache-${{ hashFiles('**/lockfiles') }}
|
|
||||||
```
|
|
||||||
|
|
||||||
Case 1: Where an user would want to reuse the key as it is
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
uses: actions/cache/save@v3
|
|
||||||
with:
|
|
||||||
key: ${{ steps.restore-cache.outputs.cache-primary-key }}
|
|
||||||
```
|
|
||||||
|
|
||||||
Case 2: Where the user would want to re-evaluate the key
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
uses: actions/cache/save@v3
|
|
||||||
with:
|
|
||||||
key: npm-cache-${{hashfiles(package-lock.json)}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Saving cache even if the build fails
|
|
||||||
|
|
||||||
There can be cases where a cache should be saved even if the build job fails. For example, a job can fail due to flaky tests but the caches can still be re-used. You can use `actions/cache/save` action to save the cache by using `if: always()` condition.
|
|
||||||
|
|
||||||
Similarly, `actions/cache/save` action can be conditionally used based on the output of the previous steps. This way you get more control on when to save the cache.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
.
|
|
||||||
. // restore if need be
|
|
||||||
.
|
|
||||||
- name: Build
|
|
||||||
run: /build.sh
|
|
||||||
- uses: actions/cache/save@v3
|
|
||||||
if: always() // or any other condition to invoke the save action
|
|
||||||
with:
|
|
||||||
path: path/to/dependencies
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Saving cache once and reusing in multiple workflows
|
|
||||||
|
|
||||||
In case of multi-module projects, where the built artifact of one project needs to be reused in subsequent child modules, the need of rebuilding the parent module again and again with every build can be eliminated. The `actions/cache` or `actions/cache/save` action can be used to build and save the parent module artifact once, and restored multiple times while building the child modules.
|
|
||||||
|
|
||||||
#### Step 1 - Build the parent module and save it
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: ./build-parent-module.sh
|
|
||||||
|
|
||||||
- uses: actions/cache/save@v3
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: path/to/dependencies
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 2 - Restore the built artifact from cache using the same key and path
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- uses: actions/cache/restore@v3
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: path/to/dependencies
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: ./install.sh
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: ./build-child-module.sh
|
|
||||||
|
|
||||||
- name: Publish package to public
|
|
||||||
run: ./publish.sh
|
|
||||||
```
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
100
examples.md
100
examples.md
|
@ -1,7 +1,6 @@
|
||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
- [C# - NuGet](#c---nuget)
|
- [C# - NuGet](#c---nuget)
|
||||||
- [Clojure - Lein Deps](#clojure---lein-deps)
|
|
||||||
- [D - DUB](#d---dub)
|
- [D - DUB](#d---dub)
|
||||||
- [POSIX](#posix)
|
- [POSIX](#posix)
|
||||||
- [Windows](#windows)
|
- [Windows](#windows)
|
||||||
|
@ -38,8 +37,6 @@
|
||||||
- [Swift, Objective-C - Carthage](#swift-objective-c---carthage)
|
- [Swift, Objective-C - Carthage](#swift-objective-c---carthage)
|
||||||
- [Swift, Objective-C - CocoaPods](#swift-objective-c---cocoapods)
|
- [Swift, Objective-C - CocoaPods](#swift-objective-c---cocoapods)
|
||||||
- [Swift - Swift Package Manager](#swift---swift-package-manager)
|
- [Swift - Swift Package Manager](#swift---swift-package-manager)
|
||||||
- [Swift - Mint](#swift---mint)
|
|
||||||
- [* - Bazel](#---bazel)
|
|
||||||
|
|
||||||
## C# - NuGet
|
## C# - NuGet
|
||||||
|
|
||||||
|
@ -69,7 +66,7 @@ With `actions/cache@v3` you can now exclude unwanted packages with [exclude patt
|
||||||
```
|
```
|
||||||
|
|
||||||
Or you could move the cache folder like below.
|
Or you could move the cache folder like below.
|
||||||
> **Note** This workflow does not work for projects that require files to be placed in user profile package folder
|
>Note: This workflow does not work for projects that require files to be placed in user profile package folder
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
env:
|
env:
|
||||||
|
@ -83,19 +80,6 @@ steps:
|
||||||
${{ runner.os }}-nuget-
|
${{ runner.os }}-nuget-
|
||||||
```
|
```
|
||||||
|
|
||||||
## Clojure - Lein Deps
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Cache lein project dependencies
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: ~/.m2/repository
|
|
||||||
key: ${{ runner.os }}-clojure-${{ hashFiles('**/project.clj') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-clojure
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## D - DUB
|
## D - DUB
|
||||||
|
|
||||||
### POSIX
|
### POSIX
|
||||||
|
@ -281,7 +265,7 @@ We cache the elements of the Cabal store separately, as the entirety of `~/.caba
|
||||||
|
|
||||||
## Java - Gradle
|
## Java - Gradle
|
||||||
|
|
||||||
> **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.
|
>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
|
```yaml
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v3
|
||||||
|
@ -311,29 +295,14 @@ We cache the elements of the Cabal store separately, as the entirety of `~/.caba
|
||||||
For npm, cache files are stored in `~/.npm` on Posix, or `~\AppData\npm-cache` on Windows, but it's possible to use `npm config get cache` to find the path on any platform. See [the npm docs](https://docs.npmjs.com/cli/cache#cache) for more details.
|
For npm, cache files are stored in `~/.npm` on Posix, or `~\AppData\npm-cache` on Windows, but it's possible to use `npm config get cache` to find the path on any platform. See [the npm docs](https://docs.npmjs.com/cli/cache#cache) for more details.
|
||||||
|
|
||||||
If using `npm config` to retrieve the cache directory, ensure you run [actions/setup-node](https://github.com/actions/setup-node) first to ensure your `npm` version is correct.
|
If using `npm config` to retrieve the cache directory, ensure you run [actions/setup-node](https://github.com/actions/setup-node) first to ensure your `npm` version is correct.
|
||||||
After [deprecation](https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/) of save-state and set-output commands, the correct way to set output is using `${GITHUB_OUTPUT}`. For linux, we can use `${GITHUB_OUTPUT}` whereas for windows we need to use `${env:GITHUB_OUTPUT}` due to two different default shells in these two different OS ie `bash` and `pwsh` respectively.
|
|
||||||
|
|
||||||
> **Note** It is not recommended to cache `node_modules`, as it can break across Node versions and won't work with `npm ci`
|
>Note: It is not recommended to cache `node_modules`, as it can break across Node versions and won't work with `npm ci`
|
||||||
|
|
||||||
### **Get npm cache directory using same shell**
|
|
||||||
### Bash shell
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Get npm cache directory
|
- name: Get npm cache directory
|
||||||
id: npm-cache-dir
|
id: npm-cache-dir
|
||||||
shell: bash
|
run: |
|
||||||
run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT}
|
echo "::set-output name=dir::$(npm config get cache)"
|
||||||
```
|
|
||||||
|
|
||||||
### PWSH shell
|
|
||||||
```yaml
|
|
||||||
- name: Get npm cache directory
|
|
||||||
id: npm-cache-dir
|
|
||||||
shell: pwsh
|
|
||||||
run: echo "dir=$(npm config get cache)" >> ${env:GITHUB_OUTPUT}
|
|
||||||
```
|
|
||||||
`Get npm cache directory` step can then be used with `actions/cache` as shown below
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v3
|
||||||
id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true'
|
id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true'
|
||||||
with:
|
with:
|
||||||
|
@ -349,7 +318,7 @@ After [deprecation](https://github.blog/changelog/2022-10-11-github-actions-depr
|
||||||
- name: restore lerna
|
- name: restore lerna
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: '**/node_modules'
|
path: **/node_modules
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -359,7 +328,7 @@ The yarn cache directory will depend on your operating system and version of `ya
|
||||||
```yaml
|
```yaml
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v3
|
||||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||||
|
@ -377,7 +346,7 @@ The yarn 2 cache directory will depend on your config. See https://yarnpkg.com/c
|
||||||
```yaml
|
```yaml
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v3
|
||||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||||
|
@ -422,7 +391,7 @@ Esy allows you to export built dependencies and import pre-built dependencies.
|
||||||
- name: Get Composer Cache Directory
|
- name: Get Composer Cache Directory
|
||||||
id: composer-cache
|
id: composer-cache
|
||||||
run: |
|
run: |
|
||||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
|
@ -509,12 +478,12 @@ jobs:
|
||||||
|
|
||||||
### Using pip to get cache location
|
### Using pip to get cache location
|
||||||
|
|
||||||
> **Note** This requires pip 20.1+
|
> Note: This requires pip 20.1+
|
||||||
```yaml
|
```yaml
|
||||||
- name: Get pip cache dir
|
- name: Get pip cache dir
|
||||||
id: pip-cache
|
id: pip-cache
|
||||||
run: |
|
run: |
|
||||||
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
|
echo "::set-output name=dir::$(pip cache dir)"
|
||||||
|
|
||||||
- name: pip cache
|
- name: pip cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
|
@ -643,50 +612,3 @@ whenever possible:
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-spm-
|
${{ runner.os }}-spm-
|
||||||
```
|
```
|
||||||
|
|
||||||
## Swift - Mint
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
env:
|
|
||||||
MINT_PATH: .mint/lib
|
|
||||||
MINT_LINK_PATH: .mint/bin
|
|
||||||
steps:
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: .mint
|
|
||||||
key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-mint-
|
|
||||||
```
|
|
||||||
|
|
||||||
## * - Bazel
|
|
||||||
|
|
||||||
[`bazelisk`](https://github.com/bazelbuild/bazelisk) does not have be to separately downloaded and installed because it's already included in GitHub's `ubuntu-latest` and `macos-latest` base images.
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Cache Bazel
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/bazel
|
|
||||||
key: ${{ runner.os }}-bazel-${{ hashFiles('.bazelversion', '.bazelrc', 'WORKSPACE', 'WORKSPACE.bazel', 'MODULE.bazel') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bazel-
|
|
||||||
- run: bazelisk test //...
|
|
||||||
```
|
|
||||||
|
|
||||||
### macOS
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Cache Bazel
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
/private/var/tmp/_bazel_runner/
|
|
||||||
key: ${{ runner.os }}-bazel-${{ hashFiles('.bazelversion', '.bazelrc', 'WORKSPACE', 'WORKSPACE.bazel', 'MODULE.bazel') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bazel-
|
|
||||||
- run: bazelisk test //...
|
|
||||||
```
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "cache",
|
"name": "cache",
|
||||||
"version": "3.3.2",
|
"version": "3.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Cache dependencies and build outputs",
|
"description": "Cache dependencies and build outputs",
|
||||||
"main": "dist/restore/index.js",
|
"main": "dist/restore/index.js",
|
||||||
"scripts": {
|
"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",
|
"build": "tsc && ncc build -o dist/restore src/restore.ts && ncc build -o dist/save src/save.ts",
|
||||||
"test": "tsc --noEmit && jest --coverage",
|
"test": "tsc --noEmit && jest --coverage",
|
||||||
"lint": "eslint **/*.ts --cache",
|
"lint": "eslint **/*.ts --cache",
|
||||||
"format": "prettier --write **/*.ts",
|
"format": "prettier --write **/*.ts",
|
||||||
|
@ -23,29 +23,29 @@
|
||||||
"author": "GitHub",
|
"author": "GitHub",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/cache": "^3.2.2",
|
"@actions/cache": "file:actions-cache-3.0.1.tgz",
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.7.0",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@actions/io": "^1.1.2"
|
"@actions/io": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.0",
|
||||||
"@types/nock": "^11.1.0",
|
"@types/nock": "^11.1.0",
|
||||||
"@types/node": "^16.18.3",
|
"@types/node": "^16.11.33",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.22.0",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.22.0",
|
||||||
"@zeit/ncc": "^0.20.5",
|
"@zeit/ncc": "^0.20.5",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.14.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jest": "^26.9.0",
|
"eslint-plugin-jest": "^26.1.5",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||||
"jest": "^28.1.3",
|
"jest": "^28.0.3",
|
||||||
"jest-circus": "^27.5.1",
|
"jest-circus": "^27.5.1",
|
||||||
"nock": "^13.2.9",
|
"nock": "^13.2.4",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.6.2",
|
||||||
"ts-jest": "^28.0.8",
|
"ts-jest": "^28.0.2",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.6.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,135 +0,0 @@
|
||||||
# Restore action
|
|
||||||
|
|
||||||
The restore action restores a cache. It works similarly to the `cache` action except that it doesn't have a post step to save the cache. This action provides granular ability to restore a cache without having to save it. It accepts the same set of inputs as the `cache` action.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
### Inputs
|
|
||||||
|
|
||||||
* `key` - An explicit key for a cache entry. See [creating a cache key](../README.md#creating-a-cache-key).
|
|
||||||
* `path` - A list of files, directories, and wildcard patterns to restore. See [`@actions/glob`](https://github.com/actions/toolkit/tree/main/packages/glob) for supported patterns.
|
|
||||||
* `restore-keys` - An ordered list of prefix-matched keys to use for restoring stale cache if no cache hit occurred for key.
|
|
||||||
* `fail-on-cache-miss` - Fail the workflow if cache entry is not found. Default: `false`
|
|
||||||
* `lookup-only` - If true, only checks if cache entry exists and skips download. Default: `false`
|
|
||||||
|
|
||||||
### Outputs
|
|
||||||
|
|
||||||
* `cache-hit` - A boolean value to indicate an exact match was found for the key.
|
|
||||||
* `cache-primary-key` - Cache primary key passed in the input to use in subsequent steps of the workflow.
|
|
||||||
* `cache-matched-key` - Key of the cache that was restored, it could either be the primary key on cache-hit or a partial/complete match of one of the restore keys.
|
|
||||||
|
|
||||||
> **Note**
|
|
||||||
`cache-hit` will be set to `true` only when cache hit occurs for the exact `key` match. For a partial key match via `restore-keys` or a cache miss, it will be set to `false`.
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
* `SEGMENT_DOWNLOAD_TIMEOUT_MINS` - Segment download timeout (in minutes, default `10`) to abort download of the segment if not completed in the defined number of minutes. [Read more](https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout)
|
|
||||||
|
|
||||||
## Use cases
|
|
||||||
|
|
||||||
As this is a newly introduced action to give users more control in their workflows, below are some use cases where one can use this action.
|
|
||||||
|
|
||||||
### Only restore cache
|
|
||||||
|
|
||||||
If you are using separate jobs to create and save your cache(s) to be reused by other jobs in a repository, this action will take care of your cache restoring needs.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- uses: actions/cache/restore@v3
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: path/to/dependencies
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: /install.sh
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: /build.sh
|
|
||||||
|
|
||||||
- name: Publish package to public
|
|
||||||
run: /publish.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Once the cache is restored, unlike `actions/cache`, this action won't run a post step to do post-processing, and the rest of the workflow will run as usual.
|
|
||||||
|
|
||||||
### Save intermediate private build artifacts
|
|
||||||
|
|
||||||
In case of multi-module projects, where the built artifact of one project needs to be reused in subsequent child modules, the need to rebuild the parent module again and again with every build can be eliminated. The `actions/cache` or `actions/cache/save` action can be used to build and save the parent module artifact once, and it can be restored multiple times while building the child modules.
|
|
||||||
|
|
||||||
#### Step 1 - Build the parent module and save it
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: /build-parent-module.sh
|
|
||||||
|
|
||||||
- uses: actions/cache/save@v3
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: path/to/dependencies
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 2 - Restore the built artifact from cache using the same key and path
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- uses: actions/cache/restore@v3
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: path/to/dependencies
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: /install.sh
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: /build-child-module.sh
|
|
||||||
|
|
||||||
- name: Publish package to public
|
|
||||||
run: /publish.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Exit workflow on cache miss
|
|
||||||
|
|
||||||
You can use `fail-on-cache-miss: true` to exit a workflow on a cache miss. This way you can restrict your workflow to only build when there is a `cache-hit`.
|
|
||||||
|
|
||||||
To fail if there is no cache hit for the primary key, leave `restore-keys` empty!
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- uses: actions/cache/restore@v3
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: path/to/dependencies
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
|
||||||
fail-on-cache-miss: true
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: /build.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tips
|
|
||||||
|
|
||||||
### Reusing primary key and restored key in the save action
|
|
||||||
|
|
||||||
Usually you may want to use the same `key` with both `actions/cache/restore` and `actions/cache/save` actions. To achieve this, use `outputs` from the `restore` action to reuse the same primary key (or the key of the cache that was restored).
|
|
||||||
|
|
||||||
### Using restore action outputs to make save action behave just like the cache action
|
|
||||||
|
|
||||||
The outputs `cache-primary-key` and `cache-matched-key` can be used to check if the restored cache is same as the given primary key. Alternatively, the `cache-hit` output can also be used to check if the restored was a complete match or a partially restored cache.
|
|
||||||
|
|
||||||
### Ensuring proper restores and save happen across the actions
|
|
||||||
|
|
||||||
It is very important to use the same `key` and `path` that were used by either `actions/cache` or `actions/cache/save` while saving the cache. Learn more about cache key [naming](https://github.com/actions/cache#creating-a-cache-key) and [versioning](https://github.com/actions/cache#cache-version) here.
|
|
|
@ -1,38 +0,0 @@
|
||||||
name: 'Restore Cache'
|
|
||||||
description: 'Restore Cache artifacts like dependencies and build outputs to improve workflow execution time'
|
|
||||||
author: 'GitHub'
|
|
||||||
inputs:
|
|
||||||
path:
|
|
||||||
description: 'A list of files, directories, and wildcard patterns to restore'
|
|
||||||
required: true
|
|
||||||
key:
|
|
||||||
description: 'An explicit key for restoring the cache'
|
|
||||||
required: true
|
|
||||||
restore-keys:
|
|
||||||
description: 'An ordered list of keys to use for restoring stale cache if no cache hit occurred for key. Note `cache-hit` returns false in this case.'
|
|
||||||
required: false
|
|
||||||
enableCrossOsArchive:
|
|
||||||
description: 'An optional boolean when enabled, allows windows runners to restore caches that were saved on other platforms'
|
|
||||||
default: 'false'
|
|
||||||
required: false
|
|
||||||
fail-on-cache-miss:
|
|
||||||
description: 'Fail the workflow if cache entry is not found'
|
|
||||||
default: 'false'
|
|
||||||
required: false
|
|
||||||
lookup-only:
|
|
||||||
description: 'Check if a cache entry exists for the given input(s) (key, restore-keys) without downloading the cache'
|
|
||||||
default: 'false'
|
|
||||||
required: false
|
|
||||||
outputs:
|
|
||||||
cache-hit:
|
|
||||||
description: 'A boolean value to indicate an exact match was found for the primary key'
|
|
||||||
cache-primary-key:
|
|
||||||
description: 'A resolved cache key for which cache match was attempted'
|
|
||||||
cache-matched-key:
|
|
||||||
description: 'Key of the cache that was restored, it could either be the primary key on cache-hit or a partial/complete match of one of the restore keys'
|
|
||||||
runs:
|
|
||||||
using: 'node16'
|
|
||||||
main: '../dist/restore-only/index.js'
|
|
||||||
branding:
|
|
||||||
icon: 'archive'
|
|
||||||
color: 'gray-dark'
|
|
|
@ -1,88 +0,0 @@
|
||||||
# Save action
|
|
||||||
|
|
||||||
The save action saves a cache. It works similarly to the `cache` action except that it doesn't first do a restore. This action provides granular ability to save a cache without having to restore it, or to do a save at any stage of the workflow job -- not only in post phase.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
### Inputs
|
|
||||||
|
|
||||||
* `key` - An explicit key for a cache entry. See [creating a cache key](../README.md#creating-a-cache-key).
|
|
||||||
* `path` - A list of files, directories, and wildcard patterns to cache. See [`@actions/glob`](https://github.com/actions/toolkit/tree/main/packages/glob) for supported patterns.
|
|
||||||
* `upload-chunk-size` - The chunk size used to split up large files during upload, in bytes
|
|
||||||
|
|
||||||
### Outputs
|
|
||||||
|
|
||||||
This action has no outputs.
|
|
||||||
|
|
||||||
## Use cases
|
|
||||||
|
|
||||||
|
|
||||||
### Only save cache
|
|
||||||
|
|
||||||
If you are using separate jobs for generating common artifacts and sharing them across jobs, this action will take care of your cache saving needs.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: /install.sh
|
|
||||||
|
|
||||||
- name: Build artifacts
|
|
||||||
run: /build.sh
|
|
||||||
|
|
||||||
- uses: actions/cache/save@v3
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: path/to/dependencies
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Re-evaluate cache key while saving
|
|
||||||
|
|
||||||
With this save action, the key can now be re-evaluated while executing the action. This helps in cases where lockfiles are generated during the build.
|
|
||||||
|
|
||||||
Let's say we have a restore step that computes a key at runtime.
|
|
||||||
|
|
||||||
#### Restore a cache
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
uses: actions/cache/restore@v3
|
|
||||||
id: restore-cache
|
|
||||||
with:
|
|
||||||
key: cache-${{ hashFiles('**/lockfiles') }}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Case 1 - Where a user would want to reuse the key as it is
|
|
||||||
```yaml
|
|
||||||
uses: actions/cache/save@v3
|
|
||||||
with:
|
|
||||||
key: ${{ steps.restore-cache.outputs.cache-primary-key }}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Case 2 - Where the user would want to re-evaluate the key
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
uses: actions/cache/save@v3
|
|
||||||
with:
|
|
||||||
key: npm-cache-${{hashfiles(package-lock.json)}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Always save cache
|
|
||||||
|
|
||||||
There are instances where some flaky test cases would fail the entire workflow and users would get frustrated because the builds would run for hours and the cache couldn't be saved as the workflow failed in between. For such use-cases, users now have the ability to use the `actions/cache/save` action to save the cache by using an `if: always()` condition. This way the cache will always be saved if generated, or a warning will be generated that nothing is found on the cache path. Users can also use the `if` condition to only execute the `actions/cache/save` action depending on the output of previous steps. This way they get more control of when to save the cache.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
.
|
|
||||||
. // restore if need be
|
|
||||||
.
|
|
||||||
- name: Build
|
|
||||||
run: /build.sh
|
|
||||||
- uses: actions/cache/save@v3
|
|
||||||
if: always() // or any other condition to invoke the save action
|
|
||||||
with:
|
|
||||||
path: path/to/dependencies
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
|
||||||
```
|
|
|
@ -1,23 +0,0 @@
|
||||||
name: 'Save a cache'
|
|
||||||
description: 'Save Cache artifacts like dependencies and build outputs to improve workflow execution time'
|
|
||||||
author: 'GitHub'
|
|
||||||
inputs:
|
|
||||||
path:
|
|
||||||
description: 'A list of files, directories, and wildcard patterns to cache'
|
|
||||||
required: true
|
|
||||||
key:
|
|
||||||
description: 'An explicit key for saving the cache'
|
|
||||||
required: true
|
|
||||||
upload-chunk-size:
|
|
||||||
description: 'The chunk size used to split up large files during upload, in bytes'
|
|
||||||
required: false
|
|
||||||
enableCrossOsArchive:
|
|
||||||
description: 'An optional boolean when enabled, allows windows runners to save caches that can be restored on other platforms'
|
|
||||||
default: 'false'
|
|
||||||
required: false
|
|
||||||
runs:
|
|
||||||
using: 'node16'
|
|
||||||
main: '../dist/save-only/index.js'
|
|
||||||
branding:
|
|
||||||
icon: 'archive'
|
|
||||||
color: 'gray-dark'
|
|
|
@ -1,17 +1,12 @@
|
||||||
export enum Inputs {
|
export enum Inputs {
|
||||||
Key = "key", // Input for cache, restore, save action
|
Key = "key",
|
||||||
Path = "path", // Input for cache, restore, save action
|
Path = "path",
|
||||||
RestoreKeys = "restore-keys", // Input for cache, restore action
|
RestoreKeys = "restore-keys",
|
||||||
UploadChunkSize = "upload-chunk-size", // Input for cache, save action
|
UploadChunkSize = "upload-chunk-size"
|
||||||
EnableCrossOsArchive = "enableCrossOsArchive", // Input for cache, restore, save action
|
|
||||||
FailOnCacheMiss = "fail-on-cache-miss", // Input for cache, restore action
|
|
||||||
LookupOnly = "lookup-only" // Input for cache, restore action
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Outputs {
|
export enum Outputs {
|
||||||
CacheHit = "cache-hit", // Output from cache, restore action
|
CacheHit = "cache-hit"
|
||||||
CachePrimaryKey = "cache-primary-key", // Output from restore action
|
|
||||||
CacheMatchedKey = "cache-matched-key" // Output from restore action
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
|
|
|
@ -1,3 +1,62 @@
|
||||||
import { restoreRun } from "./restoreImpl";
|
import * as cache from "@actions/cache";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
restoreRun(true);
|
import { Events, Inputs, State } from "./constants";
|
||||||
|
import * as utils from "./utils/actionUtils";
|
||||||
|
|
||||||
|
async function run(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!utils.isCacheFeatureAvailable()) {
|
||||||
|
utils.setCacheHitOutput(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate inputs, this can cause task failure
|
||||||
|
if (!utils.isValidEvent()) {
|
||||||
|
utils.logWarning(
|
||||||
|
`Event Validation Error: The event type ${
|
||||||
|
process.env[Events.Key]
|
||||||
|
} is not supported because it's not tied to a branch or tag ref.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryKey = core.getInput(Inputs.Key, { required: true });
|
||||||
|
core.saveState(State.CachePrimaryKey, primaryKey);
|
||||||
|
|
||||||
|
const restoreKeys = utils.getInputAsArray(Inputs.RestoreKeys);
|
||||||
|
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
||||||
|
required: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const cacheKey = await cache.restoreCache(
|
||||||
|
cachePaths,
|
||||||
|
primaryKey,
|
||||||
|
restoreKeys
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cacheKey) {
|
||||||
|
core.info(
|
||||||
|
`Cache not found for input keys: ${[
|
||||||
|
primaryKey,
|
||||||
|
...restoreKeys
|
||||||
|
].join(", ")}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the matched cache key
|
||||||
|
utils.setCacheState(cacheKey);
|
||||||
|
|
||||||
|
const isExactKeyMatch = utils.isExactKeyMatch(primaryKey, cacheKey);
|
||||||
|
utils.setCacheHitOutput(isExactKeyMatch);
|
||||||
|
core.info(`Cache restored from key: ${cacheKey}`);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
core.setFailed((error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
|
|
||||||
|
export default run;
|
||||||
|
|
|
@ -1,122 +0,0 @@
|
||||||
import * as cache from "@actions/cache";
|
|
||||||
import * as core from "@actions/core";
|
|
||||||
|
|
||||||
import { Events, Inputs, Outputs, State } from "./constants";
|
|
||||||
import {
|
|
||||||
IStateProvider,
|
|
||||||
NullStateProvider,
|
|
||||||
StateProvider
|
|
||||||
} from "./stateProvider";
|
|
||||||
import * as utils from "./utils/actionUtils";
|
|
||||||
|
|
||||||
export async function restoreImpl(
|
|
||||||
stateProvider: IStateProvider
|
|
||||||
): Promise<string | undefined> {
|
|
||||||
try {
|
|
||||||
if (!utils.isCacheFeatureAvailable()) {
|
|
||||||
core.setOutput(Outputs.CacheHit, "false");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate inputs, this can cause task failure
|
|
||||||
if (!utils.isValidEvent()) {
|
|
||||||
utils.logWarning(
|
|
||||||
`Event Validation Error: The event type ${
|
|
||||||
process.env[Events.Key]
|
|
||||||
} is not supported because it's not tied to a branch or tag ref.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const primaryKey = core.getInput(Inputs.Key, { required: true });
|
|
||||||
stateProvider.setState(State.CachePrimaryKey, primaryKey);
|
|
||||||
|
|
||||||
const restoreKeys = utils.getInputAsArray(Inputs.RestoreKeys);
|
|
||||||
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
|
||||||
required: true
|
|
||||||
});
|
|
||||||
const enableCrossOsArchive = utils.getInputAsBool(
|
|
||||||
Inputs.EnableCrossOsArchive
|
|
||||||
);
|
|
||||||
const failOnCacheMiss = utils.getInputAsBool(Inputs.FailOnCacheMiss);
|
|
||||||
const lookupOnly = utils.getInputAsBool(Inputs.LookupOnly);
|
|
||||||
|
|
||||||
const cacheKey = await cache.restoreCache(
|
|
||||||
cachePaths,
|
|
||||||
primaryKey,
|
|
||||||
restoreKeys,
|
|
||||||
{ lookupOnly: lookupOnly },
|
|
||||||
enableCrossOsArchive
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!cacheKey) {
|
|
||||||
if (failOnCacheMiss) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${primaryKey}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
core.info(
|
|
||||||
`Cache not found for input keys: ${[
|
|
||||||
primaryKey,
|
|
||||||
...restoreKeys
|
|
||||||
].join(", ")}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the matched cache key in states
|
|
||||||
stateProvider.setState(State.CacheMatchedKey, cacheKey);
|
|
||||||
|
|
||||||
const isExactKeyMatch = utils.isExactKeyMatch(
|
|
||||||
core.getInput(Inputs.Key, { required: true }),
|
|
||||||
cacheKey
|
|
||||||
);
|
|
||||||
|
|
||||||
core.setOutput(Outputs.CacheHit, isExactKeyMatch.toString());
|
|
||||||
if (lookupOnly) {
|
|
||||||
core.info(`Cache found and can be restored from key: ${cacheKey}`);
|
|
||||||
} else {
|
|
||||||
core.info(`Cache restored from key: ${cacheKey}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cacheKey;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
core.setFailed((error as Error).message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run(
|
|
||||||
stateProvider: IStateProvider,
|
|
||||||
earlyExit: boolean | undefined
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
await restoreImpl(stateProvider);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
if (earlyExit) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// node will stay alive if any promises are not resolved,
|
|
||||||
// which is a possibility if HTTP requests are dangling
|
|
||||||
// due to retries or timeouts. We know that if we got here
|
|
||||||
// that all promises that we care about have successfully
|
|
||||||
// resolved, so simply exit with success.
|
|
||||||
if (earlyExit) {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function restoreOnlyRun(
|
|
||||||
earlyExit?: boolean | undefined
|
|
||||||
): Promise<void> {
|
|
||||||
await run(new NullStateProvider(), earlyExit);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function restoreRun(
|
|
||||||
earlyExit?: boolean | undefined
|
|
||||||
): Promise<void> {
|
|
||||||
await run(new StateProvider(), earlyExit);
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { restoreOnlyRun } from "./restoreImpl";
|
|
||||||
|
|
||||||
restoreOnlyRun(true);
|
|
57
src/save.ts
57
src/save.ts
|
@ -1,8 +1,59 @@
|
||||||
import saveImpl from "./saveImpl";
|
import * as cache from "@actions/cache";
|
||||||
import { StateProvider } from "./stateProvider";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
import { Events, Inputs, State } from "./constants";
|
||||||
|
import * as utils from "./utils/actionUtils";
|
||||||
|
|
||||||
|
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
|
||||||
|
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
|
||||||
|
// throw an uncaught exception. Instead of failing this action, just warn.
|
||||||
|
process.on("uncaughtException", e => utils.logWarning(e.message));
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
await saveImpl(new StateProvider());
|
try {
|
||||||
|
if (!utils.isCacheFeatureAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!utils.isValidEvent()) {
|
||||||
|
utils.logWarning(
|
||||||
|
`Event Validation Error: The event type ${
|
||||||
|
process.env[Events.Key]
|
||||||
|
} is not supported because it's not tied to a branch or tag ref.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = utils.getCacheState();
|
||||||
|
|
||||||
|
// Inputs are re-evaluted before the post action, so we want the original key used for restore
|
||||||
|
const primaryKey = core.getState(State.CachePrimaryKey);
|
||||||
|
if (!primaryKey) {
|
||||||
|
utils.logWarning(`Error retrieving key from state.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utils.isExactKeyMatch(primaryKey, state)) {
|
||||||
|
core.info(
|
||||||
|
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
||||||
|
required: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const cacheId = await cache.saveCache(cachePaths, primaryKey, {
|
||||||
|
uploadChunkSize: utils.getInputAsInt(Inputs.UploadChunkSize)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cacheId != -1) {
|
||||||
|
core.info(`Cache saved with key: ${primaryKey}`);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
utils.logWarning((error as Error).message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
import * as cache from "@actions/cache";
|
|
||||||
import * as core from "@actions/core";
|
|
||||||
|
|
||||||
import { Events, Inputs, State } from "./constants";
|
|
||||||
import { IStateProvider } from "./stateProvider";
|
|
||||||
import * as utils from "./utils/actionUtils";
|
|
||||||
|
|
||||||
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
|
|
||||||
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
|
|
||||||
// throw an uncaught exception. Instead of failing this action, just warn.
|
|
||||||
process.on("uncaughtException", e => utils.logWarning(e.message));
|
|
||||||
|
|
||||||
async function saveImpl(stateProvider: IStateProvider): Promise<number | void> {
|
|
||||||
let cacheId = -1;
|
|
||||||
try {
|
|
||||||
if (!utils.isCacheFeatureAvailable()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!utils.isValidEvent()) {
|
|
||||||
utils.logWarning(
|
|
||||||
`Event Validation Error: The event type ${
|
|
||||||
process.env[Events.Key]
|
|
||||||
} is not supported because it's not tied to a branch or tag ref.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If restore has stored a primary key in state, reuse that
|
|
||||||
// Else re-evaluate from inputs
|
|
||||||
const primaryKey =
|
|
||||||
stateProvider.getState(State.CachePrimaryKey) ||
|
|
||||||
core.getInput(Inputs.Key);
|
|
||||||
|
|
||||||
if (!primaryKey) {
|
|
||||||
utils.logWarning(`Key is not specified.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If matched restore key is same as primary key, then do not save cache
|
|
||||||
// NO-OP in case of SaveOnly action
|
|
||||||
const restoredKey = stateProvider.getCacheState();
|
|
||||||
|
|
||||||
if (utils.isExactKeyMatch(primaryKey, restoredKey)) {
|
|
||||||
core.info(
|
|
||||||
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
|
||||||
required: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const enableCrossOsArchive = utils.getInputAsBool(
|
|
||||||
Inputs.EnableCrossOsArchive
|
|
||||||
);
|
|
||||||
|
|
||||||
cacheId = await cache.saveCache(
|
|
||||||
cachePaths,
|
|
||||||
primaryKey,
|
|
||||||
{ uploadChunkSize: utils.getInputAsInt(Inputs.UploadChunkSize) },
|
|
||||||
enableCrossOsArchive
|
|
||||||
);
|
|
||||||
|
|
||||||
if (cacheId != -1) {
|
|
||||||
core.info(`Cache saved with key: ${primaryKey}`);
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
utils.logWarning((error as Error).message);
|
|
||||||
}
|
|
||||||
return cacheId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default saveImpl;
|
|
|
@ -1,15 +0,0 @@
|
||||||
import * as core from "@actions/core";
|
|
||||||
|
|
||||||
import saveImpl from "./saveImpl";
|
|
||||||
import { NullStateProvider } from "./stateProvider";
|
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
|
||||||
const cacheId = await saveImpl(new NullStateProvider());
|
|
||||||
if (cacheId === -1) {
|
|
||||||
core.warning(`Cache save failed.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
|
|
||||||
export default run;
|
|
|
@ -1,46 +0,0 @@
|
||||||
import * as core from "@actions/core";
|
|
||||||
|
|
||||||
import { Outputs, State } from "./constants";
|
|
||||||
|
|
||||||
export interface IStateProvider {
|
|
||||||
setState(key: string, value: string): void;
|
|
||||||
getState(key: string): string;
|
|
||||||
|
|
||||||
getCacheState(): string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
class StateProviderBase implements IStateProvider {
|
|
||||||
getCacheState(): string | undefined {
|
|
||||||
const cacheKey = this.getState(State.CacheMatchedKey);
|
|
||||||
if (cacheKey) {
|
|
||||||
core.debug(`Cache state/key: ${cacheKey}`);
|
|
||||||
return cacheKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
|
||||||
setState = (key: string, value: string) => {};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
getState = (key: string) => "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class StateProvider extends StateProviderBase {
|
|
||||||
setState = core.saveState;
|
|
||||||
getState = core.getState;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NullStateProvider extends StateProviderBase {
|
|
||||||
stateToOutputMap = new Map<string, string>([
|
|
||||||
[State.CacheMatchedKey, Outputs.CacheMatchedKey],
|
|
||||||
[State.CachePrimaryKey, Outputs.CachePrimaryKey]
|
|
||||||
]);
|
|
||||||
|
|
||||||
setState = (key: string, value: string) => {
|
|
||||||
core.setOutput(this.stateToOutputMap.get(key) as string, value);
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
getState = (key: string) => "";
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as cache from "@actions/cache";
|
import * as cache from "@actions/cache";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
import { RefKey } from "../constants";
|
import { Outputs, RefKey, State } from "../constants";
|
||||||
|
|
||||||
export function isGhes(): boolean {
|
export function isGhes(): boolean {
|
||||||
const ghUrl = new URL(
|
const ghUrl = new URL(
|
||||||
|
@ -19,6 +19,30 @@ export function isExactKeyMatch(key: string, cacheKey?: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setCacheState(state: string): void {
|
||||||
|
core.saveState(State.CacheMatchedKey, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCacheHitOutput(isCacheHit: boolean): void {
|
||||||
|
core.setOutput(Outputs.CacheHit, isCacheHit.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setOutputAndState(key: string, cacheKey?: string): void {
|
||||||
|
setCacheHitOutput(isExactKeyMatch(key, cacheKey));
|
||||||
|
// Store the matched cache key if it exists
|
||||||
|
cacheKey && setCacheState(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCacheState(): string | undefined {
|
||||||
|
const cacheKey = core.getState(State.CacheMatchedKey);
|
||||||
|
if (cacheKey) {
|
||||||
|
core.debug(`Cache state/key: ${cacheKey}`);
|
||||||
|
return cacheKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function logWarning(message: string): void {
|
export function logWarning(message: string): void {
|
||||||
const warningPrefix = "[warning]";
|
const warningPrefix = "[warning]";
|
||||||
core.info(`${warningPrefix}${message}`);
|
core.info(`${warningPrefix}${message}`);
|
||||||
|
@ -37,7 +61,7 @@ export function getInputAsArray(
|
||||||
return core
|
return core
|
||||||
.getInput(name, options)
|
.getInput(name, options)
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map(s => s.replace(/^!\s+/, "!").trim())
|
.map(s => s.trim())
|
||||||
.filter(x => x !== "");
|
.filter(x => x !== "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,29 +76,19 @@ export function getInputAsInt(
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInputAsBool(
|
|
||||||
name: string,
|
|
||||||
options?: core.InputOptions
|
|
||||||
): boolean {
|
|
||||||
const result = core.getInput(name, options);
|
|
||||||
return result.toLowerCase() === "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCacheFeatureAvailable(): boolean {
|
export function isCacheFeatureAvailable(): boolean {
|
||||||
if (cache.isFeatureAvailable()) {
|
if (!cache.isFeatureAvailable()) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGhes()) {
|
if (isGhes()) {
|
||||||
logWarning(
|
logWarning(
|
||||||
`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.
|
"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)`
|
|
||||||
);
|
);
|
||||||
return false;
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
logWarning(
|
logWarning(
|
||||||
"An internal error has occurred in cache backend. Please check https://www.githubstatus.com/ for any ongoing issue in actions."
|
"An internal error has occurred in cache backend. Please check https://www.githubstatus.com/ for any ongoing issue in actions."
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
@ -13,9 +13,6 @@ interface CacheInput {
|
||||||
path: string;
|
path: string;
|
||||||
key: string;
|
key: string;
|
||||||
restoreKeys?: string[];
|
restoreKeys?: string[];
|
||||||
enableCrossOsArchive?: boolean;
|
|
||||||
failOnCacheMiss?: boolean;
|
|
||||||
lookupOnly?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setInputs(input: CacheInput): void {
|
export function setInputs(input: CacheInput): void {
|
||||||
|
@ -23,15 +20,6 @@ export function setInputs(input: CacheInput): void {
|
||||||
setInput(Inputs.Key, input.key);
|
setInput(Inputs.Key, input.key);
|
||||||
input.restoreKeys &&
|
input.restoreKeys &&
|
||||||
setInput(Inputs.RestoreKeys, input.restoreKeys.join("\n"));
|
setInput(Inputs.RestoreKeys, input.restoreKeys.join("\n"));
|
||||||
input.enableCrossOsArchive !== undefined &&
|
|
||||||
setInput(
|
|
||||||
Inputs.EnableCrossOsArchive,
|
|
||||||
input.enableCrossOsArchive.toString()
|
|
||||||
);
|
|
||||||
input.failOnCacheMiss !== undefined &&
|
|
||||||
setInput(Inputs.FailOnCacheMiss, input.failOnCacheMiss.toString());
|
|
||||||
input.lookupOnly !== undefined &&
|
|
||||||
setInput(Inputs.LookupOnly, input.lookupOnly.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearInputs(): void {
|
export function clearInputs(): void {
|
||||||
|
@ -39,7 +27,4 @@ export function clearInputs(): void {
|
||||||
delete process.env[getInputName(Inputs.Key)];
|
delete process.env[getInputName(Inputs.Key)];
|
||||||
delete process.env[getInputName(Inputs.RestoreKeys)];
|
delete process.env[getInputName(Inputs.RestoreKeys)];
|
||||||
delete process.env[getInputName(Inputs.UploadChunkSize)];
|
delete process.env[getInputName(Inputs.UploadChunkSize)];
|
||||||
delete process.env[getInputName(Inputs.EnableCrossOsArchive)];
|
|
||||||
delete process.env[getInputName(Inputs.FailOnCacheMiss)];
|
|
||||||
delete process.env[getInputName(Inputs.LookupOnly)];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
# Tips and workarounds
|
|
||||||
|
|
||||||
## Cache segment restore timeout
|
|
||||||
|
|
||||||
A cache gets downloaded in multiple segments of fixed sizes (`1GB` for a `32-bit` runner and `2GB` for a `64-bit` runner). Sometimes, a segment download gets stuck which causes the workflow job to be stuck forever and fail. Version `v3.0.8` of `actions/cache` introduces a segment download timeout. The segment download timeout will allow the segment download to get aborted and hence allow the job to proceed with a cache miss.
|
|
||||||
|
|
||||||
Default value of this timeout is 10 minutes and can be customized by specifying an [environment variable](https://docs.github.com/en/actions/learn-github-actions/environment-variables) named `SEGMENT_DOWNLOAD_TIMEOUT_MINS` with timeout value in minutes.
|
|
||||||
|
|
||||||
## Update a cache
|
|
||||||
|
|
||||||
A cache today is immutable and cannot be updated. But some use cases require the cache to be saved even though there was a "hit" during restore. To do so, use a `key` which is unique for every run and use `restore-keys` to restore the nearest cache. For example:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: update cache on every commit
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: prime-numbers
|
|
||||||
key: primes-${{ runner.os }}-${{ github.run_id }} # Can use time based key as well
|
|
||||||
restore-keys: |
|
|
||||||
primes-${{ runner.os }}
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Cross OS cache
|
|
||||||
|
|
||||||
From `v3.2.3` cache is cross-os compatible when `enableCrossOsArchive` input is passed as true. This means that a cache created on `ubuntu-latest` or `mac-latest` can be used by `windows-latest` and vice versa, provided the workflow which runs on `windows-latest` have input `enableCrossOsArchive` as true. This is useful to cache dependencies which are independent of the runner platform. This will help reduce the consumption of the cache quota and help build for multiple platforms from the same cache. Things to keep in mind while using this feature:
|
|
||||||
|
|
||||||
- Only cache files that are compatible across OSs.
|
|
||||||
- Caching symlinks might cause issues while restoring them as they behave differently on different OSs.
|
|
||||||
- Be mindful when caching files from outside your github workspace directory as the directory is located at different places across OS.
|
|
||||||
- Avoid using directory pointers such as `${{ github.workspace }}` or `~` (home) which eventually evaluate to an absolute path that does not match across OSs.
|
|
||||||
|
|
||||||
## Force deletion of caches overriding default cache eviction policy
|
|
||||||
|
|
||||||
Caches have [branch scope restriction](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache) in place. This means that if caches for a specific branch are using a lot of storage quota, it may result into more frequently used caches from `default` branch getting thrashed. For example, if there are many pull requests happening on a repo and are creating caches, these cannot be used in default branch scope but will still occupy a lot of space till they get cleaned up by [eviction policy](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy). But sometime we want to clean them up on a faster cadence so as to ensure default branch is not thrashing. In order to achieve this, [gh-actions-cache cli](https://github.com/actions/gh-actions-cache/) can be used to delete caches for specific branches.
|
|
||||||
|
|
||||||
This workflow uses `gh-actions-cache` to delete all the caches created by a branch.
|
|
||||||
<details>
|
|
||||||
<summary>Example</summary>
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: cleanup caches by a branch
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- closed
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cleanup:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
# `actions:write` permission is required to delete caches
|
|
||||||
# See also: https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#delete-a-github-actions-cache-for-a-repository-using-a-cache-id
|
|
||||||
actions: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Cleanup
|
|
||||||
run: |
|
|
||||||
gh extension install actions/gh-actions-cache
|
|
||||||
|
|
||||||
REPO=${{ github.repository }}
|
|
||||||
BRANCH=refs/pull/${{ github.event.pull_request.number }}/merge
|
|
||||||
|
|
||||||
echo "Fetching list of cache key"
|
|
||||||
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
|
|
||||||
|
|
||||||
## Setting this to not fail the workflow while deleting cache keys.
|
|
||||||
set +e
|
|
||||||
echo "Deleting caches..."
|
|
||||||
for cacheKey in $cacheKeysForPR
|
|
||||||
do
|
|
||||||
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
|
||||||
done
|
|
||||||
echo "Done"
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
Loading…
Reference in New Issue