diff --git a/apps/action/dist/index.js b/apps/action/dist/index.js index a15c0db..c213344 100644 --- a/apps/action/dist/index.js +++ b/apps/action/dist/index.js @@ -4196,6 +4196,8 @@ function normalizeGitHubEventToEnvelope(input) { return normalizeReleasePayload(input.payload); case "push": return normalizePushPayload(input.payload); + case "check_suite": + return normalizeCheckSuitePayload(input.payload); default: return null; } @@ -4280,6 +4282,35 @@ function normalizePushPayload(payload) { } }; } +function normalizeCheckSuitePayload(payload) { + const checkSuite = payload.check_suite; + const repository = payload.repository; + const headSha = typeof checkSuite?.head_sha === "string" && checkSuite.head_sha.length > 0 ? checkSuite.head_sha : typeof payload.after === "string" && payload.after.length > 0 ? payload.after : typeof payload.head_sha === "string" && payload.head_sha.length > 0 ? payload.head_sha : void 0; + if (!checkSuite || !repository || !headSha) { + return null; + } + return { + eventName: "check_suite", + repository: { + id: repository.id, + owner: repository.owner.name || repository.owner.login, + repo: repository.name, + defaultBranch: repository.default_branch, + htmlUrl: repository.html_url + }, + headSha, + externalId: `check_suite:${checkSuite.id}`, + summaryContext: `check suite ${checkSuite.id}`, + detailsUrl: checkSuite.url, + provenance: { + action: payload.action, + checkSuiteStatus: checkSuite.status, + checkSuiteConclusion: checkSuite.conclusion, + pullRequests: Array.isArray(payload.check_suite?.pull_requests) ? payload.check_suite.pull_requests.length : 0, + appSlug: checkSuite.app?.slug + } + }; +} // apps/action/src/env.ts var import_node_fs = require("node:fs"); diff --git a/src/trustsignal/github.ts b/src/trustsignal/github.ts index dfd5414..1081aad 100644 --- a/src/trustsignal/github.ts +++ b/src/trustsignal/github.ts @@ -13,6 +13,8 @@ export function normalizeGitHubEventToEnvelope(input: { return normalizeReleasePayload(input.payload); case "push": return normalizePushPayload(input.payload); + case "check_suite": + return normalizeCheckSuitePayload(input.payload); default: return null; } @@ -106,3 +108,42 @@ export function normalizePushPayload(payload: Record): GitHubVerifi }, }; } + +export function normalizeCheckSuitePayload(payload: Record): GitHubVerificationEnvelope | null { + const checkSuite = payload.check_suite; + const repository = payload.repository; + const headSha = + typeof checkSuite?.head_sha === "string" && checkSuite.head_sha.length > 0 + ? checkSuite.head_sha + : typeof payload.after === "string" && payload.after.length > 0 + ? payload.after + : typeof payload.head_sha === "string" && payload.head_sha.length > 0 + ? payload.head_sha + : undefined; + + if (!checkSuite || !repository || !headSha) { + return null; + } + + return { + eventName: "check_suite", + repository: { + id: repository.id, + owner: repository.owner.name || repository.owner.login, + repo: repository.name, + defaultBranch: repository.default_branch, + htmlUrl: repository.html_url, + }, + headSha, + externalId: `check_suite:${checkSuite.id}`, + summaryContext: `check suite ${checkSuite.id}`, + detailsUrl: checkSuite.url, + provenance: { + action: payload.action, + checkSuiteStatus: checkSuite.status, + checkSuiteConclusion: checkSuite.conclusion, + pullRequests: Array.isArray(payload.check_suite?.pull_requests) ? payload.check_suite.pull_requests.length : 0, + appSlug: checkSuite.app?.slug, + }, + }; +} diff --git a/src/webhooks/github.ts b/src/webhooks/github.ts index 61cf616..ac25742 100644 --- a/src/webhooks/github.ts +++ b/src/webhooks/github.ts @@ -54,7 +54,12 @@ export async function handleGitHubWebhook(input: HandleWebhookInput) { verificationJob = normalizePushEvent(base); break; case "check_suite": - normalizeCheckSuiteEvent(input.payload); + verificationJob = normalizeCheckSuiteEvent({ + action: input.parsed.action, + payload: input.payload, + deliveryId: input.parsed.deliveryId, + installationId: input.parsed.installationId, + }); break; case "check_run": normalizeCheckRunEvent(input.payload, input.appName); diff --git a/src/webhooks/handlers/checkSuite.ts b/src/webhooks/handlers/checkSuite.ts index e38269c..33cdf82 100644 --- a/src/webhooks/handlers/checkSuite.ts +++ b/src/webhooks/handlers/checkSuite.ts @@ -1,7 +1,35 @@ -export function normalizeCheckSuiteEvent(payload: Record) { - if (payload.check_suite?.app?.slug === "trustsignal") { +import type { VerificationJobInput } from "../../types/github"; +import { normalizeCheckSuitePayload } from "../../trustsignal/github"; + +export function normalizeCheckSuiteEvent(input: { + action: string | undefined; + payload: Record; + deliveryId: string; + installationId: number; + githubEnterpriseVersion?: string; +}) { + if (input.action !== "requested" && input.action !== "rerequested") { + return null; + } + + const envelope = normalizeCheckSuitePayload(input.payload); + if (!envelope) { return null; } - return null; + return { + deliveryId: input.deliveryId, + eventName: "check_suite", + installationId: input.installationId, + repository: envelope.repository, + headSha: envelope.headSha, + externalId: envelope.externalId, + summaryContext: envelope.summaryContext, + detailsUrl: envelope.detailsUrl, + githubEnterpriseVersion: input.githubEnterpriseVersion, + provenance: { + ...envelope.provenance, + githubEnterpriseVersion: input.githubEnterpriseVersion, + }, + } satisfies VerificationJobInput; } diff --git a/tests/webhookFlow.test.ts b/tests/webhookFlow.test.ts index 0f4fff6..dc96cf0 100644 --- a/tests/webhookFlow.test.ts +++ b/tests/webhookFlow.test.ts @@ -85,4 +85,209 @@ describe("handleGitHubWebhook", () => { ); expect(githubClient.getWorkflowRun).toHaveBeenCalledWith(7, "acme", "repo", 101); }); + it("processes requested check_suite events", async () => { + const githubClient = { + getWorkflowRun: vi.fn(), + createCheckRun: vi + .fn() + .mockResolvedValueOnce({ id: 84, html_url: "https://github.com/acme/repo/runs/84", status: "in_progress" }), + updateCheckRun: vi + .fn() + .mockResolvedValueOnce({ id: 84, html_url: "https://github.com/acme/repo/runs/84", status: "completed", conclusion: "success" }), + } as any; + const verificationService = { + verify: vi.fn().mockResolvedValue({ + status: "completed", + conclusion: "success", + title: "Artifact verification completed", + summary: "Verification succeeded", + detailsUrl: "https://trustsignal.example.com/receipts/rcpt_2", + receiptId: "rcpt_2", + verificationTimestamp: "2026-03-14T00:00:00.000Z", + provenanceNote: "check_suite event check_suite:321", + }), + }; + const logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + } as any; + + const result = await handleGitHubWebhook({ + parsed: { + deliveryId: "delivery-2", + event: "check_suite", + action: "requested", + installationId: 11, + }, + payload: { + action: "requested", + check_suite: { + id: 321, + head_sha: "def5678", + status: "queued", + conclusion: null, + head_branch: "main", + app: { + slug: "other-app", + }, + pull_requests: [], + }, + repository: { + id: 22, + name: "repo", + default_branch: "main", + html_url: "https://github.com/acme/repo", + owner: { login: "acme" }, + }, + }, + githubClient, + verificationService, + logger, + appName: "TrustSignal", + }); + + expect(result).toEqual({ accepted: true, ignored: false, receiptId: "rcpt_2" }); + expect(githubClient.createCheckRun).toHaveBeenCalledTimes(1); + expect(githubClient.updateCheckRun).toHaveBeenCalledTimes(1); + const createCall = (githubClient.createCheckRun as any).mock.calls[0]; + expect(createCall[0]).toBe(11); + expect(createCall[1]).toBe("acme"); + expect(createCall[2]).toBe("repo"); + expect(createCall[3]).toMatchObject({ + status: "in_progress", + output: { + title: "Verification started", + }, + }); + }); + + it("processes requested check_suite events using after as fallback", async () => { + const githubClient = { + createCheckRun: vi + .fn() + .mockResolvedValueOnce({ id: 91, html_url: "https://github.com/acme/repo/runs/91", status: "in_progress" }), + updateCheckRun: vi + .fn() + .mockResolvedValueOnce({ id: 91, html_url: "https://github.com/acme/repo/runs/91", status: "completed", conclusion: "success" }), + } as any; + const verificationService = { + verify: vi.fn().mockResolvedValue({ + status: "completed", + conclusion: "success", + title: "Artifact verification completed", + summary: "Verification succeeded", + detailsUrl: "https://trustsignal.example.com/receipts/rcpt_3", + receiptId: "rcpt_3", + verificationTimestamp: "2026-03-14T00:00:00.000Z", + provenanceNote: "check_suite event check_suite:654", + }), + }; + const logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + } as any; + + const result = await handleGitHubWebhook({ + parsed: { + deliveryId: "delivery-3", + event: "check_suite", + action: "requested", + installationId: 11, + }, + payload: { + action: "requested", + after: "def9999", + check_suite: { + id: 654, + status: "queued", + conclusion: null, + head_branch: "main", + app: { + slug: "other-app", + }, + pull_requests: [], + }, + repository: { + id: 22, + name: "repo", + default_branch: "main", + html_url: "https://github.com/acme/repo", + owner: { login: "acme" }, + }, + }, + githubClient, + verificationService, + logger, + appName: "TrustSignal", + }); + + expect(result).toEqual({ accepted: true, ignored: false, receiptId: "rcpt_3" }); + }); + + it("processes requested check_suite events even when the check suite app matches the GitHub app", async () => { + const githubClient = { + createCheckRun: vi + .fn() + .mockResolvedValueOnce({ id: 101, html_url: "https://github.com/acme/repo/runs/101", status: "in_progress" }), + updateCheckRun: vi + .fn() + .mockResolvedValueOnce({ id: 101, html_url: "https://github.com/acme/repo/runs/101", status: "completed", conclusion: "success" }), + } as any; + const verificationService = { + verify: vi.fn().mockResolvedValue({ + status: "completed", + conclusion: "success", + title: "Artifact verification completed", + summary: "Verification succeeded", + detailsUrl: "https://trustsignal.example.com/receipts/rcpt_4", + receiptId: "rcpt_4", + verificationTimestamp: "2026-03-14T00:00:00.000Z", + provenanceNote: "check_suite event check_suite:999", + }), + }; + const logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + } as any; + + const result = await handleGitHubWebhook({ + parsed: { + deliveryId: "delivery-4", + event: "check_suite", + action: "requested", + installationId: 11, + }, + payload: { + action: "requested", + check_suite: { + id: 999, + head_sha: "def5678", + status: "queued", + conclusion: null, + head_branch: "main", + app: { + name: "TrustSignal-Verify", + slug: "trustsignal-verify", + }, + pull_requests: [], + }, + repository: { + id: 22, + name: "repo", + default_branch: "main", + html_url: "https://github.com/acme/repo", + owner: { login: "acme" }, + }, + }, + githubClient, + verificationService, + logger, + appName: "TrustSignal-Verify", + }); + + expect(result).toEqual({ accepted: true, ignored: false, receiptId: "rcpt_4" }); + }); });