From 94791a34d89f87a88728936a639f94a2a2238e41 Mon Sep 17 00:00:00 2001 From: brokemac79 Date: Sun, 19 Jul 2026 13:12:45 +0100 Subject: [PATCH] fix(workflow): make direct PR webhook intake canonical --- .github/workflows/sweep.yml | 5 ++- test/clawsweeper.test.ts | 22 +++++++++++++ test/dashboard-worker.test.ts | 61 +++++++++++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sweep.yml b/.github/workflows/sweep.yml index da206f7a53..403a9e2e72 100644 --- a/.github/workflows/sweep.yml +++ b/.github/workflows/sweep.yml @@ -228,7 +228,10 @@ jobs: legacy-event-queue-intake: name: Queue legacy exact-review event - if: ${{ github.event_name == 'repository_dispatch' && github.event.action != 'clawsweeper_target_sweep' && github.event.client_payload.queue_lease_id == '' && !(github.event.client_payload.target_repo == 'openclaw/clawhub' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') }} + # Pull-request events arrive through the direct GitHub App webhook. The + # legacy pull_request_target dispatch is a compatibility duplicate and must + # not create a second durable exact-review intent. + if: ${{ github.event_name == 'repository_dispatch' && github.event.action != 'clawsweeper_target_sweep' && github.event.client_payload.queue_lease_id == '' && (github.event.client_payload.source_event != 'pull_request_target' || github.event.client_payload.item_kind != 'pull_request') && !(github.event.client_payload.target_repo == 'openclaw/clawhub' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') }} runs-on: ubuntu-latest timeout-minutes: 5 steps: diff --git a/test/clawsweeper.test.ts b/test/clawsweeper.test.ts index d96fdcbcaa..fe0b635c28 100644 --- a/test/clawsweeper.test.ts +++ b/test/clawsweeper.test.ts @@ -2276,6 +2276,28 @@ test("sweep workflow executes only durable queue leases without runner-side admi assert.doesNotMatch(exactReviewStep, /--codex-timeout-ms 600000/); }); +test("sweep workflow suppresses only legacy pull_request_target PR duplicate ingress", () => { + const workflow = readText(".github/workflows/sweep.yml"); + const legacyIntakeBlock = workflow.slice( + workflow.indexOf("\n legacy-event-queue-intake:"), + workflow.indexOf("\n event-review-apply:"), + ); + const legacyIngressWouldQueue = (sourceEvent: string, itemKind: string) => + sourceEvent !== "pull_request_target" || itemKind !== "pull_request"; + + assert.equal(legacyIngressWouldQueue("pull_request_target", "pull_request"), false); + assert.equal(legacyIngressWouldQueue("pull_request", "pull_request"), true); + assert.equal(legacyIngressWouldQueue("issues", "issue"), true); + assert.equal(legacyIngressWouldQueue("issue_comment", "pull_request"), true); + assert.match( + legacyIntakeBlock, + /github\.event\.client_payload\.source_event != 'pull_request_target' \|\| github\.event\.client_payload\.item_kind != 'pull_request'/, + ); + assert.match(legacyIntakeBlock, /\/internal\/exact-review\/enqueue/); + assert.doesNotMatch(legacyIntakeBlock, /source_event != 'pull_request'/); + assert.doesNotMatch(legacyIntakeBlock, /source_event != 'issue_comment'/); +}); + test("sweep workflow gives high-context Codex reviews twenty minutes by default", () => { const workflow = readText(".github/workflows/sweep.yml"); diff --git a/test/dashboard-worker.test.ts b/test/dashboard-worker.test.ts index 2940b0be70..9282b0df2c 100644 --- a/test/dashboard-worker.test.ts +++ b/test/dashboard-worker.test.ts @@ -10392,6 +10392,59 @@ test("hosted webhook enqueues item events with the repository default branch", a }); }); +test("hosted webhook preserves pull-request revision and body updates", async () => { + const storage = new MemoryDurableStorage(); + const queue = new ExactReviewQueue({ storage }, {}); + const env = { + CLAWSWEEPER_WEBHOOK_SECRET: "test-secret", + EXACT_REVIEW_QUEUE: new MemoryDurableNamespace(queue), + }; + const repository = { + full_name: "openclaw/gogcli", + default_branch: "trunk", + private: false, + archived: false, + fork: false, + has_issues: true, + }; + + for (const [deliveryId, action] of [ + ["pr-revision", "synchronize"], + ["pr-body", "edited"], + ] as const) { + const response = await worker.fetch( + signedGithubWebhookRequest({ + event: "pull_request", + secret: "test-secret", + deliveryId, + payload: { + action, + repository, + pull_request: { number: 597 }, + installation: { id: 123 }, + }, + }), + env, + ); + assert.equal(response.status, 202); + assert.deepEqual(await response.json(), { + ok: true, + queued: true, + item_key: "openclaw/gogcli#597", + }); + } + + const state = (await storage.get("exact-review-queue")) as { + items: Record< + string, + { revision: number; decision: { sourceEvent: string; sourceAction: string } } + >; + }; + assert.equal(state.items["openclaw/gogcli#597"].revision, 2); + assert.equal(state.items["openclaw/gogcli#597"].decision.sourceEvent, "pull_request"); + assert.equal(state.items["openclaw/gogcli#597"].decision.sourceAction, "edited"); +}); + test("hosted webhook requeues unlocked and close-guard removal events", async () => { const closeGuardLabels = [ "security", @@ -11103,23 +11156,27 @@ function signedGithubWebhookRequest({ event, secret, payload, + deliveryId, }: { event: string; secret: string; payload: unknown; + deliveryId?: string; }) { const body = JSON.stringify(payload); - return signedGithubWebhookBodyRequest({ event, secret, body }); + return signedGithubWebhookBodyRequest({ event, secret, body, deliveryId }); } function signedGithubWebhookBodyRequest({ event, secret, body, + deliveryId = "test-delivery", }: { event: string; secret: string; body: string; + deliveryId?: string; }) { const signature = `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`; return new Request("https://clawsweeper.openclaw.ai/github/webhook", { @@ -11127,7 +11184,7 @@ function signedGithubWebhookBodyRequest({ headers: { "content-type": "application/json", "x-github-event": event, - "x-github-delivery": "test-delivery", + "x-github-delivery": deliveryId, "x-hub-signature-256": signature, }, body,