Compare commits

..

No commits in common. "main" and "v2.0.0" have entirely different histories.
main ... v2.0.0

128 changed files with 13092 additions and 274261 deletions

View File

@ -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"
}

View File

@ -9,14 +9,15 @@
"plugin:import/errors", "plugin:import/errors",
"plugin:import/warnings", "plugin:import/warnings",
"plugin:import/typescript", "plugin:import/typescript",
"plugin:prettier/recommended" "plugin:prettier/recommended",
"prettier/@typescript-eslint"
], ],
"plugins": ["@typescript-eslint", "simple-import-sort", "jest"], "plugins": ["@typescript-eslint", "simple-import-sort", "jest"],
"rules": { "rules": {
"import/first": "error", "import/first": "error",
"import/newline-after-import": "error", "import/newline-after-import": "error",
"import/no-duplicates": "error", "import/no-duplicates": "error",
"simple-import-sort/imports": "error", "simple-import-sort/sort": "error",
"sort-imports": "off" "sort-imports": "off"
} }
} }

2
.gitattributes vendored
View File

@ -1,2 +0,0 @@
.licenses/** -diff linguist-generated=true
* text=auto eol=lf

1
.github/CODEOWNERS vendored
View File

@ -1 +0,0 @@
* @actions/actions-cache

View File

@ -1,22 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
minor-actions-dependencies:
update-types: [minor, patch]
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
allow:
- dependency-type: direct
- dependency-type: production

View File

@ -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.

View File

@ -1,19 +0,0 @@
name: Check dist/
on:
push:
branches:
- main
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
workflow_dispatch:
jobs:
call-check-dist:
name: Check dist/
uses: actions/reusable-workflows/.github/workflows/check-dist.yml@main
with:
node-version: "20.x"

View File

@ -1,22 +0,0 @@
name: Close inactive issues
on:
schedule:
- cron: "30 8 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
days-before-issue-stale: 200
days-before-issue-close: 5
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 200 days with no activity. Leave a comment to avoid closing this issue in 5 days."
close-issue-message: "This issue was closed because it has been inactive for 5 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,46 +1,35 @@
name: "Code scanning - action" name: "Code Scanning - Action"
on: on:
push: push:
pull_request:
schedule: schedule:
- cron: '0 19 * * 0' - cron: '0 0 * * 0'
jobs: jobs:
CodeQL-Build: CodeQL-Build:
strategy:
fail-fast: false
# CodeQL runs on ubuntu-latest, windows-latest, and macos-latest # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
# required for all workflows
security-events: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 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@v3 uses: github/codeql-action/autobuild@v1
# 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
# ✏️ If the Autobuild fails above, remove it and uncomment the following
# three lines and modify them (or add more) to build your code if your
# project uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v1

View File

@ -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}}"]}'

View File

@ -1,15 +0,0 @@
name: Licensed
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
jobs:
call-licensed:
name: Licensed
uses: actions/reusable-workflows/.github/workflows/licensed.yml@main

View File

@ -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}}"]}'

View File

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

View File

@ -1,28 +0,0 @@
name: Release new action version
on:
release:
types: [released]
workflow_dispatch:
inputs:
TAG_NAME:
description: 'Tag name that the major tag will point to'
required: true
env:
TAG_NAME: ${{ github.event.inputs.TAG_NAME || github.event.release.tag_name }}
permissions:
contents: write
jobs:
update_tag:
name: Update the major tag to include the ${{ github.event.inputs.TAG_NAME || github.event.release.tag_name }} changes
environment:
name: releaseNewActionVersion
runs-on: ubuntu-latest
steps:
- name: Update the ${{ env.TAG_NAME }} tag
id: update-major-tag
uses: actions/publish-action@v0.3.0
with:
source-tag: ${{ env.TAG_NAME }}
slack-webhook: ${{ secrets.SLACK_WEBHOOK }}

View File

@ -3,29 +3,43 @@ name: Tests
on: on:
pull_request: pull_request:
branches: branches:
- main - master
- releases/** - releases/**
paths-ignore:
- '**.md'
push: push:
branches: branches:
- main - master
- releases/** - releases/**
paths-ignore:
- '**.md'
jobs: jobs:
# Build and unit test # Build and unit test
build: build:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, ubuntu-16.04, windows-latest, macOS-latest]
fail-fast: false fail-fast: false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Setup Node.js 20.x - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v1
with: with:
node-version: 20.x node-version: '12.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@v1
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
@ -33,17 +47,28 @@ jobs:
run: npm run lint run: npm run lint
- name: Build & Test - name: Build & Test
run: npm run test run: npm run test
- name: Ensure dist/ folder is up-to-date
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
npm run build
if [ "$(git status --porcelain | wc -l)" -gt "0" ]; then
echo "Detected uncommitted changes after build. See status below:"
git diff
exit 1
fi
# End to end save and restore # End to end save and restore
test-save: test-save:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, ubuntu-16.04, windows-latest, macOS-latest]
fail-fast: false fail-fast: false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Generate files in working directory - name: Generate files in working directory
shell: bash shell: bash
run: __tests__/create-cache-files.sh ${{ runner.os }} test-cache run: __tests__/create-cache-files.sh ${{ runner.os }} test-cache
@ -61,12 +86,12 @@ jobs:
needs: test-save needs: test-save
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, ubuntu-16.04, windows-latest, macOS-latest]
fail-fast: false fail-fast: false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Restore cache - name: Restore cache
uses: ./ uses: ./
with: with:
@ -89,14 +114,14 @@ 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:
https_proxy: http://squid-proxy:3128 https_proxy: http://squid-proxy:3128
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Generate files - name: Generate files
run: __tests__/create-cache-files.sh proxy test-cache run: __tests__/create-cache-files.sh proxy test-cache
- name: Save cache - name: Save cache
@ -112,14 +137,14 @@ 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:
https_proxy: http://squid-proxy:3128 https_proxy: http://squid-proxy:3128
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Restore cache - name: Restore cache
uses: ./ uses: ./
with: with:

View File

@ -1,22 +0,0 @@
sources:
npm: true
allowed:
- apache-2.0
- bsd-2-clause
- bsd-3-clause
- isc
- mit
- cc0-1.0
- unlicense
- 0bsd
reviewed:
npm:
- sax
- "@protobuf-ts/plugin-framework" # Apache-2.0
- "@protobuf-ts/runtime" # Apache-2.0
- fs.realpath # ISC
- glob # ISC
- prettier # MIT
- lodash # MIT

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.

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.

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.

View File

@ -27,10 +27,6 @@ Here are a few things you can do that will increase the likelihood of your pull
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
## Licensed
This repository uses a tool called [Licensed](https://github.com/github/licensed) to verify third party dependencies. You may need to locally install licensed and run `licensed cache` to update the dependency cache if you install or update a production dependency. If licensed cache is unable to determine the dependency, you may need to modify the cache file yourself to put the correct license. You should still verify the dependency, licensed is a tool to help, but is not a substitute for human review of dependencies.
## Resources ## Resources
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)

292
README.md
View File

@ -1,103 +1,58 @@
# 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: <a href="https://github.com/actions/cache/actions?query=workflow%3ATests"><img alt="GitHub Actions status" src="https://github.com/actions/cache/workflows/Tests/badge.svg?branch=master&event=push"></a>
>
>* [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)
## 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
### ⚠️ Important changes * Added support for multiple paths, [glob patterns](https://github.com/actions/toolkit/tree/master/packages/glob), and single file caches.
The cache backend service has been rewritten from the ground up for improved performance and reliability. [actions/cache](https://github.com/actions/cache) now integrates with the new cache service (v2) APIs. ```yaml
- name: Cache multiple paths
uses: actions/cache@v2
with:
path: |
~/cache
!~/cache/exclude
**/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
```
The new service will gradually roll out as of **February 1st, 2025**. The legacy service will also be sunset on the same date. Changes in these release are **fully backward compatible**. * Increased performance and improved cache sizes using `zstd` compression for Linux and macOS runners
* Allowed caching for all events with a ref. See [events that trigger workflow](https://help.github.com/en/actions/reference/events-that-trigger-workflows) for info on which events do not have a `GITHUB_REF`
* Released the [`@actions/cache`](https://github.com/actions/toolkit/tree/master/packages/cache) npm package to allow other actions to utilize caching
* Added a best-effort cleanup step to delete the archive after extraction to reduce storage space
**We are deprecating some versions of this action**. We recommend upgrading to version `v4` or `v3` as soon as possible before **February 1st, 2025.** (Upgrade instructions below). Refer [here](https://github.com/actions/cache/blob/v1/README.md) for previous versions
If you are using pinned SHAs, please use the SHAs of versions `v4.2.0` or `v3.4.0`
If you do not upgrade, all workflow runs using any of the deprecated [actions/cache](https://github.com/actions/cache) will fail.
Upgrading to the recommended versions will not break your workflows.
Read more about the change & access the migration guide: [reference to the announcement](https://github.com/actions/cache/discussions/1510).
### v4
* Integrated with the new cache service (v2) APIs.
* Updated to node 20
### v3
* Integrated with the new cache service (v2) APIs.
* Added support for caching in GHES 3.5+.
* Fixed download issue for files > 2GB during restore.
* Updated the minimum runner version support from node 12 -> node 16.
* 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 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.
## 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 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/master/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. * `key` - An explicit key for restoring and saving the cache
* `restore-keys` - An ordered multiline string listing the prefix-matched keys, that are used for restoring stale cache if no cache hit occurred for key. * `restore-keys` - An ordered list of keys to use for restoring the cache if no cache hit occurred for key
* `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`
* `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. 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 string 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
* If there's a cache hit, this will be 'true' or 'false' to indicate if there's an exact match for `key`.
* If there's a cache miss, this will be an empty string.
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
@ -109,11 +64,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Cache Primes - name: Cache Primes
id: cache-primes id: cache-primes
uses: actions/cache@v4 uses: actions/cache@v2
with: with:
path: prime-numbers path: prime-numbers
key: ${{ runner.os }}-primes key: ${{ runner.os }}-primes
@ -126,95 +81,45 @@ 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.
#### Using a combination of restore and save actions
```yaml
name: Caching Primes
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Restore cached Primes
id: cache-primes-restore
uses: actions/cache/restore@v4
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@v4
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
Every programming language and framework has its own way of caching. 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:
* [Bun](./examples.md#bun) - [C# - Nuget](./examples.md#c---nuget)
* [C# - NuGet](./examples.md#c---nuget) - [D - DUB](./examples.md#d---dub)
* [Clojure - Lein Deps](./examples.md#clojure---lein-deps) - [Elixir - Mix](./examples.md#elixir---mix)
* [D - DUB](./examples.md#d---dub) - [Go - Modules](./examples.md#go---modules)
* [Deno](./examples.md#deno) - [Haskell - Cabal](./examples.md#haskell---cabal)
* [Elixir - Mix](./examples.md#elixir---mix) - [Java - Gradle](./examples.md#java---gradle)
* [Go - Modules](./examples.md#go---modules) - [Java - Maven](./examples.md#java---maven)
* [Haskell - Cabal](./examples.md#haskell---cabal) - [Node - npm](./examples.md#node---npm)
* [Haskell - Stack](./examples.md#haskell---stack) - [Node - Lerna](./examples.md#node---lerna)
* [Java - Gradle](./examples.md#java---gradle) - [Node - Yarn](./examples.md#node---yarn)
* [Java - Maven](./examples.md#java---maven) - [OCaml/Reason - esy](./examples.md##ocamlreason---esy)
* [Node - npm](./examples.md#node---npm) - [PHP - Composer](./examples.md#php---composer)
* [Node - Lerna](./examples.md#node---lerna) - [Python - pip](./examples.md#python---pip)
* [Node - Yarn](./examples.md#node---yarn) - [R - renv](./examples.md#r---renv)
* [OCaml/Reason - esy](./examples.md#ocamlreason---esy) - [Ruby - Bundler](./examples.md#ruby---bundler)
* [PHP - Composer](./examples.md#php---composer) - [Rust - Cargo](./examples.md#rust---cargo)
* [Python - pip](./examples.md#python---pip) - [Scala - SBT](./examples.md#scala---sbt)
* [Python - pipenv](./examples.md#python---pipenv) - [Swift, Objective-C - Carthage](./examples.md#swift-objective-c---carthage)
* [R - renv](./examples.md#r---renv) - [Swift, Objective-C - CocoaPods](./examples.md#swift-objective-c---cocoapods)
* [Ruby - Bundler](./examples.md#ruby---bundler) - [Swift - Swift Package Manager](./examples.md#swift---swift-package-manager)
* [Rust - Cargo](./examples.md#rust---cargo)
* [Scala - SBT](./examples.md#scala---sbt)
* [Swift, Objective-C - Carthage](./examples.md#swift-objective-c---carthage)
* [Swift, Objective-C - CocoaPods](./examples.md#swift-objective-c---cocoapods)
* [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@v4 - uses: actions/cache@v2
with: with:
path: | path: |
path/to/dependencies path/to/dependencies
some/other/dependencies some/other/dependencies
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
``` ```
@ -225,10 +130,10 @@ 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@v4 - uses: actions/cache@v2
with: with:
path: path/to/dependencies path: path/to/dependencies
key: ${{ runner.os }}-${{ steps.get-date.outputs.date }}-${{ hashFiles('**/lockfiles') }} key: ${{ runner.os }}-${{ steps.get-date.outputs.date }}-${{ hashFiles('**/lockfiles') }}
@ -238,19 +143,18 @@ See [Using contexts to create cache keys](https://help.github.com/en/actions/con
## Cache Limits ## Cache Limits
A repository can have up to 10GB of caches. Once the 10GB limit is reached, older caches will be evicted based on when the cache was last accessed. Caches that are not accessed within the last week will also be evicted. A repository can have up to 5GB of caches. Once the 5GB limit is reached, older caches will be evicted based on when the cache was last accessed. Caches that are not accessed within the last week will also be evicted.
## 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@v4 - uses: actions/checkout@v2
- uses: actions/cache@v4 - uses: actions/cache@v2
id: cache id: cache
with: with:
path: path/to/dependencies path: path/to/dependencies
@ -261,84 +165,10 @@ 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 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.
> 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
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache Primes
id: cache-primes
uses: actions/cache@v4
with:
path: prime-numbers
key: primes
- name: Generate Prime Numbers
if: steps.cache-primes.outputs.cache-hit != 'true'
run: ./generate-primes.sh -d prime-numbers
- name: Cache Numbers
id: cache-numbers
uses: actions/cache@v4
with:
path: numbers
key: primes
- name: Generate Numbers
if: steps.cache-numbers.outputs.cache-hit != 'true'
run: ./generate-primes.sh -d numbers
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Cache Primes
id: cache-primes
uses: actions/cache@v4
with:
path: prime-numbers
key: primes
- name: Generate Prime Numbers
if: steps.cache-primes.outputs.cache-hit != 'true'
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)

View File

@ -1,174 +0,0 @@
# Releases
### 4.2.3
- Bump `@actions/cache` to v4.0.3 (obfuscates SAS token in debug logs for cache entries)
### 4.2.2
- Bump `@actions/cache` to v4.0.2
### 4.2.1
- Bump `@actions/cache` to v4.0.1
### 4.2.0
TLDR; The cache backend service has been rewritten from the ground up for improved performance and reliability. [actions/cache](https://github.com/actions/cache) now integrates with the new cache service (v2) APIs.
The new service will gradually roll out as of **February 1st, 2025**. The legacy service will also be sunset on the same date. Changes in these release are **fully backward compatible**.
**We are deprecating some versions of this action**. We recommend upgrading to version `v4` or `v3` as soon as possible before **February 1st, 2025.** (Upgrade instructions below).
If you are using pinned SHAs, please use the SHAs of versions `v4.2.0` or `v3.4.0`
If you do not upgrade, all workflow runs using any of the deprecated [actions/cache](https://github.com/actions/cache) will fail.
Upgrading to the recommended versions will not break your workflows.
### 4.1.2
- Add GitHub Enterprise Cloud instances hostname filters to inform API endpoint choices - [#1474](https://github.com/actions/cache/pull/1474)
- Security fix: Bump braces from 3.0.2 to 3.0.3 - [#1475](https://github.com/actions/cache/pull/1475)
### 4.1.1
- Restore original behavior of `cache-hit` output - [#1467](https://github.com/actions/cache/pull/1467)
### 4.1.0
- Ensure `cache-hit` output is set when a cache is missed - [#1404](https://github.com/actions/cache/pull/1404)
- Deprecate `save-always` input - [#1452](https://github.com/actions/cache/pull/1452)
### 4.0.2
- Fixed restore `fail-on-cache-miss` not working.
### 4.0.1
- Updated `isGhes` check
### 4.0.0
- Updated minimum runner version support from node 12 -> node 20
### 3.4.0
- Integrated with the new cache service (v2) APIs
### 3.3.3
- Updates @actions/cache to v3.2.3 to fix accidental mutated path arguments to `getCacheVersion` [actions/toolkit#1378](https://github.com/actions/toolkit/pull/1378)
- Additional audit fixes of npm package(s)
### 3.3.2
- Fixes bug with Azure SDK causing blob downloads to get stuck.
### 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.0
- Added option to lookup cache without downloading it.
### 3.2.6
- Fix zstd not being used after zstd version upgrade to 1.5.4 on hosted runners.
### 3.2.5
- Added fix to prevent from setting MYSYS environment variable globally.
### 3.2.4
- Added option to fail job on cache miss.
### 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.2
- Reverted the changes made in 3.2.1 to use gnu tar and zstd by default on windows.
### 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.0
- Released the two new actions - [restore](restore/action.yml) and [save](save/action.yml) for granular control on cache
### 3.2.0-beta.1
- Added two new actions - [restore](restore/action.yml) and [save](save/action.yml) for granular control on cache.
### 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.1.0-beta.2
- Added support for fallback to gzip to restore old caches on windows.
### 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.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.0.10
- Fix a bug with sorting inputs.
- Update definition for restore-keys in README.md
### 3.0.9
- Enhanced the warning message for cache unavailablity in case of GHES.
### 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.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.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.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))
### 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))
### 3.0.3
- Fixed avoiding empty cache save when no files are available for caching. ([issue](https://github.com/actions/cache/issues/624))
### 3.0.2
- Added support for dynamic cache size cap on GHES.
### 3.0.1
- Added support for caching from GHES 3.5.
- Fixed download issue for files > 2GB during restore.
### 3.0.0
- Updated minimum runner version support from node 12 -> node 16

View File

@ -1,51 +1,15 @@
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";
jest.mock("@actions/core"); jest.mock("@actions/core");
jest.mock("@actions/cache");
let pristineEnv: NodeJS.ProcessEnv; afterEach(() => {
beforeAll(() => {
pristineEnv = process.env;
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
return jest.requireActual("@actions/core").getInput(name, options);
});
});
beforeEach(() => {
jest.resetModules();
process.env = pristineEnv;
delete process.env[Events.Key]; delete process.env[Events.Key];
delete process.env[RefKey]; delete process.env[RefKey];
}); });
afterAll(() => {
process.env = pristineEnv;
});
test("isGhes returns true if server url is not github.com", () => {
try {
process.env["GITHUB_SERVER_URL"] = "http://example.com";
expect(actionUtils.isGhes()).toBe(true);
} finally {
process.env["GITHUB_SERVER_URL"] = undefined;
}
});
test("isGhes returns false when server url is github.com", () => {
try {
process.env["GITHUB_SERVER_URL"] = "http://github.com";
expect(actionUtils.isGhes()).toBe(false);
} finally {
process.env["GITHUB_SERVER_URL"] = undefined;
}
});
test("isExactKeyMatch with undefined cache key returns false", () => { test("isExactKeyMatch with undefined cache key returns false", () => {
const key = "linux-rust"; const key = "linux-rust";
const cacheKey = undefined; const cacheKey = undefined;
@ -88,6 +52,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.";
@ -116,152 +157,3 @@ test("isValidEvent returns true for event that has a ref", () => {
expect(isValidEvent).toBe(true); expect(isValidEvent).toBe(true);
}); });
test("getInputAsArray returns empty array if not required and missing", () => {
expect(actionUtils.getInputAsArray("foo")).toEqual([]);
});
test("getInputAsArray throws error if required and missing", () => {
expect(() =>
actionUtils.getInputAsArray("foo", { required: true })
).toThrowError();
});
test("getInputAsArray handles single line correctly", () => {
testUtils.setInput("foo", "bar");
expect(actionUtils.getInputAsArray("foo")).toEqual(["bar"]);
});
test("getInputAsArray handles multiple lines correctly", () => {
testUtils.setInput("foo", "bar\nbaz");
expect(actionUtils.getInputAsArray("foo")).toEqual(["bar", "baz"]);
});
test("getInputAsArray handles different new lines correctly", () => {
testUtils.setInput("foo", "bar\r\nbaz");
expect(actionUtils.getInputAsArray("foo")).toEqual(["bar", "baz"]);
});
test("getInputAsArray handles empty lines correctly", () => {
testUtils.setInput("foo", "\n\nbar\n\nbaz\n\n");
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", () => {
expect(actionUtils.getInputAsInt("undefined")).toBeUndefined();
});
test("getInputAsInt returns value if input is valid", () => {
testUtils.setInput("foo", "8");
expect(actionUtils.getInputAsInt("foo")).toBe(8);
});
test("getInputAsInt returns undefined if input is invalid or NaN", () => {
testUtils.setInput("foo", "bar");
expect(actionUtils.getInputAsInt("foo")).toBeUndefined();
});
test("getInputAsInt throws if required and value missing", () => {
expect(() =>
actionUtils.getInputAsInt("undefined", { required: true })
).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", () => {
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => true);
expect(actionUtils.isCacheFeatureAvailable()).toBe(true);
});
test("isCacheFeatureAvailable for ac disabled on GHES", () => {
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.
Otherwise please upgrade to GHES version >= 3.5 and If you are also using Github Connect, please unretire the actions/cache namespace before upgrade (see https://docs.github.com/en/enterprise-server@3.5/admin/github-actions/managing-access-to-actions-from-githubcom/enabling-automatic-access-to-githubcom-actions-using-github-connect#automatic-retirement-of-namespaces-for-actions-accessed-on-githubcom)`;
const infoMock = jest.spyOn(core, "info");
try {
process.env["GITHUB_SERVER_URL"] = "http://example.com";
expect(actionUtils.isCacheFeatureAvailable()).toBe(false);
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`);
} finally {
delete process.env["GITHUB_SERVER_URL"];
}
});
test("isCacheFeatureAvailable for ac disabled on dotcom", () => {
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => false);
const message =
"An internal error has occurred in cache backend. Please check https://www.githubstatus.com/ for any ongoing issue in actions.";
const infoMock = jest.spyOn(core, "info");
try {
process.env["GITHUB_SERVER_URL"] = "http://github.com";
expect(actionUtils.isCacheFeatureAvailable()).toBe(false);
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`);
} finally {
delete process.env["GITHUB_SERVER_URL"];
}
});
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is not defined", async () => {
delete process.env["GITHUB_SERVER_URL"];
expect(actionUtils.isGhes()).toBeFalsy();
});
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is set to github.com", async () => {
process.env["GITHUB_SERVER_URL"] = "https://github.com";
expect(actionUtils.isGhes()).toBeFalsy();
});
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL", async () => {
process.env["GITHUB_SERVER_URL"] = "https://contoso.ghe.com";
expect(actionUtils.isGhes()).toBeFalsy();
});
test("isGhes returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix", async () => {
process.env["GITHUB_SERVER_URL"] = "https://mock-github.localhost";
expect(actionUtils.isGhes()).toBeFalsy();
});
test("isGhes returns true when the GITHUB_SERVER_URL environment variable is set to some other URL", async () => {
process.env["GITHUB_SERVER_URL"] = "https://src.onpremise.fabrikam.com";
expect(actionUtils.isGhes()).toBeTruthy();
});

View File

@ -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";
@ -20,31 +20,11 @@ beforeAll(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils"); const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent(); 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(() => { 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";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
}); });
afterEach(() => { afterEach(() => {
@ -53,13 +33,100 @@ 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 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 +138,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(
@ -94,6 +151,40 @@ test("restore with no cache found", async () => {
); );
}); });
test("restore with server error should fail", async () => {
const path = "node_modules";
const key = "node-test";
testUtils.setInputs({
path: path,
key
});
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
throw new Error("HTTP Error Occurred");
});
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
await run();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
expect(failedMock).toHaveBeenCalledTimes(0);
});
test("restore with restore keys and no cache found", async () => { test("restore with restore keys and no cache found", async () => {
const path = "node_modules"; const path = "node_modules";
const key = "node-test"; const key = "node-test";
@ -101,8 +192,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 +204,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 +222,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 +255,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);
});

View File

@ -1,467 +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);
});
test("restore failure with earlyExit should call process exit", async () => {
testUtils.setInput(Inputs.Path, "node_modules");
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
const processExitMock = jest.spyOn(process, "exit").mockImplementation();
// call restoreImpl with `earlyExit` set to true
await restoreImpl(new StateProvider(), true);
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledWith(
"Input required and not supplied: key"
);
expect(processExitMock).toHaveBeenCalledWith(1);
});

View File

@ -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);
});

View File

@ -2,7 +2,7 @@ import * as cache from "@actions/cache";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { Events, Inputs, RefKey } from "../src/constants"; import { Events, Inputs, RefKey } from "../src/constants";
import { saveRun } from "../src/saveImpl"; import run from "../src/save";
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";
@ -15,34 +15,10 @@ 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(
(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( jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => { (key, cacheResult) => {
return jest return jest
@ -60,11 +36,6 @@ beforeAll(() => {
beforeEach(() => { beforeEach(() => {
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";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
}); });
afterEach(() => { afterEach(() => {
@ -73,6 +44,220 @@ afterEach(() => {
delete process.env[RefKey]; 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();
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 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(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 infoMock = jest.spyOn(core, "info");
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(infoMock).toHaveBeenCalledWith(
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
);
expect(logWarningMock).toHaveBeenCalledTimes(0);
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(logWarningMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
expect(failedMock).toHaveBeenCalledTimes(0);
});
test("save with valid inputs uploads a cache", async () => { test("save with valid inputs uploads a cache", async () => {
const failedMock = jest.spyOn(core, "setFailed"); const failedMock = jest.spyOn(core, "setFailed");
@ -82,16 +267,15 @@ 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";
testUtils.setInput(Inputs.Path, inputPath); testUtils.setInput(Inputs.Path, inputPath);
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = 4; const cacheId = 4;
const saveCacheMock = jest const saveCacheMock = jest
@ -100,17 +284,10 @@ test("save with valid inputs uploads a cache", async () => {
return Promise.resolve(cacheId); return Promise.resolve(cacheId);
}); });
await saveRun(); await run();
expect(saveCacheMock).toHaveBeenCalledTimes(1); expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith( expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey);
[inputPath],
primaryKey,
{
uploadChunkSize: 4000000
},
false
);
expect(failedMock).toHaveBeenCalledTimes(0); expect(failedMock).toHaveBeenCalledTimes(0);
}); });

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