#424)
The story shipped smaller than its issue described, and measuring is what
showed why.
#424 claimed four gates would start running once the workflow got a step
and a token. Simulating a runner checkout first:
- actions/checkout leaves exactly ONE local branch, so the gates' bare
`git merge-base --is-ancestor main development` exits 128 and they
skip themselves — even at fetch-depth: 0, which the workflow already
had. Two `git branch -f` lines fix it. Without them the step would
have run, passed, and asserted nothing.
- The local-clone branch gate would have PASSED TRIVIALLY on a runner
rather than skipped: a fresh runner has one local branch, so it
reports coverage it does not have. It now skips with a reason.
- TestF1VersionBumped is xfail. It cannot fail a suite anywhere, so
scheduling it would add a green tick and no information.
So of the four named: one redundant, one unblocked, one demoted to an
honest skip, one impossible until #425. C-341 is NARROWED, not closed —
closing it would claim a coverage the xfail denies. The gain the issue
never mentioned: the conflict-free back-merge check, which nothing was
running anywhere, now runs.
C-342 resolved: `uv lock --check` in the test job, before `uv sync`.
Drilled clean/dirty/clean. It is the one merge-blocking check added and
it differs in kind from the deploy gates — those redden for reasons
unrelated to the change (a diverged main is CORRECT between a release and
its back-merge, which is C-320); this reddens only when the PR itself
left the lock stale. Stated in the workflow comment so nobody re-derives
it. Confirmed on its own change: this PR adds pyyaml for the new guard,
and the check agreed after re-locking.
Two near-misses, both recorded rather than quietly fixed:
A PARSE IS NOT A VERIFICATION. The lock-check step was first inserted
between `- name: Install dependencies` and its `run:`, producing a
duplicate `run:` key. yaml.safe_load reported "YAML parses" — PyYAML
silently keeps the last duplicate — so the check had been overwritten by
`uv sync` and would never have run. Twenty minutes after registering
C-345. tests/test_ci_gates.py now asserts step well-formedness.
AND THE DRILL OF THAT GUARD WAS WRONG FIRST. Its criterion was rc != 0,
which cannot tell "guard caught it" from "the file failed to import" —
and it was failing to import, because pyyaml was not a dependency. All
four mutations reported CAUGHT while nothing ran. Re-drilled with rc == 1
meaning caught and rc >= 2 meaning error: five mutations, five genuine
catches.
Workflow display name widened to "Release hygiene"; filename kept so run
history and existing citations stay valid.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #424 (by hand —
Closes #Nis inert on PRs to a non-default branch). Part of #421, Story 3.This story shipped smaller than its issue described, and that is the finding.
#424's own table was wrong
The issue said four gates would start running once the workflow got a step and a token. I simulated a runner checkout before writing the step:
rc=128—actions/checkoutleaves one local branch, so baremaindoes not resolve even atfetch-depth: 0, which the workflow already hadxfail— cannot fail a suite anywhereSo: one redundant, one needing two
git branch -flines to unblock, one needing demotion to an honest skip, and one impossible until #425.Had I implemented the issue as written, the step would have run, gone green, and asserted almost nothing — C-341 restored under the appearance of a fix.
The gain the issue never mentioned:
test_merge_main_into_development_is_conflict_freenow runs. Nothing was running it anywhere, and the bespoke shell check never did it.C-341 — narrowed, not closed
TestF1VersionBumped::test_version_not_already_taggedis@pytest.mark.xfail. Scheduling it would add a green tick and no information. #425 owns that, and this entry stays open until it lands. Closing it now would claim a coverage thexfaildenies.C-342 — resolved
uv lock --checkin thetestjob, beforeuv sync. Drilled:Why this one blocks a PR when the deploy gates deliberately do not. Those redden for reasons unrelated to the change under review — a diverged
mainis correct between a release and its back-merge, which is C-320. This reddens only when the pull request itself left the lock stale: its own fault, fixable inside it with one command. The distinction is in the workflow comment so nobody re-derives it.Confirmed on its own change: this PR adds
pyyamlto the dev group for the new guard, anduv lock --checkagreed after re-locking.Two near-misses, recorded rather than quietly fixed
A parse is not a verification. The lock-check step was first inserted between
- name: Install dependenciesand itsrun:, producing a step with a duplicaterun:key.yaml.safe_loadreported "YAML parses" — PyYAML silently keeps the last duplicate — so the check had been overwritten byuv syncand would never have run. Twenty minutes after registering C-345, the same defect in the same shape.And the drill of the new guard was wrong first. Its pass criterion was
rc != 0, which cannot distinguish "the guard caught it" from "the test file failed to import" — and it was failing to import, becausepyyamlwas not yet a dependency. All four mutations reported CAUGHT while nothing ran.Re-drilled with
rc == 1= caught,rc >= 2= error:New guard
tests/test_ci_gates.pyasserts the two orderings that fail green when wrong —uv lock --checkbeforeuv sync, and the local-ref step before the gates — plus step well-formedness, which is what catches the duplicate-key class.Also
Workflow display name widened to Release hygiene; the filename is kept so run history, the tracking issue's provenance links, and the citations in
publishing_to_pypi.mdand the register stay valid. The tracking issue is generalised to cover both checks, and the close step handles the old title so an issue opened by the narrower workflow still closes.Verification
ruff check .→ All checks passedpytest→ PYTEST_EXIT=0,FAILED count: 0(unpiped, two readers)docs/validate_docs.sh→ PASSEDrun/uses, allsteps.*references resolve to existing ids🤖 Generated with Claude Code