Skip to content

deduplicate-by-title never runs: rate-limit guard compares the search bucket (max 30/min) against a threshold of 500 #48070

Description

@graphaelli

create-issue's deduplicate-by-title option is silently inert. Its rate-limit pre-check reads the search rate-limit bucket but compares it against a threshold of 500, and that bucket's ceiling is 30 requests/minute — so the guard short-circuits on every invocation, for every consumer, and the dedup search never runs.

The handler fails open: it logs a warning and creates the issue anyway.

Source

actions/setup/js/create_issue.cjs (current main):

const TITLE_DEDUP_MIN_SEARCH_RATE_LIMIT_REMAINING = 500;              // line 38
...
const rawRemaining = response?.data?.resources?.search?.remaining;    // line 497
const remaining = Number(rawRemaining);
if (!Number.isFinite(remaining)) { ... return false; }
if (remaining <= TITLE_DEDUP_MIN_SEARCH_RATE_LIMIT_REMAINING) {       // line 503
  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})`);
  return true;                                                        // → skip dedup
}

Why it's unsatisfiable rather than merely strict

remaining can never exceed limit, and for resources.search the limit is 30/min (10/min for code_search). A fresh, completely unused budget still fails the check:

$ gh api /rate_limit --jq '.resources.search, .resources.code_search, .resources.core'
{"limit":30,   "remaining":30,   "reset":...,"used":0}
{"limit":10,   "remaining":10,   "reset":...,"used":0}
{"limit":5000, "remaining":4980, "reset":...,"used":20}

500 looks like a value chosen for resources.core (5000/hr) that ended up applied to resources.search.

Observed behavior

Every run logs the pair below and then creates the issue regardless — the first line makes it read as though dedup is active:

Title deduplication enabled (Levenshtein distance <= 15)
##[warning]Skipping repo-level title dedup search for OWNER/REPO:
          search rate limit remaining is 30 (threshold <= 500)

Note remaining is 30 — an untouched quota, not a depleted one.

Impact

Any scheduled workflow relying on deduplicate-by-title re-files the same finding every run. I hit this on a benchmark-triage workflow that filed one finding six times across four consecutive scheduled runs before it was noticed. deduplicate-by-title is often the only dedup guard configured, since the docs present it as the mechanism for exactly this.

This is distinct from #36510 (the config not being emitted at all), which is fixed — the config now loads correctly, which is why Title deduplication enabled appears.

Suggested fix

Compare proportionally against the bucket's own ceiling rather than an absolute from a different bucket:

const TITLE_DEDUP_MIN_SEARCH_RATE_LIMIT_FRACTION = 0.2;
const { remaining, limit } = response?.data?.resources?.search ?? {};
if (Number.isFinite(remaining) && Number.isFinite(limit) &&
    remaining <= limit * TITLE_DEDUP_MIN_SEARCH_RATE_LIMIT_FRACTION) { ...skip... }

This also stays correct on GHES, where instance admins can tune the ceilings. A minimal alternative is to drop the constant to something under 30, but that hardcodes a value the platform owns.

Two smaller points worth considering alongside:

  • Skipping dedup is currently core.warning, which is easy to miss in a green run. Given it silently changes correctness, core.error or surfacing it in the run summary would help.
  • deduplicate-by-title consumes at most TITLE_DEDUP_MAX_SEARCH_PAGES (2) requests. Gating 2 requests behind a 500-request reserve is conservative even against the right bucket.

Version: v0.82.14; also present on main at time of writing.

(Reported by Claude Code)

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions