Skip to content
Merged
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
24 changes: 24 additions & 0 deletions src/extractors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,4 +570,28 @@ describe("extractPullRequestNumbersForCommit", () => {
const result = extractPullRequestNumbersForCommit({ sha: "abc", message });
expect(result).toEqual([]);
});

// Numbers above the GraphQL Int (32-bit) bound cannot be GitHub PR numbers
// and must be filtered so they don't poison the release sync mutation.
it.each([
[
"FLEX-2816: fix something\n\nTwo issues from cursor[bot] review #4211934690.",
[],
"fallback drops oversized #NNN (cursor review id)",
],
[
"Fix something (#51876)\n\nTwo issues from cursor[bot] review #4211934690.",
[51876],
"squash match keeps valid PR; fallback would have grabbed the oversized id but is skipped",
],
["Fix bug\n\nRelated to (#4211934690)", [], "fallback drops oversized parens form"],
["Fix bug\n\nSee #123 and sentry #9999999999", [123], "fallback keeps small numbers and drops oversized"],
["Title (#4211934690)", [], "squash format with oversized number is dropped"],
["Merge pull request #4211934690 from x/y", [], "merge format with oversized number is dropped"],
[`Title (#${2_147_483_647})`, [2_147_483_647], "Int32 max is allowed"],
[`Title (#${2_147_483_648})`, [], "one above Int32 max is dropped"],
])("message %j should yield %j (%s)", (message, expected) => {
const result = extractPullRequestNumbersForCommit({ sha: "abc", message });
expect(result).toEqual(expected);
});
});
31 changes: 23 additions & 8 deletions src/extractors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { CommitContext } from "./types";

const MAX_KEY_LENGTH = 7;

/**
* Linear's API types `pullRequestReferences[].number` as a GraphQL `Int`
* (signed 32-bit). A `#NNN` token whose value exceeds this cannot be a real
* GitHub PR number and would cause the entire release sync to be rejected,
* so we filter such tokens out at extraction time.
*/
const MAX_PR_NUMBER = 2_147_483_647;

/**
* Regex for matching issue identifiers with proper word boundaries.
* Matches the same patterns as Linear's issue identifier detection.
Expand Down Expand Up @@ -200,28 +208,35 @@ export function extractPullRequestNumbersForCommit(commit: CommitContext): numbe
}

const prNumbers: number[] = [];
const pushIfValid = (raw: string, source: string): void => {
const number = Number.parseInt(raw, 10);
if (number > MAX_PR_NUMBER) {
verbose(
`Ignoring #${raw} in commit ${commit.sha} (${source}): exceeds max PR number ${MAX_PR_NUMBER}, likely not a GitHub PR reference`,
);
return;
}
verbose(`Found PR number ${number} in commit ${commit.sha} (${source}): "${message}"`);
prNumbers.push(number);
};

// GitHub squash: "Title (#123)" - must be at end of title (first line)
const title = message.split(/\r?\n/)[0] ?? "";
const squashMatch = title.match(/\(#(\d+)\)$/);
if (squashMatch) {
verbose(`Found PR number ${squashMatch[1]} in commit ${commit.sha} using squash format: "${message}"`);
prNumbers.push(Number.parseInt(squashMatch[1]!, 10));
pushIfValid(squashMatch[1]!, "squash format");
}

// GitHub merge: "Merge pull request #123 from ..." - must be at start
const mergeMatch = message.match(/^Merge pull request #(\d+)/i);
if (mergeMatch) {
verbose(`Found PR number ${mergeMatch[1]} in commit ${commit.sha} using merge format: "${message}"`);
prNumbers.push(Number.parseInt(mergeMatch[1]!, 10));
pushIfValid(mergeMatch[1]!, "merge format");
}

// Only use fallback if no matches from squash/merge formats
if (prNumbers.length === 0) {
const messageMatches = message.matchAll(/#(\d+)/g);
for (const match of messageMatches) {
verbose(`Found PR number ${match[1]} in commit ${commit.sha} by extracting from message: "${message}"`);
prNumbers.push(Number.parseInt(match[1]!, 10));
for (const match of message.matchAll(/#(\d+)/g)) {
pushIfValid(match[1]!, "message scan");
}
}

Expand Down
Loading