Refactor webhook.ts by concern — split HTTP / handlers / CI / config + extract forgejo-api.ts #34

Closed
opened 2026-04-18 10:33:15 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As a maintainer of claude-hooks, I want src/webhook.ts decomposed along its three distinct concerns (HTTP glue, event handlers, CI state machine) plus a reusable Forgejo API client, so that the 1000-line file stops mixing signature verification, event dispatch, and CI dedup timers — and future changes to one concern don't force readers to hold the other two in their head.

Context

src/webhook.ts has grown to ~1020 lines. The real weight is the CI state machine (aggregate-state polling, dedup maps, review-fallback timers, fix-ci log prefetch) — not the dispatch switch. A pure per-event split would force every new file to import the same CI helpers, so split by concern, not by event.

Acceptance criteria

New file layout

  • src/webhook.tsHTTP only. Signature verification, body parse, the event switch, orchestration of handler dispatch + cleanup. Target ≤ 200 lines.
  • src/webhook-handlers.tsEvent handlers. One exported function per event type (handleIssueAssigned, handleReviewRequested, handleChangesRequested, handleApproved, handlePullRequestOpened, handlePullRequestClosed, handleIssueClosed, handleStatusEvent, handleActionRunEvent). No global state; config and Forgejo client injected.
  • src/webhook-ci.tsCI gate. prMergeable, fetchAggregateState, findOpenPRForHead, fetchFailingRunLogs, dispatchFixCi, requestReviewIfFresh, repoHasWorkflows, armReviewFallback / cancelReviewFallback, the _fixCiDispatched and _reviewFallbacks state, the REVIEW_FALLBACK_MS constant.
  • src/webhook-config.tsConfig. loadWebhookConfig, WebhookConfig, AgentConfig types, singleton accessor. (Consider freezing the returned config object.)
  • src/forgejo-api.tsCentralized HTTP client. All fetch(\${config.forgejoUrl}/api/v1/...`) calls go through helpers here (getPullRequest, listPullReviews, getAggregateStatus, listWorkflowJobs, getWorkflowLogs, requestReviewers, getContentsList). Shared auth header, shared 5s AbortSignal.timeout`, shared error typing.

Behavior-preserving

  • Public entry point from src/main.ts is unchanged — the HTTP server still imports one symbol from src/webhook.ts (handleForgejoWebhook).
  • Existing webhook tests (src/webhook.test.ts) continue to pass without change. If a test needs to be updated because a handler now lives elsewhere, update the import only; no behavior changes to assert.
  • just qa passes (typecheck + lint + format + tests).
  • A live E2E on charles/dummy-app still goes issue → dev PR → no-workflows fast path → reviewer → approve → boss merge.

Seams enforced

  • webhook.ts does not import anything from forgejo-api.ts directly. Every API call sits behind either a handler (for request processing) or webhook-ci.ts (for state-machine work).
  • webhook-handlers.ts does not mutate global CI state. If a handler needs to arm a fallback or mark a SHA dispatched, it calls a webhook-ci.ts export.
  • No module re-exports another module's internals (no export * from). Every cross-module symbol is named.

Tests relocated

  • Existing webhook tests split into webhook.test.ts (routing), webhook-handlers.test.ts (per-handler), webhook-ci.test.ts (state machine). Each file stays under ~300 lines.

Out of scope

  • Expanding test coverage (see #D — Expand webhook test depth). This issue preserves existing coverage; deepening it is separate.
  • Bounded maps + fetch timeouts (see #B — Hardening). The new forgejo-api.ts should already use AbortSignal.timeout(5000) for the calls it owns, but the in-place patching of remaining timeouts and the BoundedMap introduction live in #B so #A stays a pure shape refactor.
  • main.ts decomposition (see #C).

References

  • Codebase audit 2026-04-18 (see conversation history).
  • Sibling refactor tickets: #B (hardening), #C (main.ts split), #D (tests).

Dependencies

  • Blocks: #D
  • Not blocked by: anything
  • Branch off: main
## User story As a **maintainer** of claude-hooks, I want `src/webhook.ts` decomposed along its three distinct concerns (HTTP glue, event handlers, CI state machine) plus a reusable Forgejo API client, so that the 1000-line file stops mixing signature verification, event dispatch, and CI dedup timers — and future changes to one concern don't force readers to hold the other two in their head. ## Context `src/webhook.ts` has grown to ~1020 lines. The real weight is the CI state machine (aggregate-state polling, dedup maps, review-fallback timers, fix-ci log prefetch) — not the dispatch switch. A pure per-event split would force every new file to import the same CI helpers, so split **by concern, not by event**. ## Acceptance criteria ### New file layout - [ ] `src/webhook.ts` — **HTTP only.** Signature verification, body parse, the event switch, orchestration of handler dispatch + cleanup. Target ≤ 200 lines. - [ ] `src/webhook-handlers.ts` — **Event handlers.** One exported function per event type (`handleIssueAssigned`, `handleReviewRequested`, `handleChangesRequested`, `handleApproved`, `handlePullRequestOpened`, `handlePullRequestClosed`, `handleIssueClosed`, `handleStatusEvent`, `handleActionRunEvent`). No global state; config and Forgejo client injected. - [ ] `src/webhook-ci.ts` — **CI gate.** `prMergeable`, `fetchAggregateState`, `findOpenPRForHead`, `fetchFailingRunLogs`, `dispatchFixCi`, `requestReviewIfFresh`, `repoHasWorkflows`, `armReviewFallback` / `cancelReviewFallback`, the `_fixCiDispatched` and `_reviewFallbacks` state, the `REVIEW_FALLBACK_MS` constant. - [ ] `src/webhook-config.ts` — **Config.** `loadWebhookConfig`, `WebhookConfig`, `AgentConfig` types, singleton accessor. (Consider freezing the returned config object.) - [ ] `src/forgejo-api.ts` — **Centralized HTTP client.** All `fetch(\`${config.forgejoUrl}/api/v1/...\`)` calls go through helpers here (`getPullRequest`, `listPullReviews`, `getAggregateStatus`, `listWorkflowJobs`, `getWorkflowLogs`, `requestReviewers`, `getContentsList`). Shared auth header, shared 5s `AbortSignal.timeout`, shared error typing. ### Behavior-preserving - [ ] Public entry point from `src/main.ts` is unchanged — the HTTP server still imports one symbol from `src/webhook.ts` (`handleForgejoWebhook`). - [ ] Existing webhook tests (`src/webhook.test.ts`) continue to pass without change. If a test needs to be updated because a handler now lives elsewhere, update the import only; no behavior changes to assert. - [ ] `just qa` passes (typecheck + lint + format + tests). - [ ] A live E2E on `charles/dummy-app` still goes issue → dev PR → no-workflows fast path → reviewer → approve → boss merge. ### Seams enforced - [ ] `webhook.ts` does **not** import anything from `forgejo-api.ts` directly. Every API call sits behind either a handler (for request processing) or `webhook-ci.ts` (for state-machine work). - [ ] `webhook-handlers.ts` does **not** mutate global CI state. If a handler needs to arm a fallback or mark a SHA dispatched, it calls a `webhook-ci.ts` export. - [ ] No module re-exports another module's internals (no `export * from`). Every cross-module symbol is named. ### Tests relocated - [ ] Existing webhook tests split into `webhook.test.ts` (routing), `webhook-handlers.test.ts` (per-handler), `webhook-ci.test.ts` (state machine). Each file stays under ~300 lines. ## Out of scope - Expanding test coverage (see **#D — Expand webhook test depth**). This issue preserves existing coverage; deepening it is separate. - Bounded maps + fetch timeouts (see **#B — Hardening**). The new `forgejo-api.ts` should already use `AbortSignal.timeout(5000)` for the calls it owns, but the in-place patching of remaining timeouts and the `BoundedMap` introduction live in #B so #A stays a pure shape refactor. - `main.ts` decomposition (see **#C**). ## References - Codebase audit 2026-04-18 (see conversation history). - Sibling refactor tickets: `#B` (hardening), `#C` (main.ts split), `#D` (tests). ## Dependencies - **Blocks:** `#D` - **Not blocked by:** anything - **Branch off:** `main`
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#34
No description provided.