From e12a720b4ce27afea1de9f604cd19ffb946ca24d Mon Sep 17 00:00:00 2001 From: Chris Hodapp Date: Mon, 7 Sep 2026 18:04:18 -0700 Subject: [PATCH] feat(ci): keep only what cache.nixos.org lacks in the Actions cache The cache entry becomes a Nix binary cache on disk (narinfo files and zstd-compressed NARs) that the checks read as a trusted substituter, instead of an archive of the whole rooted store. After the checks, the run copies the closure of its outputs into it, asks cache.nixos.org which of those paths it serves (a HEAD request per narinfo file), records the rest as this build's root, and prunes the cache to the union of the surviving roots' lists before the save. Sizes for the 10 GB budget come from the narinfo FileSize fields, so they are the compressed sizes GitHub counts rather than the uncompressed closure sizes used before. Keys move from the nix- prefix to cache- so that no entry of the earlier form is restored; the earlier entries expire unused. The push to the clhodapp cache covers this run's outputs; earlier runs pushed theirs. This is the same change the other five pipeline repos carry, with their two follow-up fixes folded in. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/check-pr.yml | 136 ++++++++++++++++------- .github/workflows/check.yml | 190 +++++++++++++++++++++------------ 2 files changed, 221 insertions(+), 105 deletions(-) diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index 9d6a0bc..4afc2c8 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -37,6 +37,14 @@ jobs: check-pr: runs-on: ubuntu-latest timeout-minutes: 30 + env: + # The build cache on disk: a Nix binary cache plus the root tree + # under roots/, carried whole in one Actions cache entry. The + # hosted runners' home directory, spelled out because nix.conf + # cannot expand it. + CI_CACHE: /home/runner/ci-cache + # Caches whose paths are left out of the entry, space-separated. + UPSTREAM_CACHES: https://cache.nixos.org steps: - name: Decide whether this run builds id: guard @@ -57,6 +65,27 @@ jobs: with: persist-credentials: false + # Restores the newest entry this run can see (its own earlier one, + # else main's); the save, under this request's scope, one entry + # per head commit, is the last step. cache-cleanup.yml removes + # them all when the request closes. + - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + if: steps.guard.outputs.build == 'true' + with: + path: ${{ env.CI_CACHE }} + key: cache-${{ runner.os }}-${{ github.event.pull_request.head.sha }} + restore-keys: cache-${{ runner.os }}- + + # A binary cache needs its directories and its info file before + # anything opens it; created here, owned by this user, so that + # neither the daemon (which opens it as a substituter, as root) + # nor `nix copy` (which writes it, as this user) is the first to. + - name: Prepare the build cache + if: steps.guard.outputs.build == 'true' + run: | + mkdir -p "$CI_CACHE/nar" "$CI_CACHE/roots" + [[ -e "$CI_CACHE/nix-cache-info" ]] || echo "StoreDir: /nix/store" > "$CI_CACHE/nix-cache-info" + - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31 if: steps.guard.outputs.build == 'true' with: @@ -64,19 +93,9 @@ jobs: extra_nix_config: | experimental-features = nix-command flakes sandbox = true - - # Restores the newest entry this run can see (its own earlier one, - # else main's); saves under this request's scope, one entry per - # head commit. cache-cleanup.yml removes them all when the request - # closes. The cap of zero makes the pruning before the save remove - # every path the roots do not reach. - - uses: nix-community/cache-nix-action@7df957e333c1e5da7721f60227dbba6d06080569 # v7 - if: steps.guard.outputs.build == 'true' - with: - primary-key: nix-${{ runner.os }}-${{ github.event.pull_request.head.sha }} - restore-prefixes-first-match: nix-${{ runner.os }}- - gc-max-store-size-linux: 0 - purge: false + # The restored cache. Its paths carry no signatures; `trusted` + # accepts them from this store alone. + extra-substituters = file://${{ env.CI_CACHE }}?trusted=true # Pull from the clhodapp cache; a public cache needs no credentials # to read, and this run has none. Nothing is pushed from here. @@ -101,19 +120,21 @@ jobs: ROOT_ID: ${{ format('{0}/{1}', github.event.pull_request.number, github.event.pull_request.head.sha) }} CURRENT_MAIN: ${{ github.event.pull_request.base.sha }} OWN_REF: ${{ github.ref }} - KEY_PREFIX: nix-${{ runner.os }}- + KEY_PREFIX: cache-${{ runner.os }}- MAY_DELETE: "false" run: | set -euo pipefail - # The persistent root tree, under /nix but outside - # /nix/var/nix, which the cache action excludes apart from the - # database; mirrored into Nix's root directory at the end. - roots=/nix/ci-roots + # The persistent root tree, inside the cache directory so the + # entry carries it. A root lists the store paths its build + # contributes to the entry: its closure, less what the + # upstream caches served. + roots="$CI_CACHE/roots" + store="file://$CI_CACHE?compression=zstd¶llel-compression=true" # 1. Root what this run built. own="$roots/$ROOT_CLASS/$ROOT_ID" - sudo rm -rf "$own" - sudo mkdir -p "$own" + rm -rf "$own" + mkdir -p "$own" # Checks and packages both: a check's output need not reference # what it built, and the pruning keeps only what the roots reach. # An output the flake does not provide is skipped; any other @@ -124,12 +145,29 @@ jobs: continue fi for name in $(jq -r '.[]' <<<"$names"); do - out=$(nix build --no-link --print-out-paths ".#$kind.x86_64-linux.$name") - sudo ln -s "$out" "$own/$kind-$name" + nix build --no-link --print-out-paths ".#$kind.x86_64-linux.$name" >> "$own/outputs" done done - # 2. Order every root directory, highest priority first. + # 2. Copy the closure into the cache, then ask each upstream cache + # which of it it serves (a cache serves a path when it has + # the path's narinfo file; a failed request counts as not + # served, which only keeps more); the root lists the rest. + # The whole closure goes in because a binary cache refuses a + # path whose references it lacks; the served paths come out + # again in step 6, and compressing them costs seconds. + xargs nix copy --to "$store" < "$own/outputs" + xargs nix path-info -r < "$own/outputs" | sort -u > "$RUNNER_TEMP/closure" + for cache in $UPSTREAM_CACHES; do + # shellcheck disable=SC2016 + cut -c 12-43 "$RUNNER_TEMP/closure" | xargs -P 32 -I{} sh -c \ + 'if curl -sfI --retry 3 -o /dev/null "$1/$2.narinfo"; then echo "$2"; fi' _ "$cache" {} + done | sort -u > "$RUNNER_TEMP/served" + awk 'FILENAME == ARGV[1] { served[$1] = 1; next } !(substr($0, 12, 32) in served)' \ + "$RUNNER_TEMP/served" "$RUNNER_TEMP/closure" > "$own/paths" + echo "This build: $(wc -l < "$RUNNER_TEMP/closure") paths in the closure, $(wc -l < "$RUNNER_TEMP/served") served upstream, $(wc -l < "$own/paths") kept." + + # 3. Order every root directory, highest priority first. open=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --limit 500 --json number --jq '.[].number') is_open() { grep -qx "$1" <<<"$open"; } # Entry names under a directory, newest first by modification time. @@ -144,7 +182,7 @@ jobs: n=$(basename "$prdir") newest=$(newest_in "$prdir" | head -n1) for sha in "$prdir"/*/; do - [[ "$(basename "$sha")" == "$newest" ]] || sudo rm -rf "$sha" + [[ "$(basename "$sha")" == "$newest" ]] || rm -rf "$sha" done is_open "$n" && ordered+=("$prdir$newest") done @@ -156,7 +194,7 @@ jobs: is_open "$n" || ordered+=("$prdir$(newest_in "$prdir" | head -n1)") done - # 3. The budget, as in check.yml. + # 4. The budget, as in check.yml. gib=$((1024 * 1024 * 1024)) caches=$(gh cache list --repo "$GITHUB_REPOSITORY" --limit 1000 --json key,ref,sizeInBytes) used_by_own_scope=$(jq --arg ref "$OWN_REF" --arg p "$KEY_PREFIX" \ @@ -166,33 +204,53 @@ jobs: budget=$((10 * gib - used_by_own_scope - used_by_others)) echo "Budget for this entry: $budget bytes ($used_by_others used by other scopes, $used_by_own_scope by this one)." - closure_size() { - local targets - targets=$(for d in "$@"; do find "$d" -maxdepth 1 -type l -exec readlink {} +; done) - [[ -n "$targets" ]] || { echo 0; return; } - # shellcheck disable=SC2086 - nix path-info -r $targets | sort -u | xargs nix path-info -s | awk '{s += $2} END {print s + 0}' + # The compressed size of every path in the cache, by store hash, + # from the narinfo files. The entry is these files, so the sum + # is close to what GitHub will count. + find "$CI_CACHE" -maxdepth 1 -name '*.narinfo' \ + -exec awk 'FNR == 1 { h = FILENAME; sub(/.*\//, "", h); sub(/\.narinfo$/, "", h) } /^FileSize: / { print h, $2 }' {} + \ + > "$RUNNER_TEMP/sizes" + listed_paths() { + # The union of the paths listed by the given root directories. + for d in "$@"; do cat "$d/paths"; done | sort -u + } + entry_size() { + # The size of an entry holding what the given roots list. + listed_paths "$@" | awk 'FILENAME == ARGV[1] { size[$1] = $2; next } { total += size[substr($0, 12, 32)] } END { print total + 0 }' "$RUNNER_TEMP/sizes" - } - # 4. Keep the highest-priority roots that fit, drop the rest. + # 5. Keep the highest-priority roots that fit, drop the rest. # This run deletes nothing outside its own scope; if not # even the current main fits, it is saved anyway and # GitHub's own eviction is the backstop. kept=() for d in "${ordered[@]}"; do - size=$(closure_size "${kept[@]}" "$d") + size=$(entry_size "${kept[@]}" "$d") if (( size <= budget )) || (( ${#kept[@]} == 0 )); then (( size <= budget )) || echo "::warning::The current main build ($size bytes) exceeds the cache budget ($budget bytes); saving it anyway." kept+=("$d") continue fi - echo "Dropping roots $d (closure would be $size bytes, budget $budget)." - sudo rm -rf "$d" + echo "Dropping roots $d (the entry would be $size bytes, budget $budget)." + rm -rf "$d" done echo "Kept roots:" printf ' %s\n' "${kept[@]}" - # 5. Mirror the surviving tree into Nix's root directory so the - # pruning before the save honours it. - sudo rm -rf /nix/var/nix/gcroots/ci - sudo cp -a "$roots" /nix/var/nix/gcroots/ci + # 6. Prune the cache to what the surviving roots list: first + # every narinfo for a path none of them lists (this run's + # upstream-served paths among them), then every NAR no + # remaining narinfo names. + listed_paths "${kept[@]}" | cut -c 12-43 | sort > "$RUNNER_TEMP/keep" + find "$CI_CACHE" -maxdepth 1 -name '*.narinfo' -printf '%f\n' | sed 's/\.narinfo$//' | sort \ + | comm -23 - "$RUNNER_TEMP/keep" | sed "s|.*|$CI_CACHE/&.narinfo|" | xargs -r rm -f + find "$CI_CACHE" -maxdepth 1 -name '*.narinfo' -exec sed -n 's|^URL: ||p' {} + | sort -u > "$RUNNER_TEMP/keep-nars" + find "$CI_CACHE/nar" -type f -printf 'nar/%f\n' | sort \ + | comm -23 - "$RUNNER_TEMP/keep-nars" | sed "s|.*|$CI_CACHE/&|" | xargs -r rm -f + echo "The entry holds $(wc -l < "$RUNNER_TEMP/keep") paths, $(du -sb "$CI_CACHE" | cut -f1) bytes on disk." + + - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + if: steps.guard.outputs.build == 'true' + with: + path: ${{ env.CI_CACHE }} + key: cache-${{ runner.os }}-${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6e73aa6..49f2d8b 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -25,18 +25,22 @@ # its own cache scope, which main can never restore. The result is # posted to the request's head commit as the "check" status. # -# One cache entry, holding a union of builds. What stays in it is -# decided by garbage-collection roots kept in a tree the cache carries, -# /nix/ci-roots, and mirrored into Nix's root directory before each -# save: main/ for main's builds, pr// for requests'. -# Each run adds its own, drops the lowest-priority roots until the -# closure fits the repository's 10 GB of cache, and then every -# unreachable path goes. The priority order, highest first: the current -# main; each open request's newest build; older mains; closed requests. -# Requests that changed the pipeline have entries of their own in their -# scope, which this run only ever deletes, closed ones first, when -# space is short. Older entries in this scope are removed by the -# `prune` job, after the new entry is confirmed to exist, never before. +# One cache entry, holding a union of builds as a Nix binary cache on +# disk (narinfo files and compressed NARs), which the checks read as a +# substituter. Paths that cache.nixos.org serves are left out of it: a +# run fetches those from there, and the entry holds only what was built +# here or came from the smaller caches. What stays in it is decided by +# a tree of roots the entry carries: main/ for main's builds, +# pr// for requests', each listing the store paths of that +# build's closure that the upstream cache did not serve. Each run adds +# its own, drops the lowest-priority roots until the union fits the +# repository's 10 GB of cache, and then every path no surviving root +# lists goes. The priority order, highest first: the current main; each +# open request's newest build; older mains; closed requests. Requests +# that changed the pipeline have entries of their own in their scope, +# which this run only ever deletes, closed ones first, when space is +# short. Older entries in this scope are removed by the `prune` job, +# after the new entry is confirmed to exist, never before. name: check @@ -111,6 +115,14 @@ jobs: # them whatever it says, which is what keeps the rule "only main # pushes to the cache" out of the files a request can edit. environment: cachix + env: + # The build cache on disk: a Nix binary cache plus the root tree + # under roots/, carried whole in one Actions cache entry. The + # hosted runners' home directory, spelled out because nix.conf + # cannot expand it. + CI_CACHE: /home/runner/ci-cache + # Caches whose paths are left out of the entry, space-separated. + UPSTREAM_CACHES: https://cache.nixos.org outputs: head: ${{ steps.target.outputs.head }} steps: @@ -147,23 +159,33 @@ jobs: ref: ${{ steps.target.outputs.head }} persist-credentials: false + # Restores the newest entry; the save, under this commit's key, is + # the explicit step after the roots are settled. Purging older + # entries is left to the `prune` job, which checks the save landed. + - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ${{ env.CI_CACHE }} + key: cache-${{ runner.os }}-${{ steps.target.outputs.head }} + restore-keys: cache-${{ runner.os }}- + + # A binary cache needs its directories and its info file before + # anything opens it; created here, owned by this user, so that + # neither the daemon (which opens it as a substituter, as root) + # nor `nix copy` (which writes it, as this user) is the first to. + - name: Prepare the build cache + run: | + mkdir -p "$CI_CACHE/nar" "$CI_CACHE/roots" + [[ -e "$CI_CACHE/nix-cache-info" ]] || echo "StoreDir: /nix/store" > "$CI_CACHE/nix-cache-info" + - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} extra_nix_config: | experimental-features = nix-command flakes sandbox = true - - # Restores the newest entry and saves under this commit's key. The - # cap of zero makes the pruning before the save remove every path - # the roots do not reach, so the entry is exactly their closure. - # Purging is left to the `prune` job, which checks the save landed. - - uses: nix-community/cache-nix-action@7df957e333c1e5da7721f60227dbba6d06080569 # v7 - with: - primary-key: nix-${{ runner.os }}-${{ steps.target.outputs.head }} - restore-prefixes-first-match: nix-${{ runner.os }}- - gc-max-store-size-linux: 0 - purge: false + # The restored cache. Its paths carry no signatures; `trusted` + # accepts them from this store alone. + extra-substituters = file://${{ env.CI_CACHE }}?trusted=true # Configures the clhodapp cache as a substituter and installs the # cachix command. Pushing is left to the explicit step at the end: @@ -190,40 +212,56 @@ jobs: ROOT_ID: ${{ steps.target.outputs.id }} CURRENT_MAIN: ${{ steps.target.outputs.base }} OWN_REF: ${{ github.ref }} - KEY_PREFIX: nix-${{ runner.os }}- + KEY_PREFIX: cache-${{ runner.os }}- MAY_DELETE: "true" run: | set -euo pipefail - # The persistent root tree. It lives under /nix so the cache - # carries it, but not under /nix/var/nix, which the cache - # action excludes from its archive apart from the database. - # Nix reads roots only from /nix/var/nix/gcroots, so the last - # step mirrors the surviving tree there before the save's - # pruning runs. - roots=/nix/ci-roots + # The persistent root tree, inside the cache directory so the + # entry carries it. A root lists the store paths its build + # contributes to the entry: its closure, less what the + # upstream caches served. + roots="$CI_CACHE/roots" + store="file://$CI_CACHE?compression=zstd¶llel-compression=true" # 1. Root what this run built: the checks and the packages. # Checks alone would not do; a check's output need not # reference what it built (the VM test's is its log), and # the pruning keeps only what the roots reach. Everything - # is already built, so this evaluates and links. An output + # is already built, so this evaluates and lists. An output # the flake does not provide is skipped; any other # evaluation error fails. own="$roots/$ROOT_CLASS/$ROOT_ID" - sudo rm -rf "$own" - sudo mkdir -p "$own" + rm -rf "$own" + mkdir -p "$own" for kind in checks packages; do if ! names=$(nix eval --json ".#$kind.x86_64-linux" --apply builtins.attrNames 2>"$RUNNER_TEMP/eval.err"); then grep -q 'does not provide attribute' "$RUNNER_TEMP/eval.err" || { cat "$RUNNER_TEMP/eval.err"; exit 1; } continue fi for name in $(jq -r '.[]' <<<"$names"); do - out=$(nix build --no-link --print-out-paths ".#$kind.x86_64-linux.$name") - sudo ln -s "$out" "$own/$kind-$name" + nix build --no-link --print-out-paths ".#$kind.x86_64-linux.$name" >> "$own/outputs" done done - # 2. Order every root directory, highest priority first. Older + # 2. Copy the closure into the cache, then ask each upstream cache + # which of it it serves (a cache serves a path when it has + # the path's narinfo file; a failed request counts as not + # served, which only keeps more); the root lists the rest. + # The whole closure goes in because a binary cache refuses a + # path whose references it lacks; the served paths come out + # again in step 6, and compressing them costs seconds. + xargs nix copy --to "$store" < "$own/outputs" + xargs nix path-info -r < "$own/outputs" | sort -u > "$RUNNER_TEMP/closure" + for cache in $UPSTREAM_CACHES; do + # shellcheck disable=SC2016 + cut -c 12-43 "$RUNNER_TEMP/closure" | xargs -P 32 -I{} sh -c \ + 'if curl -sfI --retry 3 -o /dev/null "$1/$2.narinfo"; then echo "$2"; fi' _ "$cache" {} + done | sort -u > "$RUNNER_TEMP/served" + awk 'FILENAME == ARGV[1] { served[$1] = 1; next } !(substr($0, 12, 32) in served)' \ + "$RUNNER_TEMP/served" "$RUNNER_TEMP/closure" > "$own/paths" + echo "This build: $(wc -l < "$RUNNER_TEMP/closure") paths in the closure, $(wc -l < "$RUNNER_TEMP/served") served upstream, $(wc -l < "$own/paths") kept." + + # 3. Order every root directory, highest priority first. Older # builds of the same request are superseded outright. open=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --limit 500 --json number --jq '.[].number') is_open() { grep -qx "$1" <<<"$open"; } @@ -239,7 +277,7 @@ jobs: n=$(basename "$prdir") newest=$(newest_in "$prdir" | head -n1) for sha in "$prdir"/*/; do - [[ "$(basename "$sha")" == "$newest" ]] || sudo rm -rf "$sha" + [[ "$(basename "$sha")" == "$newest" ]] || rm -rf "$sha" done is_open "$n" && ordered+=("$prdir$newest") done @@ -251,12 +289,12 @@ jobs: is_open "$n" || ordered+=("$prdir$(newest_in "$prdir" | head -n1)") done - # 3. The budget: the repository's 10 GB, less the entries of + # 4. The budget: the repository's 10 GB, less the entries of # requests that changed the pipeline (they live in their own # scopes), less this scope's current entry, which coexists # with the new one until the `prune` job removes it. Sizes - # are compared as uncompressed closure sizes, which errs on - # the safe side since entries are compressed. + # are the compressed sizes the narinfo files record, which + # is what the entry is made of. gib=$((1024 * 1024 * 1024)) caches=$(gh cache list --repo "$GITHUB_REPOSITORY" --limit 1000 --json key,ref,sizeInBytes,lastAccessedAt) used_by_own_scope=$(jq --arg ref "$OWN_REF" --arg p "$KEY_PREFIX" \ @@ -266,22 +304,28 @@ jobs: budget=$((10 * gib - used_by_own_scope - used_by_others)) echo "Budget for this entry: $budget bytes ($used_by_others used by other scopes, $used_by_own_scope by this one)." - closure_size() { - # Union closure size of the roots under the given directories. - local targets - targets=$(for d in "$@"; do find "$d" -maxdepth 1 -type l -exec readlink {} +; done) - [[ -n "$targets" ]] || { echo 0; return; } - # shellcheck disable=SC2086 - nix path-info -r $targets | sort -u | xargs nix path-info -s | awk '{s += $2} END {print s + 0}' + # The compressed size of every path in the cache, by store hash, + # from the narinfo files. The entry is these files, so the sum + # is close to what GitHub will count. + find "$CI_CACHE" -maxdepth 1 -name '*.narinfo' \ + -exec awk 'FNR == 1 { h = FILENAME; sub(/.*\//, "", h); sub(/\.narinfo$/, "", h) } /^FileSize: / { print h, $2 }' {} + \ + > "$RUNNER_TEMP/sizes" + listed_paths() { + # The union of the paths listed by the given root directories. + for d in "$@"; do cat "$d/paths"; done | sort -u + } + entry_size() { + # The size of an entry holding what the given roots list. + listed_paths "$@" | awk 'FILENAME == ARGV[1] { size[$1] = $2; next } { total += size[substr($0, 12, 32)] } END { print total + 0 }' "$RUNNER_TEMP/sizes" - } - # 4. Keep the highest-priority roots that fit, drop the rest. + # 5. Keep the highest-priority roots that fit, drop the rest. # If not even the current main fits, make room by deleting # pipeline requests' entries, closed ones first, then the # oldest open ones. kept=() for d in "${ordered[@]}"; do - size=$(closure_size "${kept[@]}" "$d") + size=$(entry_size "${kept[@]}" "$d") if (( size <= budget )); then kept+=("$d") continue @@ -312,23 +356,37 @@ jobs: kept+=("$d") continue fi - echo "Dropping roots $d (closure would be $size bytes, budget $budget)." - sudo rm -rf "$d" + echo "Dropping roots $d (the entry would be $size bytes, budget $budget)." + rm -rf "$d" done echo "Kept roots:" printf ' %s\n' "${kept[@]}" - # 5. Mirror the surviving tree into Nix's root directory so the - # pruning before the save honours it (symlinks stay symlinks). - sudo rm -rf /nix/var/nix/gcroots/ci - sudo cp -a "$roots" /nix/var/nix/gcroots/ci + # 6. Prune the cache to what the surviving roots list: first + # every narinfo for a path none of them lists (this run's + # upstream-served paths among them), then every NAR no + # remaining narinfo names. + listed_paths "${kept[@]}" | cut -c 12-43 | sort > "$RUNNER_TEMP/keep" + find "$CI_CACHE" -maxdepth 1 -name '*.narinfo' -printf '%f\n' | sed 's/\.narinfo$//' | sort \ + | comm -23 - "$RUNNER_TEMP/keep" | sed "s|.*|$CI_CACHE/&.narinfo|" | xargs -r rm -f + find "$CI_CACHE" -maxdepth 1 -name '*.narinfo' -exec sed -n 's|^URL: ||p' {} + | sort -u > "$RUNNER_TEMP/keep-nars" + find "$CI_CACHE/nar" -type f -printf 'nar/%f\n' | sort \ + | comm -23 - "$RUNNER_TEMP/keep-nars" | sed "s|.*|$CI_CACHE/&|" | xargs -r rm -f + echo "The entry holds $(wc -l < "$RUNNER_TEMP/keep") paths, $(du -sb "$CI_CACHE" | cut -f1) bytes on disk." - # What main's cache entry holds is what hosts and consumers may - # pull: the closure of the surviving roots, signed with the key - # from the environment. Paths the cache already has are skipped. - - name: Push the kept closure to the clhodapp cache - run: | - find /nix/ci-roots -type l -exec readlink {} + | sort -u | xargs cachix push clhodapp + # What this run built is what hosts and consumers may pull: the + # closure of its outputs, signed with the key from the environment; + # earlier runs pushed theirs. Paths the cache already has are + # skipped. + - name: Push this build's closure to the clhodapp cache + env: + OWN: ${{ steps.target.outputs.class }}/${{ steps.target.outputs.id }} + run: xargs cachix push clhodapp < "$CI_CACHE/roots/$OWN/outputs" + + - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ${{ env.CI_CACHE }} + key: cache-${{ runner.os }}-${{ steps.target.outputs.head }} - name: Report the result on the pull request if: always() && inputs.pr != '' @@ -342,8 +400,8 @@ jobs: -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" > /dev/null # Older entries in this scope go only once the new one is confirmed - # to exist. The cache action would purge regardless, and a save can - # fail (a read-only token did, twice), which would leave nothing. + # to exist: a save can fail (a read-only token did, twice), and + # purging first would leave nothing. prune: needs: check if: needs.check.result == 'success' @@ -355,8 +413,8 @@ jobs: - name: Remove the older entries in this scope env: GH_TOKEN: ${{ github.token }} - KEY: nix-${{ runner.os }}-${{ needs.check.outputs.head }} - KEY_PREFIX: nix-${{ runner.os }}- + KEY: cache-${{ runner.os }}-${{ needs.check.outputs.head }} + KEY_PREFIX: cache-${{ runner.os }}- OWN_REF: ${{ github.ref }} run: | set -euo pipefail