Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/action/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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");
Expand Down
41 changes: 41 additions & 0 deletions src/trustsignal/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -106,3 +108,42 @@ export function normalizePushPayload(payload: Record<string, any>): GitHubVerifi
},
};
}

export function normalizeCheckSuitePayload(payload: Record<string, any>): 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,
},
};
}
7 changes: 6 additions & 1 deletion src/webhooks/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
34 changes: 31 additions & 3 deletions src/webhooks/handlers/checkSuite.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
export function normalizeCheckSuiteEvent(payload: Record<string, any>) {
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<string, any>;
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;
}
205 changes: 205 additions & 0 deletions tests/webhookFlow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
});
});
Loading