docs/guides/publishing_to_pypi.md and tests/test_falsification_deploy_v160.py describe a promotion procedure we no longer perform — and cannot perform.
What the record says
TestF2FastForwardMerge::test_main_is_ancestor_of_development docstring:
Quick Deploy says git merge development --ff-only. This requires main's HEAD to be an ancestor of development's HEAD. If not, the documented procedure fails with fatal: Not possible to fast-forward, aborting.
A fast-forward promotion never diverges: main becomes byte-identical to development, and the ancestor property holds for free. No back-merge step existed because none was needed. The gate is enforcing a precondition for that procedure.
What actually happens
Branch protection (2026-07-31, C-320) requires a PR into main. You cannot fast-forward through a GitHub PR — the UI offers merge-commit, squash, or rebase, and only merge-commit preserves the release SHAs. So the real procedure silently became merge-commit promotion, which leaves exactly one commit on main that is not on development, every single release.
So this is not a forgotten step. The documented command can no longer be executed, and the guide has never said so.
Why it went unnoticed
The gate that detects it is skipped in CI (shallow checkout can't answer merge-base — C-320), so it only fires when someone runs the full suite locally. After v1.10.0 that happened about four hours after the release, by accident. It has been silently true after every prior release.
Root cause
Third instance of the same failure mode in one session: the written record describing a world that changed underneath it.
Each was true when written. Each stopped being true without anything failing loudly.
Fix
- Update
publishing_to_pypi.md — document merge-commit promotion (what we do), delete the --ff-only Quick Deploy text, and add the post-release back-merge.
- Correct the test docstring so it stops citing a dead procedure. The assertion stays valid; its rationale changes from "so
--ff-only works" to "so branch topology stays meaningful and main..development keeps counting only unreleased work".
- Automate the back-merge — an Action on
release: published that opens the sync PR (--base development --head main, merge-commit). Preferred over documenting a manual step: a step a tired operator must remember after every release is a step that will eventually be missed. That is precisely what happened here.
- Consider running the deploy gates in CI with
fetch-depth: 0 on main-targeted PRs, so detection stops depending on someone running the suite locally at the right moment.
Item 3 makes items 1 and 2 mostly cosmetic — worth doing anyway so the record stops lying.
Stale-branch half of this issue is resolved: delete_branch_on_merge was false, now true (2026-08-02). Eleven merged branches had accumulated; they were deleted by hand and GitHub now removes them automatically.
Refs: #400 (fixes the v1.10.0 instance), C-320 (why the gate skips in CI), ADR-051.
docs/guides/publishing_to_pypi.mdandtests/test_falsification_deploy_v160.pydescribe a promotion procedure we no longer perform — and cannot perform.What the record says
TestF2FastForwardMerge::test_main_is_ancestor_of_developmentdocstring:A fast-forward promotion never diverges:
mainbecomes byte-identical todevelopment, and the ancestor property holds for free. No back-merge step existed because none was needed. The gate is enforcing a precondition for that procedure.What actually happens
Branch protection (2026-07-31, C-320) requires a PR into
main. You cannot fast-forward through a GitHub PR — the UI offers merge-commit, squash, or rebase, and only merge-commit preserves the release SHAs. So the real procedure silently became merge-commit promotion, which leaves exactly one commit onmainthat is not ondevelopment, every single release.So this is not a forgotten step. The documented command can no longer be executed, and the guide has never said so.
Why it went unnoticed
The gate that detects it is skipped in CI (shallow checkout can't answer merge-base — C-320), so it only fires when someone runs the full suite locally. After v1.10.0 that happened about four hours after the release, by accident. It has been silently true after every prior release.
Root cause
Third instance of the same failure mode in one session: the written record describing a world that changed underneath it.
"9 sources"— a count matching no artifact, in 8 places (fixed, fix: P5 + P6 audit findings, C-330 correction, credential ownership (#391, #392) #394/chore: bump to v1.10.0 #398)/root/views-datafactory/...after the pipeline moved toviews-deploy—missingokmade it fail silently for 4 months--ff-onlythat branch protection made impossibleEach was true when written. Each stopped being true without anything failing loudly.
Fix
publishing_to_pypi.md— document merge-commit promotion (what we do), delete the--ff-onlyQuick Deploy text, and add the post-release back-merge.--ff-onlyworks" to "so branch topology stays meaningful andmain..developmentkeeps counting only unreleased work".release: publishedthat opens the sync PR (--base development --head main, merge-commit). Preferred over documenting a manual step: a step a tired operator must remember after every release is a step that will eventually be missed. That is precisely what happened here.fetch-depth: 0onmain-targeted PRs, so detection stops depending on someone running the suite locally at the right moment.Item 3 makes items 1 and 2 mostly cosmetic — worth doing anyway so the record stops lying.
Stale-branch half of this issue is resolved:
delete_branch_on_mergewasfalse, nowtrue(2026-08-02). Eleven merged branches had accumulated; they were deleted by hand and GitHub now removes them automatically.Refs: #400 (fixes the v1.10.0 instance), C-320 (why the gate skips in CI), ADR-051.