Delete the retired shared browser receipt

The per-commit receipt change left
frontend-modern/browser-verification.json in the tree, frozen, so that
it would not conflict with the pull requests still rewriting it. Nothing
reads the file at the tip: range mode reads a receipt from the commit
that recorded it. Left in place it invites the old habit and keeps those
pull requests conflicting with each other on it.

The guard now refuses a staged tree or a range tip that carries the
file, rather than only a commit that writes it. Without that, resolving
the modify/delete conflict by keeping the file changes nothing against
HEAD, passes the hook, and restores the file on main when the branch
merges.

A pull request that still rewrites the file is repaired by merging main
and removing the file with git rm. Its commits remain valid evidence in
range mode.
This commit is contained in:
rcourtman
2026-10-03 11:15:28 +01:00
parent fe82cb29a3
commit 2a247e244e
4 changed files with 56 additions and 134 deletions
@@ -30,9 +30,7 @@ RECEIPT_DIR = "frontend-modern/browser-verification"
# The single shared receipt every frontend commit rewrote before per-commit
# receipts. It is retired: nothing may write it again. Range mode still reads
# it from commits that modified it, because those commits were verified
# honestly against a tree that carried it and are still merging. The file
# itself is deleted in a change of its own, since deleting it conflicts with
# every open pull request that still rewrites it.
# honestly against a tree that carried it and are still merging.
LEGACY_RECEIPT_PATH = "frontend-modern/browser-verification.json"
RECEIPT_FINGERPRINT_LENGTH = 16
DEFAULT_LANDED_REF = "origin/main"
@@ -452,7 +450,8 @@ PRUNE_SEPARATELY = (
RETIRED_RECEIPT = (
f"{LEGACY_RECEIPT_PATH} is retired and must not be written. Each commit now "
f"{LEGACY_RECEIPT_PATH} is retired and must not exist. Remove it with git rm; a "
"receipt it held stays valid in the commit that recorded it. Each commit now "
f"records its receipt in its own file under {RECEIPT_DIR}/, so concurrent "
"frontend changes no longer conflict."
)
@@ -472,11 +471,15 @@ def range_receipt_coverage(
that fails contributes no coverage and is reported as a diagnostic.
Violations are returned separately because they block the range even when
coverage is complete: a commit that both records and deletes receipts.
coverage is complete: a tip that still carries the retired shared receipt
(recreated, or kept while resolving a merge with the main that deleted
it), or a commit that both records and deletes receipts.
"""
covered: dict[str, set[str]] = {}
diagnostics: list[str] = []
violations: list[str] = []
if LEGACY_RECEIPT_PATH in blob_ids(head, [LEGACY_RECEIPT_PATH], repo_root=repo_root):
violations.append(RETIRED_RECEIPT)
for commit in receipt_commits_in_range(base, head, repo_root=repo_root):
try:
changes = receipt_changes(commit=commit, repo_root=repo_root)
@@ -677,20 +680,25 @@ def check_commit(
inherited = merged_in_paths(
sorted({*changes, *frontend_paths}), commit=commit, repo_root=repo_root
)
# The index is held to the tree it will become: a merge that keeps the
# shared receipt changes nothing against HEAD yet would restore it on
# main. A named commit is held only to what it writes itself.
retired_receipt_written = (
changes.get(LEGACY_RECEIPT_PATH, "D") != "D"
if commit
else LEGACY_RECEIPT_PATH
in blob_ids(None, [LEGACY_RECEIPT_PATH], repo_root=repo_root)
)
except subprocess.CalledProcessError as exc:
return blocked(f"unable to inspect browser verification state: {exc}")
changes = {path: status for path, status in changes.items() if path not in inherited}
if changes.get(LEGACY_RECEIPT_PATH, "D") != "D":
if retired_receipt_written:
return blocked(
RETIRED_RECEIPT,
advice=[
"While concluding a merge, take the merged parent's version: "
f"git checkout MERGE_HEAD -- {LEGACY_RECEIPT_PATH}"
if merging
else f"Unstage it and run: {GUARD_COMMAND} --write-template"
],
advice=[f"For a new receipt, run: {GUARD_COMMAND} --write-template"],
)
changes = {path: status for path, status in changes.items() if path not in inherited}
frontend_paths = [path for path in frontend_paths if path not in inherited]
recorded = sorted(
path for path, status in changes.items() if status != "D" and path != LEGACY_RECEIPT_PATH
@@ -61,6 +61,9 @@ class BrowserVerificationGuardTest(unittest.TestCase):
self.assertLess(formatter, guard)
def test_shared_receipt_is_retired_from_the_tree(self) -> None:
self.assertFalse((REPO_ROOT / LEGACY_RECEIPT_PATH).exists())
def test_frontend_runtime_paths_exclude_tests_and_receipt(self) -> None:
self.assertEqual(
frontend_runtime_paths(
@@ -722,23 +725,21 @@ class IntegrationRangeTest(GuardRepoTestCase):
class RetiredSharedReceiptTest(GuardRepoTestCase):
"""Work verified against the shared receipt keeps its evidence; nothing new may write it."""
"""Work verified against the shared receipt keeps its evidence; nothing new may use it."""
def setUp(self) -> None:
super().setUp()
self.write_receipt([CHANGED_PATH], at=LEGACY_RECEIPT_PATH)
self.base = self.commit("main while the shared receipt was current")
def shared_receipt_change(self, path: str, text: str, message: str) -> str:
self.write(path, text)
self.write_receipt([path], at=LEGACY_RECEIPT_PATH)
return self.commit(message)
def test_in_flight_commit_is_repaired_by_merging_main(self) -> None:
def test_in_flight_commit_survives_a_merge_of_the_retirement(self) -> None:
self.git("checkout", "--quiet", "-b", "in-flight", self.base)
self.shared_receipt_change(CHANGED_PATH, "export const a = 1;\n", "in flight")
self.write(CHANGED_PATH, "export const a = 1;\n")
self.write_receipt([CHANGED_PATH], at=LEGACY_RECEIPT_PATH)
self.commit("verified before the retirement")
self.git("checkout", "--quiet", "main")
landed = self.shared_receipt_change(OTHER_PATH, "export const b = 1;\n", "landed first")
self.git("rm", "--quiet", LEGACY_RECEIPT_PATH)
retired = self.commit("retire the shared receipt")
self.git("checkout", "--quiet", "in-flight")
merge = subprocess.run(
@@ -746,20 +747,33 @@ class RetiredSharedReceiptTest(GuardRepoTestCase):
"merge", "--quiet", "--no-ff", "main"],
cwd=self.repo_root, capture_output=True, text=True, env=self.env,
)
self.assertNotEqual(merge.returncode, 0, "both sides rewrote the shared receipt")
self.assertNotEqual(merge.returncode, 0, "modify/delete on the shared receipt")
# A hand-combined resolution is a new write of the retired file.
self.write(LEGACY_RECEIPT_PATH, "{}\n")
# Keeping the file changes nothing against HEAD, but would restore it
# on main once this branch merges.
self.git("add", LEGACY_RECEIPT_PATH)
status, _, stderr = self.run_guard()
self.assertEqual(status, 1)
self.assertIn(f"{LEGACY_RECEIPT_PATH} is retired", stderr)
self.assertIn("git checkout MERGE_HEAD", stderr)
self.git("checkout", "MERGE_HEAD", "--", LEGACY_RECEIPT_PATH)
self.git("rm", "--quiet", "--force", LEGACY_RECEIPT_PATH)
self.assertEqual(self.run_guard()[0], 0)
self.commit("merge main")
self.assertEqual(self.run_range(base=landed), (0, ""))
self.assertEqual(self.run_range(base=retired), (0, ""))
def test_recreating_the_shared_receipt_is_rejected_even_with_a_valid_one(self) -> None:
self.git("rm", "--quiet", LEGACY_RECEIPT_PATH)
retired = self.commit("retire the shared receipt")
self.write(CHANGED_PATH, "export const a = 1;\n")
self.write_receipt([CHANGED_PATH])
self.write_receipt([CHANGED_PATH], at=LEGACY_RECEIPT_PATH)
self.commit("verified, but also recreates the shared receipt")
status, stderr = self.run_range(base=retired)
self.assertEqual(status, 1)
self.assertIn(f"{LEGACY_RECEIPT_PATH} is retired", stderr)
self.assertNotIn("is not verified", stderr)
class PruneTest(GuardRepoTestCase):