fix(github): evaluate CI-status coalescing for the merge/close actuation path — found unsafe, documented - #2600
Conversation
…hared live-facts cache Closes #2539. The staged-merge approval-queue acceptance path (decidePendingAgentAction's own #2126 re-check) and the auto-maintain action executor's pre-mutation re-check (#2128) both call fetchLiveCiAggregate independently, moments apart in the SAME synchronous accept pass, using the identical unfiltered fetchLiveCiAggregate(..., requiredContexts: undefined, ...) shape -- a genuine, safe-to-coalesce duplicate fetch for the exact same question about the exact same commit. Thread the accept-time read forward as an optional prefetchedLiveCi on AgentActionExecutionContext, keyed by headSha: the executor's own re-check reuses it only when the headSha matches exactly, otherwise it fetches fresh -- identical to today's behavior for every other caller. Scope note: processors.ts's readiness/planner/post-publish call sites already share a DIFFERENT, required-context-FILTERED live-CI cache (#1941). That cache is deliberately NOT threaded into this fix -- reusing a filtered aggregate for this unfiltered fold-all re-check would silently loosen the merge-safety gate for repos with required-status-checks configured. The two call sites this PR wires together are the only pair that share both timing (same synchronous pass) and semantics (unfiltered).
|
Tip 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ Gittensory review result - approve/merge recommendedReview updated: 2026-07-02 21:28:18 UTC
✅ Suggested Action - Approve/Merge
Review summary Nits — 5 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 #2600 +/- ##
=======================================
Coverage 96.05% 96.05%
=======================================
Files 234 234
Lines 26280 26280
Branches 9531 9531
=======================================
Hits 25244 25244
Misses 425 425
Partials 611 611 🚀 New features to boost your workflow:
|
…irmed unsafe The gate's own AI review correctly flagged this: the "moments apart, same synchronous pass" assumption behind prefetchedLiveCi was wrong. Traced the actual call chain between decidePendingAgentAction's accept-time CI read and the executor's pre-mutation re-check and found real, non-trivial async work in between -- isHoldOnly/isCloseHoldOnly (DB reads), createInstallationToken + resolveLinkedIssueHardRule (a GitHub API call when hard rules are configured), and, inside the executor itself, fetchPullRequestFreshness (an unconditional live GitHub call) -- all running before the (previously coalesced) CI check. That is exactly the window #2128's pre-mutation re-check exists to catch a CI flip in; reusing the earlier read defeated the guard it was reusing. Reverts prefetchedLiveCi entirely: the field on AgentActionExecutionContext, the capture/thread-through in decidePendingAgentAction, and the reuse branch in executeAgentMaintenanceActions's pre-mutation check, which now always fetches fresh again, exactly as before #2539. Replaces the coalescing tests with one regression test asserting the correct (always-two-fetches) behavior, so this specific unsafe shortcut can't be silently reintroduced. #2539's other orphaned call site (the duplicate-sibling reconciliation / gate-override reads coalesced via cachedFetchLivePullRequestMergeState / cachedFetchLivePullRequestState, already shipped in #2537) remains correctly coalesced -- those are genuinely non-authoritative reads, not the merge/close actuation boundary this revert is about.
Summary
Closes #2539.
An earlier roadmap item (#1941) collapsed most repeated check/status reads onto a shared per-request cache used by the readiness check, the maintenance planner, and the post-publish boundary. This issue identified two remaining call sites that bypass it: the staged-merge approval-queue acceptance path (
decidePendingAgentAction) and the auto-maintain action executor's own pre-mutation re-check (executeAgentMaintenanceActions).This PR's history reflects a real correction, not just an implementation. My first commit implemented a coalescing shortcut (
prefetchedLiveCi) between these two call sites, on the assumption that they run "moments apart, in the same synchronous accept pass." The gate's own AI review correctly flagged this as unsafe. I traced the actual call chain and confirmed the review's finding — the assumption was wrong, and I reverted the shortcut in a follow-up commit. Both commits are visible in this PR's history for anyone who wants the full story.What the gate found, and what I verified
Between
decidePendingAgentAction's accept-time CI read and the eventualmergePullRequestmutation, there is real, non-trivial async work:Promise.all([isHoldOnly(env, ...), isCloseHoldOnly(env, ...)])— two DB reads.createInstallationToken(...)+resolveLinkedIssueHardRule(...)— a live GitHub API call when hard rules are configured for the repo.executeAgentMaintenanceActionsitself, before reaching the (previously coalesced) CI check: the freshness guard'sfetchPullRequestFreshness(...)— an unconditional live GitHub call checking the PR's current head SHA.That is exactly the window the executor's pre-mutation CI re-check (#2128) exists to catch a CI flip in — its own doc comment says so explicitly: "Re-read live CI right before the mutation so a check that flipped in this narrow window is never acted on from stale information." Reusing the earlier accept-time read defeated the guard it was reusing, on the merge/close actuation boundary — exactly the class of bug #4220 was about.
What changed
prefetchedLiveCientirely: the field onAgentActionExecutionContext, its capture/thread-through indecidePendingAgentAction, and the reuse branch inexecuteAgentMaintenanceActions's pre-mutation check, which now always fetches fresh again — byte-identical to before fix(github): close the remaining CI-status re-fetch gap outside the shared live-facts cache #2539.cachedFetchLivePullRequestState/cachedFetchLivePullRequestHeadSha), already shipped in feat(github): extend the head-SHA snapshot cache to live PR reads and reviews #2537 — those are genuinely non-authoritative reads, not the merge/close actuation boundary this PR is about, and don't share the same async-gap risk.Why this is still a legitimate resolution of #2539
The issue's own requirement was explicit: "Preserve the requirement that these are act-boundary reads... this is a coalescing fix, not a staleness-tolerance fix." Having verified that the ONE remaining orphaned pairing (
decidePendingAgentAction↔executeAgentMaintenanceActions) cannot be coalesced without becoming a staleness-tolerance change, the correct outcome — consistent with how #2541 (review-enrichment cost-class parallelization) and #2543 (rate-limit-observation write batching) were also resolved in this same roadmap — is to document why, not force an unsafe implementation to satisfy the issue's original framing.Tests
test/unit/agent-action-executor.test.ts: removed the 4 tests exercising the revertedprefetchedLiveCireuse/mismatch/heuristic-close/no-prefetch paths.test/unit/agent-approval-queue.test.ts: replaced the 3 coalescing tests with one regression test assertingfetchLiveCiAggregateis called exactly twice for a full successful staged-merge accept (once for the fix(agent-actions): approval-queue accept replays a staged merge with no live CI/mergeable/review re-check #2126 accept-time re-check, once for the executor's fix(agent-actions): actuation freshness guard never re-verifies CI/mergeable state before merge or close #2128 pre-mutation re-check) — locking in the correct, non-coalesced behavior.Validation
npm run typechecknpx vitest run test/unit/agent-action-executor.test.ts test/unit/agent-approval-queue.test.ts(118 tests pass)npm run test:ci(the full local gate, exit 0)git diff --checkcoverage/lcov.infoScope
site/,CNAME,**/lovable/**, orCHANGELOG.mdSafety