Compare commits

..

15 Commits

Author SHA1 Message Date
Ian Butterworth
64e2b31172 update to actions/cache v4.0.2 commit 2024-04-03 12:21:49 -04:00
Ian Butterworth
5898e5c965 Apply suggestions from code review
Co-authored-by: Curtis Vogt <curtis.vogt@gmail.com>
2024-01-23 12:11:22 -05:00
Ian Butterworth
1470e42d30 add save-always 2024-01-18 10:26:13 -05:00
Sascha Mann
216aaef29a Pin third party action to hash (#106)
Third party actions should never not be pinned to a hash. Otherwise, in case the action repo is taken over by a malicious actor, they can change what runs in all of the workflows that julia-actions/cache is used in as well. Pinning to a hash prevents that.
2024-01-18 10:21:26 -05:00
nathan musoke
3e0649aaee fix: Attempt to install jq on all runners (#105) 2024-01-17 17:38:32 -05:00
Ian Butterworth
dc1a3cdeac make depot if not restored (#91) 2024-01-16 10:48:25 -05:00
Curtis Vogt
4491ed7a86 Pass cache-name between save/restore jobs (#103)
* Pass cache-name between save/restore

* Use save job name in cache-name

* Re-order test jobs

* Fix typo in registry warning
2024-01-16 09:40:31 -06:00
Curtis Vogt
b84ca24db8 Avoid corrupting existing cloned Julia registries (#102)
* Reproduce add-julia-registry issue

* Skip registries restore when already present

* Expand ~

* Refactor paths step to use bash array
2024-01-15 19:28:33 -06:00
Curtis Vogt
b3b34e3264 Test cache action against Julia 1.0 and nightly (#101)
* CI test action on Julia 1.0

* Avoid quoting Julia shell string

* Test against Julia nightly
2024-01-15 15:20:46 -06:00
Curtis Vogt
0c5d92d73a Delete cache entries only on the workflow branch (#97)
* Delete cache entries on the workflow branch

* Grant permissions for cache cleanup

* Add delete-old-caches required for testing purposes

* Revise help message

* Faster generate-key

* Use distinct cache-names for matrix/no-matrix jobs

* Remove redundant permissions

* Better fork detection logic
2024-01-15 10:15:36 -06:00
Curtis Vogt
fca1a91340 URL encode any invalid key characters (#93)
* URL encode any invalid key characters

* Test we handle invalid chars

* Job matrices must match

* Empty commit

* Empty commit
2024-01-09 16:50:08 -05:00
Curtis Vogt
207a5a0786 Unique cache-key for job matrix objects (#88)
* Unique cache-key for job matrix objects

* Update workflow to use object in job matrix

* Restore key should always match startswith

* fixup! Unique cache-key for job matrix objects

* Debug no-matrix

* Tests require overriding workflow/job

* Skip generating matrix_key when no matrix is used

* Install jq for self-hosted runners

* fixup! Install jq for self-hosted runners

* Skip install when not needed

* fixup! Skip install when not needed

* fixup! Skip install when not needed

* Improve `cache-name` description

Co-authored-by: Ian Butterworth <i.r.butterworth@gmail.com>

* Update  description in README

* Use actions/checkout@v4 in example

* add missing period

---------

Co-authored-by: Ian Butterworth <i.r.butterworth@gmail.com>
2024-01-04 20:29:52 -05:00
Ian Butterworth
fab7d6ae0a enable registries by default (#62) 2024-01-04 16:39:29 -05:00
Curtis Vogt
b430ec2989 Add option for user specified Julia depot (#83)
* Add input user-depot

* Only list restored depot directory size on cache-hit

* Remove unnecessary quoting on inputs/outputs

* Rename input to just depot

* Include the depot path in the key

* Documentation update

* Allow JULIA_DEPOT_PATH to specify default depot

* fixup! Allow JULIA_DEPOT_PATH to specify default depot
2024-01-03 19:19:34 -05:00
Curtis Vogt
930a18227b Documentation consistency fixes (#84)
* Consistent use of periods

* Consistent casing of GitHub

* Use 2-space indentation for YAML example

* Add links to for further details
2024-01-02 21:59:12 -05:00
4 changed files with 350 additions and 163 deletions

View File

@@ -20,99 +20,177 @@ permissions:
contents: read contents: read
jobs: jobs:
generate-key: generate-prefix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
cache-name: ${{ steps.name.outputs.cache-name }} cache-prefix: ${{ steps.name.outputs.cache-prefix }}
steps: steps:
- name: Generate random file - name: Generate random cache-prefix
shell: 'julia --color=yes {0}'
run: 'write("random.txt", string(rand(10)))'
- name: Set cache-name as output
id: name id: name
run: echo "cache-name=${{ hashFiles('random.txt') }}" >> $GITHUB_OUTPUT run: |
cache_prefix=$(head -n 100 </dev/urandom | shasum -a 256 | cut -d ' ' -f 1)
echo "cache-prefix=$cache_prefix" >>"$GITHUB_OUTPUT"
test-save: test-save:
needs: generate-key needs: generate-prefix
runs-on: ${{ matrix.os }}
outputs:
cache-name: ${{ steps.cache-name.outputs.cache-name }}
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macOS-latest] nested:
- name: matrix
invalid-chars: "," # Use invalid characters in job matrix to ensure we escape them
version:
- "1.0"
- "1"
- "nightly"
os:
- ubuntu-latest
- windows-latest
- macOS-latest
exclude:
# Test Julia "1.0" on Linux only
- version: "1.0"
os: windows-latest
- version: "1.0"
os: macOS-latest
fail-fast: false fail-fast: false
runs-on: ${{ matrix.os }} env:
JULIA_DEPOT_PATH: /tmp/julia-depot
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Set cache-name
id: cache-name
shell: bash
run: |
echo "cache-name=${{ needs.generate-prefix.outputs.cache-prefix }}-${{ github.job }}" >>"$GITHUB_OUTPUT"
- uses: julia-actions/setup-julia@v1
with:
version: ${{ matrix.version }}
- name: Save cache - name: Save cache
id: cache id: cache
uses: ./ uses: ./
with: with:
cache-name: ${{ needs.generate-key.outputs.cache-name }} cache-name: ${{ steps.cache-name.outputs.cache-name }}
delete-old-caches: required
- name: Check no artifacts dir - name: Check no artifacts dir
shell: 'julia --color=yes {0}' shell: julia --color=yes {0}
run: | run: |
dir = joinpath(first(DEPOT_PATH), "artifacts") dir = joinpath(first(DEPOT_PATH), "artifacts")
@assert !isdir(dir) @assert !isdir(dir)
- name: Install a small binary - name: Install a small binary
shell: 'julia --color=yes {0}' shell: julia --color=yes {0}
run: 'using Pkg; Pkg.add("pandoc_jll")' run: |
using Pkg
if VERSION >= v"1.3"
Pkg.add(PackageSpec(name="pandoc_jll", version="3"))
else
Pkg.add(PackageSpec(name="Scratch", version="1"))
using Scratch
get_scratch!("test")
end
test-restore:
needs: test-save
runs-on: ${{ matrix.os }}
strategy:
matrix:
nested:
- name: matrix
invalid-chars: "," # Use invalid characters in job matrix to ensure we escape them
version:
- "1.0"
- "1"
- "nightly"
os:
- ubuntu-latest
- windows-latest
- macOS-latest
exclude:
# Test Julia "1.0" on Linux only
- version: "1.0"
os: windows-latest
- version: "1.0"
os: macOS-latest
fail-fast: false
env:
JULIA_DEPOT_PATH: /tmp/julia-depot
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- uses: julia-actions/setup-julia@v1
with:
version: ${{ matrix.version }}
- name: Restore cache
id: cache
uses: ./
with:
cache-name: ${{ needs.test-save.outputs.cache-name }}
# Cannot require a successful cache delete on forked PRs as the permissions for actions is limited to read
delete-old-caches: ${{ github.event.pull_request.head.repo.fork && 'false' || 'required' }}
- name: Test cache-hit output
shell: julia --color=yes {0}
run: |
@show ENV["cache-hit"]
@assert ENV["cache-hit"] == "true"
env:
cache-hit: ${{ steps.cache.outputs.cache-hit }}
- name: Check existance or emptiness of affected dirs
shell: julia --color=yes {0}
run: |
# These dirs should exist as they've been cached
artifacts_dir = joinpath(first(DEPOT_PATH), "artifacts")
if VERSION >= v"1.3"
@assert !isempty(readdir(artifacts_dir))
else
@assert !isdir(artifacts_dir)
end
packages_dir = joinpath(first(DEPOT_PATH), "packages")
@assert !isempty(readdir(packages_dir))
compiled_dir = joinpath(first(DEPOT_PATH), "compiled")
@assert !isempty(readdir(compiled_dir))
scratchspaces_dir = joinpath(first(DEPOT_PATH), "scratchspaces")
@assert !isempty(readdir(scratchspaces_dir))
logs_dir = joinpath(first(DEPOT_PATH), "logs")
@assert !isempty(readdir(logs_dir))
# Do tests with no matrix also given the matrix is auto-included in cache key # Do tests with no matrix also given the matrix is auto-included in cache key
test-save-nomatrix: test-save-nomatrix:
needs: generate-key needs: generate-prefix
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
cache-name: ${{ steps.cache-name.outputs.cache-name }}
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Set cache-name
id: cache-name
run: |
echo "cache-name=${{ needs.generate-prefix.outputs.cache-prefix }}-${{ github.job }}" >>"$GITHUB_OUTPUT"
- name: Save cache - name: Save cache
id: cache id: cache
uses: ./ uses: ./
with: with:
cache-name: ${{ needs.generate-key.outputs.cache-name }} cache-name: ${{ steps.cache-name.outputs.cache-name }}
delete-old-caches: required
- name: Check no artifacts dir - name: Check no artifacts dir
shell: 'julia --color=yes {0}' shell: julia --color=yes {0}
run: | run: |
dir = joinpath(first(DEPOT_PATH), "artifacts") dir = joinpath(first(DEPOT_PATH), "artifacts")
@assert !isdir(dir) @assert !isdir(dir)
- name: Install a small binary - name: Install a small binary
shell: 'julia --color=yes {0}' shell: julia --color=yes {0}
run: 'using Pkg; Pkg.add("pandoc_jll")'
test-restore:
needs: [generate-key, test-save]
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Restore cache
id: cache
uses: ./
with:
cache-name: ${{ needs.generate-key.outputs.cache-name }}
- name: Test cache-hit output
shell: 'julia --color=yes {0}'
run: | run: |
@show ENV["cache-hit"] using Pkg
@assert ENV["cache-hit"] == "true" if VERSION >= v"1.3"
env: Pkg.add(PackageSpec(name="pandoc_jll", version="3"))
cache-hit: ${{ steps.cache.outputs.cache-hit }} else
- name: Check existance or emptiness of affected dirs Pkg.add(PackageSpec(name="Scratch", version="1"))
shell: 'julia --color=yes {0}' using Scratch
run: | get_scratch!("test")
# These dirs should exist as they've been cached end
artifacts_dir = joinpath(first(DEPOT_PATH), "artifacts")
@assert !isempty(readdir(artifacts_dir))
packages_dir = joinpath(first(DEPOT_PATH), "packages")
@assert !isempty(readdir(packages_dir))
compiled_dir = joinpath(first(DEPOT_PATH), "compiled")
@assert !isempty(readdir(compiled_dir))
scratchspaces_dir = joinpath(first(DEPOT_PATH), "scratchspaces")
@assert !isempty(readdir(scratchspaces_dir))
logs_dir = joinpath(first(DEPOT_PATH), "logs")
@assert !isempty(readdir(logs_dir))
test-restore-nomatrix: test-restore-nomatrix:
needs: [generate-key, test-save-nomatrix] needs: test-save-nomatrix
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
@@ -120,20 +198,26 @@ jobs:
id: cache id: cache
uses: ./ uses: ./
with: with:
cache-name: ${{ needs.generate-key.outputs.cache-name }} cache-name: ${{ needs.test-save-nomatrix.outputs.cache-name }}
# Cannot require a successful cache delete on forked PRs as the permissions for actions is limited to read
delete-old-caches: ${{ github.event.pull_request.head.repo.fork && 'false' || 'required' }}
- name: Test cache-hit output - name: Test cache-hit output
shell: 'julia --color=yes {0}' shell: julia --color=yes {0}
run: | run: |
@show ENV["cache-hit"] @show ENV["cache-hit"]
@assert ENV["cache-hit"] == "true" @assert ENV["cache-hit"] == "true"
env: env:
cache-hit: ${{ steps.cache.outputs.cache-hit }} cache-hit: ${{ steps.cache.outputs.cache-hit }}
- name: Check existance or emptiness of affected dirs - name: Check existance or emptiness of affected dirs
shell: 'julia --color=yes {0}' shell: julia --color=yes {0}
run: | run: |
# These dirs should exist as they've been cached # These dirs should exist as they've been cached
artifacts_dir = joinpath(first(DEPOT_PATH), "artifacts") artifacts_dir = joinpath(first(DEPOT_PATH), "artifacts")
@assert !isempty(readdir(artifacts_dir)) if VERSION >= v"1.3"
@assert !isempty(readdir(artifacts_dir))
else
@assert !isdir(artifacts_dir)
end
packages_dir = joinpath(first(DEPOT_PATH), "packages") packages_dir = joinpath(first(DEPOT_PATH), "packages")
@assert !isempty(readdir(packages_dir)) @assert !isempty(readdir(packages_dir))
compiled_dir = joinpath(first(DEPOT_PATH), "compiled") compiled_dir = joinpath(first(DEPOT_PATH), "compiled")
@@ -143,3 +227,54 @@ jobs:
logs_dir = joinpath(first(DEPOT_PATH), "logs") logs_dir = joinpath(first(DEPOT_PATH), "logs")
@assert !isempty(readdir(logs_dir)) @assert !isempty(readdir(logs_dir))
test-save-cloned-registry:
needs: generate-prefix
runs-on: ubuntu-latest
outputs:
cache-name: ${{ steps.cache-name.outputs.cache-name }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Set cache-name
id: cache-name
run: |
echo "cache-name=${{ needs.generate-prefix.outputs.cache-prefix }}-${{ github.job }}" >>"$GITHUB_OUTPUT"
- name: Save cache
uses: ./
with:
cache-name: ${{ steps.cache-name.outputs.cache-name }}
# Cannot require a successful cache delete on forked PRs as the permissions for actions is limited to read
delete-old-caches: ${{ github.event.pull_request.head.repo.fork && 'false' || 'required' }}
- name: Add General registry clone
shell: julia --color=yes {0}
run: |
using Pkg
Pkg.Registry.add("General")
env:
JULIA_PKG_SERVER: ""
# Set the registry worktree to an older state to simulate the cache storing an old version of the registry.
- name: Use outdated General worktree
run: git -C ~/.julia/registries/General reset --hard HEAD~20
test-restore-cloned-registry:
needs: test-save-cloned-registry
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Add General registry clone
shell: julia --color=yes {0}
run: |
using Pkg
Pkg.Registry.add("General")
env:
JULIA_PKG_SERVER: ""
- name: Restore cache
uses: ./
with:
cache-name: ${{ needs.test-save-cloned-registry.outputs.cache-name }}
# Cannot require a successful cache delete on forked PRs as the permissions for actions is limited to read
delete-old-caches: ${{ github.event.pull_request.head.repo.fork && 'false' || 'required' }}
- name: Test registry is not corrupt
shell: julia --color=yes {0}
run: |
using Pkg
Pkg.Registry.update()

View File

@@ -20,35 +20,36 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v1 - uses: julia-actions/setup-julia@v1
- uses: julia-actions/cache@v1 - uses: julia-actions/cache@v1
- uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-runtest@v1
``` ```
By default the majority of the depot is cached. To also cache `~/.julia/registries/`, use By default all depot directories called out below are cached.
```yaml ### Requirements
- uses: julia-actions/cache@v1
with:
cache-registries: "true"
```
However note that caching the registries may mean that the registry will not be updated each run. This action uses [`jq`](https://github.com/jqlang/jq) to parse JSON.
`jq` is installed by default in GitHub-hosted runners.
[`dcarbone/install-jq-action`](https://github.com/dcarbone/install-jq-action) is used to check that `jq` is available and install it if not.
**Note:** installing `jq` with `dcarbone/install-jq-action` requires that curl is available; this may not be the case in custom containers.
### Optional Inputs ### Optional Inputs
- `cache-name` - The cache key prefix. Defaults to `julia-cache-${{ github.workflow }}-${{ github.job }}`. The key body automatically includes matrix vars and the OS. Include any other parameters/details in this prefix to ensure one unique cache key per concurrent job type. - `cache-name` - The cache key prefix. Defaults to `julia-cache;workflow=${{ github.workflow }};job=${{ github.job }}`. The key body automatically includes the OS and, unless disabled with `include-matrix`, the matrix vars. Include any other parameters/details in this prefix to ensure one unique cache key per concurrent job type.
- `include-matrix` - Whether to include the matrix values when constructing the cache key. Defaults to `true`. - `include-matrix` - Whether to include the matrix values when constructing the cache key. Defaults to `true`.
- `cache-artifacts` - Whether to cache `~/.julia/artifacts/`. Defaults to `true`. - `depot` - Path to a Julia [depot](https://pkgdocs.julialang.org/v1/glossary/) directory where cached data will be saved to and restored from. Defaults to the first depot in [`JULIA_DEPOT_PATH`](https://docs.julialang.org/en/v1/manual/environment-variables/#JULIA_DEPOT_PATH) if specified. Otherwise, defaults to `~/.julia`.
- `cache-packages` - Whether to cache `~/.julia/packages/`. Defaults to `true`. - `cache-artifacts` - Whether to cache the depot's `artifacts` directory. Defaults to `true`.
- `cache-registries` - Whether to cache `~/.julia/registries/`. Defaults to `false`. Disabled to ensure CI gets latest versions. - `cache-packages` - Whether to cache the depot's `packages` directory. Defaults to `true`.
- `cache-compiled` - Whether to cache `~/.julia/compiled/`. Defaults to `true`. - `cache-registries` - Whether to cache the depot's `registries` directory. Defaults to `true`.
- `cache-scratchspaces` - Whether to cache `~/.julia/scratchspaces/`. Defaults to `true`. - `cache-compiled` - Whether to cache the depot's `compiled` directory. Defaults to `true`.
- `cache-logs` - Whether to cache `~/.julia/logs/`. Defaults to `true`. Helps auto-`Pkg.gc()` keep the cache small. - `cache-scratchspaces` - Whether to cache the depot's `scratchspaces` directory. Defaults to `true`.
- `delete-old-caches` - Whether to delete old caches for the given key. Defaults to `true` - `cache-logs` - Whether to cache the depot's `logs` directory. Defaults to `true`. Helps auto-`Pkg.gc()` keep the cache small.
- `token` - A github PAT. Defaults to `github.token`. Requires `repo` scope to enable the deletion of old caches. - `save-always` - Whether to save the cache even when the job fails. Defaults to `true`. This is useful as the Julia depot should mostly be reusable from a failing job.
- `delete-old-caches` - Whether to delete old caches for the given key. Defaults to `true`.
- `token` - A [GitHub PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). Defaults to `github.token`. Requires `repo` scope to enable the deletion of old caches.
### Outputs ### Outputs
@@ -82,16 +83,16 @@ This action automatically deletes old caches that match the first 4 fields of th
- All variables in the `matrix` (unless disabled via `include-matrix: 'false'`) - All variables in the `matrix` (unless disabled via `include-matrix: 'false'`)
- The `runner.os` (may be in the matrix too, but included for safety) - The `runner.os` (may be in the matrix too, but included for safety)
Which means your caches files will not grow needlessly. Github also deletes cache files after Which means your caches files will not grow needlessly. GitHub also deletes cache files after
[90 days which can be increased in private repos to up to 400 days](https://docs.github.com/en/organizations/managing-organization-settings/configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-organization) [90 days which can be increased in private repos to up to 400 days](https://docs.github.com/en/organizations/managing-organization-settings/configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-organization)
> [!NOTE] > [!NOTE]
> To allow deletion of caches you will likely need to grant the following to the default > To allow deletion of caches you will likely need to [grant the following permissions](https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs)
> `GITHUB_TOKEN` by adding this to your yml: > to the `GITHUB_TOKEN` by adding this to your GitHub actions workflow:
> ``` > ```yaml
> permissions: > permissions:
> actions: write > actions: write
> contents: read > contents: read
> ``` > ```
> (Note this won't work for fork PRs but should once merged) > (Note this won't work for fork PRs but should once merged)
> Or provide a token with `repo` scope via the `token` input option. > Or provide a token with `repo` scope via the `token` input option.

View File

@@ -8,90 +8,132 @@ branding:
inputs: inputs:
cache-name: cache-name:
description: 'The cache key prefix. Unless disabled the key body automatically includes matrix vars, and the OS. Include any other parameters/details in this prefix to ensure one unique cache key per concurrent job type.' description: >-
default: 'julia-cache-${{ github.workflow }}-${{ github.job }}' The cache key prefix. The key body automatically includes the OS and, unless disabled, the matrix vars.
Include any other parameters/details in this prefix to ensure one unique cache key per concurrent job type.
default: julia-cache;workflow=${{ github.workflow }};job=${{ github.job }}
include-matrix: include-matrix:
description: 'Whether to include the matrix values when constructing the cache key' description: Whether to include the matrix values when constructing the cache key.
default: 'true' default: 'true'
depot:
description: Path to a Julia depot directory where cached data will be saved to and restored from.
default: ''
cache-artifacts: cache-artifacts:
description: 'Whether to cache ~/.julia/artifacts/' description: Whether to cache the depot's `artifacts` directory.
default: 'true' default: 'true'
cache-packages: cache-packages:
description: 'Whether to cache ~/.julia/packages/' description: Whether to cache the depot's `packages` directory.
default: 'true' default: 'true'
cache-registries: cache-registries:
description: 'Whether to cache ~/.julia/registries/. This is off by default to ensure CI gets latest versions' description: Whether to cache the depot's `registries` directory.
default: 'false' default: 'true'
cache-compiled: cache-compiled:
description: 'Whether to cache ~/.julia/compiled/' description: Whether to cache the depot's `compiled` directory.
default: 'true' default: 'true'
cache-scratchspaces: cache-scratchspaces:
description: 'Whether to cache ~/.julia/scratchspaces/' description: Whether to cache the depot's `scratchspaces` directory.
default: 'true' default: 'true'
cache-logs: cache-logs:
description: 'Whether to cache ~/.julia/logs/. This helps automatic Pkg.gc() keep the cache size down' description: Whether to cache the depot's `logs` directory. This helps automatic `Pkg.gc()` keep the cache size down.
default: 'true'
save-always:
description: Whether to save the cache even when the job fails. This is useful as the Julia depot should mostly be reusable from a failing job.
default: 'true' default: 'true'
delete-old-caches: delete-old-caches:
description: 'Whether to delete old caches for the given key' description: Whether to delete old caches for the given key.
default: 'true' default: 'true'
token: token:
description: 'A github PAT. Requires `repo` scope to enable the deletion of old caches' description: A GitHub PAT. Requires `repo` scope to enable the deletion of old caches.
default: '${{ github.token }}' default: ${{ github.token }}
outputs: outputs:
cache-hit: cache-hit:
description: 'A boolean value to indicate an exact match was found for the primary key. Returns \"\" when the key is new. Forwarded from actions/cache' description: A boolean value to indicate an exact match was found for the primary key. Returns "" when the key is new. Forwarded from actions/cache.
value: ${{ steps.hit.outputs.cache-hit }} value: ${{ steps.hit.outputs.cache-hit }}
runs: runs:
using: 'composite' using: 'composite'
steps: steps:
- name: Install jq
uses: dcarbone/install-jq-action@8867ddb4788346d7c22b72ea2e2ffe4d514c7bcb # v2.1.0
with:
force: false # Skip install when an existing `jq` is present
- id: paths - id: paths
run: | run: |
[ "${{ inputs.cache-artifacts }}" = "true" ] && A_PATH="~/.julia/artifacts" if [ -n "${{ inputs.depot }}" ]; then
echo "artifacts-path=$A_PATH" >> $GITHUB_OUTPUT depot="${{ inputs.depot }}"
[ "${{ inputs.cache-packages }}" = "true" ] && P_PATH="~/.julia/packages" elif [ -n "$JULIA_DEPOT_PATH" ]; then
echo "packages-path=$P_PATH" >> $GITHUB_OUTPUT # Use the first depot path
[ "${{ inputs.cache-registries }}" = "true" ] && R_PATH="~/.julia/registries" depot=$(echo $JULIA_DEPOT_PATH | cut -d$PATH_DELIMITER -f1)
echo "registries-path=$R_PATH" >> $GITHUB_OUTPUT else
[ "${{ inputs.cache-compiled }}" = "true" ] && PCC_PATH="~/.julia/compiled" depot="~/.julia"
echo "compiled-path=$PCC_PATH" >> $GITHUB_OUTPUT fi
[ "${{ inputs.cache-scratchspaces }}" = "true" ] && S_PATH="~/.julia/scratchspaces" echo "depot=$depot" | tee -a "$GITHUB_OUTPUT"
echo "scratchspaces-path=$S_PATH" >> $GITHUB_OUTPUT
[ "${{ inputs.cache-logs }}" = "true" ] && L_PATH="~/.julia/logs"
echo "logs-path=$L_PATH" >> $GITHUB_OUTPUT
shell: bash
# MATRIX_STRING is a join of all matrix variables that helps concurrent runs have a unique cache key. cache_paths=()
# The underscore at the end of the restore key demarks the end of the restore section. Without this artifacts_path="${depot}/artifacts"
# a runner without a matrix has a restore key that will cause impropper clearing of caches from those [ "${{ inputs.cache-artifacts }}" = "true" ] && cache_paths+=("$artifacts_path")
# with a matrix. packages_path="${depot}/packages"
- id: keys [ "${{ inputs.cache-packages }}" = "true" ] && cache_paths+=("$packages_path")
registries_path="${depot}/registries"
if [ "${{ inputs.cache-registries }}" = "true" ]; then
if [ ! -d "${registries_path/#\~/$HOME}" ]; then
cache_paths+=("$registries_path")
else
echo "::warning::Julia depot registries already exist. Skipping restoring of cached registries to avoid potential merge conflicts when updating. Please ensure that \`julia-actions/cache\` precedes any workflow steps which add registries."
fi
fi
compiled_path="${depot}/compiled"
[ "${{ inputs.cache-compiled }}" = "true" ] && cache_paths+=("$compiled_path")
scratchspaces_path="${depot}/scratchspaces"
[ "${{ inputs.cache-scratchspaces }}" = "true" ] && cache_paths+=("$scratchspaces_path")
logs_path="${depot}/logs"
[ "${{ inputs.cache-logs }}" = "true" ] && cache_paths+=("$logs_path")
{
echo "cache-paths<<EOF"
printf "%s\n" "${cache_paths[@]}"
echo "EOF"
} | tee -a "$GITHUB_OUTPUT"
shell: bash
env:
PATH_DELIMITER: ${{ runner.OS == 'Windows' && ';' || ':' }}
- name: Generate Keys
id: keys
run: | run: |
[ "${{ inputs.include-matrix }}" == "true" ] && MATRIX_STRING="${{ join(matrix.*, '-') }}" # `matrix_key` joins all of matrix keys/values (including nested objects) to ensure that concurrent runs each use a unique cache key.
[ -n "$MATRIX_STRING" ] && MATRIX_STRING="-${MATRIX_STRING}" # When `matrix` isn't set for the job then `MATRIX_JSON=null`.
RESTORE_KEY="${{ inputs.cache-name }}-${{ runner.os }}${MATRIX_STRING}_" if [ "${{ inputs.include-matrix }}" == "true" ] && [ "$MATRIX_JSON" != "null" ]; then
echo "restore-key=${RESTORE_KEY}" >> $GITHUB_OUTPUT matrix_key=$(echo "$MATRIX_JSON" | jq 'paths(type != "object") as $p | ($p | join("-")) + "=" + (getpath($p) | tostring)' | jq -rs 'join(";") | . + ";"')
echo "key=${RESTORE_KEY}${{ github.run_id }}-${{ github.run_attempt }}" >> $GITHUB_OUTPUT fi
restore_key="${{ inputs.cache-name }};os=${{ runner.os }};${matrix_key}"
# URL encode any restricted characters:
# https://github.com/actions/toolkit/blob/5430c5d84832076372990c7c27f900878ff66dc9/packages/cache/src/cache.ts#L38-L43
restore_key=$(sed 's/,/%2C/g' <<<"${restore_key}")
key="${restore_key}run_id=${{ github.run_id }};run_attempt=${{ github.run_attempt }}"
echo "restore-key=${restore_key}" >> $GITHUB_OUTPUT
echo "key=${key}" >> $GITHUB_OUTPUT
shell: bash shell: bash
env:
MATRIX_JSON: ${{ toJSON(matrix) }}
- uses: actions/cache@4d4ae6ae148a43d0fd1eda1800170683e9882738 - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: cache id: cache
with: with:
path: | path: |
${{ steps.paths.outputs.artifacts-path }} ${{ steps.paths.outputs.cache-paths }}
${{ steps.paths.outputs.packages-path }}
${{ steps.paths.outputs.registries-path }}
${{ steps.paths.outputs.scratchspaces-path }}
${{ steps.paths.outputs.compiled-path }}
${{ steps.paths.outputs.logs-path }}
key: ${{ steps.keys.outputs.key }} key: ${{ steps.keys.outputs.key }}
restore-keys: ${{ steps.keys.outputs.restore-key }} restore-keys: ${{ steps.keys.outputs.restore-key }}
enableCrossOsArchive: false enableCrossOsArchive: false
save-always: ${{ inputs.save-always }}
- name: list restored depot directory sizes # if it wasn't restored make the depot anyway as a signal that this action ran
run: du -shc ~/.julia/* || true # for other julia actions to check, like https://github.com/julia-actions/julia-buildpkg/pull/41
- name: make depot if not restored, then list depot directory sizes
run: |
mkdir -p ${{ steps.paths.outputs.depot }}
du -shc ${{ steps.paths.outputs.depot }}/* || true
shell: bash shell: bash
# github and actions/cache doesn't provide a way to update a cache at a given key, so we delete any # github and actions/cache doesn't provide a way to update a cache at a given key, so we delete any
@@ -99,21 +141,21 @@ runs:
# Not windows # Not windows
- uses: pyTooling/Actions/with-post-step@adef08d3bdef092282614f3b683897cefae82ee3 - uses: pyTooling/Actions/with-post-step@adef08d3bdef092282614f3b683897cefae82ee3
if: ${{ inputs.delete-old-caches == 'true' && runner.OS != 'Windows' }} if: ${{ inputs.delete-old-caches != 'false' && runner.OS != 'Windows' }}
with: with:
# seems like there has to be a `main` step in this action. Could list caches for info if we wanted # seems like there has to be a `main` step in this action. Could list caches for info if we wanted
# main: julia ${{ github.action_path }}/handle_caches.jl "${{ github.repository }}" "list" # main: julia ${{ github.action_path }}/handle_caches.jl "${{ github.repository }}" "list"
main: echo "" main: echo ""
post: julia $GITHUB_ACTION_PATH/handle_caches.jl "${{ github.repository }}" "rm" "${{ steps.keys.outputs.restore-key }}" post: julia $GITHUB_ACTION_PATH/handle_caches.jl rm "${{ github.repository }}" "${{ steps.keys.outputs.restore-key }}" "${{ github.ref }}" "${{ inputs.delete-old-caches != 'required' }}"
env: env:
GH_TOKEN: ${{ inputs.token }} GH_TOKEN: ${{ inputs.token }}
# Windows (because this action uses command prompt on windows) # Windows (because this action uses command prompt on windows)
- uses: pyTooling/Actions/with-post-step@adef08d3bdef092282614f3b683897cefae82ee3 - uses: pyTooling/Actions/with-post-step@adef08d3bdef092282614f3b683897cefae82ee3
if: ${{ inputs.delete-old-caches == 'true' && runner.OS == 'Windows' }} if: ${{ inputs.delete-old-caches != 'false' && runner.OS == 'Windows' }}
with: with:
main: echo "" main: echo ""
post: cd %GITHUB_ACTION_PATH% && julia handle_caches.jl "${{ github.repository }}" "rm" "${{ steps.keys.outputs.restore-key }}" post: cd %GITHUB_ACTION_PATH% && julia handle_caches.jl rm "${{ github.repository }}" "${{ steps.keys.outputs.restore-key }}" "${{ github.ref }}" "${{ inputs.delete-old-caches != 'required' }}"
env: env:
GH_TOKEN: ${{ inputs.token }} GH_TOKEN: ${{ inputs.token }}

View File

@@ -1,58 +1,67 @@
using Pkg, Dates using Pkg, Dates
function handle_caches() function handle_caches()
repo = ARGS[1] subcommand = ARGS[1]
func = ARGS[2]
restore_key = get(ARGS, 3, "")
if func == "list" if subcommand == "list"
repo = ARGS[2]
println("Listing existing caches") println("Listing existing caches")
run(`gh cache list --limit 100 --repo $repo`) run(`gh cache list --limit 100 --repo $repo`)
elseif func == "rm" elseif subcommand == "rm"
caches = String[] repo, restore_key, ref = ARGS[2:4]
failed = String[] allow_failure = ARGS[5] == "true"
for _ in 1:5 # limit to avoid accidental rate limiting
hits = split(strip(read(`gh cache list --limit 100 --repo $repo`, String)), keepempty=false) endpoint = "/repos/$repo/actions/caches"
search_again = length(hits) == 100 page = 1
filter!(contains(restore_key), hits) per_page = 100
isempty(hits) && break escaped_restore_key = replace(restore_key, "\"" => "\\\"")
# We can delete everything that matches the restore key because the new cache is saved later. query = ".actions_caches[] | select(.key | startswith(\"$escaped_restore_key\")) | .id"
for c in hits
deletions = String[]
failures = String[]
while 1 <= page <= 5 # limit to avoid accidental rate limiting
cmd = `gh api -X GET $endpoint -F ref=$ref -F per_page=$per_page -F page=$page --jq $query`
ids = split(read(cmd, String); keepempty=false)
page = length(ids) == per_page ? page + 1 : -1
# We can delete all cache entries on this branch that matches the restore key
# because the new cache is saved later.
for id in ids
try try
run(`gh cache delete $(split(c)[1]) --repo $repo`) run(`gh cache delete $id --repo $repo`)
push!(caches, c) push!(deletions, id)
catch e catch e
@error e @error e
push!(failed, c) push!(failures, id)
end end
end end
search_again || break
end end
if isempty(failed) && isempty(caches) if isempty(failures) && isempty(deletions)
println("No existing caches found for restore key `$restore_key`") println("No existing caches found on ref `$ref` matching restore key `$restore_key`")
else else
if !isempty(failed) if !isempty(failures)
println("Failed to delete $(length(failed)) existing caches for restore key `$restore_key`") println("Failed to delete $(length(failures)) existing caches on ref `$ref` matching restore key `$restore_key`")
println.(failed) println.(failures)
@info """ @info """
To delete caches you need to grant the following to the default `GITHUB_TOKEN` by adding To delete caches you need to grant the following to the default `GITHUB_TOKEN` by adding
this to your yml: this to your workflow:
``` ```
permissions: permissions:
actions: write actions: write
contents: read contents: read
``` ```
(Note this won't work for fork PRs but should once merged) (Note this won't work for fork PRs but should once merged)
Or provide a token with `repo` scope via the `token` input option. Or provide a token with `repo` scope via the `token` input option.
See https://cli.github.com/manual/gh_cache_delete See https://cli.github.com/manual/gh_cache_delete
""" """
allow_failure || exit(1)
end end
if !isempty(caches) if !isempty(deletions)
println("$(length(caches)) existing caches deleted that match restore key `$restore_key`:") println("Deleted $(length(deletions)) caches on ref `$ref` matching restore key `$restore_key`")
println.(caches) println.(deletions)
end end
end end
else else
throw(ArgumentError("Unexpected second argument: $func")) throw(ArgumentError("Unexpected subcommand: $subcommand"))
end end
end end