diff --git a/actions/setup/js/create_issue.cjs b/actions/setup/js/create_issue.cjs index 411b0b12aba..71040c6643d 100644 --- a/actions/setup/js/create_issue.cjs +++ b/actions/setup/js/create_issue.cjs @@ -35,7 +35,7 @@ const ISSUE_FIELD_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; const RECENTLY_CLOSED_DEDUP_DAYS = 30; const TITLE_DEDUP_SEARCH_PER_PAGE = 100; const TITLE_DEDUP_MAX_SEARCH_PAGES = 2; -const TITLE_DEDUP_MIN_SEARCH_RATE_LIMIT_REMAINING = 500; +const TITLE_DEDUP_MIN_SEARCH_RATE_LIMIT_FRACTION = 0.2; /** * Create a dedicated GitHub client for copilot assignment operations. @@ -494,14 +494,16 @@ async function getRepoTitleDedupCandidates(githubClient, owner, repo) { async function shouldSkipRepoTitleDedupSearch(githubClient, owner, repo) { try { const response = await githubClient.rest.rateLimit.get(); - const rawRemaining = response?.data?.resources?.search?.remaining; + const { remaining: rawRemaining, limit: rawLimit } = response?.data?.resources?.search ?? {}; const remaining = Number(rawRemaining); - if (!Number.isFinite(remaining)) { - core.warning(`Could not determine search rate limit remaining for ${owner}/${repo}; proceeding with repo-level title dedup search`); + const limit = Number(rawLimit); + if (!Number.isFinite(remaining) || !Number.isFinite(limit)) { + core.warning(`Could not determine search rate limit values for ${owner}/${repo} (remaining=${rawRemaining}, limit=${rawLimit}); proceeding with repo-level title dedup search`); return false; } - if (remaining <= TITLE_DEDUP_MIN_SEARCH_RATE_LIMIT_REMAINING) { - core.warning(`Skipping repo-level title dedup search for ${owner}/${repo}: search rate limit remaining is ${remaining} (threshold <= ${TITLE_DEDUP_MIN_SEARCH_RATE_LIMIT_REMAINING})`); + const threshold = limit * TITLE_DEDUP_MIN_SEARCH_RATE_LIMIT_FRACTION; + if (remaining <= threshold) { + core.warning(`Skipping repo-level title dedup search for ${owner}/${repo}: search rate limit remaining is ${remaining}/${limit} (threshold <= ${Math.floor(threshold)})`); return true; } } catch (error) { diff --git a/actions/setup/js/create_issue.test.cjs b/actions/setup/js/create_issue.test.cjs index 236b7c7407b..456dafbe96e 100644 --- a/actions/setup/js/create_issue.test.cjs +++ b/actions/setup/js/create_issue.test.cjs @@ -46,7 +46,8 @@ describe("create_issue", () => { data: { resources: { search: { - remaining: 1000, + limit: 30, + remaining: 30, }, }, }, @@ -680,7 +681,8 @@ describe("create_issue", () => { data: { resources: { search: { - remaining: 1, + limit: 35, + remaining: 6, }, }, }, @@ -701,6 +703,71 @@ describe("create_issue", () => { expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Skipping repo-level title dedup search")); }); + it("should skip repo-level search when search rate limit is exactly at 20 percent threshold", async () => { + mockGithub.rest.rateLimit.get.mockResolvedValue({ + data: { + resources: { + search: { + limit: 30, + remaining: 6, + }, + }, + }, + }); + + const handler = await main({ + deduplicate_by_title: true, + }); + const result = await handler({ title: "At threshold title" }); + + expect(result.success).toBe(true); + expect(mockGithub.rest.search.issuesAndPullRequests).not.toHaveBeenCalled(); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Skipping repo-level title dedup search")); + }); + + it("should not skip repo-level search when search rate limit is just above 20 percent threshold", async () => { + mockGithub.rest.rateLimit.get.mockResolvedValue({ + data: { + resources: { + search: { + limit: 30, + remaining: 7, + }, + }, + }, + }); + + const handler = await main({ + deduplicate_by_title: true, + }); + const result = await handler({ title: "Above threshold title" }); + + expect(result.success).toBe(true); + expect(mockGithub.rest.search.issuesAndPullRequests).toHaveBeenCalled(); + expect(mockCore.warning).not.toHaveBeenCalledWith(expect.stringContaining("Skipping repo-level title dedup search")); + }); + + it("should proceed with repo-level search when search rate limit values are missing", async () => { + mockGithub.rest.rateLimit.get.mockResolvedValue({ + data: { + resources: { + search: { + remaining: 30, + }, + }, + }, + }); + + const handler = await main({ + deduplicate_by_title: true, + }); + const result = await handler({ title: "Missing limit title" }); + + expect(result.success).toBe(true); + expect(mockGithub.rest.search.issuesAndPullRequests).toHaveBeenCalled(); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("remaining=30, limit=undefined")); + }); + it("should reject invalid deduplicate-by-title configuration", async () => { await expect(main({ deduplicate_by_title: "invalid" })).rejects.toThrow("deduplicate-by-title"); await expect(main({ deduplicate_by_title: 101 })).rejects.toThrow("deduplicate-by-title");