├── .github ├── dependabot.yml └── workflows │ └── CI.yml ├── LICENSE ├── README.md ├── action.yml ├── devdocs └── making_a_new_release.md └── handle_caches.jl /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 99 8 | labels: 9 | - "dependencies" 10 | - "github-actions" 11 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'action.yml' 9 | - 'handle_caches.jl' 10 | - '.github/**' 11 | pull_request: 12 | paths: 13 | - 'action.yml' 14 | - 'handle_caches.jl' 15 | - '.github/**' 16 | 17 | # needed to allow julia-actions/cache to delete old caches that it has created 18 | permissions: 19 | actions: write 20 | contents: read 21 | 22 | jobs: 23 | generate-prefix: 24 | runs-on: ubuntu-latest 25 | outputs: 26 | cache-prefix: ${{ steps.name.outputs.cache-prefix }} 27 | steps: 28 | - name: Generate random cache-prefix 29 | id: name 30 | run: | 31 | cache_prefix=$(head -n 100 >"$GITHUB_OUTPUT" 33 | 34 | test-save: 35 | needs: generate-prefix 36 | runs-on: ${{ matrix.os }} 37 | outputs: 38 | cache-name: ${{ steps.cache-name.outputs.cache-name }} 39 | strategy: 40 | matrix: 41 | nested: 42 | - name: matrix 43 | invalid-chars: "," # Use invalid characters in job matrix to ensure we escape them 44 | version: 45 | - "1.0" 46 | - "1" 47 | - "nightly" 48 | os: 49 | - ubuntu-latest 50 | - windows-latest 51 | - macOS-latest 52 | exclude: 53 | # Test Julia "1.0" on Linux only 54 | - version: "1.0" 55 | os: windows-latest 56 | - version: "1.0" 57 | os: macOS-latest 58 | fail-fast: false 59 | env: 60 | JULIA_DEPOT_PATH: /tmp/julia-depot 61 | steps: 62 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 63 | - name: Set cache-name 64 | id: cache-name 65 | shell: bash 66 | run: | 67 | echo "cache-name=${{ needs.generate-prefix.outputs.cache-prefix }}-${{ github.job }}" >>"$GITHUB_OUTPUT" 68 | - uses: julia-actions/setup-julia@v2 69 | with: 70 | version: ${{ matrix.version }} 71 | - name: Save cache 72 | id: cache 73 | uses: ./ 74 | with: 75 | cache-name: ${{ steps.cache-name.outputs.cache-name }} 76 | delete-old-caches: required 77 | - name: Check no artifacts dir 78 | shell: julia --color=yes {0} 79 | run: | 80 | dir = joinpath(first(DEPOT_PATH), "artifacts") 81 | @assert !isdir(dir) 82 | - name: Install a small binary 83 | shell: julia --color=yes {0} 84 | run: | 85 | using Pkg 86 | if VERSION >= v"1.3" 87 | Pkg.add(PackageSpec(name="pandoc_jll", version="3")) 88 | else 89 | Pkg.add(PackageSpec(name="Scratch", version="1")) 90 | using Scratch 91 | get_scratch!("test") 92 | end 93 | 94 | test-restore: 95 | needs: test-save 96 | runs-on: ${{ matrix.os }} 97 | strategy: 98 | matrix: 99 | nested: 100 | - name: matrix 101 | invalid-chars: "," # Use invalid characters in job matrix to ensure we escape them 102 | version: 103 | - "1.0" 104 | - "1" 105 | - "nightly" 106 | os: 107 | - ubuntu-latest 108 | - windows-latest 109 | - macOS-latest 110 | exclude: 111 | # Test Julia "1.0" on Linux only 112 | - version: "1.0" 113 | os: windows-latest 114 | - version: "1.0" 115 | os: macOS-latest 116 | fail-fast: false 117 | env: 118 | JULIA_DEPOT_PATH: /tmp/julia-depot 119 | steps: 120 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 121 | - uses: julia-actions/setup-julia@v2 122 | with: 123 | version: ${{ matrix.version }} 124 | - name: Restore cache 125 | id: cache 126 | uses: ./ 127 | with: 128 | cache-name: ${{ needs.test-save.outputs.cache-name }} 129 | # Cannot require a successful cache delete on forked PRs as the permissions for actions is limited to read 130 | delete-old-caches: ${{ github.event.pull_request.head.repo.fork && 'false' || 'required' }} 131 | - name: Test cache-hit output 132 | shell: julia --color=yes {0} 133 | run: | 134 | @show ENV["cache-hit"] 135 | @assert ENV["cache-hit"] == "true" 136 | env: 137 | cache-hit: ${{ steps.cache.outputs.cache-hit }} 138 | - name: Check existance or emptiness of affected dirs 139 | shell: julia --color=yes {0} 140 | run: | 141 | # These dirs should exist as they've been cached 142 | artifacts_dir = joinpath(first(DEPOT_PATH), "artifacts") 143 | if VERSION >= v"1.3" 144 | @assert !isempty(readdir(artifacts_dir)) 145 | else 146 | @assert !isdir(artifacts_dir) 147 | end 148 | packages_dir = joinpath(first(DEPOT_PATH), "packages") 149 | @assert !isempty(readdir(packages_dir)) 150 | compiled_dir = joinpath(first(DEPOT_PATH), "compiled") 151 | @assert !isempty(readdir(compiled_dir)) 152 | scratchspaces_dir = joinpath(first(DEPOT_PATH), "scratchspaces") 153 | @assert !isempty(readdir(scratchspaces_dir)) 154 | logs_dir = joinpath(first(DEPOT_PATH), "logs") 155 | @assert !isempty(readdir(logs_dir)) 156 | 157 | # Do tests with no matrix also given the matrix is auto-included in cache key 158 | test-save-nomatrix: 159 | needs: generate-prefix 160 | runs-on: ubuntu-latest 161 | outputs: 162 | cache-name: ${{ steps.cache-name.outputs.cache-name }} 163 | steps: 164 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 165 | - name: Set cache-name 166 | id: cache-name 167 | run: | 168 | echo "cache-name=${{ needs.generate-prefix.outputs.cache-prefix }}-${{ github.job }}" >>"$GITHUB_OUTPUT" 169 | - name: Save cache 170 | id: cache 171 | uses: ./ 172 | with: 173 | cache-name: ${{ steps.cache-name.outputs.cache-name }} 174 | delete-old-caches: required 175 | - name: Check no artifacts dir 176 | shell: julia --color=yes {0} 177 | run: | 178 | dir = joinpath(first(DEPOT_PATH), "artifacts") 179 | @assert !isdir(dir) 180 | - name: Install a small binary 181 | shell: julia --color=yes {0} 182 | run: | 183 | using Pkg 184 | if VERSION >= v"1.3" 185 | Pkg.add(PackageSpec(name="pandoc_jll", version="3")) 186 | else 187 | Pkg.add(PackageSpec(name="Scratch", version="1")) 188 | using Scratch 189 | get_scratch!("test") 190 | end 191 | 192 | test-restore-nomatrix: 193 | needs: test-save-nomatrix 194 | runs-on: ubuntu-latest 195 | steps: 196 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 197 | - name: Restore cache 198 | id: cache 199 | uses: ./ 200 | with: 201 | cache-name: ${{ needs.test-save-nomatrix.outputs.cache-name }} 202 | # Cannot require a successful cache delete on forked PRs as the permissions for actions is limited to read 203 | delete-old-caches: ${{ github.event.pull_request.head.repo.fork && 'false' || 'required' }} 204 | - name: Test cache-hit output 205 | shell: julia --color=yes {0} 206 | run: | 207 | @show ENV["cache-hit"] 208 | @assert ENV["cache-hit"] == "true" 209 | env: 210 | cache-hit: ${{ steps.cache.outputs.cache-hit }} 211 | - name: Check existance or emptiness of affected dirs 212 | shell: julia --color=yes {0} 213 | run: | 214 | # These dirs should exist as they've been cached 215 | artifacts_dir = joinpath(first(DEPOT_PATH), "artifacts") 216 | if VERSION >= v"1.3" 217 | @assert !isempty(readdir(artifacts_dir)) 218 | else 219 | @assert !isdir(artifacts_dir) 220 | end 221 | packages_dir = joinpath(first(DEPOT_PATH), "packages") 222 | @assert !isempty(readdir(packages_dir)) 223 | compiled_dir = joinpath(first(DEPOT_PATH), "compiled") 224 | @assert !isempty(readdir(compiled_dir)) 225 | scratchspaces_dir = joinpath(first(DEPOT_PATH), "scratchspaces") 226 | @assert !isempty(readdir(scratchspaces_dir)) 227 | logs_dir = joinpath(first(DEPOT_PATH), "logs") 228 | @assert !isempty(readdir(logs_dir)) 229 | 230 | test-save-cloned-registry: 231 | needs: generate-prefix 232 | runs-on: ubuntu-latest 233 | outputs: 234 | cache-name: ${{ steps.cache-name.outputs.cache-name }} 235 | steps: 236 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 237 | - name: Set cache-name 238 | id: cache-name 239 | run: | 240 | echo "cache-name=${{ needs.generate-prefix.outputs.cache-prefix }}-${{ github.job }}" >>"$GITHUB_OUTPUT" 241 | - name: Save cache 242 | uses: ./ 243 | with: 244 | cache-name: ${{ steps.cache-name.outputs.cache-name }} 245 | # Cannot require a successful cache delete on forked PRs as the permissions for actions is limited to read 246 | delete-old-caches: ${{ github.event.pull_request.head.repo.fork && 'false' || 'required' }} 247 | - name: Add General registry clone 248 | shell: julia --color=yes {0} 249 | run: | 250 | using Pkg 251 | Pkg.Registry.add("General") 252 | env: 253 | JULIA_PKG_SERVER: "" 254 | # Set the registry worktree to an older state to simulate the cache storing an old version of the registry. 255 | - name: Use outdated General worktree 256 | run: git -C ~/.julia/registries/General reset --hard HEAD~20 257 | 258 | test-restore-cloned-registry: 259 | needs: test-save-cloned-registry 260 | runs-on: ubuntu-latest 261 | steps: 262 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 263 | - name: Add General registry clone 264 | shell: julia --color=yes {0} 265 | run: | 266 | using Pkg 267 | Pkg.Registry.add("General") 268 | env: 269 | JULIA_PKG_SERVER: "" 270 | - name: Restore cache 271 | uses: ./ 272 | with: 273 | cache-name: ${{ needs.test-save-cloned-registry.outputs.cache-name }} 274 | # Cannot require a successful cache delete on forked PRs as the permissions for actions is limited to read 275 | delete-old-caches: ${{ github.event.pull_request.head.repo.fork && 'false' || 'required' }} 276 | - name: Test registry is not corrupt 277 | shell: julia --color=yes {0} 278 | run: | 279 | using Pkg 280 | Pkg.Registry.update() 281 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Julia Actions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # julia-actions/cache Action 2 | 3 | A shortcut action to cache Julia depot contents to reduce GitHub Actions running time. 4 | 5 | ## Usage 6 | 7 | An example workflow that uses this action might look like this: 8 | 9 | ```yaml 10 | name: CI 11 | 12 | on: [push, pull_request] 13 | 14 | # needed to allow julia-actions/cache to delete old caches that it has created 15 | permissions: 16 | actions: write 17 | contents: read 18 | 19 | jobs: 20 | test: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: julia-actions/setup-julia@v2 25 | - uses: julia-actions/cache@v2 26 | - uses: julia-actions/julia-buildpkg@v1 27 | - uses: julia-actions/julia-runtest@v1 28 | ``` 29 | 30 | By default all depot directories called out below are cached. 31 | 32 | ### Requirements 33 | 34 | - `jq`: This action uses [`jq`](https://github.com/jqlang/jq) to parse JSON. For GitHub-hosted runners, `jq` is installed by default. On self-hosted runners and custom containers, if `jq` is not already available, this action will automatically use [`dcarbone/install-jq-action`](https://github.com/dcarbone/install-jq-action) to install `jq` (Note: `dcarbone/install-jq-action` requires that `curl` is installed; this may not always be the case in custom containers and self-hosted runners). 35 | - `bash`: This action requires `bash`. For GitHub-hosted runners `bash` is installed by default. Self-hosted runners will need to ensure that `bash` is installed and available on the `PATH`. 36 | 37 | ### Optional Inputs 38 | 39 | - `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. 40 | - `include-matrix` - Whether to include the matrix values when constructing the cache key. Defaults to `true`. 41 | - `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`. 42 | - `cache-artifacts` - Whether to cache the depot's `artifacts` directory. Defaults to `true`. 43 | - `cache-packages` - Whether to cache the depot's `packages` directory. Defaults to `true`. 44 | - `cache-registries` - Whether to cache the depot's `registries` directory. Defaults to `true`. 45 | - `cache-compiled` - Whether to cache the depot's `compiled` directory. Defaults to `true`. 46 | - `cache-scratchspaces` - Whether to cache the depot's `scratchspaces` directory. Defaults to `true`. 47 | - `cache-logs` - Whether to cache the depot's `logs` directory. Defaults to `true`. Helps auto-`Pkg.gc()` keep the cache small. 48 | - `delete-old-caches` - Whether to delete old caches for the given key. Defaults to `true`. 49 | - `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. 50 | 51 | ### Outputs 52 | 53 | - `cache-hit` - A boolean value to indicate an exact match was found for the primary key. Returns \"\" when the key is new. Forwarded from actions/cache. 54 | - `cache-paths` - A list of paths (as a newline-separated string) that were cached. 55 | - `cache-key` - The cache key that was used for this run. 56 | 57 | ## How It Works 58 | 59 | This action is a wrapper around . 60 | In summary, this action stores the files in the aforementioned paths in one compressed file when running for the first time. 61 | This cached file is then restored upon the second run, and afterwards resaved under a new key, and the previous cache deleted. 62 | The benefit of caching is that downloading one big file is quicker than downloading many different files from many different locations 63 | and precompiling them. 64 | 65 | By default, this action removes caches that were previously made by jobs on the same branch with the same restore key. 66 | GitHub automatically removes old caches after a certain period or when the repository cache allocation is full. 67 | It is, however, more efficient to explicitly remove old caches to improve caching for less frequently run jobs. 68 | 69 | For more information about GitHub caching generically, for example how to manually delete caches, see 70 | [this GitHub documentation page](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows#managing-caches). 71 | 72 | ### Cache keys 73 | 74 | The cache key that the cache will be saved as is based on: 75 | - The `cache-name` input 76 | - All variables in the `matrix` (unless disabled via `include-matrix: 'false'`) 77 | - The `runner.os` (may be in the matrix too, but included for safety) 78 | - The run id 79 | - The run attempt number 80 | 81 | > [!NOTE] 82 | > If there is job concurrency that is not fully defined by a matrix you should ensure that `cache-name` is 83 | > unique for each concurrent job, otherwise caching may not be effective. 84 | 85 | ### Cache Retention 86 | 87 | This action automatically deletes old caches that match the first 4 fields of the above key: 88 | - The `cache-name` input 89 | - All variables in the `matrix` (unless disabled via `include-matrix: 'false'`) 90 | - The `runner.os` (may be in the matrix too, but included for safety) 91 | 92 | Which means your caches files will not grow needlessly. GitHub also deletes cache files after 93 | [7 days of not being accessed](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy), and there is a limit of 10 GB for the total size of cache files associated to each repository. 94 | 95 | > [!NOTE] 96 | > 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) 97 | > to the `GITHUB_TOKEN` by adding this to your GitHub actions workflow: 98 | > ```yaml 99 | > permissions: 100 | > actions: write 101 | > contents: read 102 | > ``` 103 | > (Note this won't work for fork PRs but should once merged) 104 | > Or provide a token with `repo` scope via the `token` input option. 105 | > See https://cli.github.com/manual/gh_cache_delete 106 | 107 | To disable deletion set input `delete-old-caches: 'false'`. 108 | 109 | ### Caching even if an intermediate job fails 110 | 111 | Just like [the basic actions/cache workflow](https://github.com/actions/cache), this action has a cache restore step, and also a save step which runs after the workflow completes. 112 | By default, if any job in the workflow fails, the entire workflow will be stopped, and the cache will not be saved. 113 | 114 | Due to current limitations in GitHub Actions syntax, there is no built-in option for this action to save the cache even if the job fails. 115 | However, it does output information which you can feed into `actions/cache` yourself to achieve the same effect. 116 | For example, this workflow will ensure that the cache is saved if a step fails (but skipping it if the cache was hit, i.e. there's no need to cache it again). 117 | 118 | ```yaml 119 | steps: 120 | - uses: actions/checkout@v4 121 | 122 | - name: Load Julia packages from cache 123 | id: julia-cache 124 | uses: julia-actions/cache@v2 125 | 126 | # do whatever you want here (that might fail) 127 | 128 | - name: Save Julia depot cache on cancel or failure 129 | id: julia-cache-save 130 | if: cancelled() || failure() 131 | uses: actions/cache/save@v4 132 | with: 133 | path: | 134 | ${{ steps.julia-cache.outputs.cache-paths }} 135 | key: ${{ steps.julia-cache.outputs.cache-key }} 136 | ``` 137 | 138 | ### Cache Garbage Collection 139 | 140 | Caches are restored and re-saved after every run, retaining the state of the depot throughout runs. 141 | Their size will be regulated like a local depot automatically by the automatic `Pkg.gc()` functionality that 142 | clears out old content, which is made possible because the `/log` contents are cached. 143 | 144 | ## Third Party Notice 145 | 146 | This action is built around [`actions/cache`](https://github.com/actions/cache/) and includes parts of that action. `actions/cache` has been released under the following licence: 147 | 148 | > The MIT License (MIT) 149 | > 150 | > Copyright (c) 2018 GitHub, Inc. and contributors 151 | > 152 | > Permission is hereby granted, free of charge, to any person obtaining a copy 153 | > of this software and associated documentation files (the "Software"), to deal 154 | > in the Software without restriction, including without limitation the rights 155 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 156 | > copies of the Software, and to permit persons to whom the Software is 157 | > furnished to do so, subject to the following conditions: 158 | > 159 | > The above copyright notice and this permission notice shall be included in 160 | > all copies or substantial portions of the Software. 161 | > 162 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 163 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 164 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 165 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 166 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 167 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 168 | > THE SOFTWARE. 169 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Cache Julia artifacts, packages and registry' 2 | description: 'Cache Julia using actions/cache' 3 | author: 'Sascha Mann, Rik Huijzer, and contributors' 4 | 5 | branding: 6 | icon: 'archive' 7 | color: 'purple' 8 | 9 | inputs: 10 | cache-name: 11 | description: >- 12 | The cache key prefix. The key body automatically includes the OS and, unless disabled, the matrix vars. 13 | Include any other parameters/details in this prefix to ensure one unique cache key per concurrent job type. 14 | default: julia-cache;workflow=${{ github.workflow }};job=${{ github.job }} 15 | include-matrix: 16 | description: Whether to include the matrix values when constructing the cache key. 17 | default: 'true' 18 | depot: 19 | description: Path to a Julia depot directory where cached data will be saved to and restored from. 20 | default: '' 21 | cache-artifacts: 22 | description: Whether to cache the depot's `artifacts` directory. 23 | default: 'true' 24 | cache-packages: 25 | description: Whether to cache the depot's `packages` directory. 26 | default: 'true' 27 | cache-registries: 28 | description: Whether to cache the depot's `registries` directory. 29 | default: 'true' 30 | cache-compiled: 31 | description: Whether to cache the depot's `compiled` directory. 32 | default: 'true' 33 | cache-scratchspaces: 34 | description: Whether to cache the depot's `scratchspaces` directory. 35 | default: 'true' 36 | cache-logs: 37 | description: Whether to cache the depot's `logs` directory. This helps automatic `Pkg.gc()` keep the cache size down. 38 | default: 'true' 39 | delete-old-caches: 40 | description: Whether to delete old caches for the given key. 41 | default: 'true' 42 | token: 43 | description: A GitHub PAT. Requires `repo` scope to enable the deletion of old caches. 44 | default: ${{ github.token }} 45 | 46 | outputs: 47 | cache-hit: 48 | 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. 49 | value: ${{ steps.hit.outputs.cache-hit }} 50 | cache-paths: 51 | description: The paths that were cached 52 | value: ${{ steps.paths.outputs.cache-paths }} 53 | cache-key: 54 | description: The full cache key used 55 | value: ${{ steps.keys.outputs.key }} 56 | 57 | runs: 58 | using: 'composite' 59 | steps: 60 | - name: Install jq 61 | uses: dcarbone/install-jq-action@f0e10f46ff84f4d32178b4b76e1ef180b16f82c3 # v3.1.1 62 | with: 63 | force: false # Skip install when an existing `jq` is present 64 | 65 | - id: paths 66 | run: | 67 | if [ -n "${{ inputs.depot }}" ]; then 68 | depot="${{ inputs.depot }}" 69 | elif [ -n "$JULIA_DEPOT_PATH" ]; then 70 | # Use the first depot path 71 | depot=$(echo $JULIA_DEPOT_PATH | cut -d$PATH_DELIMITER -f1) 72 | else 73 | depot="~/.julia" 74 | fi 75 | if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then 76 | depot="${depot/#\~/$USERPROFILE}" # Windows paths 77 | depot="${depot//\\//}" # Replace backslashes with forward slashes 78 | else 79 | depot="${depot/#\~/$HOME}" # Unix-like paths 80 | fi 81 | echo "depot=$depot" | tee -a "$GITHUB_OUTPUT" 82 | 83 | cache_paths=() 84 | artifacts_path="${depot}/artifacts" 85 | [ "${{ inputs.cache-artifacts }}" = "true" ] && cache_paths+=("$artifacts_path") 86 | packages_path="${depot}/packages" 87 | [ "${{ inputs.cache-packages }}" = "true" ] && cache_paths+=("$packages_path") 88 | registries_path="${depot}/registries" 89 | if [ "${{ inputs.cache-registries }}" = "true" ]; then 90 | if [ ! -d "${registries_path}" ]; then 91 | cache_paths+=("$registries_path") 92 | else 93 | 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." 94 | fi 95 | fi 96 | compiled_path="${depot}/compiled" 97 | [ "${{ inputs.cache-compiled }}" = "true" ] && cache_paths+=("$compiled_path") 98 | scratchspaces_path="${depot}/scratchspaces" 99 | [ "${{ inputs.cache-scratchspaces }}" = "true" ] && cache_paths+=("$scratchspaces_path") 100 | logs_path="${depot}/logs" 101 | [ "${{ inputs.cache-logs }}" = "true" ] && cache_paths+=("$logs_path") 102 | { 103 | echo "cache-paths<> $GITHUB_OUTPUT 125 | echo "key=${key}" >> $GITHUB_OUTPUT 126 | shell: bash 127 | env: 128 | MATRIX_JSON: ${{ toJSON(matrix) }} 129 | 130 | - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 131 | id: cache 132 | with: 133 | path: | 134 | ${{ steps.paths.outputs.cache-paths }} 135 | key: ${{ steps.keys.outputs.key }} 136 | restore-keys: ${{ steps.keys.outputs.restore-key }} 137 | enableCrossOsArchive: false 138 | 139 | # if it wasn't restored make the depot anyway as a signal that this action ran 140 | # for other julia actions to check, like https://github.com/julia-actions/julia-buildpkg/pull/41 141 | - name: make depot if not restored, then list depot directory sizes 142 | run: | 143 | mkdir -p ${{ steps.paths.outputs.depot }} 144 | du -shc ${{ steps.paths.outputs.depot }}/* || true 145 | shell: bash 146 | 147 | # issue https://github.com/julia-actions/cache/issues/110 148 | # Pkg may not run `Registry.update()` if a manifest exists, which may exist because of a 149 | # `Pkg.dev` call or because one is added to the repo. So be safe and update cached registries here. 150 | # Older (~v1.0) versions of julia that don't have `Pkg.Registry.update()` seem to always update registries in 151 | # Pkg operations. So this is only necessary for newer julia versions. 152 | - name: Update any cached registries 153 | if: ${{ inputs.cache-registries == 'true' }} 154 | continue-on-error: true 155 | run: | 156 | if [ -d "${{ steps.paths.outputs.depot }}/registries" ] && [ -n "$(ls -A "${{ steps.paths.outputs.depot }}/registries")" ]; then 157 | echo "Registries directory exists and is non-empty. Updating any registries" 158 | julia -e "import Pkg; isdefined(Pkg, :Registry) && Pkg.Registry.update();" 159 | else 160 | echo "Registries directory does not exist or is empty. Skipping registry update" 161 | fi 162 | shell: bash 163 | 164 | # GitHub actions cache entries are immutable and cannot be updated. In order to have both the Julia 165 | # depot cache be up-to-date and avoid storing redundant cache entries we'll manually cleanup old 166 | # cache entries before the new cache is saved. However, we need to be careful with our manual 167 | # cleanup as otherwise we can cause cache misses for jobs which would have normally had a cache hit. 168 | # Some scenarios to keep in mind include: 169 | # 170 | # - Job failures result in the post-action for `actions/cache` being skipped. If we delete all cache 171 | # entries for the branch we may have no cache entry available for the next run. 172 | # - We should avoid deleting old cache entries for the default branch since these entries serve as 173 | # the fallback if no earlier cache entry exists on a branch. We can rely on GitHub's default cache 174 | # eviction policy here which will remove the oldest cache entry first. 175 | # 176 | # References: 177 | # - https://github.com/actions/cache/blob/main/tips-and-workarounds.md#update-a-cache 178 | # - https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy 179 | 180 | # Not windows 181 | - uses: pyTooling/Actions/with-post-step@33edd82e6f283fa4bb95cf46eeea4ee24da28f04 # v4.3.0 182 | if: ${{ inputs.delete-old-caches != 'false' && 183 | github.ref != format('refs/heads/{0}', github.event.repository.default_branch) && 184 | runner.OS != 'Windows' }} 185 | with: 186 | # seems like there has to be a `main` step in this action. Could list caches for info if we wanted 187 | # main: julia ${{ github.action_path }}/handle_caches.jl "${{ github.repository }}" "list" 188 | main: echo "" 189 | post: julia $GITHUB_ACTION_PATH/handle_caches.jl rm "${{ github.repository }}" "${{ steps.keys.outputs.restore-key }}" "${{ github.ref }}" "${{ inputs.delete-old-caches != 'required' }}" 190 | env: 191 | GH_TOKEN: ${{ inputs.token }} 192 | 193 | # Windows (because this action uses command prompt on windows) 194 | - uses: pyTooling/Actions/with-post-step@33edd82e6f283fa4bb95cf46eeea4ee24da28f04 # v4.3.0 195 | if: ${{ inputs.delete-old-caches != 'false' && 196 | github.ref != format('refs/heads/{0}', github.event.repository.default_branch) && 197 | runner.OS == 'Windows' }} 198 | with: 199 | main: echo "" 200 | post: cd %GITHUB_ACTION_PATH% && julia handle_caches.jl rm "${{ github.repository }}" "${{ steps.keys.outputs.restore-key }}" "${{ github.ref }}" "${{ inputs.delete-old-caches != 'required' }}" 201 | env: 202 | GH_TOKEN: ${{ inputs.token }} 203 | 204 | - id: hit 205 | run: echo "cache-hit=$CACHE_HIT" >> $GITHUB_OUTPUT 206 | env: 207 | CACHE_HIT: ${{ steps.cache.outputs.cache-hit }} 208 | shell: bash 209 | -------------------------------------------------------------------------------- /devdocs/making_a_new_release.md: -------------------------------------------------------------------------------- 1 | # Making a new release 2 | 3 | In this guide, as an example, `v2.2.0` refers to the version number of the new release that you want to make. 4 | 5 | ## Part 1: Use the Git CLI to create and push the Git tags 6 | 7 | Step 1: Create a new lightweight tag of the form `vMAJOR.MINOR.PATCH`. 8 | 9 | ```bash 10 | git clone git@github.com:julia-actions/cache.git 11 | cd cache 12 | git fetch --all --tags 13 | 14 | git checkout main 15 | 16 | git --no-pager log -1 17 | # Take note of the commit hash here. 18 | 19 | # Now, create a new lightweight tag of the form `vMAJOR.MINOR.PATCH`. 20 | # 21 | # Replace `commit_hash` with the commit hash that you obtained from the 22 | # `git log -1` step. 23 | # 24 | # Replace `v2.2.0` with the actual version number that you want to use. 25 | git tag v2.2.0 commit_hash 26 | ``` 27 | 28 | Step 2: Once you've created the new release, you need to update the `v2` tag to point to the new release. For example, suppose that the previous release was `v2.1.0`, and suppose that you just created the new release `v2.2.0`. You need to update the `v2` tag so that it points to `v2.2.0`. Here are the commands: 29 | 30 | ```bash 31 | # Create/update the new v2 tag locally, where the new v2 tag will point to the 32 | # release that you created in the previous step. 33 | # 34 | # Make sure to change `v2.2.0` to the actual value for the tag that you just 35 | # created in the previous step. 36 | # 37 | # The `-f` flag forcibly overwrites the old 38 | # `v2` tag (if it exists). 39 | git tag -f v2 v2.2.0 40 | ``` 41 | 42 | Step 3: Now you need to push the tags: 43 | 44 | ```bash 45 | # Regular-push the new `v2.2.0` tag: 46 | git push origin tag v2.2.0 47 | 48 | # Force-push the new v2 tag: 49 | git push origin tag v2 --force 50 | ``` 51 | 52 | ## Part 2: Create the GitHub Release 53 | 54 | Go to the [Releases](https://github.com/julia-actions/cache/releases) section of this repo and create a new release (using the GitHub web interface). 55 | 56 | For the "choose a tag" drop-down field, select the `v2.2.0` tag that you created and pushed in Part 1 of this guide. 57 | -------------------------------------------------------------------------------- /handle_caches.jl: -------------------------------------------------------------------------------- 1 | using Pkg, Dates 2 | function handle_caches() 3 | subcommand = ARGS[1] 4 | 5 | if subcommand == "list" 6 | repo = ARGS[2] 7 | println("Listing existing caches") 8 | run(`gh cache list --limit 100 --repo $repo`) 9 | elseif subcommand == "rm" 10 | repo, restore_key, ref = ARGS[2:4] 11 | allow_failure = ARGS[5] == "true" 12 | 13 | page = 1 14 | per_page = 100 15 | skipped = String[] 16 | deleted = String[] 17 | failed = String[] 18 | while 1 <= page <= 5 # limit to avoid accidental rate limiting 19 | # https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#list-github-actions-caches-for-a-repository 20 | # Note: The `key` field matches on the full key or a prefix. 21 | cmd = ``` 22 | gh api -X GET /repos/$repo/actions/caches 23 | --field per_page=$per_page 24 | --field page=$page 25 | --field ref=$ref 26 | --field key=$restore_key 27 | --field sort=last_accessed_at 28 | --field direction=desc 29 | --jq '.actions_caches[].id' 30 | ``` 31 | ids = split(read(cmd, String); keepempty=false) 32 | 33 | # Avoid deleting the latest used cache entry. This is particularly important for 34 | # job failures where a new cache entry will not be saved after this. 35 | page == 1 && !isempty(ids) && push!(skipped, popfirst!(ids)) 36 | 37 | for id in ids 38 | try 39 | run(`gh cache delete $id --repo $repo`) 40 | push!(deleted, id) 41 | catch e 42 | @error e 43 | push!(failed, id) 44 | end 45 | end 46 | 47 | page = length(ids) == per_page ? page + 1 : -1 48 | end 49 | if isempty(skipped) && isempty(deleted) && isempty(failed) 50 | println("No existing caches found on ref `$ref` matching restore key `$restore_key`") 51 | else 52 | if !isempty(failed) 53 | println("Failed to delete $(length(failed)) existing caches on ref `$ref` matching restore key `$restore_key`") 54 | println.(failed) 55 | @info """ 56 | To delete caches you need to grant the following to the default `GITHUB_TOKEN` by adding 57 | this to your workflow: 58 | ``` 59 | permissions: 60 | actions: write 61 | contents: read 62 | ``` 63 | (Note this won't work for fork PRs but should once merged) 64 | Or provide a token with `repo` scope via the `token` input option. 65 | See https://cli.github.com/manual/gh_cache_delete 66 | """ 67 | allow_failure || exit(1) 68 | end 69 | if !isempty(deleted) 70 | println("Deleted $(length(deleted)) caches on ref `$ref` matching restore key `$restore_key`") 71 | println.(deleted) 72 | end 73 | end 74 | else 75 | throw(ArgumentError("Unexpected subcommand: $subcommand")) 76 | end 77 | end 78 | 79 | try 80 | # do a gc with the standard 7-day delay 81 | Pkg.gc() 82 | handle_caches() 83 | catch e 84 | @error "An error occurred while managing existing caches" e 85 | end 86 | --------------------------------------------------------------------------------