diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 93623eb0c5..5f8753c5b7 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -7273,12 +7273,18 @@ export function extractLinkedIssueNumbersWithOverflow(text: string, repoFullName const normalizedLimit = Math.max(0, Math.floor(limit)); const target = repoFullName.toLowerCase(); + // Strip inline code spans before scanning: GitHub's own native closing-keyword linker does not treat + // backtick-wrapped text as a real "Closes #N" directive, and this repo's own PR template checklist item + // contains the literal example text "(e.g. `Closes #123`)" -- without this, every PR that keeps the + // unmodified template checklist would spuriously link to issue #123. + const withoutCodeSpans = text.replace(/`[^`\n]*`/g, " "); + const linkedIssues: number[] = []; const seen = new Set(); // Matches both GitHub's bare `KEYWORD #N` and fully-qualified `KEYWORD owner/repo#N` closing syntax (#3862) -- // the qualified form only counts when owner/repo case-insensitively matches THIS repo; a reference to a // different repo closes an issue there, not here, and must not spoof a same-repo linked-issue match. - for (const match of text.matchAll(/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:([\w.-]+\/[\w.-]+)#|#)(\d+)\b/gi)) { + for (const match of withoutCodeSpans.matchAll(/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:([\w.-]+\/[\w.-]+)#|#)(\d+)\b/gi)) { const owner = match[1]; if (owner && owner.toLowerCase() !== target) continue; const value = Number(match[2]); diff --git a/test/unit/db-parsers.test.ts b/test/unit/db-parsers.test.ts index 29299d62a3..848f073c20 100644 --- a/test/unit/db-parsers.test.ts +++ b/test/unit/db-parsers.test.ts @@ -59,6 +59,15 @@ describe("database row parser hardening", () => { expect(extractLinkedIssueNumbers("Fixes #1", "owner/repo", -5)).toEqual([]); }); + it("REGRESSION: ignores a closing keyword inside an inline code span, e.g. this repo's own PR template checklist example", () => { + // .github/pull_request_template.md literally contains "(e.g. `Closes #123`)" -- every PR that keeps the + // unmodified checklist item must NOT spuriously link to issue #123. + const templateLine = "- [ ] I linked a currently open issue this PR resolves (e.g. `Closes #123`) — a linked open issue is required for every contributor PR."; + expect(extractLinkedIssueNumbers(templateLine, "owner/repo")).toEqual([]); + // A real closing keyword elsewhere in the same body still counts. + expect(extractLinkedIssueNumbers(`Closes #42\n\n${templateLine}`, "owner/repo")).toEqual([42]); + }); + it("recognizes the fully-qualified `Fixes owner/repo#N` closing syntax when owner/repo matches this repo (#3862)", () => { expect(extractLinkedIssueNumbers("Closes owner/repo#42", "owner/repo")).toEqual([42]); // Case-insensitive, matching GitHub's own repo-name matching.