You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(gate): force a fresh rebase + CI recheck immediately before merge when base has advanced
The update_branch action (which merges the current base into head and
would naturally re-trigger CI on a truly-rebased branch) only fired
when liveMergeState === "behind" inside the review-gating pass, not
immediately before the merge action itself. The merge decision's own
gate treats GitHub's mergeable_state as the sole base-freshness
signal, and mergeable_state only detects git-level TEXTUAL conflicts
-- a base that has advanced with a new, non-conflicting sibling commit
(e.g. a second PR's distinct-but-colliding migration file) still reads
clean. So a PR sitting green + approved + clean gets merged without
ever being forced through a fresh rebase-and-CI-recheck cycle against
whatever landed on the base moments earlier from a concurrent merge.
Added a new config-driven gate.requireFreshRebaseWindow setting
(minutes, off by default) wired through the full config-as-code chain:
migration 0093, Drizzle schema, RepositorySettings type, the DB
round-trip in repositories.ts (default/row-mapping/insert/update),
focus-manifest.ts's gate config parse/serialize/resolve, the OpenAPI
schema, and .gittensory.yml.example docs.
The base-freshness check itself lives in runAgentMaintenancePlanAndExecute,
immediately before planAgentMaintenanceActions is called with
merge-eligible inputs: a new fetchLiveBaseBranchAdvancedAt (backfill.ts)
reads the base branch's live tip-commit timestamp via GET /commits/{ref}.
When it landed within the configured window, maybeForceFreshRebase
forces an update_branch through the SAME action class/write-permission/
dry-run/kill-switch stack prReadyForReview's own BEHIND-branch path
already uses -- not a new action class, just a new trigger condition.
A bounded retry cap (3 attempts, keyed per head SHA in the transient
cache, mirroring ciPendingDeferStuck's pattern) prevents a fast-moving
base from live-locking the PR; past the cap it falls through to a
normal merge with an audit trail. Every failure mode (unreadable base
commit, cap reached, forced action not authorized) fails open to
today's merge behavior.
Off by default -- zero behavior change for any repo that hasn't opted
in, and the new live GitHub read only fires when a repo has explicitly
configured the window AND the PR is otherwise merge-mechanically-ready.
Tests: DB round-trip in data-spine.test.ts, full gate.requireFreshRebaseWindow
parse/round-trip/resolve coverage in focus-manifest.test.ts, and 6 new
end-to-end queue.test.ts scenarios covering a normal merge, a forced
rebase, the off-by-default path, the bounded-retry fallback, an
unreadable base commit, and an unauthorized forced action -- each
driving the real processJob path and asserting on audit_events.
100% branch coverage on the new code.
Closes#2552
detail: `base advanced within the ${windowMinutes}m freshness window, but the ${MAX_FRESH_REBASE_FORCES}-attempt forced-rebase cap was already reached for this head SHA — falling through to a normal merge decision`,
/* v8 ignore next -- an installed-App PR webhook always carries an installation record; the null is defensive (mirrors runAgentMaintenancePlanAndExecute's own identical merge-time read). */
0 commit comments