#!/usr/bin/env bash # Consume side: make this run's own target dir exist, warm, and private. # # Usage: seed-target-dir.sh [fallback-dir] [lock-id] # own-key cache key for this run's own ref # base-key cache key for the ref to layer over ("" for a run whose own # ref IS a reference branch) # cache-root mount point of the persistent volume # tag a per-job-per-run unique string, used to name the staging # directory so two concurrent jobs can never collide on it # fallback-dir optional absolute path to seed from when no snapshot exists # (a legacy flat cache dir during a migration, typically) # lock-id optional cache-lock id. When given, the lock marker is # written on EVERY path out of this script — including into # the staging tree before its rename — so the directory is # never observable under its final name without a lock. The # action acquires the lock in a later step too; that step is # idempotent, and this closes the window before it runs, where # a concurrent job's prune pass could evict a directory that # exists but is not yet held. # # The design in one paragraph: a PR branch's first run hardlink-clones the # base branch's PUBLISHED SNAPSHOT. Hardlink, because the clone then costs # time proportional to inode count rather than data volume — on ext4, with no # reflink support, that is the only way to make "layer over the base" cheap. # Snapshot rather than the base's live target dir, because a live directory is # being written by its own job while a consumer reads it, and a torn read # pairs one build's fingerprint with another build's artifact — which degrades # to a WRONG reuse, not to a safe miss. Neither half is novel; the combination # is what makes the scheme both cheap and sound, instead of one or the other. # # Everything about correctness under concurrency lives in # hardlink_clone_into() and unshare_mutable_paths() in cache-lib.sh — read # those before changing anything here. set -euo pipefail . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/cache-lib.sh" if [ $# -lt 4 ] || [ $# -gt 6 ]; then echo "::error::seed-target-dir.sh: expected 4 to 6 arguments (own-key, base-key, cache-root, tag, [fallback-dir], [lock-id])" >&2 exit 1 fi OWN_KEY="$1"; BASE_KEY="$2"; ROOT="$3"; TAG="$4"; FALLBACK="${5:-}"; LOCK_ID="${6:-}" OWN_DIR=$(target_dir_for "$ROOT" "$OWN_KEY") mkdir -p "$ROOT" if [ -d "$OWN_DIR" ]; then write_cache_lock "$OWN_DIR" "$LOCK_ID" echo "seed: reusing this ref's own cache at ${OWN_DIR} ($(usage_gb "$OWN_DIR") GB)" echo "seeded-from=own" >> "${GITHUB_OUTPUT:-/dev/null}" exit 0 fi # Source preference, most specific first: # # 1. the base ref's snapshot — the ordinary PR case, and the whole point. # 2. this ref's OWN snapshot — a reference branch whose live target dir was # evicted under disk pressure can restore itself from the last snapshot # it published, instead of paying a cold rebuild. Free, given the # snapshot already exists. # 3. an explicit fallback directory — migration off a pre-existing flat # cache, so the first run under this scheme isn't a needless cold build. # # The list itself lives in cache-lib.sh because prune-cache.sh reads it too: # it runs ahead of this step and has to free enough disk for the clone below, # which means resolving the same source this loop will pick. mapfile -t CANDIDATES < <(seed_source_candidates "$ROOT" "$OWN_KEY" "$BASE_KEY" "$FALLBACK") for entry in "${CANDIDATES[@]}"; do src="${entry%%:*}"; label="${entry#*:}" [ -d "$src" ] || continue echo "seed: hardlink-cloning ${label} ${src} ($(usage_gb "$src") GB) -> ${OWN_DIR}" start=$(date +%s) # The status is captured and dispatched on explicitly. Calling this as a # bare `if` condition — which is what this script used to do — suppresses # `set -e` for the whole call, so a hard clone failure could not abort the # seed even in principle; the three outcomes are genuinely distinct and each # needs its own handling. clone_rc=0 hardlink_clone_into "$src" "$OWN_DIR" "$TAG" "$LOCK_ID" || clone_rc=$? case "$clone_rc" in 0) echo "seed: cloned in $(( $(date +%s) - start ))s" echo "seeded-from=${label// /-}" >> "${GITHUB_OUTPUT:-/dev/null}" ;; 1) # Another job sharing this cache key won the rename while we were # cloning. Its directory is complete (the rename is the publish step), # so there is nothing to do but use it — and nothing was ever # observable in a half-seeded state. write_cache_lock "$OWN_DIR" "$LOCK_ID" echo "seed: another job seeded ${OWN_DIR} concurrently; discarded our staging copy and using theirs" echo "seeded-from=concurrent-peer" >> "${GITHUB_OUTPUT:-/dev/null}" ;; *) # A source that could not be read consistently. Failing the job is the # only safe answer: the alternative that used to happen here was # seeding a truncated tree and reporting success, which hands Cargo a # directory whose fingerprints and artifacts disagree. echo "::error::seed: could not clone ${label} ${src} consistently — refusing to build against a partial cache" >&2 exit 1 ;; esac exit 0 done echo "seed: no snapshot or fallback available — ${OWN_DIR} starts cold" echo "seeded-from=cold" >> "${GITHUB_OUTPUT:-/dev/null}" mkdir -p "$OWN_DIR" write_cache_lock "$OWN_DIR" "$LOCK_ID"