feat(gate): re-check migration-file collisions against live main before merge (#2550)#2585
Conversation
…re merge (#2550) scripts/check-migrations.mjs only validates a PR against its own branch snapshot at CI time; it can never see a sibling PR that merges a same-numbered migration file to the base branch in the meantime. That exact race hit this repo four times (0015/0017, 0074, 0075/0076, 0090). This adds a live, opt-in recheck (gate.premergeContentRecheck) immediately before an agent-driven merge: fetch the base branch's current migrations/ filenames, union them with this PR's own new migration files, and run the same collision-detection logic CI uses. A live collision holds the merge with a distinct gittensory:migration-collision label and a rebase-needed comment instead of merging blind. Config-gated (off by default) and path-gated (only a PR touching migrations/** pays the extra GitHub API call), with a short cross-invocation cache so the recurring maintenance sweep doesn't re-fetch the same repo+base-ref on every pass. The collision-detection algorithm is extracted into a shared, fs-free module (src/db/migration-collisions.ts) that both the CI script and the live Worker-side recheck import, so the two can never silently disagree about what counts as a collision; scripts/check-migrations.mjs now runs via tsx instead of plain node so it can resolve that .ts import.
|
Warning 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏸️ Gittensory review result - manual review recommendedReview updated: 2026-07-02 11:30:31 UTC
⏸️ Suggested Action - Manual Review
Review summary Nits — 6 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2585 +/- ##
==========================================
+ Coverage 95.98% 95.99% +0.01%
==========================================
Files 229 231 +2
Lines 25810 25879 +69
Branches 9389 9415 +26
==========================================
+ Hits 24774 24843 +69
Misses 425 425
Partials 611 611
🚀 New features to boost your workflow:
|
…ale pre-merge snapshot (#2550) The 180s TTL cache in cachedListMigrationFilenamesAtRef was keyed by repo+baseRef without the base SHA, so a snapshot taken before a sibling PR merged its own colliding migration could be served to a later-processed PR within the TTL window — silently defeating the exact race this recheck exists to catch. The live tree fetch is cheap and already rides the existing GitHub rate-limit admission/backoff path, so correctness wins over the saved call.
…heck and harden the tsx shebang Renaming an already-merged base migration (same number, e.g. a typo fix) unioned both the old name (still live on main pre-merge) and the new name, self-colliding even though the merged tree would only ever have one file at that number. Now the PR's own removed/renamed-away base filenames are subtracted from the live tree before the union. Also harden scripts/check-migrations.mjs's shebang with --no-install so a missing local tsx fails closed instead of npx silently fetching an unverified package from the registry, per the security scanner finding.
Summary
migrations/**-touching PRs:scripts/check-migrations.mjsonly validates a PR against its own branch snapshot at CI time, so it can never see a sibling PR that merges a same-numbered migration file to the base branch in the meantime. This exact race has hit this repo 4 times (0015/0017, 0074, 0075/0076, most recently 0090 — PR feat(agent-actions): auto-close a contributor's PR over the open-PR cap (#2270) #2479 + PR fix(selfhost): cap historical PR file hydration and cache by head SHA #2527 → PR fix(migrations): grandfather the shipped 0090 duplicate #2532 grandfather patch).gate.premergeContentRecheck(config-as-code only, off by default, mirrorsgate.dryRun's no-DB-column pattern) fetches the base branch's CURRENTmigrations/filenames, unions them with this PR's own new migration files, and runs the same collision-detection logic CI uses. A live collision holds the merge — never a close — with a distinctgittensory:migration-collisionlabel and a rebase-needed comment.migrations/**, or a repo that hasn't opted in, pays zero extra latency/API calls.src/db/migration-collisions.ts) imported by BOTH the CI script and the live Worker-side recheck, so the two can never silently disagree about what counts as a collision.scripts/check-migrations.mjsnow runs viatsx(already an established devDependency) instead of plainnode, since it needs to resolve that.tsimport — verified byte-identical CLI output before/after.repo+baseRef(shared across every PR checking the same base), so the recurring maintenance sweep doesn't re-issue the full recursive Trees API call on every ~2-minute pass for the life of an open PR.Validation history
This went through a design workflow (4 parallel research agents → synthesis) before implementation, then a dedicated adversarial-review workflow (4 review dimensions → independent adversarial verification of every finding) before opening this PR. The adversarial pass caught — and this PR fixes — a real, confirmed correctness bug in the first draft: deriving "this PR's own migration filenames" from the shared
changedPathsForGuardrailhelper (which unions both a renamed file's OLD and NEW names for its own unrelated guardrail-matching purpose) meant a contributor simply renaming their own not-yet-merged migration file — including performing the EXACT renumber remediation this feature's own hold comment recommends — would self-collide or leave a stale, permanent false hold. Fixed by deriving the PR's own migration filenames directly from the changed-files list (current path only, excluding removed files). Three regression tests confirm the fix (and were verified to actually fail against the pre-fix code before being fixed).The adversarial pass also surfaced and this PR addresses: no caching on the live Trees API call (added the 180s cross-invocation cache above), GitHub's
truncatedtree-response flag not being checked (now checked, fails open/inconclusive on a truncated response), and three test-coverage gaps (theprNumbersscoping filter that excludes unrelated pre-existing collisions, multiple simultaneous colliding numbers reaching the webhook level, and the realKNOWN_MIGRATION_DUPLICATESgrandfather list interacting correctly at the webhook level) — all now covered.Scope
type(scope): short summaryConventional Commit format.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run actionlint(vianpm run test:ci)npm run typechecknpm run test:coveragelocally — 100% branch coverage on every new/changed line insrc/db/migration-collisions.ts,src/github/migration-tree.ts, and the new sections ofsrc/queue/processors.ts/src/settings/agent-actions.ts(verified via manualcoverage/lcov.infoBRDA inspection); the few remaining uncovered branches in touched files are pre-existing, outside this diff.npm run test:workers(vianpm run test:ci)npm run build:mcp/npm run test:mcp-pack(vianpm run test:ci)npm run ui:openapi:check(vianpm run test:ci) — no OpenAPI schema change needed;gate.premergeContentRecheckfollows the config-as-code-only, no-DB-column pattern already established bygate.dryRun/gate.size.mode, which are likewise absent from the OpenAPI schema (a pre-existing gap tracked separately under fix(openapi): add 11 missing RepositorySettings fields + a structural-drift CI check #2556).npm run ui:lint/ui:typecheck/ui:test/ui:build(vianpm run test:ci)npm audit --audit-level=moderatetest/unit/migration-collisions.test.ts,test/unit/migration-tree.test.ts), planner short-circuit tests (test/unit/agent-actions.test.ts), config-as-code parse/round-trip/resolve tests (test/unit/focus-manifest.test.ts), the refactored CI script's existing test suite re-verified (test/unit/check-migrations-script.test.ts), and 16 webhook-level integration tests intest/unit/queue.test.tscovering: hold-on-collision, no-collision-merges, path-gate-skip, off-by-default-skip, fetch-failure-fail-open, no-base-ref-fail-open, cross-pass caching, corrupt-cache fallback, theprNumbersunrelated-collision exclusion, multiple simultaneous collisions, the real grandfathered-pair non-interference, and the 3 rename/removed-file regression tests for the bug the adversarial review caught.Safety
.gittensory.yml.exampledocuments the newgate.premergeContentRecheckkey); no changelog edit.Notes