Skip to content
Closed
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
1 change: 1 addition & 0 deletions src/integrations/linear-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export type LinearNativeLinkResult = {
*/
export async function findLinearNativeLink(ctx: ProjectTrackerContext, prUrl: string): Promise<LinearNativeLinkResult> {
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<AttachmentsForUrlResponse>(
Expand Down
11 changes: 2 additions & 9 deletions src/integrations/project-tracker-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -96,7 +89,7 @@ export class GitHubMilestonesAdapter implements ProjectTrackerAdapter {
}

async attachToMilestone(ctx: ProjectTrackerContext, pullNumber: number, milestoneId: string): Promise<ProjectTrackerAttachResult> {
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);
Expand Down
14 changes: 14 additions & 0 deletions test/unit/linear-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
Expand Down
2 changes: 1 addition & 1 deletion test/unit/project-tracker-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Expand Down
Loading