Skip to content

fix(webhook): catch a base retarget with no new commit, tighten event health, and stop label/issue churn from going stale - #9169

Merged
JSONbored merged 1 commit into
mainfrom
fix/9055-9058-9059-webhooks
Jul 27, 2026
Merged

fix(webhook): catch a base retarget with no new commit, tighten event health, and stop label/issue churn from going stale#9169
JSONbored merged 1 commit into
mainfrom
fix/9055-9058-9059-webhooks

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

#9055 — base retarget with no new commit

A contributor can move a green PR onto a new base after CI passed against the old one, with the head SHA unchanged. GitHub does not re-run pull_request workflows or emit a new head for a base-only edit, so nothing else notices: the stored diff/patches, the AI-review cache (fingerprint deliberately excludes baseSha), and the CI aggregate all keep describing the abandoned base — permanently, since the sweep only re-syncs on head/label drift.

  • classifyPullRequestFreshness gains an explicit base_changed reason, checked right after head/state and before the draft check.
  • Checked at the last possible moment — immediately before a merge/approve mutation — using the same live fetch that already proves the head, no extra GitHub call.
  • Threaded through AgentActionExecutionContext.expectedBaseRef from the stored PR row, exactly like expectedHeadSha already is.
  • On the webhook side, pull_request.edited with changes.base now forces a fresh file sync (bypassing the head-keyed filesUpToDate check) and invalidates the PR-state, CI-state, and review caches.

#9058 — event-subscription gaps

check_run/check_suite/pull_request_review are now required events — an installation missing them used to report "healthy" while silently degrading from event-driven to sweep-only for CI settlement (that path is documented in-code as the auto-merge / close-on-red trigger). The self-host setup wizard's manifest is now derived from the same required-events list instead of hand-maintained — it had drifted, omitting issue_comment and repository.

#9059 — two coverage gaps

Verification

Per instruction, no full local gate run this pass — targeted tests only. All pass locally: test/unit/{issue-label-churn-upsert,pr-base-retarget-webhook,pr-labeled-public-surface,pr-freshness,agent-action-executor,pending-closure-watchdog,pr-outcome-reconciler,queue-2}.test.ts (369 tests). CI runs the full gate on push.

Closes #9055
Closes #9058
Closes #9059

… health, and stop label/issue churn from going stale

#9055 — a contributor can move a green PR onto a new base after CI passed
against the old one, with the head SHA unchanged. Nothing else notices: GitHub
does not re-run pull_request workflows or emit a new head for a base-only edit,
so the stored diff/patches, the AI-review cache (its fingerprint deliberately
excludes baseSha), and the CI aggregate all keep describing the ABANDONED base
-- permanently, since the sweep only re-syncs on head/label drift.

classifyPullRequestFreshness gains an explicit `base_changed` reason, checked
right after head/state and before the draft check, so a live base mismatch is
caught at the last possible moment -- immediately before a merge/approve
mutation -- using the SAME live fetch that already proves the head, no extra
GitHub call. Threaded through AgentActionExecutionContext.expectedBaseRef from
the stored PR row, exactly like expectedHeadSha already is.

The webhook side: `pull_request.edited` with `changes.base` is GitHub's own
signal that this specific mutation happened. It now forces a fresh file sync
(bypassing the head-keyed `filesUpToDate` check that would otherwise skip the
refetch since the head hasn't moved) and invalidates the PR-state, CI-state,
and review caches -- treated as review-invalidating exactly like a new commit.

#9058 — two related event-subscription gaps. `check_run`/`check_suite` were
not required events, so an installation missing them reported "healthy" while
silently degrading from event-driven to sweep-only for CI settlement --
despite that path being documented in-code as THE auto-merge / close-on-red
trigger. `pull_request_review` had the same gap for the human-approval signal
the merge gate reads. All three are now required, and the self-host setup
wizard's manifest is derived from the same list instead of hand-maintained --
it had already drifted, omitting `issue_comment` and `repository`, so a
wizard-created App started with every `@loopover …` command dead and no
rename handling.

#9059 — two bounded coverage gaps from the same audit.
(a) Label/assignment churn on a linked issue never reached
upsertIssueFromGitHub: maybeReReviewOnLinkedIssueChange returns true
unconditionally once repo+installation+issue are present, and the caller
short-circuits on that before handleIssueWebhookEvent -- the only other place
the row gets upserted for an `issues` event. So `issues.labels_json` and
assignee state only advanced on opened/edited/closed/reopened, or the (up to
6-hourly) backfill -- while label churn is the single most common issue
mutation, read by issue-side advisories, slop triage, enrichment, and the
MCP/API issue surfaces. The upsert now runs first, before the wake fan-out.
(b) `pull_request.labeled`/`unlabeled` did a row re-sync but were never in
PR_PUBLIC_SURFACE_ACTIONS, so a maintainer adding or removing the
manual-review hold label was only picked up by the sweep -- unbounded lag
under REST-budget backpressure. Both actions are disposition INPUTS (the hold
reads straight off the PR's labels), so they now join edited/synchronize.
Part (c) of #9059 (status/workflow_run never triggering a re-review) was
already closed by #9116 -- verified via ciReReviewCoalesced/
reReviewStoredPullRequest already wired into maybeInvalidateCiCacheOnLegacyCiEvent.

Targeted tests only (no full local gate run this pass, per instruction): the
files this change touches plus the new coverage tests all pass locally
(test/unit/{issue-label-churn-upsert,pr-base-retarget-webhook,
pr-labeled-public-surface,pr-freshness,agent-action-executor,
pending-closure-watchdog,pr-outcome-reconciler,queue-2}.test.ts -- 369 tests).
CI runs the full gate on push.

Closes #9055
Closes #9058
Closes #9059
@JSONbored JSONbored self-assigned this Jul 27, 2026
@JSONbored
JSONbored merged commit b852d51 into main Jul 27, 2026
3 of 4 checks passed
@JSONbored
JSONbored deleted the fix/9055-9058-9059-webhooks branch July 27, 2026 05:05
@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 27, 2026
JSONbored added a commit that referenced this pull request Jul 27, 2026
…abels only (#9175) (#9176)

#9169 added "labeled"/"unlabeled" to PR_PUBLIC_SURFACE_ACTIONS unconditionally so the
manual-review hold re-syncs immediately, but that also made every unrelated label change
(e.g. tagging a PR "bug") run the full public-surface publish pipeline, breaking the
noisy-PR-event debounce. Now only a label matching one of the resolved disposition labels
(manual-review, ready-to-merge, changes-requested, migration-collision, pending-closure)
triggers the immediate path; anything else stays debounced to the sweep.

Closes #9175
JSONbored added a commit that referenced this pull request Jul 27, 2026
…s with the #9169 required-events expansion (#9203)

#9169 promoted pull_request_review/check_run/check_suite to REQUIRED_INSTALLATION_EVENTS and
switched the self-host setup wizard's manifest to derive default_events from that same canonical
list. Three test fixtures (one in api.test.ts, two in backfill.test.ts) still mocked GitHub's live
installation payload with the pre-#9169 event set, so their live-refresh assertions incorrectly
expected a healthy status with no missing events. The self-hosting-github-app.mdx docs page's
"Events: ..." sentence was similarly left describing the old 7-event list instead of the now-
canonical 12-event set, failing the docs/manifest parity test.

Also fixes an unrelated branding-drift regression surfaced by the same full test:ci run: a comment
in predicted-gate-agreement.ts (added in #9173) still said "gittensory" instead of "loopover".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment