ci(release): the milestone gate is blind to a revert, vouches for a no-op, and calls HEAD the release - #291
Merged
Merged
Conversation
…ne now `check-milestone-landed.sh` asks one question: is this PR's merge commit an ancestor of the tree being released? That detects a PR that NEVER REACHED main -- #245, stranded on a branch whose base was squash-merged first. It is structurally blind to the other way the same claim goes wrong: a PR that REACHED MAIN AND WAS TAKEN BACK OUT. A reverted PR's merge commit stays an ancestor forever, so the ancestry test reports it landed while the tree has none of its lines. Both end in the identical false statement -- the milestone says the release contains a change the tree does not have -- and only one of them was checked. **This repository contains a live instance right now.** PR #276 reverted #259 (`90c96f0`) and #261 (`d0118fc`): git merge-base --is-ancestor 90c96f0 HEAD -> exit 0 "LANDED" git merge-base --is-ancestor d0118fc HEAD -> exit 0 "LANDED" grep -c 'is-ancestor|not on main' check-release-tag.sh at 90c96f0: 3 in the tree: 0 grep -c mariadb src/recotem/training/search.py at d0118fc: 5 in the tree: 0 Not a 2.1.0 problem: both carry milestone 2.2.0, so the 2.1.0 check never looks at them. But **on the day 2.2.0 is cut, this gate would green-light a release missing exactly the content it was written to catch.** That is #245's failure one turn of the crank later, and the reason to fix it before the milestone that contains the instance comes up rather than after. ## The check Each PR that passes the ancestry test is now also checked for a revert that still stands, keyed on the canonical `This reverts commit <40-hex>` trailer -- a record `git revert` and GitHub's own Revert button both write, so it is produced by the tooling rather than by a convention anyone has to remember. `--fixed-strings` keeps the SHA out of the regex engine. Two things it deliberately does NOT do: * **A revert of a revert is not reported.** "We put it back" leaves the content in the tree, and a gate that fails a healthy release is a gate that gets switched off. Depth is one un-revert, documented at the function: anything deeper is rare enough that the existing waiver is a better answer than more recursion, and a loud wrong answer an operator clears by hand beats a silent one. * **A revert is not a stranding and does not get the stranding remedy.** Being told to cherry-pick a change someone deliberately backed out is the wrong instruction. The message offers both real options and names the usual one first: re-land it and record the replacement, or move the PR off the milestone -- which for a deliberate revert is the honest record and clears the check on its own. The `relanded-prs.tsv` waiver now serves both failure modes, and gained the matching condition: a replacement clears the original only if its own merge commit is an ancestor **and has not itself been reverted**. Without that second half a reverted re-land would keep clearing the original, so the waiver would outlive the change it vouches for. The file's header says all of this, and says not to add a row for a deliberate revert. ## Guard `tests/unit/test_check_milestone_landed.py`, 15 -> 24 tests, driving the real script against synthetic repositories with a fake `gh`, as the existing ones do. `test_ancestry_alone_cannot_see_a_revert` is the positive control on the *defect*: it asserts a reverted commit is still an ancestor and that the content really is gone, so nothing below can pass for the wrong reason. Two run against this repository's own history rather than a fixture, using #259/#261/#276 -- the shape as someone really made it, not one written to be caught. They skip rather than fail where the history is absent (a shallow clone), because "cannot see" and "not present" are different states of knowledge. Mutation-tested, each mutation syntax-checked before the run: revert detection disabled (the shipped, blind behaviour) -> 5 failed waiver stops checking whether the replacement was reverted-> 1 failed depth-1 un-revert dropped (revert-of-a-revert reports) -> 1 failed trailer match dropped (any commit in range counts) -> 8 failed restored -> 24 passed The first row is the one that matters: it is the behaviour on main today, and it takes the live-history test with it. 2891 passed (+9), 4 deselected; ruff check and ruff format --check clean; `bash -n` clean.
… unchecked
P3 found a third class, and it is sharper than the revert one: a PR can merge
cleanly and change nothing, and after merging the gate does not merely fail to
notice -- it ASSERTS the milestone is complete on the strength of it.
class instance ancestry alone
--------------------------------- --------- -------------------------
merged, never reached main #245 catches
reached main, then reverted #259 #261 blind: reports LANDED
merges cleanly, moves no bytes #277 blind: VOUCHES for it
PR #277 is titled "re-land of #259" and is stacked on #276, which reverted
#259. Its branch therefore reverts #259, reverts #261, then restores #259 --
and the first and third cancel. Measured by merging it:
git merge --no-ff pr/277 -> "Merge made by the 'ort' strategy"
git diff --stat origin/main HEAD-> (empty)
check-release-tag.sh -> 305 lines, not #259's 555
'is-ancestor|not on main' hits -> 0, not 3
A PR named for re-landing #259 restores none of it, merges cleanly, and becomes
an ancestor.
It also opens a hole in the revert check in the first commit of this PR, which
is why the two ship together: record that no-op in relanded-prs.tsv as the
re-land of #259 and the waiver clears #259 on the strength of a PR that
restored nothing. The gate would not just miss the gap, it would certify it
closed.
## What this adds
`contributes_nothing` -- the commit's diff against its first parent is empty --
applied in two places: the main loop (a milestone PR that carried nothing is
reported, with its own remedy) and the waiver (a no-op can never clear the PR
it is recorded against). First parent is the right comparison for both shapes
here: for a squash merge it is "what did this add to main", and for a true
merge it is the same question asked of the mainline.
**False-positive rate, counted rather than predicted.** Over all 329
first-parent commits on main -- 4 of them true merges, 1 a root -- the number
with an empty first-parent diff is **ZERO**. A gate that cries wolf on ordinary
refactoring is a gate someone switches off, so this is pinned by a test that
re-counts it against the live history and fails if the number ever moves.
## What this deliberately does NOT add, and why the output now says so
A fourth shape exists: a later commit removes the change with no revert
trailer -- a rewrite, a refactor, a hand-edit. That is a CONTENT question and
content is materially harder than reachability. The cheap forms do not survive
contact with a real repository: re-applying each PR's diff to see whether it is
a no-op flags every file another PR has legitimately touched since, and
hunk-grepping flags every reformat. It is not attempted.
The two classes taken here were taken precisely because they are exact -- "is
there a revert trailer naming this commit" and "is this commit's diff empty"
are both decidable with no heuristic and no threshold.
So the success message stops implying a completeness the gate does not have.
It was a bare "is an ancestor of <sha>", which reads as "the milestone is
complete". It now enumerates the three questions asked and names the one that
is not, in the gate's own output where a reader will actually meet it:
OK: every merged PR in milestone '2.1.0' is in the tree at <sha>.
Checked, per PR: the merge commit is an ancestor; no revert of it still
stands; the merge is not an empty no-op.
NOT checked: whether a later commit removed the change WITHOUT a
'This reverts commit' trailer -- a rewrite or a refactor that drops a
change silently is invisible here. [...]
A gate that announces its own boundary is worth more than one a reader infers
completeness from.
## Guard
`tests/unit/test_check_milestone_landed.py`, 24 -> 30 tests. One builds #277's
actual shape (revert then restore, merged) rather than asserting it, with
fixture self-checks that the merge really is both a no-op and an ancestor. One
is the negative control -- an ordinary change must stay green. One pins the
false-positive count against the live history. One asserts the success message
declares what it does not check.
Mutation-tested, each mutant `bash -n`-checked first:
M1 revert detection disabled (behaviour on main today) -> 6 failed
M2 waiver stops checking the replacement for a revert -> 1 failed
M3 depth-1 un-revert dropped -> 1 failed
M4 revert trailer match dropped -> 9 failed
M5 no-op detection disabled -> 2 failed
M6 waiver stops checking the replacement for a no-op -> 1 failed
M7 emptiness test inverted to always-true (over-broad) -> 9 failed
M8 success message stops declaring what is NOT checked -> 1 failed
restored -> 30 passed
M7 matters as much as M5: it shows the check is bounded in both directions, so
a future edit cannot quietly turn it into something that flags real work.
2897 passed (+6), 4 deselected; ruff check and ruff format --check clean;
`bash -n` clean.
…owns that R9-P3 found a fourth class, and it is not about a PR's content being missing -- it is this gate answering a true question about the wrong tree. Every question here is asked of `git rev-parse HEAD`. "Every milestone PR is an ancestor of HEAD" stays true when HEAD is main plus something smuggled on top: the extra commit is not any PR's merge commit, and nothing here looks for commits that no PR explains. P3 demonstrated it on a real off-main commit carrying a marker -- the script exited 0 and printed the smuggled SHA in its own success line as though it were the release. Reproduced independently here before writing this, on a commit built on top of 7871f9f and not on main: smuggled HEAD 9ffec607d8b5 is it on main? NO milestone PRs checked 13 milestone PRs NOT ancestors of it 0 All thirteen pass. The claim the success line makes is true, and much narrower than it reads. ## Why the rule is not enforced here 1. **It belongs to the tag guard.** `check-release-tag.sh` runs on the tag, where HEAD is always the tagged commit, and #259 implemented it there: shallow-clone refusal first (a shallow clone answers ancestry *wrongly* rather than failing), then `origin/main` else `refs/heads/main`, then `merge-base --is-ancestor HEAD <main>`. Two owners for one rule is how a rule ends up with none. 2. **This script is documented as a local pre-flight**, run from a branch before tagging -- see Usage in the header. A hard "HEAD must be on main" check would make its own documented workflow fail. So this does not add the check. It stops the output implying one. ## What it does add The success line was already enumerating what it checks and one thing it does not; it now names the second, and names the owner: NOT checked, 2 of 2: that <sha> is on main. Everything above is asked of HEAD, and 'every milestone PR is an ancestor of HEAD' stays true when HEAD is main plus a commit no PR explains. This line is not a statement that the release is main. check-release-tag.sh owns that rule -- and it is NOT on main today: #259 added it, #276 reverted it, and #277's re-land is an empty no-op. It returns with #277's rebuild. The last sentence is the part that matters and is why this is not merely a docstring. **The protection for P3's class existed and was reverted.** #259 added it, #276 backed it out, and #277's re-land is one of the no-ops this same PR now refuses -- so today NEITHER script checks that the released commit is on main. Verified: the current `check-release-tag.sh` makes no ancestry call at all, and #259's version has the check at line 455. Naming an open hole in the gate's own output beats leaving a reader to infer completeness from a success message, which is the whole argument of the commit before this one. ## Guard One test, `test_success_message_does_not_present_head_as_main`, asserting both the disclaimer and the owner's name -- the second because a disclaimer that does not say who *does* check reads as "nobody does" rather than "someone else does". **And a correction to the test added in the previous commit.** Numbering the disclaimers "1 of 2" and "2 of 2" silently weakened `test_success_message_states_what_is_not_checked`, which asserted the shared substring `"NOT checked"`: with two disclaimers present, deleting the first left the second satisfying the assertion. Caught by the mutation matrix -- M8 went from 1 failed to 31 passed -- not by reading. The test now asserts each disclaimer by its distinguishing marker, and M8 was widened to delete the whole block rather than one line of it. This is the second species of green-but-empty guard, introduced and caught inside one PR. Mutation matrix, all nine dead, each mutant `bash -n`-checked first: M1 revert detection disabled (behaviour on main today) -> 6 failed M2 waiver stops checking the replacement for a revert -> 1 failed M3 depth-1 un-revert dropped -> 1 failed M4 revert trailer match dropped -> 10 failed M5 no-op detection disabled -> 2 failed M6 waiver stops checking the replacement for a no-op -> 1 failed M7 emptiness test inverted to always-true (over-broad) -> 10 failed M8 the "silently removed" disclaimer deleted -> 1 failed M9 the "HEAD is not main" disclaimer deleted -> 2 failed restored -> 31 passed 2898 passed (+1), 4 deselected; ruff check and ruff format --check clean; `bash -n` clean.
… own advice
R9-P3, reviewing the previous commit, found the assumption it disclaims still
printed one screen further down. The stranded-PR remedy ended:
To confirm by hand:
git merge-base --is-ancestor <merge-commit> HEAD && echo on-main
HEAD is the tree being released, which this script does not establish is main --
that is the whole point of the second NOT-checked line added in the previous
commit. Telling the operator that an ancestor of HEAD is "on-main" is the exact
assumption an off-main tag exploits, printed in the gate's own help text, to
someone who has already hit a failure and is looking for a command to trust.
Now `echo in-this-tree`, with the reason at the call site so it is not
"corrected" back.
Guard: an assertion in `test_stranded_commit_is_refused`, which already
captures that stderr. Reverting the wording to `echo on-main` fails it (1
failed, 30 passed); restored, 31 passed.
Verified against the shapes other lanes reported this round, because a
deletion-only matrix scores a presence check as if it were a behaviour check:
R9-P8/P5 shape 4 -- delete the guarded passage outright, rather than
reverting it:
#282 delete the whole Azure CHANGELOG bullet -> 1 failed
#283 delete the per-probe attribution sentence -> 1 failed
#291 delete the whole success-message block -> 3 failed
R9-P6 -- preserve the matched token and break the behaviour anyway:
#282 move the bullet into the 2.0.0 section -> 1 failed
#283 keep every path, drop the probe-name tokens -> 1 failed
#291 keep the no-op call, make its reporting dead -> 2 failed
#291 keep the revert call, discard its result -> 6 failed
All eight die. The extractors survive shape 4 for the reason P5 identifies:
they assert their anchor exists before extracting rather than returning a
default, so a missing anchor is a loud failure instead of an empty scan.
One of those probes initially reported a survivor, and it was the probe that
was wrong: "move the bullet into the 2.0.0 section" inserted at the index of
`## [2.0.0]`, which is the *end of the unreleased section*, so the entry never
left it. Corrected to insert after that heading line, the guard fails as it
should. That is R9-P5's third shape -- a mutation that edits the wrong object
produces a false negative about the guard, and looks identical to a real
survivor. The rule both directions share: anchor on something unique and prove
the anchor is unique, on the assertion side and the mutation side alike.
2898 passed, 4 deselected; ruff and `bash -n` clean.
…nnot know
R9-P3, reading the previous commit, found two defects in one sentence -- and it
is a sentence printed on every release run:
check-release-tag.sh owns that rule -- and it is NOT on main today: #259
added it, #276 reverted it, and #277's re-land is an empty no-op. It
returns with #277's rebuild.
**The PR number is wrong in the costly direction.** #277 is not going to
rebuild -- it is still open and still a no-op, re-measured after #297 was
opened. The rebuild is **#297**. An operator following that pointer lands on
the PR that does nothing, which is precisely the trap the sentence exists to
warn about.
**The status half goes false on merge, in either order.** If #297 lands first,
this file ships already saying something untrue. If this lands first, it goes
untrue the moment #297 does. No merge order avoids it, and nothing in either
repository would notice: the gate would print "NOT on main today" at every
release while the check runs one file away.
That is this round's own recurring defect -- prose restating a fact that lives
in another file -- reappearing inside a success message, in a PR whose entire
argument is that a gate must not overstate what it knows. Writing it was the
same reflex the round has been cataloguing all day.
## The fix, and why not the cleverer one
P3 offered three options and ranked a conditional line first -- derive the
sibling's state by grepping it, so the message stays true through either merge
order. It is a good idea and I did not take it, for a reason from this round's
own taxonomy: a grep for a token in another file is a presence check standing
in for a behaviour, R9-P6's species. It would keep the sentence true until
somebody guts the check and leaves the token, and then it would lie again with
more authority than before, because it would look derived rather than written.
So the durable split instead: **ownership is permanent, state is not.**
printed on every run: "check-release-tag.sh owns that rule. Whether that
script currently implements it is a fact about that
file and not about this one: read it there rather
than trusting a sentence here, which cannot know
when it went out of date."
header comment: the history, in the past tense throughout, with #297
named as the re-land -- past tense cannot expire.
The limit being declared is that *this* script does not check whether HEAD is
on main. That is true regardless of what the sibling does, which is exactly why
it is the half worth printing.
## Guard
`test_success_message_makes_no_dated_claim_about_another_file` requires the
owner to still be named, and refuses `NOT on main today`, `on main today`,
`re-land`, `rebuild`, and **any `#\\d+`** in the success output. The PR-number
rule is the one that would have caught the original defect on its own.
Scoped to the success path deliberately: the header may narrate history,
because past tense cannot go stale.
Mutation: restoring the original sentence verbatim -> 1 failed, 31 passed;
restored -> 32 passed.
2899 passed (+1), 4 deselected; ruff and `bash -n` clean.
# Conflicts: # tests/unit/test_check_milestone_landed.py
Two follow-ons to the merge of current main. 1. `.github/relanded-prs.tsv` gains the rows this branch's own gate demands. Run against milestone 2.2.0 the new check refuses the release and names #259 and #261: both reached main, both were reverted by #276, and their merge commits are ancestors forever, so ancestry alone still reports them LANDED. That is the finding, not a regression -- but the milestone is only honest once the record says where the content went. Both re-landed onto current main under new numbers, #259 as #297 and #261 as #280, and the gate re-checks each replacement (ancestor, not itself reverted, not an empty diff) rather than taking the row's word for it. 2. The prose is brought back in line with what the repository now contains. #277 was open when this branch was written, and the header described it in the present tense as a no-op that would merge cleanly and carry nothing. It was closed unmerged, so no merged instance of that class exists here. The check stays -- nothing but someone noticing stopped it -- but it now says so instead of pointing at a live PR that is not live. The "329 first-parent commits" figure is dropped in both places for the reason #424 gave for dropping the milestone count: main only grows, so a number written into a comment is exact the day it is typed and misleading after the next merge. The count is re-measured on every run of `test_no_commit_in_this_repositorys_history_is_a_false_positive`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR closes two of the four ways a milestone's "MERGED" claim goes wrong,
and states the other two in the gate's own output rather than leaving a reader
to infer completeness. The gate asks an ancestry question; every failure is
about content, and the two coincide only in the first row.
The last row is not a content question at all: it is the gate answering a true
question about the wrong tree. It is not enforced here because
check-release-tag.showns it and this script is a documented local pre-flightrun from a branch — but that owner's implementation is not on main: #259
added it, #276 reverted it, #277's re-land is one of the no-ops this PR now
refuses. The gate says so out loud.
Commit 1 — reverts
check-milestone-landed.shasks one question: is this PR's merge commit anancestor of the tree being released? That detects a PR that NEVER REACHED main
-- #245, stranded on a branch whose base was squash-merged first. It is
structurally blind to the other way the same claim goes wrong: a PR that
REACHED MAIN AND WAS TAKEN BACK OUT. A reverted PR's merge commit stays an
ancestor forever, so the ancestry test reports it landed while the tree has
none of its lines.
Both end in the identical false statement -- the milestone says the release
contains a change the tree does not have -- and only one of them was checked.
This repository contains a live instance right now. PR #276 reverted #259
(
90c96f0) and #261 (d0118fc):Not a 2.1.0 problem: both carry milestone 2.2.0, so the 2.1.0 check never looks
at them. But on the day 2.2.0 is cut, this gate would green-light a release
missing exactly the content it was written to catch. That is #245's failure
one turn of the crank later, and the reason to fix it before the milestone that
contains the instance comes up rather than after.
The check
Each PR that passes the ancestry test is now also checked for a revert that
still stands, keyed on the canonical
This reverts commit <40-hex>trailer --a record
git revertand GitHub's own Revert button both write, so it isproduced by the tooling rather than by a convention anyone has to remember.
--fixed-stringskeeps the SHA out of the regex engine.Two things it deliberately does NOT do:
in the tree, and a gate that fails a healthy release is a gate that gets
switched off. Depth is one un-revert, documented at the function: anything
deeper is rare enough that the existing waiver is a better answer than more
recursion, and a loud wrong answer an operator clears by hand beats a silent
one.
told to cherry-pick a change someone deliberately backed out is the wrong
instruction. The message offers both real options and names the usual one
first: re-land it and record the replacement, or move the PR off the
milestone -- which for a deliberate revert is the honest record and clears
the check on its own.
The
relanded-prs.tsvwaiver now serves both failure modes, and gained thematching condition: a replacement clears the original only if its own merge
commit is an ancestor and has not itself been reverted. Without that second
half a reverted re-land would keep clearing the original, so the waiver would
outlive the change it vouches for. The file's header says all of this, and says
not to add a row for a deliberate revert.
Guard
tests/unit/test_check_milestone_landed.py, 15 -> 24 tests, driving the realscript against synthetic repositories with a fake
gh, as the existing ones do.test_ancestry_alone_cannot_see_a_revertis the positive control on thedefect: it asserts a reverted commit is still an ancestor and that the content
really is gone, so nothing below can pass for the wrong reason.
Two run against this repository's own history rather than a fixture, using
#259/#261/#276 -- the shape as someone really made it, not one written to be
caught. They skip rather than fail where the history is absent (a shallow
clone), because "cannot see" and "not present" are different states of
knowledge.
Mutation-tested, each mutation syntax-checked before the run:
The first row is the one that matters: it is the behaviour on main today, and
it takes the live-history test with it.
2891 passed (+9), 4 deselected; ruff check and ruff format --check clean;
bash -nclean.Commit 2 — no-op merges, and the declared boundary
P3 found a third class, and it is sharper than the revert one: a PR can merge
cleanly and change nothing, and after merging the gate does not merely fail to
notice -- it ASSERTS the milestone is complete on the strength of it.
PR #277 is titled "re-land of #259" and is stacked on #276, which reverted
#259. Its branch therefore reverts #259, reverts #261, then restores #259 --
and the first and third cancel. Measured by merging it:
A PR named for re-landing #259 restores none of it, merges cleanly, and becomes
an ancestor.
It also opens a hole in the revert check in the first commit of this PR, which
is why the two ship together: record that no-op in relanded-prs.tsv as the
re-land of #259 and the waiver clears #259 on the strength of a PR that
restored nothing. The gate would not just miss the gap, it would certify it
closed.
What this adds
contributes_nothing-- the commit's diff against its first parent is empty --applied in two places: the main loop (a milestone PR that carried nothing is
reported, with its own remedy) and the waiver (a no-op can never clear the PR
it is recorded against). First parent is the right comparison for both shapes
here: for a squash merge it is "what did this add to main", and for a true
merge it is the same question asked of the mainline.
False-positive rate, counted rather than predicted. Over all 329
first-parent commits on main -- 4 of them true merges, 1 a root -- the number
with an empty first-parent diff is ZERO. A gate that cries wolf on ordinary
refactoring is a gate someone switches off, so this is pinned by a test that
re-counts it against the live history and fails if the number ever moves.
What this deliberately does NOT add, and why the output now says so
A fourth shape exists: a later commit removes the change with no revert
trailer -- a rewrite, a refactor, a hand-edit. That is a CONTENT question and
content is materially harder than reachability. The cheap forms do not survive
contact with a real repository: re-applying each PR's diff to see whether it is
a no-op flags every file another PR has legitimately touched since, and
hunk-grepping flags every reformat. It is not attempted.
The two classes taken here were taken precisely because they are exact -- "is
there a revert trailer naming this commit" and "is this commit's diff empty"
are both decidable with no heuristic and no threshold.
So the success message stops implying a completeness the gate does not have.
It was a bare "is an ancestor of ", which reads as "the milestone is
complete". It now enumerates the three questions asked and names the one that
is not, in the gate's own output where a reader will actually meet it:
A gate that announces its own boundary is worth more than one a reader infers
completeness from.
Guard
tests/unit/test_check_milestone_landed.py, 24 -> 30 tests. One builds #277'sactual shape (revert then restore, merged) rather than asserting it, with
fixture self-checks that the merge really is both a no-op and an ancestor. One
is the negative control -- an ordinary change must stay green. One pins the
false-positive count against the live history. One asserts the success message
declares what it does not check.
Mutation-tested, each mutant
bash -n-checked first:M7 matters as much as M5: it shows the check is bounded in both directions, so
a future edit cannot quietly turn it into something that flags real work.
2897 passed (+6), 4 deselected; ruff check and ruff format --check clean;
bash -nclean.Commit 3 — HEAD is not the release tree
R9-P3 found a fourth class, and it is not about a PR's content being missing --
it is this gate answering a true question about the wrong tree.
Every question here is asked of
git rev-parse HEAD. "Every milestone PR is anancestor of HEAD" stays true when HEAD is main plus something smuggled on top:
the extra commit is not any PR's merge commit, and nothing here looks for
commits that no PR explains. P3 demonstrated it on a real off-main commit
carrying a marker -- the script exited 0 and printed the smuggled SHA in its own
success line as though it were the release.
Reproduced independently here before writing this, on a commit built on top of
7871f9f and not on main:
All thirteen pass. The claim the success line makes is true, and much narrower
than it reads.
Why the rule is not enforced here
check-release-tag.shruns on the tag,where HEAD is always the tagged commit, and fix(release): close six gate holes, and refuse a tag that is not on main #259 implemented it there:
shallow-clone refusal first (a shallow clone answers ancestry wrongly
rather than failing), then
origin/mainelserefs/heads/main, thenmerge-base --is-ancestor HEAD <main>. Two owners for one rule is how arule ends up with none.
tagging -- see Usage in the header. A hard "HEAD must be on main" check would
make its own documented workflow fail.
So this does not add the check. It stops the output implying one.
What it does add
The success line was already enumerating what it checks and one thing it does
not; it now names the second, and names the owner:
The last sentence is the part that matters and is why this is not merely a
docstring. The protection for P3's class existed and was reverted. #259
added it, #276 backed it out, and #277's re-land is one of the no-ops this same
PR now refuses -- so today NEITHER script checks that the released commit is on
main. Verified: the current
check-release-tag.shmakes no ancestry call atall, and #259's version has the check at line 455. Naming an open hole in the
gate's own output beats leaving a reader to infer completeness from a success
message, which is the whole argument of the commit before this one.
Guard
One test,
test_success_message_does_not_present_head_as_main, asserting boththe disclaimer and the owner's name -- the second because a disclaimer that does
not say who does check reads as "nobody does" rather than "someone else does".
And a correction to the test added in the previous commit. Numbering the
disclaimers "1 of 2" and "2 of 2" silently weakened
test_success_message_states_what_is_not_checked, which asserted the sharedsubstring
"NOT checked": with two disclaimers present, deleting the first leftthe second satisfying the assertion. Caught by the mutation matrix -- M8 went
from 1 failed to 31 passed -- not by reading. The test now asserts each
disclaimer by its distinguishing marker, and M8 was widened to delete the whole
block rather than one line of it. This is the second species of green-but-empty
guard, introduced and caught inside one PR.
Mutation matrix, all nine dead, each mutant
bash -n-checked first:2898 passed (+1), 4 deselected; ruff check and ruff format --check clean;
bash -nclean.Update — brought onto current main
This branch was written when main was 100 commits younger. Rebasing it changed
three things about what it says, and one thing about what the repository has to
record.
.github/relanded-prs.tsvgains two rows, because the new check firesRun against milestone 2.2.0 on the rebased branch, the gate refuses the release
and names the live pair:
That is the finding working, not a regression. Both changes did come back:
#259 as #297 and #261 as #280, each merged onto current main. The rows record
that, and the waiver re-checks each replacement itself — ancestor, not
reverted, not an empty diff — rather than taking the row's word for it:
Without the rows, 2.2.0 would have been green-lit as containing two changes
whose every line had been removed and restored under other numbers.
#277 is closed, not merged
The header and the
contributes_nothingdocstring described it in the presenttense as a no-op that merges cleanly and carries nothing — true when this was
written, when #277 was open. It was closed unmerged, so the no-op class now has
no merged instance in this history. The check stays, because nothing but
somebody noticing stopped that merge, and both comments now say which of the
two it is instead of pointing at a PR that is not live.
The "329 first-parent commits" figure is dropped, both places
Same reason #424 gave for dropping the milestone count it replaced: main only
grows, so a number written into a comment is exact the day it is typed and
misleading after the next merge. The claim it supported — zero commits in this
history have an empty first-parent diff — is re-measured on every run of
test_no_commit_in_this_repositorys_history_is_a_false_positive, which is thecopy that cannot go stale.
Conflict resolution
tests/unit/test_check_milestone_landed.pyconflicted at the end of the file:this branch appended its 16 new tests there and #362 appended two of its own
(
test_a_full_page_of_results_is_refused_not_checked_partially,test_one_short_of_the_cap_is_still_checked). Both blocks kept; no test fromeither side dropped or edited.
check-milestone-landed.shauto-merged —#424 touched only the
PR_LIMITcomment, which this branch does not.Verified on the merged tree