This repository was archived by the owner on Aug 11, 2026. It is now read-only.
fix(ci): reject a PR whose merge into main would leave two Alembic heads - #556
Merged
Merged
Conversation
The Migration order job validated the PR branch in isolation, so a branch that was linear against the main it was cut from stayed green after a second migration landed. That is the 2026-07-28 outage: #505's last check ran 2026-07-27T18:59, #524 landed 85 minutes later, and #505 merged ~21 hours after that on a check that had been true when it ran and false ever since. main ended up with two heads, 1183 DB test errors and a dead deploy. Resolve the merge result -- base plus branch -- rather than the branch, and assert exactly one head. Migrations are immutable and none may be deleted, both asserted against the merge base, so the merge is the union of the two file sets and needs no actual merge to evaluate. That also lets the check run for any PR from any checkout, which the second half of the fix needs. The script alone would not have caught this. GitHub re-runs a PR's checks when the PR moves, not when its base does, and this repo does not require branches to be up to date before merging, so the stale green was mergeable the whole time. Every push to main now re-checks each open PR that adds a migration and posts a commit status, so a PR that has just been made undeployable says so on the PR. Failures name both revisions, the file each lives in, which side each came from, and both remedies. A branch that merely inherits a divergent main is told it is not at fault. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Peyton-Spencer
marked this pull request as ready for review
July 28, 2026 19:27
10 tasks
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Summary
mainwent red and undeployable on 2026-07-28 with two Alembic heads — 1183 DBtest errors and a dead Deploy job — and the
Migration orderjob that exists toprevent exactly this was green on both PRs the whole time. #555 fixed the
symptom. This closes the hole that let it through.
The job validated the PR branch in isolation. A branch that is perfectly
linear against the
mainit was cut from stays linear no matter what lands onmainafterwards, so the divergence existed only in the merge — the thing thatactually gets deployed. This resolves the merge result and asserts exactly
one head.
Why the existing check didn't fire
Not a logic bug. A freshness bug:
Migration orderrun — green, and correctc7a4f1e2b903, 85 minutes laterGitHub re-runs a PR's checks when the PR moves, not when its base moves.
"Require branches to be up to date before merging" is off on this repo
(
strict: false), so nothing forced a re-evaluation, and the stale green stayedmergeable the entire time. A script-only fix would not have caught this —
hence the second half below.
Changes
scripts/check_migration_order.py— resolvesbase + branchrather than thebranch. Migrations are immutable and none may be deleted (both asserted, now
against the merge base rather than the base tip), so the merge is exactly the
union of the two file sets and needs no actual merge to evaluate. That also lets
the check run for any PR from any checkout, which the sweep needs. Takes an
optional
head_ref.scripts/recheck_open_pr_migrations.sh(new) — runs on every push tomainand re-checks each open PR against the
mainthat now exists, posting amigration-order/merge-resultcommit status. This is what closes the window: aPR that has just been made undeployable now says so on the PR, instead of
staying green until someone merges it. PRs that add no migration are reported
green without being fetched, so the context is present on every PR and a stale
red can't outlive the migration that caused it. If
mainitself is divergent thesweep fails loudly and posts nothing — red statuses on innocent PRs are how a
guard gets muted.
Failures name both revisions, the file each lives in, which side each came from,
and both remedies:
A branch that merely inherits a divergent
mainis told it is not at fault.CLAUDE.md— the "rebase onto currentorigin/main" advice is nowenforceable, so it says so. Also documents the stale-test-template gotcha found
during #555: a cached
ditto_test_templatebuilt against a broken chain failsevery DB test long after the chain is fixed, in a way that reads as your change's
fault (513 failures on a clean worktree; green after
make test-db-reset). Thetell is breadth — a real regression fails tests near what you touched.
It already found two live recurrences
Run against the open PRs as they stand today, both of which would re-break
mainthe moment they merge:
a7c41f8b2e93(2026_07_27_add_score_model_usage.py)7b41d0e29c85(2026_07_26_add_owner_attestations.py)Both are green on the current check. Neither needs anything but a rebase.
Test plan
repo — asserts the stale branch was genuinely green against the base it
was cut from, and is rejected against
mainas it is now. The oldscript passes this input; the new one fails it.
mainclears the failure, i.e. theremedy the message recommends actually works.
mainmoved is not misreported as adeletion (merge-base, not base-tip).
gh apistubbed: 20 green, feat(scores): record what a scored run spent on the language model #525and feat(attestation): self-serve cryptographic owner-link attestation #503 red.
make lint,make lint-copy,make typecheckclean;shellcheckclean.make test— 2482 passed in deterministic order (-p no:randomly). Arandomized-order run first showed 3 failures in
test_inference.py/test_public.py(caplog assertions); all 3 pass in isolation and areorder-dependent flakes, not this change — which adds nothing outside
scripts/,.github/,CLAUDE.mdandditto/tests/scripts/.until it is on
main).One thing this PR cannot do
Migration orderis not in this repo's required status checks — onlycopy-lintandlint-and-test (3.12)are. So even when it goes red it does notblock a merge today. Branch protection can't be changed from a PR; worth adding
Migration order(and/ormigration-order/merge-result) to the requiredcontexts, and considering
strict: true, which would independently close thestaleness window.
🤖 Generated with Claude Code