Skip to content

Commit 15c5af0

Browse files
fix(ci): correct the Hacktoberfest tracker's open-issue count and add a countdown (#15235)
* fix(ci): count open issues reliably and add a Hacktoberfest countdown The tracker's "Open issues" line was showing the pull-request total (e.g. 621) instead of the issue total (107). GitHub's `/search/issues` `is:issue` / `is:pr` qualifiers are unreliable on this large, high-churn repo -- some runs return the PR pool for *both* queries, so the two lines printed the same number. Count without the flaky qualifiers instead: - open PRs = the `Link: rel="last"` page number of `/repos/{repo}/pulls` (deterministic, page-numbered pagination); - open issues = the repo endpoint's `open_issues_count` (issues + PRs) minus the open-PR total -- self-checking and stable. Also add the requested Hacktoberfest countdown to the stats block: - days until 2026-10-01; - issues to close per day to clear the backlog; - PRs to merge or close per day to clear the backlog. Per-day figures round up (finishing a day early beats a day late) and degrade to a clear message once Hacktoberfest starts, so the block never divides by zero on the final day. * chore: re-trigger keeper after marking PR checklist
1 parent a6ca8ea commit 15c5af0

2 files changed

Lines changed: 84 additions & 6 deletions

File tree

docs/hacktober_2026_prep.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2063,9 +2063,12 @@ I can take a second pass through the remaining `awaiting triage` items (relabel
20632063

20642064
_Generated automatically by `scripts/hacktoberfest_prep_update.py` on 2026-09-09 (UTC)._
20652065

2066-
- **Open issues:** 621
2066+
- **Open issues:** 107
20672067
- **Open pull requests:** 621
20682068
- **Open PRs labelled `awaiting reviews`:** 458
2069+
- **Days until Hacktoberfest (2026-10-01):** 22
2070+
- **Issues to close per day to clear the backlog:** 5 per day (over 22 days)
2071+
- **Pull requests to merge or close per day to clear the backlog:** 29 per day (over 22 days)
20692072

20702073
**Top three directories to work on** (most open pull requests labelled `awaiting reviews`):
20712074

scripts/hacktoberfest_prep_update.py

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,53 @@ async def _search_count(
137137
return int(body.get("total_count", 0)) # type: ignore[union-attr]
138138

139139

140+
# GitHub's ``/search/issues`` ``is:issue`` / ``is:pr`` qualifiers have proven
141+
# unreliable — some runs return the pull-request pool for *both* queries, so the
142+
# "Open issues" line reported the PR count. The counts below avoid search
143+
# entirely: the repository endpoint's ``open_issues_count`` is issues + PRs, and
144+
# the open-PR total comes from the ``Link: rel="last"`` page number of the pulls
145+
# listing. Open issues is then their difference — deterministic and self-checking.
146+
_LAST_PAGE_RE = re.compile(r'[?&]page=(\d+)>;\s*rel="last"')
147+
148+
149+
async def _last_page_count(
150+
client: httpx2.AsyncClient,
151+
sem: asyncio.Semaphore,
152+
url: str,
153+
params: dict | None = None,
154+
) -> int:
155+
"""Total items in a paginated listing, read from its ``Link`` header.
156+
157+
Requests one item per page so the ``rel="last"`` page number *is* the count.
158+
Falls back to the length of the single returned page when there is no
159+
``Link`` header (0 or 1 items).
160+
"""
161+
query = {"per_page": 1, **(params or {})}
162+
body, headers = await _request(client, sem, url, query)
163+
link = headers.get("link") or headers.get("Link") or ""
164+
if match := _LAST_PAGE_RE.search(link):
165+
return int(match.group(1))
166+
return len(body) if isinstance(body, list) else 0
167+
168+
169+
async def open_issue_and_pr_counts(
170+
client: httpx2.AsyncClient, sem: asyncio.Semaphore
171+
) -> tuple[int, int]:
172+
"""Return ``(open_issues, open_prs)`` without the flaky search qualifiers.
173+
174+
``open_issues_count`` from the repo endpoint counts issues *and* pull
175+
requests; subtracting the pull-request total leaves genuine issues.
176+
"""
177+
repo, open_prs = await asyncio.gather(
178+
_request(client, sem, f"{API}/repos/{REPO}"),
179+
_last_page_count(client, sem, f"{API}/repos/{REPO}/pulls", {"state": "open"}),
180+
)
181+
repo_body, _ = repo
182+
total = int(repo_body.get("open_issues_count", 0)) # type: ignore[union-attr]
183+
open_issues = max(total - open_prs, 0)
184+
return open_issues, open_prs
185+
186+
140187
async def pr_state(
141188
client: httpx2.AsyncClient, sem: asyncio.Semaphore, number: int
142189
) -> str | None:
@@ -280,27 +327,55 @@ async def _count_or_none(query: str) -> int | None:
280327
except BestEffortError:
281328
return None
282329

283-
open_issues, open_prs, awaiting = await asyncio.gather(
284-
_count_or_none(f"repo:{REPO} is:issue is:open"),
285-
_count_or_none(f"repo:{REPO} is:pr is:open"),
330+
async def _counts_or_none() -> tuple[int | None, int | None]:
331+
try:
332+
return await open_issue_and_pr_counts(client, sem)
333+
except BestEffortError:
334+
return None, None
335+
336+
(open_issues, open_prs), awaiting = await asyncio.gather(
337+
_counts_or_none(),
286338
_count_or_none(awaiting_query),
287339
)
288-
today = dt.datetime.now(dt.UTC).date().isoformat()
340+
today = dt.datetime.now(dt.UTC).date()
341+
today_iso = today.isoformat()
289342

290343
def _fmt(value: int | None) -> str:
291344
return str(value) if value is not None else "unavailable (rate limited)"
292345

346+
# Hacktoberfest countdown: how much daily throughput clears the backlog by
347+
# 2026-10-01. ``days_left`` is inclusive of today so the target date itself
348+
# is not counted as a working day (avoids a divide-by-zero on the last day).
349+
days_left = max((HACKTOBERFEST_START - today).days, 0)
350+
351+
def _per_day(count: int | None) -> str:
352+
if count is None:
353+
return "unavailable (rate limited)"
354+
if days_left <= 0:
355+
return "Hacktoberfest has started"
356+
# Round up: finishing a day early beats finishing a day late.
357+
return f"{-(-count // days_left)} per day (over {days_left} days)"
358+
293359
lines = [
294360
STATS_HEADER,
295361
"",
296362
(
297363
f"_Generated automatically by "
298-
f"`scripts/hacktoberfest_prep_update.py` on {today} (UTC)._"
364+
f"`scripts/hacktoberfest_prep_update.py` on {today_iso} (UTC)._"
299365
),
300366
"",
301367
f"- **Open issues:** {_fmt(open_issues)}",
302368
f"- **Open pull requests:** {_fmt(open_prs)}",
303369
f"- **Open PRs labelled `{AWAITING_LABEL}`:** {_fmt(awaiting)}",
370+
(
371+
f"- **Days until Hacktoberfest ({HACKTOBERFEST_START.isoformat()}):** "
372+
f"{days_left}"
373+
),
374+
f"- **Issues to close per day to clear the backlog:** {_per_day(open_issues)}",
375+
(
376+
"- **Pull requests to merge or close per day to clear the backlog:** "
377+
f"{_per_day(open_prs)}"
378+
),
304379
"",
305380
(
306381
"**Top three directories to work on** (most open pull requests "

0 commit comments

Comments
 (0)