Janitor rule: dispatch rebase for unmergeable open PRs (closes the no-event gap) #781

Closed
opened 2026-05-02 22:16:41 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As an operator, I want unmergeable open PRs whose author is a configured agent to be auto-rebased even when no triggering webhook fires, so that a long-lived PR doesn't sit stuck after main moves on under it.

Background

Forgejo fires no event when a PR's mergeable flips to false. We currently close that loop in two places:

  • handlePostMergeRebase (apps/server/src/domain/workflow/event-handlers.ts:1069) — runs on pull_request closed && merged=true and walks siblings on the same base.
  • pr-changes-requested-graph.json mergeable_route — routes a not_mergeable arm when a reviewer requests changes.

Gaps both miss:

  • A push to main outside the PR-merge path (release tags, manual fast-forward, force push).
  • A PR that becomes unmergeable but no review activity follows.
  • The post-merge sibling sweep is best-effort: a transient API failure or a dedup-window collision skips the rebase silently.

Operator observation 2026-05-03: rebase agents not triggered "at least not all the time" on conflicts.

Proposed fix

Add a janitor rule unmergeable_pr_rebase in apps/server/src/background/janitor.ts:

  1. List open PRs per configured repo.
  2. For each: fetch detail, check mergeable === false.
  3. Resolve author via resolveAgentByUser; skip non-agent authors.
  4. Honour pr_dependencies — same three-way routing as handlePostMergeRebase (skip if PR declares parents that haven't merged).
  5. Reuse the existing post-merge dedup map + markPostMergeRebaseDispatched so a janitor pass and an event-driven pass can't double-dispatch.
  6. Dispatch using the same skillForEvent("rebase") + buildAgentRequest + dispatchByType path as handlePostMergeRebase.

Acceptance criteria

Janitor rule

  • New rule unmergeable_pr_rebase registered in _ALL_RULES and reconcileOnce
  • Lists open PRs per repo via impl.listOpenPullRequests
  • Skips PRs whose detail.mergeable !== false
  • Skips PRs whose author isn't a configured agent
  • Honours pr_dependencies markers (skip if declared parents are still open)
  • Dispatches rebase via the shared dispatchByType("rebase") path

Dedup

  • Re-uses the same _postMergeRebaseDispatched map keyed on repo#pr@sha
  • Janitor + event-driven path can't double-dispatch within the 10-minute window
  • alreadyDispatchedPostMergeRebase is exported and consumed by the rule

Idempotency

  • Janitor's per-rule canAct window also gates the rule (defence in depth)
  • markActed("unmergeable_pr_rebase", repo, "PR#N") after dispatch

Tests

  • Unit: rule dispatches when mergeable=false + agent author + no parents
  • Unit: rule skips when mergeable=true
  • Unit: rule skips when author isn't a configured agent
  • Unit: rule skips when PR declares an unmerged parent (B13 routing)
  • Unit: dedup map blocks double-dispatch when post-merge already fired
  • just qa clean

Out of scope

  • Push-to-base webhook hook (could later replace the periodic sweep — separate ticket).
  • Watchdog re-run of the rebase task itself (already handled by tail-pr-rebase-watchdog).
  • Stacked-rebase cascade (already covered by handleStackedRebaseCascade).

References

  • Hook gap: apps/server/src/domain/workflow/event-handlers.ts:1014-1018 — author's own comment names the missing event.
  • Existing dispatch path to reuse: event-handlers.ts:1069-1153.
  • Janitor rule pattern: apps/server/src/background/janitor.ts:528-564 (ruleReadyToMerge).
  • Three-way marker routing: same file, 1043-1067.
## User story As an operator, I want unmergeable open PRs whose author is a configured agent to be auto-rebased even when no triggering webhook fires, so that a long-lived PR doesn't sit stuck after `main` moves on under it. ## Background Forgejo fires no event when a PR's `mergeable` flips to `false`. We currently close that loop in two places: - `handlePostMergeRebase` (`apps/server/src/domain/workflow/event-handlers.ts:1069`) — runs on `pull_request closed && merged=true` and walks siblings on the same base. - `pr-changes-requested-graph.json` `mergeable_route` — routes a `not_mergeable` arm when a reviewer requests changes. Gaps both miss: - A push to `main` outside the PR-merge path (release tags, manual fast-forward, force push). - A PR that becomes unmergeable but no review activity follows. - The post-merge sibling sweep is best-effort: a transient API failure or a dedup-window collision skips the rebase silently. Operator observation 2026-05-03: rebase agents not triggered "at least not all the time" on conflicts. ## Proposed fix Add a janitor rule `unmergeable_pr_rebase` in `apps/server/src/background/janitor.ts`: 1. List open PRs per configured repo. 2. For each: fetch detail, check `mergeable === false`. 3. Resolve author via `resolveAgentByUser`; skip non-agent authors. 4. Honour `pr_dependencies` — same three-way routing as `handlePostMergeRebase` (skip if PR declares parents that haven't merged). 5. Reuse the existing post-merge dedup map + `markPostMergeRebaseDispatched` so a janitor pass and an event-driven pass can't double-dispatch. 6. Dispatch using the same `skillForEvent("rebase")` + `buildAgentRequest` + `dispatchByType` path as `handlePostMergeRebase`. ## Acceptance criteria ### Janitor rule - [ ] New rule `unmergeable_pr_rebase` registered in `_ALL_RULES` and `reconcileOnce` - [ ] Lists open PRs per repo via `impl.listOpenPullRequests` - [ ] Skips PRs whose `detail.mergeable !== false` - [ ] Skips PRs whose author isn't a configured agent - [ ] Honours `pr_dependencies` markers (skip if declared parents are still open) - [ ] Dispatches rebase via the shared `dispatchByType("rebase")` path ### Dedup - [ ] Re-uses the same `_postMergeRebaseDispatched` map keyed on `repo#pr@sha` - [ ] Janitor + event-driven path can't double-dispatch within the 10-minute window - [ ] `alreadyDispatchedPostMergeRebase` is exported and consumed by the rule ### Idempotency - [ ] Janitor's per-rule `canAct` window also gates the rule (defence in depth) - [ ] `markActed("unmergeable_pr_rebase", repo, "PR#N")` after dispatch ### Tests - [ ] Unit: rule dispatches when `mergeable=false` + agent author + no parents - [ ] Unit: rule skips when `mergeable=true` - [ ] Unit: rule skips when author isn't a configured agent - [ ] Unit: rule skips when PR declares an unmerged parent (B13 routing) - [ ] Unit: dedup map blocks double-dispatch when post-merge already fired - [ ] `just qa` clean ## Out of scope - Push-to-base webhook hook (could later replace the periodic sweep — separate ticket). - Watchdog re-run of the rebase task itself (already handled by `tail-pr-rebase-watchdog`). - Stacked-rebase cascade (already covered by `handleStackedRebaseCascade`). ## References - Hook gap: `apps/server/src/domain/workflow/event-handlers.ts:1014-1018` — author's own comment names the missing event. - Existing dispatch path to reuse: `event-handlers.ts:1069-1153`. - Janitor rule pattern: `apps/server/src/background/janitor.ts:528-564` (`ruleReadyToMerge`). - Three-way marker routing: same file, `1043-1067`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#781
No description provided.