From 49653c2d50008c675df162a79ba3eeb5a5e01e83 Mon Sep 17 00:00:00 2001 From: sujini kudipudi Date: Tue, 26 May 2026 13:04:30 +0530 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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); From 2887905e282579d9fd86b8a8a567545b11295402 Mon Sep 17 00:00:00 2001 From: sujini kudipudi Date: Mon, 15 Jun 2026 00:43:20 +0530 Subject: [PATCH 5/6] fix: resolve CI failures by updating tests and removing console.log --- src/app/(app)/dashboard/sync-button.tsx | 2 -- src/inngest/functions/process-pr-event.test.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/(app)/dashboard/sync-button.tsx b/src/app/(app)/dashboard/sync-button.tsx index 9a015c69..42662388 100644 --- a/src/app/(app)/dashboard/sync-button.tsx +++ b/src/app/(app)/dashboard/sync-button.tsx @@ -62,8 +62,6 @@ export function SyncButton({ lastSyncedAt, userId }: Props) { const data = await res.json(); - console.log('SYNC STATUS:', data); - if (data.status === 'completed') { clearInterval(interval); diff --git a/src/inngest/functions/process-pr-event.test.ts b/src/inngest/functions/process-pr-event.test.ts index 85413b26..fc92e782 100644 --- a/src/inngest/functions/process-pr-event.test.ts +++ b/src/inngest/functions/process-pr-event.test.ts @@ -287,7 +287,7 @@ describe('processPrEvent - linkPrToClaim issues relation array', () => { }, ], }), - update: vi.fn().mockResolvedValue({ error: null }), + update: vi.fn().mockReturnThis(), }); const profilesMock = sb({ From 7f1b50eddd88b2fff75914e3f8546b7cb0d8fbbe Mon Sep 17 00:00:00 2001 From: sujini kudipudi Date: Wed, 17 Jun 2026 20:47:52 +0530 Subject: [PATCH 6/6] Fix stale PR status by adding periodic refresh and Sync button (Issue #316) --- src/app/(app)/my-prs/page.tsx | 98 +++------------------------------- src/app/actions/github-sync.ts | 91 +++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 91 deletions(-) diff --git a/src/app/(app)/my-prs/page.tsx b/src/app/(app)/my-prs/page.tsx index c5d22587..eaa6701b 100644 --- a/src/app/(app)/my-prs/page.tsx +++ b/src/app/(app)/my-prs/page.tsx @@ -6,6 +6,8 @@ import { cacheGet, cacheSet, cacheDel } from '@/lib/cache'; import { getInstallationToken } from '@/lib/github/app'; import { PRList } from './pr-list'; import type { GitHubPR } from '@/app/actions/github-sync'; +import { fetchAndBackfillPRs } from '@/app/actions/github-sync'; +import { SyncButton } from '../dashboard/sync-button'; export const dynamic = 'force-dynamic'; @@ -21,95 +23,6 @@ type PRsCache = { prs: EnrichedPR[]; }; -type GitHubSearchItem = { - id: number; - number: number; - title: string; - html_url: string; - state: string; - created_at: string; - updated_at: string; - pull_request?: { merged_at: string | null; url: string }; - repository_url: string; -}; - -async function fetchAndBackfillPRs( - service: NonNullable>, - userId: string, - githubHandle: string, - installId: number | null, -): Promise { - const headers: Record = { - Accept: 'application/vnd.github+json', - 'X-GitHub-Api-Version': '2022-11-28', - }; - - if (installId) { - try { - const token = await getInstallationToken(installId); - headers['Authorization'] = `Bearer ${token}`; - } catch { - // proceed without auth — public PRs still visible - } - } - - // Fetch up to 100 PRs authored by this user across all of GitHub - const url = `https://api.github.com/search/issues?q=is:pr+author:${encodeURIComponent(githubHandle)}&sort=created&order=desc&per_page=100`; - let items: GitHubSearchItem[] = []; - try { - const res = await fetch(url, { headers }); - if (res.ok) { - const data = (await res.json()) as { items?: GitHubSearchItem[] }; - items = data.items ?? []; - } - } catch { - return []; - } - - if (items.length === 0) return []; - - // Map to pull_requests row shape - const rows = items.map((item) => { - const repoFullName = item.repository_url.replace('https://api.github.com/repos/', ''); - const mergedAt = item.pull_request?.merged_at ?? null; - const state: 'open' | 'closed' | 'merged' = mergedAt - ? 'merged' - : item.state === 'open' - ? 'open' - : 'closed'; - - return { - github_pr_id: item.id, - repo_full_name: repoFullName, - number: item.number, - title: item.title, - author_login: githubHandle, - author_user_id: userId, - state, - url: item.html_url, - github_created_at: item.created_at, - github_updated_at: item.updated_at ?? item.created_at, - merged_at: mergedAt, - }; - }); - - // Upsert into pull_requests so webhook-future events will also exist - await service - .from('pull_requests') - .upsert(rows, { onConflict: 'github_pr_id', ignoreDuplicates: false }); - - // Re-query to get DB-assigned ids - const { data: saved } = await service - .from('pull_requests') - .select( - 'id, github_pr_id, repo_full_name, number, title, state, url, github_created_at, merged_at', - ) - .eq('author_user_id', userId) - .order('github_created_at', { ascending: false }); - - return (saved ?? []) as GitHubPR[]; -} - export default async function MyPRsPage() { const sb = await getServerSupabase(); if (!sb) @@ -131,7 +44,7 @@ export default async function MyPRsPage() { // Fetch profile with XP/level const { data: profile } = await service .from('profiles') - .select('github_handle, xp, level, avatar_url') + .select('github_handle, xp, level, avatar_url, github_stats_synced_at') .eq('id', user.id) .maybeSingle(); @@ -300,10 +213,13 @@ export default async function MyPRsPage() {
{/* Main Content */}
-
+

My Pull Requests

+
+ +
diff --git a/src/app/actions/github-sync.ts b/src/app/actions/github-sync.ts index 2543b6fb..62214cfb 100644 --- a/src/app/actions/github-sync.ts +++ b/src/app/actions/github-sync.ts @@ -19,11 +19,100 @@ export type GitHubPR = { merged_at: string | null; }; +export type GitHubSearchItem = { + id: number; + number: number; + title: string; + html_url: string; + state: string; + created_at: string; + updated_at: string; + pull_request?: { merged_at: string | null; url: string }; + repository_url: string; +}; + export type SyncOutput = { merges: number; streak: number; }; +export async function fetchAndBackfillPRs( + service: NonNullable>, + userId: string, + githubHandle: string, + installId: number | null, +): Promise { + const headers: Record = { + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + }; + + if (installId) { + try { + const token = await getInstallationToken(installId); + headers['Authorization'] = `Bearer ${token}`; + } catch { + // proceed without auth — public PRs still visible + } + } + + // Fetch up to 100 PRs authored by this user across all of GitHub + const url = `https://api.github.com/search/issues?q=is:pr+author:${encodeURIComponent(githubHandle)}&sort=created&order=desc&per_page=100`; + let items: GitHubSearchItem[] = []; + try { + const res = await fetch(url, { headers }); + if (res.ok) { + const data = (await res.json()) as { items?: GitHubSearchItem[] }; + items = data.items ?? []; + } + } catch { + return []; + } + + if (items.length === 0) return []; + + // Map to pull_requests row shape + const rows = items.map((item) => { + const repoFullName = item.repository_url.replace('https://api.github.com/repos/', ''); + const mergedAt = item.pull_request?.merged_at ?? null; + const state: 'open' | 'closed' | 'merged' = mergedAt + ? 'merged' + : item.state === 'open' + ? 'open' + : 'closed'; + + return { + github_pr_id: item.id, + repo_full_name: repoFullName, + number: item.number, + title: item.title, + author_login: githubHandle, + author_user_id: userId, + state, + url: item.html_url, + github_created_at: item.created_at, + github_updated_at: item.updated_at ?? item.created_at, + merged_at: mergedAt, + }; + }); + + // Upsert into pull_requests so webhook-future events will also exist + await service + .from('pull_requests') + .upsert(rows, { onConflict: 'github_pr_id', ignoreDuplicates: false }); + + // Re-query to get DB-assigned ids + const { data: saved } = await service + .from('pull_requests') + .select( + 'id, github_pr_id, repo_full_name, number, title, state, url, github_created_at, merged_at', + ) + .eq('author_user_id', userId) + .order('github_created_at', { ascending: false }); + + return (saved ?? []) as GitHubPR[]; +} + export async function syncGitHubStats(): Promise> { const sb = await getServerSupabase(); if (!sb) return err('not_configured', 'Auth not configured'); @@ -66,6 +155,7 @@ export async function syncGitHubStats(): Promise> { const [merges, streak] = await Promise.all([ fetchMergedCount(token, profile.github_handle), fetchContributionStreak(token, profile.github_handle), + fetchAndBackfillPRs(service, user.id, profile.github_handle, installId), ]); await service @@ -78,6 +168,7 @@ export async function syncGitHubStats(): Promise> { .eq('id', user.id); await cacheDel(`gh:dashboard:${user.id}`); + await cacheDel(`myprs:${user.id}`); return ok({ merges, streak }); } catch (e) {