hermes: accept merged lineage in handoff acceptance

The reviewed PR stack is now merged into main, so an open draft PR #19 is
no longer proof that the reviewed code is what runs. The mandatory
release.exact-lineage-is-running check now requires the merged terminal
state instead: PR #19 closed with merged=true, base main, the existing
feature ref, and the exact reviewed head, plus a new merge-ancestry step
that proves the reviewed head is an ancestor of the pinned origin/main
via git merge-base --is-ancestor. An open PR, a PR closed without
merging, a mismatched head, or a head that is not a proven ancestor of
main still fails closed; an undecidable ancestry probe is NOT_RUN. The
recorded pre-merge base SHA is no longer compared against current main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jenkins 2026-08-18 16:19:30 -03:00
parent ea97d37524
commit a5cafddd45
5 changed files with 58 additions and 11 deletions

View File

@ -136,7 +136,12 @@ unknown or reconciling state is not healthy.
`release.exact-lineage-is-running` atomically binds:
- fixed remote `main` SHA and local `origin/main`;
- open draft PR #19, base `main`, existing feature ref, and exact reviewed head;
- merged PR #19`state=closed` with `merged=true`, base `main`, the existing
feature ref, and the exact reviewed head — with that head additionally proven
an ancestor of the release `main` by `git merge-base --is-ancestor`. A PR
that is still open, closed without merging, or carrying a different head is a
`FAIL`: after integration, only the merged terminal state proves the reviewed
code is what runs;
- the exact `:git-<source-SHA>-build-<n>@sha256:<digest>` image identity,
with the source SHA equal to the release `main` SHA;
- current Deployment generation and revision;

View File

@ -225,7 +225,7 @@ def _release_lineage(targets: Targets) -> CheckSpec:
)
return check(
"release.exact-lineage-is-running",
"Remote main, reviewed PR, image, build, Flux, and running revision are identical",
"Remote main, merged reviewed PR, image, build, Flux, and running revision agree",
"release",
"release_lineage",
[
@ -251,6 +251,15 @@ def _release_lineage(targets: Targets) -> CheckSpec:
"rev-parse",
f"{targets.remote}/main",
),
step(
"merge-ancestry",
OPERATOR,
"git",
"merge-base",
"--is-ancestor",
targets.reviewed_head_sha,
f"{targets.remote}/main",
),
step(
"deployment",
OPERATOR,

View File

@ -420,11 +420,9 @@ def evaluate_release_lineage(spec: CheckSpec, outcomes: dict[str, Outcome]) -> E
return pr
fields = {
"number": expected["pr_number"],
"state": "open",
"draft": True,
"merged": False,
"state": "closed",
"merged": True,
"base.ref": "main",
"base.sha": main_sha,
"head.ref": expected["head_ref"],
"head.sha": head_sha,
}
@ -437,6 +435,10 @@ def evaluate_release_lineage(spec: CheckSpec, outcomes: dict[str, Outcome]) -> E
return Evaluation(FAIL, f"reviewed PR {path} does not match", {path: actual})
if outcomes["build-source"].stdout.strip() != build_sha:
return Evaluation(FAIL, "build source SHA does not match the fixed release build SHA")
ancestry = outcomes["merge-ancestry"]
if not ancestry.ok:
status = FAIL if ancestry.returncode == 1 else NOT_RUN
return Evaluation(status, "reviewed head is not a proven ancestor of remote main")
deployment = outcomes["deployment"].stdout.strip().split("\t")
if len(deployment) != 6:
return Evaluation(NOT_RUN, "Deployment projection is malformed")

View File

@ -134,7 +134,7 @@ def validate_targets(targets: Targets) -> list[str]:
or targets.reviewed_head_ref != "feature/hermes-full-handoff-acceptance"
):
problems.append(
"reviewed PR identity must remain fixed to draft PR #19 and its existing branch"
"reviewed PR identity must remain fixed to PR #19 and its existing branch"
)
for label, value in (
("baseline commit", targets.baseline_commit),

View File

@ -341,6 +341,7 @@ def lineage_spec():
"remote-main",
"reviewed-pr",
"build-source",
"merge-ancestry",
"deployment",
"pods",
"replicasets",
@ -364,16 +365,18 @@ def lineage_spec():
def lineage_outcomes():
pull = {
"number": 19,
"state": "open",
"draft": True,
"merged": False,
"base": {"ref": "main", "sha": MAIN},
"state": "closed",
"draft": False,
"merged": True,
# The recorded base SHA is the pre-merge base, not the current main.
"base": {"ref": "main", "sha": "3" * 40},
"head": {"ref": "feature/hermes-full-handoff-acceptance", "sha": HEAD},
}
return {
"remote-main": outcome(stdout=f"{MAIN}\trefs/heads/main\n"),
"reviewed-pr": outcome(stdout=json.dumps(pull)),
"build-source": outcome(stdout=BUILD),
"merge-ancestry": outcome(),
"deployment": outcome(stdout=f"5\t5\t2\t2\t7\t{IMAGE}"),
"replicasets": outcome(
stdout=f"rs-old\t6\t0\t0\t{IMAGE}\told\nrs-live\t7\t2\t2\t{IMAGE}\thash7\n"
@ -392,6 +395,34 @@ def test_release_lineage_atomically_binds_every_release_identity() -> None:
assert evaluate(lineage_spec(), lineage_outcomes()).status == model.PASS
@pytest.mark.parametrize(
"patch",
[
{"state": "open", "draft": True, "merged": False},
{"state": "closed", "merged": False},
{"head": {"ref": "feature/hermes-full-handoff-acceptance", "sha": "9" * 40}},
],
ids=["still-open-draft", "closed-without-merging", "wrong-head-sha"],
)
def test_release_lineage_accepts_only_the_merged_terminal_pr_state(patch) -> None:
"""After integration, only the merged PR proves the reviewed code runs."""
outcomes = lineage_outcomes()
pull = {**json.loads(outcomes["reviewed-pr"].stdout), **patch}
outcomes["reviewed-pr"] = outcome(stdout=json.dumps(pull))
assert evaluate(lineage_spec(), outcomes).status == model.FAIL
def test_release_lineage_requires_the_merged_head_to_be_an_ancestor_of_main() -> None:
not_ancestor = lineage_outcomes()
not_ancestor["merge-ancestry"] = outcome(returncode=1)
assert evaluate(lineage_spec(), not_ancestor).status == model.FAIL
undecidable = lineage_outcomes()
undecidable["merge-ancestry"] = outcome(returncode=128)
assert evaluate(lineage_spec(), undecidable).status == model.NOT_RUN
@pytest.mark.parametrize(
("key", "bad"),
[