From b1a1d5892d4c1c93e8ac21168dbea18d0c913722 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 23 Sep 2026 18:45:26 -0700 Subject: [PATCH 1/3] test: reload-build caches must survive ref spelling and branch changes Run 35938367902 dispatched reload-build with a short SHA and rebuilt cold in 753 seconds; a follow-up dispatch with the full SHA on the same branch also restored nothing. The DerivedData key embeds the raw `ref` input, and the only cross-branch fallback is a `main-` prefix that no dispatch saves. This guard runs the workflow's real cache-metadata script and asserts that every spelling of one commit keys identically, and that a later commit under any ref text restores an earlier entry. It fails on main. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci-guards.yml | 4 + tests/test-execution.toml | 4 + tests/test_ci_reload_build_cache_keys.py | 208 +++++++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 tests/test_ci_reload_build_cache_keys.py diff --git a/.github/workflows/ci-guards.yml b/.github/workflows/ci-guards.yml index 06b8243a5587..db36c765d9ff 100644 --- a/.github/workflows/ci-guards.yml +++ b/.github/workflows/ci-guards.yml @@ -527,6 +527,10 @@ jobs: if: ${{ matrix.group == 'app-host-cache' }} run: python3 tests/test_ci_sanitize_xcode_source_packages_cache.py + - name: Validate reload-build cache keys + if: ${{ matrix.group == 'app-host-cache' }} + run: python3 tests/test_ci_reload_build_cache_keys.py + - name: Validate local build cache preflight if: ${{ matrix.group == 'app-host-cache' }} run: | diff --git a/tests/test-execution.toml b/tests/test-execution.toml index 1d5d2facabaa..f10e0349cc73 100644 --- a/tests/test-execution.toml +++ b/tests/test-execution.toml @@ -439,6 +439,10 @@ lane = "linux-guard" path = "tests/test_ci_sanitize_xcode_source_packages_cache.py" lane = "linux-guard" +[[test]] +path = "tests/test_ci_reload_build_cache_keys.py" +lane = "linux-guard" + [[test]] path = "tests/test_ci_select_package_tests.py" lane = "linux-guard" diff --git a/tests/test_ci_reload_build_cache_keys.py b/tests/test_ci_reload_build_cache_keys.py new file mode 100644 index 000000000000..50a839125df9 --- /dev/null +++ b/tests/test_ci_reload_build_cache_keys.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""Behavioral guard: reload-build's macOS caches stay warm across ref spellings. + +reload-cloud dispatches reload-build.yml with a `ref` input that may be a +branch, a short SHA, or a full SHA, often for an ephemeral branch that exists +for one call. Run 35938367902 (ref=9a0702e0f5, a short SHA) and run +35941854226 (ref=a full SHA on the same branch) both restored nothing; the +first rebuilt cold in 753 seconds. The DerivedData key embedded the raw +`ref` text, so every spelling produced a new key, and the only cross-branch +fallback was a `main-` prefix no run ever saved. + +This test runs the workflow's real "Prepare macOS cache metadata" script in a +scratch Git repository and resolves the restore-keys the workflow passes to +actions/cache, then asserts: + +- every spelling of one commit produces the same save key; +- a later commit, dispatched under any ref text, restores an entry saved by an + earlier one through a restore-key that names neither a branch nor a SHA. +""" + +from __future__ import annotations + +import os +import re +import subprocess +import tempfile +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / ".github" / "workflows" / "reload-build.yml" +METADATA_STEP = "Prepare macOS cache metadata" +OUTPUT_REF = re.compile(r"^\$\{\{ steps\.cache_meta\.outputs\.([A-Za-z0-9_]+) \}\}$") + +FAKE_XCODEBUILD = """#!/bin/sh +printf 'Xcode 27.0\\nBuild version 27A266a\\n' +""" + + +def build_steps() -> list[dict]: + workflow = yaml.safe_load(WORKFLOW.read_text()) + return workflow["jobs"]["build"]["steps"] + + +def step_named(name: str) -> dict: + for step in build_steps(): + if step.get("name") == name: + return step + raise AssertionError(f"reload-build.yml has no step named {name!r}") + + +def git(repo: Path, *args: str) -> str: + return subprocess.run( + ["git", "-C", str(repo), *args], + check=True, + text=True, + capture_output=True, + env={**os.environ, "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_NOSYSTEM": "1"}, + ).stdout.strip() + + +def make_repo(root: Path) -> Path: + repo = root / "work" / "cmux" / "cmux" + repo.mkdir(parents=True) + git(repo, "init", "-q", "-b", "reload-blacksmith/12624-cloud-auth-latency-1789854264") + git(repo, "config", "user.email", "guard@example.invalid") + git(repo, "config", "user.name", "guard") + resolved = repo / "cmux.xcodeproj" / "project.xcworkspace" / "xcshareddata" / "swiftpm" + resolved.mkdir(parents=True) + (resolved / "Package.resolved").write_text('{"pins": [], "version": 3}\n') + git(repo, "add", "-A") + git(repo, "commit", "-q", "-m", "fixture") + return repo + + +def commit_change(repo: Path, text: str) -> None: + (repo / "Sources.swift").write_text(text) + # A package bump changes Package.resolved; the SourcePackages checkouts of + # every unchanged package are still worth restoring. + resolved = repo / "cmux.xcodeproj" / "project.xcworkspace" / "xcshareddata" / "swiftpm" + (resolved / "Package.resolved").write_text(f'{{"pins": ["{text.strip()}"], "version": 3}}\n') + git(repo, "add", "-A") + git(repo, "commit", "-q", "-m", text) + + +def cache_metadata(repo: Path, source_ref: str, run_id: str) -> dict[str, str]: + script = step_named(METADATA_STEP)["run"] + bin_dir = repo.parent.parent.parent / "bin" + bin_dir.mkdir(exist_ok=True) + fake = bin_dir / "xcodebuild" + fake.write_text(FAKE_XCODEBUILD) + fake.chmod(0o755) + output = repo.parent.parent.parent / f"github-output-{run_id}" + output.write_text("") + env = { + **os.environ, + "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}", + "SOURCE_REF": source_ref, + "GITHUB_OUTPUT": str(output), + "GITHUB_WORKSPACE": "/Users/runner/_work/cmux/cmux", + "GITHUB_RUN_ID": run_id, + "GITHUB_RUN_ATTEMPT": "1", + "GIT_CONFIG_GLOBAL": os.devnull, + "GIT_CONFIG_NOSYSTEM": "1", + } + result = subprocess.run( + ["bash", "-e", "-c", script], + cwd=repo, + env=env, + text=True, + capture_output=True, + check=False, + ) + assert result.returncode == 0, result.stderr + values = {} + for line in output.read_text().splitlines(): + name, _, value = line.partition("=") + values[name] = value + return values + + +def resolved_restore_keys(step_name: str, outputs: dict[str, str]) -> list[str]: + raw = step_named(step_name)["with"].get("restore-keys", "") + keys = [] + for line in str(raw).splitlines(): + line = line.strip() + if not line: + continue + match = OUTPUT_REF.match(line) + assert match, f"{step_name}: restore-key {line!r} is not a cache_meta output" + keys.append(outputs[match.group(1)]) + return [key for key in keys if key] + + +def resolved_key(step_name: str, outputs: dict[str, str]) -> str: + match = OUTPUT_REF.match(str(step_named(step_name)["with"]["key"]).strip()) + assert match, f"{step_name}: key is not a cache_meta output" + return outputs[match.group(1)] + + +def restores(step_name: str, outputs: dict[str, str], saved_key: str) -> bool: + """actions/cache semantics: exact key first, then each restore-key prefix.""" + if resolved_key(step_name, outputs) == saved_key: + return True + return any(saved_key.startswith(prefix) for prefix in resolved_restore_keys(step_name, outputs)) + + +def test_every_ref_spelling_of_one_commit_keys_identically() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + repo = make_repo(Path(temp_dir)) + branch = git(repo, "branch", "--show-current") + full_sha = git(repo, "rev-parse", "HEAD") + spellings = ["", full_sha[:10], full_sha, branch, f"refs/heads/{branch}"] + bases = { + spelling or "": cache_metadata(repo, spelling, "100")["derived_data_key_base"] + for spelling in spellings + } + assert len(set(bases.values())) == 1, ( + "DerivedData key must depend on the resolved commit, not the ref text:\n" + + "\n".join(f" {name}: {base}" for name, base in bases.items()) + ) + + +def test_later_commit_restores_an_earlier_entry_under_any_ref_text() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + repo = make_repo(Path(temp_dir)) + first_sha = git(repo, "rev-parse", "HEAD") + earlier = cache_metadata(repo, first_sha[:10], "200") + commit_change(repo, "let changed = true\n") + second_sha = git(repo, "rev-parse", "HEAD") + for spelling in (second_sha, second_sha[:7], "some-other-ephemeral-branch", ""): + later = cache_metadata(repo, spelling, "201") + for step, key in ( + ("Restore DerivedData cache", earlier["derived_data_key"]), + ("Restore SPM SourcePackages cache", earlier["spm_key"]), + ): + assert restores(step, later, key), ( + f"{step}: dispatch ref {spelling or ''!r} cannot restore " + f"{key!r}; restore-keys were {resolved_restore_keys(step, later)!r}" + ) + + +def test_generic_fallback_names_no_branch_or_commit() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + repo = make_repo(Path(temp_dir)) + sha = git(repo, "rev-parse", "HEAD") + outputs = cache_metadata(repo, sha[:10], "300") + fallback = resolved_restore_keys("Restore DerivedData cache", outputs)[-1] + branch = git(repo, "branch", "--show-current") + assert sha not in fallback and sha[:10] not in fallback, fallback + assert branch.split("/")[0] not in fallback, fallback + # The path, runner, and Xcode contracts still gate the fallback. + for part in ("xcode-27.0-27A266a", outputs["workspace_key"], outputs["runner_key"]): + assert part in fallback, (part, fallback) + + +def main() -> int: + test_every_ref_spelling_of_one_commit_keys_identically() + test_later_commit_restores_an_earlier_entry_under_any_ref_text() + test_generic_fallback_names_no_branch_or_commit() + print("PASS: reload-build caches key on the commit and fall back across refs") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 980895d58f587b93cb4d75299c60b56858a3483b Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 23 Sep 2026 18:54:37 -0700 Subject: [PATCH 2/3] ci: key reload-build caches on the commit and fall back across branches reload-build derived its DerivedData key from the raw `ref` input, so a short SHA, a full SHA, and a branch name for one commit each got their own key, and reload-cloud's per-call ephemeral branches never shared an entry. The only cross-branch fallback was `main-`, which a run saves only when someone dispatches `ref=main`, so it was effectively never present. Run 35938367902 restored nothing and built cold in 753 seconds. The DerivedData key is now `commit-`, and the last restore-key is the Xcode/runner/workspace prefix alone: the newest entry from any commit or branch with the same toolchain and checkout path. That is safe to adopt because tracked-source mtimes are already derived from blob ids, so every file that differs from the cached build recompiles, and a failed cached build already retries cold. The SPM cache gains the same prefix fallback, so a Package.resolved bump no longer discards every other package checkout. actions/cache scopes entries to the dispatch ref. A run dispatched on an ephemeral branch still restores default-branch entries but saves where no later run can read; the workflow now says so with a notice, and timings.json records `cache_scope_ref` in place of the retired `cache_branch_key`. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/reload-build.yml | 57 ++++++++++++++++-------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/.github/workflows/reload-build.yml b/.github/workflows/reload-build.yml index 2c36b36d22aa..0f38c3d11ecf 100644 --- a/.github/workflows/reload-build.yml +++ b/.github/workflows/reload-build.yml @@ -4,8 +4,10 @@ name: reload-build # # scripts/reload-cloud.sh --builder blacksmith and scripts/reload-cloud-ios.sh # --builder blacksmith (in the cmuxterm-hq control repo) dispatch this workflow -# against an ephemeral branch holding the caller's working tree, then download the -# produced artifact and install it locally. This is the Blacksmith alternative to +# for an ephemeral branch holding the caller's working tree, then download the +# produced artifact and install it locally. Dispatch on the default branch and +# pass the ephemeral branch as `ref`: caches are scoped to the dispatch ref, so a +# run dispatched on the ephemeral branch itself saves caches no later run reads. This is the Blacksmith alternative to # SSH-leasing a fleet Mac. It is workflow_dispatch ONLY: nothing here runs on push # or pull_request, so it never adds to the heavy CI fan-out. @@ -187,28 +189,23 @@ jobs: if: ${{ inputs.platform == 'macos' }} id: cache_meta env: - SOURCE_REF: ${{ inputs.ref || github.ref_name }} + DISPATCH_REF: ${{ github.ref_name }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | set -euo pipefail + # actions/cache scopes entries to the run's dispatch ref: a run may + # restore entries saved on its own ref or the default branch, and + # nothing else. Say so when this run's save will be invisible to others. + if [ -n "${DEFAULT_BRANCH:-}" ] && [ "${DISPATCH_REF:-}" != "$DEFAULT_BRANCH" ]; then + echo "::notice::Dispatched on '${DISPATCH_REF}', not '${DEFAULT_BRANCH}': this run restores ${DEFAULT_BRANCH}-scoped caches, but the caches it saves are visible only to later dispatches on '${DISPATCH_REF}'. Dispatch on ${DEFAULT_BRANCH} with -f ref= to share them." + fi xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')" xcode_build="$(xcodebuild -version | awk 'NR == 2 { print $3 }')" xcode_key="$(printf 'xcode-%s-%s' "$xcode_version" "$xcode_build" | tr -c 'A-Za-z0-9._-' '-')" - branch_name="${SOURCE_REF#refs/heads/}" - branch_name="${branch_name#refs/tags/}" - if [ -z "$branch_name" ]; then - branch_name="$(git branch --show-current)" - fi - if [ -z "$branch_name" ]; then - branch_name="detached" - fi - branch_slug="$(printf '%s' "$branch_name" | tr -c 'A-Za-z0-9._-' '-' | sed 's/-\{2,\}/-/g; s/^-//; s/-$//')" - branch_digest="$(printf '%s' "$branch_name" | shasum -a 256 | awk '{ print substr($1, 1, 12) }')" - if [ "$branch_name" = "main" ]; then - branch_key="main" - else - branch_key="${branch_slug:0:48}-${branch_digest}" - fi + # Key on the resolved commit, never on the `ref` text. A short SHA, a + # full SHA, and a branch name for one commit must share an entry, and + # reload-cloud's per-call ephemeral branches must not strand theirs. # Xcode's SourcePackages workspace state and DerivedData build # database contain absolute checkout paths. Keep exact-state caches # tied to the path contract; a different runner workspace must cold @@ -243,11 +240,10 @@ jobs: spm_key="" spm_source_prefix="" fi - derived_data_key_base="${derived_prefix}${branch_key}-${source_sha}" + derived_data_key_base="${derived_prefix}commit-${source_sha}" { echo "xcode_key=$xcode_key" - echo "branch_key=$branch_key" echo "workspace_key=$workspace_key" echo "runner_key=$runner_key" echo "source_sha=$source_sha" @@ -256,11 +252,17 @@ jobs: echo "spm_key_base=$spm_key_base" echo "spm_key=$spm_key" echo "spm_source_prefix=$spm_source_prefix" + # Any Package.resolved: a fallback hit is sanitized before use and + # Xcode re-resolves only the packages that changed. + echo "spm_fallback_prefix=$spm_prefix" echo "derived_data_key_base=$derived_data_key_base" echo "derived_data_key=${derived_data_key_base}-run-${cache_run_key}" echo "derived_data_source_prefix=${derived_data_key_base}-run-" - echo "derived_data_branch_prefix=${derived_prefix}${branch_key}-" - echo "derived_data_main_prefix=${derived_prefix}main-" + # Newest entry from any commit or branch with the same Xcode, runner + # OS/arch, and checkout path. Safe to adopt: tracked-source mtimes are + # derived from blob ids below, so every file that differs from the + # cached build recompiles, and a cached build failure retries cold. + echo "derived_data_fallback_prefix=${derived_prefix}" } >> "$GITHUB_OUTPUT" # The verified prebuilt is the happy path for both platforms. Zig is @@ -329,7 +331,9 @@ jobs: with: path: .ci-source-packages key: ${{ steps.cache_meta.outputs.spm_key }} - restore-keys: ${{ steps.cache_meta.outputs.spm_source_prefix }} + restore-keys: | + ${{ steps.cache_meta.outputs.spm_source_prefix }} + ${{ steps.cache_meta.outputs.spm_fallback_prefix }} - name: Finish SPM cache restore if: ${{ always() && inputs.platform == 'macos' }} @@ -430,8 +434,7 @@ jobs: key: ${{ steps.cache_meta.outputs.derived_data_key }} restore-keys: | ${{ steps.cache_meta.outputs.derived_data_source_prefix }} - ${{ steps.cache_meta.outputs.derived_data_branch_prefix }} - ${{ steps.cache_meta.outputs.derived_data_main_prefix }} + ${{ steps.cache_meta.outputs.derived_data_fallback_prefix }} - name: Finish DerivedData cache restore if: ${{ always() && inputs.platform == 'macos' }} @@ -840,7 +843,7 @@ jobs: ZIG_INSTALL_SECONDS: ${{ steps.zig_fallback.outputs.seconds || '0' }} GHOSTTYKIT_FALLBACK_SECONDS: ${{ steps.ghosttykit_fallback.outputs.seconds || '0' }} CACHE_XCODE_KEY: ${{ steps.cache_meta.outputs.xcode_key }} - CACHE_BRANCH_KEY: ${{ steps.cache_meta.outputs.branch_key }} + CACHE_SCOPE_REF: ${{ github.ref_name }} CACHE_WORKSPACE_KEY: ${{ steps.cache_meta.outputs.workspace_key }} CACHE_RUNNER_KEY: ${{ steps.cache_meta.outputs.runner_key }} SPM_CACHE_PRIMARY_KEY: ${{ steps.cache_meta.outputs.spm_key }} @@ -899,7 +902,7 @@ jobs: "zig_install_seconds": integer("ZIG_INSTALL_SECONDS"), "ghosttykit_fallback_seconds": integer("GHOSTTYKIT_FALLBACK_SECONDS"), "cache_xcode_key": os.environ.get("CACHE_XCODE_KEY", ""), - "cache_branch_key": os.environ.get("CACHE_BRANCH_KEY", ""), + "cache_scope_ref": os.environ.get("CACHE_SCOPE_REF", ""), "cache_workspace_key": os.environ.get("CACHE_WORKSPACE_KEY", ""), "cache_runner_key": os.environ.get("CACHE_RUNNER_KEY", ""), "spm_cache_primary_key": os.environ.get("SPM_CACHE_PRIMARY_KEY", ""), From 5ff0959cd5df55c01e5194e047bc43a55bc41535 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 23 Sep 2026 19:02:42 -0700 Subject: [PATCH 3/3] test: feed reload-build's cache metadata step its own env per dispatch The spelling guard injected SOURCE_REF, which the fixed step no longer reads, so it could not catch ref-text keying reintroduced under another name. It now evaluates the step's own env block with each spelling substituted for every ref-bearing expression, fails on an expression it does not model, and checks the notice for dispatches off the default branch. It still fails against the workflow on main. Also rewraps the workflow header. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/reload-build.yml | 7 ++-- tests/test_ci_reload_build_cache_keys.py | 52 ++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/.github/workflows/reload-build.yml b/.github/workflows/reload-build.yml index 0f38c3d11ecf..1742117a7df2 100644 --- a/.github/workflows/reload-build.yml +++ b/.github/workflows/reload-build.yml @@ -7,9 +7,10 @@ name: reload-build # for an ephemeral branch holding the caller's working tree, then download the # produced artifact and install it locally. Dispatch on the default branch and # pass the ephemeral branch as `ref`: caches are scoped to the dispatch ref, so a -# run dispatched on the ephemeral branch itself saves caches no later run reads. This is the Blacksmith alternative to -# SSH-leasing a fleet Mac. It is workflow_dispatch ONLY: nothing here runs on push -# or pull_request, so it never adds to the heavy CI fan-out. +# run dispatched on the ephemeral branch itself saves caches no later run reads. +# This is the Blacksmith alternative to SSH-leasing a fleet Mac. It is +# workflow_dispatch ONLY: nothing here runs on push or pull_request, so it never +# adds to the heavy CI fan-out. on: workflow_dispatch: diff --git a/tests/test_ci_reload_build_cache_keys.py b/tests/test_ci_reload_build_cache_keys.py index 50a839125df9..d030d01d8b53 100644 --- a/tests/test_ci_reload_build_cache_keys.py +++ b/tests/test_ci_reload_build_cache_keys.py @@ -32,6 +32,7 @@ ROOT = Path(__file__).resolve().parents[1] WORKFLOW = ROOT / ".github" / "workflows" / "reload-build.yml" METADATA_STEP = "Prepare macOS cache metadata" +EXPRESSION = re.compile(r"\$\{\{\s*(.*?)\s*\}\}") OUTPUT_REF = re.compile(r"^\$\{\{ steps\.cache_meta\.outputs\.([A-Za-z0-9_]+) \}\}$") FAKE_XCODEBUILD = """#!/bin/sh @@ -85,8 +86,42 @@ def commit_change(repo: Path, text: str) -> None: git(repo, "commit", "-q", "-m", text) -def cache_metadata(repo: Path, source_ref: str, run_id: str) -> dict[str, str]: +def step_env(source_ref: str, dispatch_ref: str) -> dict[str, str]: + """Evaluate the step's own `env:` block for one dispatch. + + Every expression that can carry ref text receives the spelling under test, + so ref-text keying fails here under any env name the step chooses. + """ + values = { + "inputs.ref": source_ref, + "github.ref_name": dispatch_ref, + "github.ref": f"refs/heads/{dispatch_ref}", + "github.head_ref": dispatch_ref, + "github.event.repository.default_branch": "main", + } + env = {} + for name, raw in (step_named(METADATA_STEP).get("env") or {}).items(): + match = EXPRESSION.fullmatch(str(raw).strip()) + if not match: + env[name] = str(raw) + continue + text = "" + for operand in match.group(1).split("||"): + operand = operand.strip() + assert operand in values, f"{METADATA_STEP}: unmodelled env expression {raw!r}" + text = values[operand] + if text: + break + env[name] = text + return env + + +def cache_metadata( + repo: Path, source_ref: str, run_id: str, dispatch_ref: str | None = None +) -> dict[str, str]: script = step_named(METADATA_STEP)["run"] + if dispatch_ref is None: + dispatch_ref = source_ref or "main" bin_dir = repo.parent.parent.parent / "bin" bin_dir.mkdir(exist_ok=True) fake = bin_dir / "xcodebuild" @@ -96,8 +131,8 @@ def cache_metadata(repo: Path, source_ref: str, run_id: str) -> dict[str, str]: output.write_text("") env = { **os.environ, + **step_env(source_ref, dispatch_ref), "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}", - "SOURCE_REF": source_ref, "GITHUB_OUTPUT": str(output), "GITHUB_WORKSPACE": "/Users/runner/_work/cmux/cmux", "GITHUB_RUN_ID": run_id, @@ -114,7 +149,7 @@ def cache_metadata(repo: Path, source_ref: str, run_id: str) -> dict[str, str]: check=False, ) assert result.returncode == 0, result.stderr - values = {} + values = {"_stdout": result.stdout} for line in output.read_text().splitlines(): name, _, value = line.partition("=") values[name] = value @@ -196,10 +231,21 @@ def test_generic_fallback_names_no_branch_or_commit() -> None: assert part in fallback, (part, fallback) +def test_off_default_dispatch_warns_that_its_save_is_private() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + repo = make_repo(Path(temp_dir)) + sha = git(repo, "rev-parse", "HEAD") + private = cache_metadata(repo, sha, "400", dispatch_ref="reload-blacksmith/one-call") + shared = cache_metadata(repo, sha, "401", dispatch_ref="main") + assert "::notice::" in private["_stdout"], private["_stdout"] + assert "::notice::" not in shared["_stdout"], shared["_stdout"] + + def main() -> int: test_every_ref_spelling_of_one_commit_keys_identically() test_later_commit_restores_an_earlier_entry_under_any_ref_text() test_generic_fallback_names_no_branch_or_commit() + test_off_default_dispatch_warns_that_its_save_is_private() print("PASS: reload-build caches key on the commit and fall back across refs") return 0