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
7 changes: 6 additions & 1 deletion src/integrations/project-tracker-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ type GitHubMilestone = {
// (mirrors src/github/comments.ts's COMMENT_SEARCH_PAGE_LIMIT): 3 pages * 100 = 300 items is generously above
// any realistic open-milestone/open-project/PR-comment count, while still bounding worst-case API calls.
const GITHUB_LIST_PAGE_LIMIT = 3;
// #8889: the comment-MARKER search specifically must match comments.ts's COMMENT_SEARCH_PAGE_LIMIT, which was
// bumped 3 -> 10 (#7232, commit 18fe74628) because a >300-comment thread let the marker hide and caused a
// duplicate post. The milestone/project LIST searches keep the 3-page bound; only marker detection needs the
// deeper scan to stay double-post-safe.
const COMMENT_MARKER_SEARCH_PAGE_LIMIT = 10;

/** 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. */
Expand Down Expand Up @@ -399,7 +404,7 @@ export async function maybeSuggestProjectOrMilestoneMatch(
const octokit = makeInstallationOctokit(ctx.env, token, "live", githubRateLimitAdmissionKeyForInstallation(ctx.installationId));
const botLogin = `${ctx.env.GITHUB_APP_SLUG}[bot]`;
let alreadyPosted = false;
for (let page = 1; page <= GITHUB_LIST_PAGE_LIMIT && !alreadyPosted; page += 1) {
for (let page = 1; page <= COMMENT_MARKER_SEARCH_PAGE_LIMIT && !alreadyPosted; page += 1) {
const existing = await octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}/comments", {
owner,
repo,
Expand Down
39 changes: 39 additions & 0 deletions test/unit/project-tracker-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,45 @@ describe("maybeSuggestProjectOrMilestoneMatch (#3183/#3184)", () => {
expect(posted).toBe(false);
});

it("finds the marker on page 4 (past the old 3-page cap) and does not double-post (#8889)", async () => {
// A >300-comment thread: pages 1-3 are full of unrelated comments, the marker is on page 4. The old
// 3-page cap stopped before page 4, missed the marker, and double-posted. The raised cap must reach it.
const fullPage = Array.from({ length: 100 }, (_, i) => ({ body: `unrelated comment ${i}`, user: { type: "User", login: "someone" } }));
let posted = false;
const requestedPages: number[] = [];
vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
const method = init?.method ?? "GET";
if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" });
if (url.includes("/milestones")) return Response.json([{ number: 14, title: "Self-host reliability roadmap" }]);
if (url.endsWith("/graphql")) return Response.json(noOpenProjectsGraphQlBody());
if (url.includes("/issues/4/comments") && method === "GET") {
const page = Number(new URL(url).searchParams.get("page") ?? "1");
requestedPages.push(page);
if (page <= 3) return Response.json(fullPage);
if (page === 4) return Response.json([{ body: PROJECT_TRACKER_SUGGEST_COMMENT_MARKER, user: { type: "Bot", login: "loopover-orb[bot]" } }]);
return Response.json([]);
}
if (url.includes("/issues/4/comments") && method === "POST") {
posted = true;
return Response.json({ id: 1 });
}
return new Response("unexpected", { status: 500 });
});
const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "loopover-orb" });
const result = await maybeSuggestProjectOrMilestoneMatch(
{ env, installationId: 123, repoFullName: "JSONbored/gittensory" },
4,
"Improve self-host reliability roadmap convergence",
"Follow-up on the self-host reliability roadmap work",
"github",
"https://github.com/JSONbored/gittensory/pull/4",
);
expect(requestedPages).toEqual([1, 2, 3, 4]);
expect(result).toEqual({ suggested: false });
expect(posted).toBe(false);
});

it("does nothing when neither a milestone nor a project matches (never calls the comment POST endpoint)", async () => {
let posted = false;
vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
Expand Down