From 49653c2d50008c675df162a79ba3eeb5a5e01e83 Mon Sep 17 00:00:00 2001 From: sujini kudipudi Date: Tue, 26 May 2026 13:04:30 +0530 Subject: [PATCH 1/4] fix: correctly handle issues relation array in linkPrToClaim --- src/inngest/functions/process-pr-event.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/inngest/functions/process-pr-event.ts b/src/inngest/functions/process-pr-event.ts index e62a6256..5ec48aab 100644 --- a/src/inngest/functions/process-pr-event.ts +++ b/src/inngest/functions/process-pr-event.ts @@ -170,12 +170,11 @@ async function linkPrToClaim( .is('linked_pr_url', null); for (const claim of claims ?? []) { - const issuesField = claim['issues'] as unknown as { - repo_full_name: string; - github_issue_number: number; - }; - const issue = issuesField; - if (!issue) continue; + const raw = (claim as unknown as { issues: unknown }).issues; + const issue = Array.isArray(raw) + ? (raw[0] as { repo_full_name?: string; github_issue_number?: number } | undefined) + : (raw as { repo_full_name?: string; github_issue_number?: number } | undefined); + if (!issue?.repo_full_name || typeof issue.github_issue_number !== 'number') continue; if (issue.repo_full_name === repo && issueRefs.includes(issue.github_issue_number)) { await sb.from('recommendations').update({ linked_pr_url: prUrl }).eq('id', claim.id); return { linked: true, recId: claim.id }; From 07a608eb224948e0b35d29370e219b5500053bf7 Mon Sep 17 00:00:00 2001 From: sujini kudipudi Date: Thu, 28 May 2026 16:19:11 +0530 Subject: [PATCH 2/4] fix(webhook): ignore bare issue references for xp awards --- src/inngest/functions/process-pr-event.test.ts | 6 +++--- src/inngest/functions/process-pr-event.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/inngest/functions/process-pr-event.test.ts b/src/inngest/functions/process-pr-event.test.ts index e5ca9654..d9498ab2 100644 --- a/src/inngest/functions/process-pr-event.test.ts +++ b/src/inngest/functions/process-pr-event.test.ts @@ -10,12 +10,12 @@ describe('extractIssueNumbers', () => { expect(extractIssueNumbers('fixes #45 and resolves #67')).toEqual([45, 67]); }); - it('finds bare "#7" references', () => { - expect(extractIssueNumbers('related to #7')).toEqual([7]); + it('ignores bare "#7" references', () => { + expect(extractIssueNumbers('related to #7')).toEqual([]); }); it('dedupes repeated numbers', () => { - expect(extractIssueNumbers('#5 #5 closes #5')).toEqual([5]); + expect(extractIssueNumbers('closes #5 fixes #5')).toEqual([5]); }); it('ignores non-issue # like #foo', () => { diff --git a/src/inngest/functions/process-pr-event.ts b/src/inngest/functions/process-pr-event.ts index 5ec48aab..7c73a543 100644 --- a/src/inngest/functions/process-pr-event.ts +++ b/src/inngest/functions/process-pr-event.ts @@ -49,13 +49,13 @@ type PrPayload = { }; }; -const ISSUE_REF = /(?:close[sd]?|fixe[sd]?|resolve[sd]?)\s+#(\d+)|#(\d+)/gi; +const ISSUE_REF = /(?:close[sd]?|fixe[sd]?|resolve[sd]?)\s+#(\d+)/gi; export function extractIssueNumbers(text: string | null | undefined): number[] { if (!text) return []; const found = new Set(); for (const m of text.matchAll(ISSUE_REF)) { - const n = parseInt(m[1] ?? m[2] ?? '', 10); + const n = parseInt(m[1] ?? '', 10); if (Number.isFinite(n)) found.add(n); } return [...found]; From edc0a05f53249dcda32c3c03cdcc4ff6ab7fa5da Mon Sep 17 00:00:00 2001 From: sujini kudipudi Date: Sun, 31 May 2026 22:41:14 +0530 Subject: [PATCH 3/4] fix: apply array unwrapping to all issues relation call sites --- .../functions/process-pr-event.test.ts | 91 +++++++++++++++++++ src/inngest/functions/process-pr-event.ts | 15 ++- .../functions/recommendations-build.ts | 7 +- src/lib/supabase/inner-join.ts | 3 + 4 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 src/lib/supabase/inner-join.ts diff --git a/src/inngest/functions/process-pr-event.test.ts b/src/inngest/functions/process-pr-event.test.ts index 83edcc8e..c0d2e072 100644 --- a/src/inngest/functions/process-pr-event.test.ts +++ b/src/inngest/functions/process-pr-event.test.ts @@ -270,3 +270,94 @@ describe('processPrEvent - awardRecommendedMerge XP capping', () => { ); }); }); + +describe('processPrEvent - linkPrToClaim issues relation array', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const setupMock = (issuesArray: unknown) => { + const recommendationsMock = sb({ + is: vi.fn().mockResolvedValue({ + data: [ + { + id: 1, + issue_id: 101, + issues: issuesArray, + }, + ], + }), + update: vi.fn().mockResolvedValue({ error: null }), + }); + + const profilesMock = sb({ + maybeSingle: vi.fn().mockResolvedValue({ data: { id: 'contributor-id' } }), + }); + + const pullRequestsMock = sb({ + upsert: vi.fn().mockResolvedValue({ error: null }), + }); + + const installationRepositoriesMock = sb({ + maybeSingle: vi.fn().mockResolvedValue({ data: { repo_full_name: 'owner/repo' } }), + }); + + wire({ + recommendations: recommendationsMock, + profiles: profilesMock, + pull_requests: pullRequestsMock, + installation_repositories: installationRepositoriesMock, + }); + + return { recommendationsMock }; + }; + + const evOpened = () => ({ + data: { + payload: { + action: 'opened', + pull_request: { + id: 1234, + number: 1, + html_url: 'https://github.com/owner/repo/pull/1', + title: 'Fix issue', + body: 'Closes #123', + state: 'open', + draft: false, + merged: false, + merged_at: null, + closed_at: null, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + user: { login: 'contributor' }, + base: { repo: { full_name: 'owner/repo' } }, + }, + }, + }, + }); + + it('handles issues relation returned as an array', async () => { + const { recommendationsMock } = setupMock([ + { repo_full_name: 'owner/repo', github_issue_number: 123 }, + ]); + + await prRun({ event: evOpened(), step }); + + expect(recommendationsMock.update).toHaveBeenCalledWith( + expect.objectContaining({ linked_pr_url: 'https://github.com/owner/repo/pull/1' }), + ); + }); + + it('handles issues relation returned as a single object', async () => { + const { recommendationsMock } = setupMock({ + repo_full_name: 'owner/repo', + github_issue_number: 123, + }); + + await prRun({ event: evOpened(), step }); + + expect(recommendationsMock.update).toHaveBeenCalledWith( + expect.objectContaining({ linked_pr_url: 'https://github.com/owner/repo/pull/1' }), + ); + }); +}); diff --git a/src/inngest/functions/process-pr-event.ts b/src/inngest/functions/process-pr-event.ts index eb6aefa1..a0016c53 100644 --- a/src/inngest/functions/process-pr-event.ts +++ b/src/inngest/functions/process-pr-event.ts @@ -4,6 +4,7 @@ import { insertXpEvent } from '@/lib/xp/events'; import { XP_SOURCE, xpForMerge, refIds, XP_REWARDS } from '@/lib/xp/sources'; import { cacheDelByPrefix } from '@/lib/cache'; import { buildPrRow, type IngestiblePr } from '@/lib/maintainer/pr-ingest'; +import { unwrapJoin } from '@/lib/supabase/inner-join'; /** * Webhook handler for GitHub `pull_request` events. @@ -170,10 +171,9 @@ async function linkPrToClaim( .is('linked_pr_url', null); for (const claim of claims ?? []) { - const raw = (claim as unknown as { issues: unknown }).issues; - const issue = Array.isArray(raw) - ? (raw[0] as { repo_full_name?: string; github_issue_number?: number } | undefined) - : (raw as { repo_full_name?: string; github_issue_number?: number } | undefined); + const issue = unwrapJoin<{ repo_full_name?: string; github_issue_number?: number }>( + (claim as unknown as { issues: unknown }).issues, + ); if (!issue?.repo_full_name || typeof issue.github_issue_number !== 'number') continue; if (issue.repo_full_name === repo && issueRefs.includes(issue.github_issue_number)) { await sb.from('recommendations').update({ linked_pr_url: prUrl }).eq('id', claim.id); @@ -317,10 +317,9 @@ async function tryLinkByIssueRef( for (const claim of claims ?? []) { // Supabase types the joined `issues` field as an array even for a // single-row !inner join. Normalise. - const raw = (claim as unknown as { issues: unknown }).issues; - const issue = Array.isArray(raw) - ? (raw[0] as { repo_full_name?: string; github_issue_number?: number } | undefined) - : (raw as { repo_full_name?: string; github_issue_number?: number } | undefined); + const issue = unwrapJoin<{ repo_full_name?: string; github_issue_number?: number }>( + (claim as unknown as { issues: unknown }).issues, + ); if (!issue?.repo_full_name || typeof issue.github_issue_number !== 'number') continue; if (issue.repo_full_name === repo && issueRefs.includes(issue.github_issue_number)) { return (claim as { id: number }).id; diff --git a/src/inngest/functions/recommendations-build.ts b/src/inngest/functions/recommendations-build.ts index e31ac30f..d5a51478 100644 --- a/src/inngest/functions/recommendations-build.ts +++ b/src/inngest/functions/recommendations-build.ts @@ -1,6 +1,7 @@ import { inngest } from '../client'; import { getServiceSupabase } from '@/lib/supabase/service'; import { filterAndRank, type ScoredIssue, type SkipCounts } from '@/lib/pipeline/recommend'; +import { unwrapJoin } from '@/lib/supabase/inner-join'; import { SKIP_HISTORY_WINDOW_DAYS } from '@/lib/pipeline/constants'; /** @@ -75,10 +76,12 @@ export const recommendationsBuild = inngest.createFunction( const skipHistoryMap: Record = {}; for (const row of skipsData ?? []) { const userId = row.user_id; - const issue = row.issues as unknown as { + const issue = unwrapJoin<{ repo_full_name: string; repo_language: string | null; - }; + }>((row as unknown as { issues: unknown }).issues); + + if (!issue?.repo_full_name) continue; if (!skipHistoryMap[userId]) { skipHistoryMap[userId] = { byRepo: {}, byLanguage: {} }; diff --git a/src/lib/supabase/inner-join.ts b/src/lib/supabase/inner-join.ts new file mode 100644 index 00000000..bd941fbb --- /dev/null +++ b/src/lib/supabase/inner-join.ts @@ -0,0 +1,3 @@ +export function unwrapJoin(raw: unknown): T | undefined { + return Array.isArray(raw) ? (raw[0] as T) : (raw as T); +} From 09d06dc143b61b19abf55229c141a60d8e712fd0 Mon Sep 17 00:00:00 2001 From: sujini kudipudi Date: Fri, 5 Jun 2026 17:56:41 +0530 Subject: [PATCH 4/4] fix: resolve duplicate issue variable and formatting --- package-lock.json | 27 ----------------------- src/inngest/functions/process-pr-event.ts | 5 +---- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 715fd0ed..d4beec99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,7 +117,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -2081,7 +2080,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.6.tgz", "integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==", "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", @@ -2310,7 +2308,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -2423,7 +2420,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -4616,7 +4612,6 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.105.4.tgz", "integrity": "sha512-cEnx+k49knU+qdIP7rXwR6fqEXPHZs+74xFK1R0S8MgQ7v9tbePVdGxvO03n3bPympMdJWVLadARBfU4TgNHCQ==", "license": "MIT", - "peer": true, "dependencies": { "@supabase/auth-js": "2.105.4", "@supabase/functions-js": "2.105.4", @@ -4970,7 +4965,6 @@ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -4999,7 +4993,6 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -5011,7 +5004,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -5096,7 +5088,6 @@ "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", @@ -5608,7 +5599,6 @@ "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.38.0.tgz", "integrity": "sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg==", "license": "MIT", - "peer": true, "dependencies": { "uncrypto": "^0.1.3" } @@ -5859,7 +5849,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6397,7 +6386,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -7736,7 +7724,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7922,7 +7909,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9912,7 +9898,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -10691,7 +10676,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", @@ -11263,7 +11247,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -11434,7 +11417,6 @@ "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.9.tgz", "integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==", "license": "Unlicense", - "peer": true, "engines": { "node": ">=12" }, @@ -11498,7 +11480,6 @@ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11667,7 +11648,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11680,7 +11660,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13140,7 +13119,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13794,7 +13772,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14043,7 +14020,6 @@ "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -14593,7 +14569,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14607,7 +14582,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -15070,7 +15044,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/inngest/functions/process-pr-event.ts b/src/inngest/functions/process-pr-event.ts index 8be534a3..e61b939a 100644 --- a/src/inngest/functions/process-pr-event.ts +++ b/src/inngest/functions/process-pr-event.ts @@ -174,10 +174,7 @@ async function linkPrToClaim( const issue = unwrapJoin<{ repo_full_name?: string; github_issue_number?: number }>( (claim as unknown as { issues: unknown }).issues, ); - const raw = (claim as unknown as { issues: unknown }).issues; - const issue = Array.isArray(raw) - ? (raw[0] as { repo_full_name?: string; github_issue_number?: number } | undefined) - : (raw as { repo_full_name?: string; github_issue_number?: number } | undefined); + if (!issue?.repo_full_name || typeof issue.github_issue_number !== 'number') continue; if (issue.repo_full_name === repo && issueRefs.includes(issue.github_issue_number)) { await sb.from('recommendations').update({ linked_pr_url: prUrl }).eq('id', claim.id);