diff --git a/packages/webhook-ingestion/src/github/payload.ts b/packages/webhook-ingestion/src/github/payload.ts index abbd286..9f7e078 100644 --- a/packages/webhook-ingestion/src/github/payload.ts +++ b/packages/webhook-ingestion/src/github/payload.ts @@ -56,6 +56,21 @@ const optionalProviderId = (record: JsonRecord, key: string): string | undefined const withOptional = (key: K, value: V | undefined): Partial> => value === undefined ? {} : ({ [key]: value } as Record); +/** Returns the account associated with a GitHub App installation webhook payload. */ +const installationAccount = (payload: JsonRecord): JsonRecord => { + const installation = asRecord(payload.installation, "installation"); + const account = + optionalRecord(installation.account) ?? + optionalRecord(optionalRecord(payload.repository)?.owner) ?? + optionalRecord(payload.sender); + + if (!account) { + throw new WebhookPayloadError("GitHub payload is missing installation.account."); + } + + return account; +}; + const numberValue = (record: JsonRecord, key: string): number => { const value = record[key]; if (typeof value === "number") { @@ -165,9 +180,8 @@ export type NormalizedGitHubFeedback = { /** Extracts a GitHub installation account. */ export function normalizeGitHubAccount(payload: JsonRecord): NormalizedGitHubAccount { - const installation = asRecord(payload.installation, "installation"); - const account = asRecord(installation.account, "installation.account"); - const providerAccountId = stringValue(account, "id"); + const account = installationAccount(payload); + const providerAccountId = optionalProviderId(account, "id") ?? stringValue(account, "login"); const login = stringValue(account, "login"); const rawType = optionalString(account, "type")?.toLowerCase(); const accountType = diff --git a/packages/webhook-ingestion/test/fixtures.ts b/packages/webhook-ingestion/test/fixtures.ts index ba1d05c..51a459c 100644 --- a/packages/webhook-ingestion/test/fixtures.ts +++ b/packages/webhook-ingestion/test/fixtures.ts @@ -25,7 +25,9 @@ export const installationPayload = { default_branch: "main", clone_url: "https://github.com/acme/heimdall.git", owner: { + id: 42, login: "acme", + type: "Organization", }, }, ], diff --git a/packages/webhook-ingestion/test/github-normalize.test.ts b/packages/webhook-ingestion/test/github-normalize.test.ts index 0278201..fed10ed 100644 --- a/packages/webhook-ingestion/test/github-normalize.test.ts +++ b/packages/webhook-ingestion/test/github-normalize.test.ts @@ -34,6 +34,25 @@ describe("GitHub webhook normalization", () => { expect(pullRequest.snapshot.diffHash).toMatch(/^sha256:[a-f0-9]{64}$/u); }); + it("normalizes pull request payloads when installation account is omitted", () => { + const payload = { + ...pullRequestPayload, + installation: { + id: pullRequestPayload.installation.id, + }, + }; + + const installation = normalizeGitHubInstallation(payload); + const pullRequest = normalizeGitHubPullRequest(payload); + + expect(installation).toMatchObject({ + accountLogin: "acme", + accountType: "organization", + providerInstallationId: "123456", + }); + expect(pullRequest.snapshot.pullRequestNumber).toBe(7); + }); + it("normalizes pull request comments and reactions as feedback", () => { const comment = normalizeGitHubFeedback(issueCommentPayload, "issue_comment"); const reaction = normalizeGitHubFeedback(reactionPayload, "reaction");