diff --git a/.github/actions/setup-rust-build-cache/tests/fixture.sh b/.github/actions/setup-rust-build-cache/tests/fixture.sh index 019740f5..ecaf7d05 100755 --- a/.github/actions/setup-rust-build-cache/tests/fixture.sh +++ b/.github/actions/setup-rust-build-cache/tests/fixture.sh @@ -28,10 +28,9 @@ fi grep -Fxq 'fixture-id=417-1' "$WORK_DIR/seed/output" || fail 'seed does not export fixture identity' [[ "$(grep -c 'cargo build' "$ACTION_DIR/tests/assert-compiler-reuse.sh")" == 1 ]] || fail 'reuse probe primes its own cache' if grep -q 'cargo clean' "$ACTION_DIR/tests/assert-compiler-reuse.sh"; then fail 'reuse probe accepts same-job rebuilds'; fi -workflow="$ACTION_DIR/../../workflows/deploy-action.yml" +workflow="${CACHE_WORKFLOW_FILE:-$ACTION_DIR/../../workflows/deploy-action.yml}" seed=$(yq -r '.jobs."cache-restore-seed".steps[] | select(.name == "Seed native and WASM compiler outputs") | .run' "$workflow") -# shellcheck disable=SC2016 # Assert the literal shell command in workflow metadata. -[[ "$seed" == *'cargo build --locked --lib --target "$host"'* && "$seed" == *'cargo build --locked --lib --target wasm32-wasip1'* ]] || fail 'seed job does not compile both targets' +[[ "$seed" == 'bash .github/actions/setup-rust-build-cache/tests/seed-compiler-cache.sh' ]] || fail 'seed does not verify compiler publication' for job in cache-restore-seed cache-restore-smoke cache-new-revision-smoke; do JOB_ID="$job" yq -e '.jobs[strenv(JOB_ID)].steps[] | select(.name == "Install native and WASM compiler targets") | .run == "bash .github/actions/setup-rust-build-cache/tests/setup-toolchain.sh"' "$workflow" >/dev/null || fail "$job does not share its Rust toolchain" done @@ -41,7 +40,62 @@ for job in cache-restore-smoke cache-new-revision-smoke; do actual=$(JOB_ID="$job" yq -r '.jobs[strenv(JOB_ID)].steps[] | select(.id == "fixture") | .env.CACHE_FIXTURE_ID' "$workflow") [[ "$actual" == "$expected_identity" ]] || fail "$job does not reuse the seed fixture identity" done -[[ "$(yq -r '.jobs."cache-restore-seed".steps[] | select(.id == "cache") | .with."save-target"' "$workflow")" == true ]] || fail 'seed does not opt into saving' +[[ "$(yq -r '.jobs."cache-restore-seed".steps[] | select(.id == "cache") | .with."save-target"' "$workflow")" == false ]] || fail 'seed relies on unverified post-job saving' +for job in fixture-release store-free-release; do + actual=$(JOB_ID="$job" yq -r '.jobs[strenv(JOB_ID)].needs // [] | length' "$workflow") + [[ "$actual" == 0 ]] || fail "$job depends on cache probe availability" +done +# Exercise both publication surfaces against their actual workflow wiring. +for kind in source target; do + if [[ "$kind" == source ]]; then + expected_path=$(yq -r '.runs.steps[] | select(.id == "cargo-sources") | .with.path' "$ACTION_DIR/action.yml") + else + # shellcheck disable=SC2016 # Literal GitHub output reference. + expected_path='${{ steps.cache.outputs.cargo-target-dir }}' + fi + expected_key="\${{ steps.cache.outputs.$kind-cache-key }}" + retry_condition="steps.$kind-verify-1.outputs.cache-hit != 'true'" + for step in "$kind-save-1" "$kind-verify-1" "$kind-save-2" "$kind-verify-2"; do + actual=$(STEP_ID="$step" yq -r '.jobs."cache-restore-seed".steps[] | select(.id == strenv(STEP_ID)) | .with.path' "$workflow") + [[ "$actual" == "$expected_path" ]] || fail "$step uses a different cache path" + actual=$(STEP_ID="$step" yq -r '.jobs."cache-restore-seed".steps[] | select(.id == strenv(STEP_ID)) | .with.key' "$workflow") + [[ "$actual" == "$expected_key" ]] || fail "$step uses a different cache key" + actual=$(STEP_ID="$step" yq -r '.jobs."cache-restore-seed".steps[] | select(.id == strenv(STEP_ID)) | .if // ""' "$workflow") + if [[ "$step" == *-2 ]]; then + [[ "$actual" == "$retry_condition" ]] || fail "$step retries without a first-attempt miss" + else + [[ -z "$actual" ]] || fail "$step skips the initial publication attempt" + fi + done + for attempt in 1 2; do + STEP_ID="$kind-save-$attempt" yq -e '.jobs."cache-restore-seed".steps[] | select(.id == strenv(STEP_ID)) | .uses == "actions/cache/save@v5"' "$workflow" >/dev/null || fail 'missing explicit cache save' + STEP_ID="$kind-verify-$attempt" yq -e '.jobs."cache-restore-seed".steps[] | select(.id == strenv(STEP_ID)) | .with."lookup-only" == true' "$workflow" >/dev/null || fail 'cache publication is not verified' + done + STEP_ID="$kind-verify-2" yq -e '.jobs."cache-restore-seed".steps[] | select(.id == strenv(STEP_ID)) | .with."fail-on-cache-miss" == true' "$workflow" >/dev/null || fail 'exhausted publication retries stay green' + # The action can return successfully before honoring fail-on-cache-miss. + # Require actual exact-hit output, including when availability checks fail. + publication_step=".jobs.\"cache-restore-seed\".steps[] | select(.name == \"Require exact $kind seed publication\")" + publication_assertion=$(yq -r "$publication_step | .run" "$workflow") + [[ -n "$publication_assertion" && "$publication_assertion" != null ]] || fail 'missing exact publication assertion' + first_hit="\${{ steps.$kind-verify-1.outputs.cache-hit }}" + second_hit="\${{ steps.$kind-verify-2.outputs.cache-hit }}" + [[ "$(yq -r "$publication_step | .env.CACHE_SEED_FIRST_HIT" "$workflow")" == "$first_hit" ]] || fail 'publication assertion ignores first lookup output' + [[ "$(yq -r "$publication_step | .env.CACHE_SEED_SECOND_HIT" "$workflow")" == "$second_hit" ]] || fail 'publication assertion ignores second lookup output' + [[ "$(yq -r "$publication_step | .if // \"\"" "$workflow")" == '' ]] || fail 'publication assertion can be skipped after successful lookup steps' + [[ "$(yq -r "$publication_step | .\"continue-on-error\" // false" "$workflow")" == false ]] || fail 'publication assertion failure is ignored' + for first in '' false true partial; do + for second in '' false true partial; do + status=0 + CACHE_SEED_FIRST_HIT="$first" CACHE_SEED_SECOND_HIT="$second" \ + bash -e -o pipefail -c "$publication_assertion" >"$WORK_DIR/publication.log" 2>&1 || status=$? + if [[ "$first" == true || "$second" == true ]]; then + [[ "$status" == 0 ]] || fail 'publication assertion rejects a verified exact hit' + else + [[ "$status" != 0 ]] || fail "publication assertion accepts unverified outputs '$first'/'$second'" + fi + done + done +done for job in fixture-release store-free-release; do actual=$(JOB_ID="$job" yq -r '.jobs[strenv(JOB_ID)].steps[] | select(.uses == "./.github/actions/setup-rust-build-cache") | .with."cache-target"' "$workflow") [[ "$actual" == false ]] || fail "$job enables full target caching for a disposable fixture" diff --git a/.github/actions/setup-rust-build-cache/tests/publication.sh b/.github/actions/setup-rust-build-cache/tests/publication.sh new file mode 100755 index 00000000..efee1326 --- /dev/null +++ b/.github/actions/setup-rust-build-cache/tests/publication.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Mutate the workflow itself so contract checks cannot merely test a detached if. +set -euo pipefail +ACTION_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/edgezero-cache-publication-test.XXXXXX") +trap 'rm -rf -- "$WORK_DIR"' EXIT +# shellcheck source=common.sh +source "$ACTION_DIR/tests/common.sh" +workflow="$ACTION_DIR/../../workflows/deploy-action.yml" + +reject_mutation() { + local name="$1" expression="$2" diagnostic="$3" status=0 + yq "$expression" "$workflow" >"$WORK_DIR/workflow.yml" + CACHE_WORKFLOW_FILE="$WORK_DIR/workflow.yml" bash "$ACTION_DIR/tests/fixture.sh" >"$WORK_DIR/log" 2>&1 || status=$? + [[ "$status" == 1 ]] || fail "workflow guard accepted mutation: $name (exit $status)" + grep -Fq "$diagnostic" "$WORK_DIR/log" || { + cat "$WORK_DIR/log" >&2 + fail "mutation failed for an unrelated reason: $name" + } +} + +for kind in source target; do + publication=".jobs.\"cache-restore-seed\".steps[] | select(.name == \"Require exact $kind seed publication\")" + reject_mutation 'missing final gate' \ + ".jobs.\"cache-restore-seed\".steps |= map(select(.name != \"Require exact $kind seed publication\"))" \ + 'missing exact publication assertion' + reject_mutation 'hardcoded first hit' \ + "($publication | .env.CACHE_SEED_FIRST_HIT) = \"true\"" \ + 'publication assertion ignores first lookup output' + reject_mutation 'hardcoded second hit' \ + "($publication | .env.CACHE_SEED_SECOND_HIT) = \"true\"" \ + 'publication assertion ignores second lookup output' + reject_mutation 'skipped final gate' \ + "($publication | .if) = \"false\"" \ + 'publication assertion can be skipped' + reject_mutation 'ignored final failure' \ + "($publication | .\"continue-on-error\") = true" \ + 'publication assertion failure is ignored' + reject_mutation 'gate always succeeds' \ + "($publication | .run) = \"true\"" \ + 'publication assertion accepts unverified outputs' + reject_mutation 'gate always fails' \ + "($publication | .run) = \"false\"" \ + 'publication assertion rejects a verified exact hit' + for step in "$kind-save-1" "$kind-verify-1" "$kind-save-2" "$kind-verify-2"; do + for input in path key; do + reject_mutation "$step wrong $input" \ + "(.jobs.\"cache-restore-seed\".steps[] | select(.id == \"$step\") | .with.$input) = \"wrong\"" \ + "$step uses a different cache $input" + done + if [[ "$step" == *-2 ]]; then + reject_mutation "$step wrong retry condition" \ + "(.jobs.\"cache-restore-seed\".steps[] | select(.id == \"$step\") | .if) = \"false\"" \ + "$step retries without a first-attempt miss" + else + reject_mutation "$step skipped initial attempt" \ + "(.jobs.\"cache-restore-seed\".steps[] | select(.id == \"$step\") | .if) = \"false\"" \ + "$step skips the initial publication attempt" + fi + done +done +printf 'source and target publication workflow mutation contracts passed\n' diff --git a/.github/actions/setup-rust-build-cache/tests/run.sh b/.github/actions/setup-rust-build-cache/tests/run.sh index ba2af9eb..9f8fbcbf 100755 --- a/.github/actions/setup-rust-build-cache/tests/run.sh +++ b/.github/actions/setup-rust-build-cache/tests/run.sh @@ -143,3 +143,5 @@ printf 'setup-rust-build-cache action tests passed\n' bash "$ACTION_DIR/tests/revision.sh" bash "$ACTION_DIR/tests/review.sh" bash "$ACTION_DIR/tests/fixture.sh" +bash "$ACTION_DIR/tests/publication.sh" +bash "$ACTION_DIR/tests/seed.sh" diff --git a/.github/actions/setup-rust-build-cache/tests/seed-compiler-cache.sh b/.github/actions/setup-rust-build-cache/tests/seed-compiler-cache.sh new file mode 100755 index 00000000..487383d4 --- /dev/null +++ b/.github/actions/setup-rust-build-cache/tests/seed-compiler-cache.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Publish each target before downstream jobs test first-build compiler reuse. +fixture="${GITHUB_WORKSPACE:?}/cache-restore-fixture" +target_dir=$(realpath "${CARGO_TARGET_DIR:?}") +expected="$(realpath "${RUNNER_TEMP:?}")/edgezero-rust-cache/cache-restore-smoke-v2/target" +[[ "$target_dir" == "$expected" ]] || { + printf '::error::compiler seed must use its confined fixture target directory\n' >&2 + exit 1 +} +cd "$fixture" +host=$(rustc -vV | sed -n 's/^host: //p') +[[ "$host" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]] || { + printf '::error::rustc must report a safe host target component\n' >&2 + exit 1 +} +for target in "$host" wasm32-wasip1; do + for attempt in 1 2 3; do + # Cargo must call sccache again after a failed publication, rather than + # reporting locally fresh outputs as a successful cache seed. + debug_dir="$target_dir/$target/debug" + [[ ! -L "$target_dir/$target" && ! -L "$debug_dir" ]] || { + printf '::error::compiler seed cleanup must not follow target symlinks\n' >&2 + exit 1 + } + rm -rf -- "$debug_dir" + sccache --zero-stats + cargo build --locked --lib --target "$target" + stats=$(sccache --show-stats --stats-format json) + printf 'Compiler seed for %s (attempt %s): %s\n' "$target" "$attempt" "$stats" + if jq -e '((.stats.cache_hits.counts.Rust // 0) + (.stats.cache_writes // 0)) > 0 and (.stats.cache_errors.counts.Rust // 0) == 0 and (.stats.cache_read_errors // 0) == 0 and (.stats.cache_write_errors // 0) == 0' <<<"$stats" >/dev/null; then + break + fi + if [[ "$attempt" == 3 ]]; then + printf '::error::failed to publish compiler cache for %s after three attempts\n' "$target" >&2 + exit 1 + fi + printf '::warning::compiler cache seed failed; retrying after backoff\n' + sleep "$((attempt * 60))" + done +done diff --git a/.github/actions/setup-rust-build-cache/tests/seed.sh b/.github/actions/setup-rust-build-cache/tests/seed.sh new file mode 100755 index 00000000..c939af2b --- /dev/null +++ b/.github/actions/setup-rust-build-cache/tests/seed.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +set -euo pipefail +ACTION_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/edgezero-cache-seed-test.XXXXXX") +trap 'rm -rf -- "$WORK_DIR"' EXIT +# shellcheck source=common.sh +source "$ACTION_DIR/tests/common.sh" +mkdir -p "$WORK_DIR/bin" "$WORK_DIR/workspace/cache-restore-fixture" "$WORK_DIR/runner" +export GITHUB_WORKSPACE="$WORK_DIR/workspace" RUNNER_TEMP="$WORK_DIR/runner" +export CARGO_TARGET_DIR="$RUNNER_TEMP/edgezero-rust-cache/cache-restore-smoke-v2/target" +export PROBE_LOG="$WORK_DIR/calls" PROBE_COUNT="$WORK_DIR/count" +cat >"$WORK_DIR/bin/rustc" <<'MOCK' +#!/usr/bin/env bash +printf 'host: %s\n' "${PROBE_HOST:-test-host}" +MOCK +cat >"$WORK_DIR/bin/sleep" <<'MOCK' +#!/usr/bin/env bash +printf 'sleep %s\n' "$*" >>"$PROBE_LOG" +MOCK +cat >"$WORK_DIR/bin/cargo" <<'MOCK' +#!/usr/bin/env bash +printf 'build %s\n' "$*" >>"$PROBE_LOG" +[[ "${PROBE_MODE:-}" != compile-failure ]] || exit 42 +[[ "$PROBE_MODE" != compile-wasm-failure || "${*: -1}" != wasm32-wasip1 ]] || exit 42 +[[ ! -d "$CARGO_TARGET_DIR/${*: -1}/debug" ]] || exit 43 +mkdir -p "$CARGO_TARGET_DIR/${*: -1}/debug" +MOCK +cat >"$WORK_DIR/bin/sccache" <<'MOCK' +#!/usr/bin/env bash +if [[ "$1" == --zero-stats ]]; then + printf 'zero\n' >>"$PROBE_LOG" + exit 0 +fi +[[ "$1" == --show-stats ]] || exit 2 +count=$(cat "$PROBE_COUNT") +printf '%s\n' "$((count + 1))" >"$PROBE_COUNT" +if [[ "$PROBE_MODE" == permanent || + ( "$PROBE_MODE" == transient && "$count" == 0 ) || + ( "$PROBE_MODE" == wasm-transient && "$count" == 1 ) || + ( "$PROBE_MODE" == both-transient && ( "$count" == 0 || "$count" == 2 ) ) || + ( "$PROBE_MODE" == wasm-permanent && "$count" -ge 1 ) ]]; then + printf '{"stats":{"cache_write_errors":1,"cache_writes":0}}\n' +elif [[ "$PROBE_MODE" == read-error ]]; then + printf '{"stats":{"cache_writes":1,"cache_read_errors":1}}\n' +elif [[ "$PROBE_MODE" == compile-cache-error ]]; then + printf '{"stats":{"cache_hits":{"counts":{"Rust":1}},"cache_errors":{"counts":{"Rust":1}}}}\n' +elif [[ "$PROBE_MODE" == malformed ]]; then + printf 'not-json\n' +elif [[ "$PROBE_MODE" == empty ]]; then + printf '{"stats":{}}\n' +elif [[ "$PROBE_MODE" == hit ]]; then + printf '{"stats":{"cache_hits":{"counts":{"Rust":1}}}}\n' +else + printf '{"stats":{"cache_writes":1}}\n' +fi +MOCK +chmod +x "$WORK_DIR/bin/"* +export PATH="$WORK_DIR/bin:$PATH" +run_seed() { + rm -rf "$CARGO_TARGET_DIR" + mkdir -p "$CARGO_TARGET_DIR" + printf '0\n' >"$PROBE_COUNT" + : >"$PROBE_LOG" + PROBE_MODE="$1" bash "$ACTION_DIR/tests/seed-compiler-cache.sh" >"$WORK_DIR/log" 2>&1 +} +run_seed transient || fail 'compiler seed does not recover from a transient write failure' +[[ "$(grep -c '^build ' "$PROBE_LOG")" == 3 ]] || fail 'retry does not rebuild fresh outputs' +[[ "$(grep -c '^sleep ' "$PROBE_LOG")" == 1 ]] || fail 'transient error does not back off' +run_seed hit || fail 'existing compiler hit is not accepted as published' +[[ "$(grep -c '^build ' "$PROBE_LOG")" == 2 ]] || fail 'successful seed does not build both targets' +grep -q -- '--target test-host$' "$PROBE_LOG" || fail 'successful seed omits native target' +grep -q -- '--target wasm32-wasip1$' "$PROBE_LOG" || fail 'successful seed omits WASM target' +[[ "$(grep -c '^zero$' "$PROBE_LOG")" == 2 ]] || fail 'successful seed does not reset stats for each target' +for mode in wasm-transient both-transient; do + run_seed "$mode" || fail "$mode does not recover" + expected=3 + if [[ "$mode" == both-transient ]]; then expected=4; fi + [[ "$(grep -c '^build ' "$PROBE_LOG")" == "$expected" ]] || fail "$mode does not rebuild every attempt" + [[ "$(grep -c '^zero$' "$PROBE_LOG")" == "$expected" ]] || fail "$mode does not reset every attempt" +done +for mode in permanent empty compile-failure compile-wasm-failure wasm-permanent read-error compile-cache-error malformed; do + if run_seed "$mode"; then fail "accepted unsuccessful compiler seeding: $mode"; fi + case "$mode" in + compile-failure) expected=1 ;; + compile-wasm-failure) expected=2 ;; + wasm-permanent) expected=4 ;; + *) expected=3 ;; + esac + [[ "$(grep -c '^build ' "$PROBE_LOG")" == "$expected" ]] || fail "$mode violates per-target retry limits" + [[ "$(grep -c '^zero$' "$PROBE_LOG")" == "$expected" ]] || fail "$mode uses stale stats" +done +# Cleanup must never follow restored symlinks outside the fixture root. +for target in test-host wasm32-wasip1; do + for component in namespace debug; do + rm -rf -- "$CARGO_TARGET_DIR" "$WORK_DIR/outside" + mkdir -p "$CARGO_TARGET_DIR" "$WORK_DIR/outside/debug" + printf keep >"$WORK_DIR/outside/debug/marker" + if [[ "$component" == namespace ]]; then + ln -s "$WORK_DIR/outside" "$CARGO_TARGET_DIR/$target" + else + mkdir -p "$CARGO_TARGET_DIR/$target" + ln -s "$WORK_DIR/outside/debug" "$CARGO_TARGET_DIR/$target/debug" + fi + status=0 + PROBE_MODE=hit bash "$ACTION_DIR/tests/seed-compiler-cache.sh" >"$WORK_DIR/log" 2>&1 || status=$? + [[ -f "$WORK_DIR/outside/debug/marker" ]] || fail "cleanup escaped through $target $component symlink" + [[ "$status" != 0 ]] || fail "accepted $target $component symlink" + grep -Fq 'compiler seed cleanup must not follow target symlinks' "$WORK_DIR/log" || fail 'symlink case failed for an unrelated reason' + done +done +rm -rf -- "$CARGO_TARGET_DIR" +mkdir -p "$CARGO_TARGET_DIR" "$WORK_DIR/runner/edgezero-rust-cache/debug" +printf keep >"$WORK_DIR/runner/edgezero-rust-cache/debug/marker" +if PROBE_HOST=../.. PROBE_MODE=hit bash "$ACTION_DIR/tests/seed-compiler-cache.sh" >"$WORK_DIR/log" 2>&1; then + fail 'accepted traversal in compiler host' +fi +[[ -f "$WORK_DIR/runner/edgezero-rust-cache/debug/marker" ]] || fail 'removed output through host traversal' +grep -Fq 'rustc must report a safe host target component' "$WORK_DIR/log" || fail 'traversal case failed for an unrelated reason' +# A mistaken directory must never be removed by retry cleanup. +export CARGO_TARGET_DIR="$WORK_DIR/outside" +mkdir -p "$CARGO_TARGET_DIR" +printf keep >"$CARGO_TARGET_DIR/marker" +if PROBE_MODE=hit bash "$ACTION_DIR/tests/seed-compiler-cache.sh" >"$WORK_DIR/log" 2>&1; then + fail 'accepted unconfined compiler seed directory' +fi +[[ -f "$CARGO_TARGET_DIR/marker" ]] || fail 'removed unconfined output' +printf 'compiler seed backoff contracts passed\n' diff --git a/.github/workflows/deploy-action.yml b/.github/workflows/deploy-action.yml index 15e797ce..bfb18d8d 100644 --- a/.github/workflows/deploy-action.yml +++ b/.github/workflows/deploy-action.yml @@ -138,7 +138,7 @@ jobs: with: app-name: cache-restore-smoke-v2 cache-target: true - save-target: true + save-target: false target-cache-revision: ${{ steps.fixture.outputs.revision }} working-directory: cache-restore-fixture @@ -149,12 +149,114 @@ jobs: printf '%s\n' 'edgezero-source-cache-smoke-v2' >"$HOME/.cargo/git/db/edgezero-source-cache-smoke-v2" - name: Seed native and WASM compiler outputs - working-directory: cache-restore-fixture + run: bash .github/actions/setup-rust-build-cache/tests/seed-compiler-cache.sh + + # Sources otherwise save only in a warning-only post-job hook. Verify + # them before allowing the source-restore consumer to start. The combined + # action may later attempt an immutable duplicate save on an initial miss. + - name: Save seed source cache + id: source-save-1 + uses: actions/cache/save@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ steps.cache.outputs.source-cache-key }} + - name: Verify seed source publication + id: source-verify-1 + uses: actions/cache/restore@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ steps.cache.outputs.source-cache-key }} + lookup-only: true + - name: Back off before retrying source publication + if: steps.source-verify-1.outputs.cache-hit != 'true' + run: sleep 60 + - name: Retry seed source cache save + id: source-save-2 + if: steps.source-verify-1.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ steps.cache.outputs.source-cache-key }} + - name: Require seed source publication + id: source-verify-2 + if: steps.source-verify-1.outputs.cache-hit != 'true' + uses: actions/cache/restore@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ steps.cache.outputs.source-cache-key }} + lookup-only: true + fail-on-cache-miss: true + + # Cache availability checks can return successfully before applying + # fail-on-cache-miss. Only an exact lookup hit establishes publication. + - name: Require exact source seed publication + env: + CACHE_SEED_FIRST_HIT: ${{ steps.source-verify-1.outputs.cache-hit }} + CACHE_SEED_SECOND_HIT: ${{ steps.source-verify-2.outputs.cache-hit }} run: | - host=$(rustc -vV | sed -n 's/^host: //p') - test -n "$host" - cargo build --locked --lib --target "$host" - cargo build --locked --lib --target wasm32-wasip1 + if [[ "$CACHE_SEED_FIRST_HIT" != true && "$CACHE_SEED_SECOND_HIT" != true ]]; then + echo '::error::source seed was not verified with an exact cache hit after two attempts' + exit 1 + fi + + # Cache save warnings do not fail the action. Verify publication explicitly + # so a throttled seed cannot report success and fail both consumer jobs. + - name: Save seed target cache + id: target-save-1 + uses: actions/cache/save@v5 + with: + path: ${{ steps.cache.outputs.cargo-target-dir }} + key: ${{ steps.cache.outputs.target-cache-key }} + - name: Verify seed target publication + id: target-verify-1 + uses: actions/cache/restore@v5 + with: + path: ${{ steps.cache.outputs.cargo-target-dir }} + key: ${{ steps.cache.outputs.target-cache-key }} + lookup-only: true + - name: Back off before retrying target publication + if: steps.target-verify-1.outputs.cache-hit != 'true' + run: sleep 60 + - name: Retry seed target cache save + id: target-save-2 + if: steps.target-verify-1.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ${{ steps.cache.outputs.cargo-target-dir }} + key: ${{ steps.cache.outputs.target-cache-key }} + - name: Require seed target publication + id: target-verify-2 + if: steps.target-verify-1.outputs.cache-hit != 'true' + uses: actions/cache/restore@v5 + with: + path: ${{ steps.cache.outputs.cargo-target-dir }} + key: ${{ steps.cache.outputs.target-cache-key }} + lookup-only: true + fail-on-cache-miss: true + + # Cache availability checks can return successfully before applying + # fail-on-cache-miss. Only an exact lookup hit establishes publication. + - name: Require exact target seed publication + env: + CACHE_SEED_FIRST_HIT: ${{ steps.target-verify-1.outputs.cache-hit }} + CACHE_SEED_SECOND_HIT: ${{ steps.target-verify-2.outputs.cache-hit }} + run: | + if [[ "$CACHE_SEED_FIRST_HIT" != true && "$CACHE_SEED_SECOND_HIT" != true ]]; then + echo '::error::target seed was not verified with an exact cache hit after two attempts' + exit 1 + fi cache-restore-smoke: needs: cache-restore-seed