diff --git a/src/integrations/linear-adapter.ts b/src/integrations/linear-adapter.ts index ba8eda5ff8..88efad9d29 100644 --- a/src/integrations/linear-adapter.ts +++ b/src/integrations/linear-adapter.ts @@ -111,6 +111,7 @@ export type LinearNativeLinkResult = { */ export async function findLinearNativeLink(ctx: ProjectTrackerContext, prUrl: string): Promise { const none: LinearNativeLinkResult = { project: null, milestone: null }; + if (typeof prUrl !== "string" || prUrl.trim().length === 0) return none; const apiKey = await getDecryptedRepositoryLinearKey(ctx.env, ctx.repoFullName); if (!apiKey) return none; const data = await linearGraphQl( diff --git a/src/integrations/project-tracker-adapter.ts b/src/integrations/project-tracker-adapter.ts index cc95147b3b..30db88c666 100644 --- a/src/integrations/project-tracker-adapter.ts +++ b/src/integrations/project-tracker-adapter.ts @@ -3,7 +3,7 @@ import { githubRateLimitAdmissionKeyForInstallation, makeInstallationOctokit } f import { createIssueComment } from "../github/pr-actions"; import { findLinearNativeLink, LinearAdapter } from "./linear-adapter"; import { termOverlap, tokenize, type CollisionTerms } from "../signals/engine"; -import { errorMessage } from "../utils/json"; +import { errorMessage, parsePositiveInt } from "../utils/json"; /** Repo-scoped context shared by every ProjectTrackerAdapter call (#3183). */ export type ProjectTrackerContext = { @@ -55,13 +55,6 @@ type GitHubMilestone = { // any realistic open-milestone/open-project/PR-comment count, while still bounding worst-case API calls. const GITHUB_LIST_PAGE_LIMIT = 3; -/** A positive-integer milestone/issue number as a string, or null if `value` isn't one. Guards against a - * malformed/forged `milestoneId` reaching GitHub's PATCH as `NaN` or a negative/zero number. */ -function parsePositiveIntegerId(value: string): number | null { - const parsed = Number(value); - return Number.isInteger(parsed) && parsed > 0 ? parsed : null; -} - /** GitHub REST implementation of {@link ProjectTrackerAdapter}. Only the Milestone half is real (#3183) -- * Projects v2 lives in the separate {@link GitHubProjectsAdapter} (#3184), so those two methods are inert here. */ export class GitHubMilestonesAdapter implements ProjectTrackerAdapter { @@ -96,7 +89,7 @@ export class GitHubMilestonesAdapter implements ProjectTrackerAdapter { } async attachToMilestone(ctx: ProjectTrackerContext, pullNumber: number, milestoneId: string): Promise { - const milestoneNumber = parsePositiveIntegerId(milestoneId); + const milestoneNumber = parsePositiveInt(milestoneId); if (milestoneNumber === null) return { attached: false }; const { owner, repo } = parseRepoFullName(ctx.repoFullName); const token = await createInstallationToken(ctx.env, ctx.installationId); diff --git a/test/unit/linear-adapter.test.ts b/test/unit/linear-adapter.test.ts index 6420744f85..c650a0140b 100644 --- a/test/unit/linear-adapter.test.ts +++ b/test/unit/linear-adapter.test.ts @@ -117,6 +117,20 @@ describe("findLinearNativeLink (#3186)", () => { expect(result).toEqual({ project: null, milestone: null }); }); + it("returns nulls for a blank PR URL without calling Linear", async () => { + let called = false; + vi.stubGlobal("fetch", async () => { + called = true; + return new Response("unexpected", { status: 500 }); + }); + const env = createTestEnv({ TOKEN_ENCRYPTION_SECRET: SECRET }); + await upsertRepositoryLinearKey(env, { repoFullName: "acme/widgets", key: "lin_api_test_key" }); + for (const prUrl of ["", " "]) { + await expect(findLinearNativeLink({ env, installationId: 123, repoFullName: "acme/widgets" }, prUrl)).resolves.toEqual({ project: null, milestone: null }); + } + expect(called).toBe(false); + }); + it("finds a native-linked issue's project and milestone via attachmentsForURL", async () => { const env = createTestEnv({ TOKEN_ENCRYPTION_SECRET: SECRET }); await upsertRepositoryLinearKey(env, { repoFullName: "acme/widgets", key: "lin_api_test_key" }); diff --git a/test/unit/project-tracker-adapter.test.ts b/test/unit/project-tracker-adapter.test.ts index 05f22dacab..f22c9a73cb 100644 --- a/test/unit/project-tracker-adapter.test.ts +++ b/test/unit/project-tracker-adapter.test.ts @@ -118,7 +118,7 @@ describe("GitHubMilestonesAdapter (#3183)", () => { }); const adapter = new GitHubMilestonesAdapter(); const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem() }); - for (const invalidId of ["not-a-number", "0", "-5", "3.5", ""]) { + for (const invalidId of ["not-a-number", "0", "-5", "3.5", "", " 14", "1e2"]) { const result = await adapter.attachToMilestone({ env, installationId: 123, repoFullName: "JSONbored/gittensory" }, 4, invalidId); expect(result).toEqual({ attached: false }); }