#!/usr/bin/env bash # Consume side: make this run's own target dir exist, warm, and private. # # Usage: seed-target-dir.sh [fallback-dir] # 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) # # 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 5 ]; then echo "::error::seed-target-dir.sh: expected 4 or 5 arguments (own-key, base-key, cache-root, tag, [fallback-dir])" >&2 exit 1 fi OWN_KEY="$1"; BASE_KEY="$2"; ROOT="$3"; TAG="$4"; FALLBACK="${5:-}" OWN_DIR=$(target_dir_for "$ROOT" "$OWN_KEY") mkdir -p "$ROOT" if [ -d "$OWN_DIR" ]; then 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. CANDIDATES=() [ -n "$BASE_KEY" ] && CANDIDATES+=("$(snapshot_dir_for "$ROOT" "$BASE_KEY"):base snapshot") CANDIDATES+=("$(snapshot_dir_for "$ROOT" "$OWN_KEY"):own snapshot") [ -n "$FALLBACK" ] && CANDIDATES+=("${FALLBACK}:fallback dir") 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) if hardlink_clone_into "$src" "$OWN_DIR" "$TAG"; then echo "seed: cloned in $(( $(date +%s) - start ))s" echo "seeded-from=${label// /-}" >> "${GITHUB_OUTPUT:-/dev/null}" else # 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. echo "seed: another job seeded ${OWN_DIR} concurrently; discarded our staging copy and using theirs" echo "seeded-from=concurrent-peer" >> "${GITHUB_OUTPUT:-/dev/null}" fi 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"