Skip to content
Closed
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
5 changes: 4 additions & 1 deletion docs/hacktober_2026_prep.md
Original file line number Diff line number Diff line change
Expand Up @@ -2063,9 +2063,12 @@ I can take a second pass through the remaining `awaiting triage` items (relabel

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

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

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

Expand Down
85 changes: 80 additions & 5 deletions scripts/hacktoberfest_prep_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,53 @@ async def _search_count(
return int(body.get("total_count", 0)) # type: ignore[union-attr]


# GitHub's ``/search/issues`` ``is:issue`` / ``is:pr`` qualifiers have proven
# unreliable — some runs return the pull-request pool for *both* queries, so the
# "Open issues" line reported the PR count. The counts below avoid search
# entirely: the repository endpoint's ``open_issues_count`` is issues + PRs, and
# the open-PR total comes from the ``Link: rel="last"`` page number of the pulls
# listing. Open issues is then their difference — deterministic and self-checking.
_LAST_PAGE_RE = re.compile(r'[?&]page=(\d+)>;\s*rel="last"')


async def _last_page_count(
client: httpx2.AsyncClient,
sem: asyncio.Semaphore,
url: str,
params: dict | None = None,
) -> int:
"""Total items in a paginated listing, read from its ``Link`` header.

Requests one item per page so the ``rel="last"`` page number *is* the count.
Falls back to the length of the single returned page when there is no
``Link`` header (0 or 1 items).
"""
query = {"per_page": 1, **(params or {})}
body, headers = await _request(client, sem, url, query)
link = headers.get("link") or headers.get("Link") or ""
if match := _LAST_PAGE_RE.search(link):
return int(match.group(1))
return len(body) if isinstance(body, list) else 0


async def open_issue_and_pr_counts(
client: httpx2.AsyncClient, sem: asyncio.Semaphore
) -> tuple[int, int]:
"""Return ``(open_issues, open_prs)`` without the flaky search qualifiers.

``open_issues_count`` from the repo endpoint counts issues *and* pull
requests; subtracting the pull-request total leaves genuine issues.
"""
repo, open_prs = await asyncio.gather(
_request(client, sem, f"{API}/repos/{REPO}"),
_last_page_count(client, sem, f"{API}/repos/{REPO}/pulls", {"state": "open"}),
)
repo_body, _ = repo
total = int(repo_body.get("open_issues_count", 0)) # type: ignore[union-attr]
open_issues = max(total - open_prs, 0)
return open_issues, open_prs


async def pr_state(
client: httpx2.AsyncClient, sem: asyncio.Semaphore, number: int
) -> str | None:
Expand Down Expand Up @@ -280,27 +327,55 @@ async def _count_or_none(query: str) -> int | None:
except BestEffortError:
return None

open_issues, open_prs, awaiting = await asyncio.gather(
_count_or_none(f"repo:{REPO} is:issue is:open"),
_count_or_none(f"repo:{REPO} is:pr is:open"),
async def _counts_or_none() -> tuple[int | None, int | None]:
try:
return await open_issue_and_pr_counts(client, sem)
except BestEffortError:
return None, None

(open_issues, open_prs), awaiting = await asyncio.gather(
_counts_or_none(),
_count_or_none(awaiting_query),
)
today = dt.datetime.now(dt.UTC).date().isoformat()
today = dt.datetime.now(dt.UTC).date()
today_iso = today.isoformat()

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

# Hacktoberfest countdown: how much daily throughput clears the backlog by
# 2026-10-01. ``days_left`` is inclusive of today so the target date itself
# is not counted as a working day (avoids a divide-by-zero on the last day).
days_left = max((HACKTOBERFEST_START - today).days, 0)

def _per_day(count: int | None) -> str:
if count is None:
return "unavailable (rate limited)"
if days_left <= 0:
return "Hacktoberfest has started"
# Round up: finishing a day early beats finishing a day late.
return f"{-(-count // days_left)} per day (over {days_left} days)"

lines = [
STATS_HEADER,
"",
(
f"_Generated automatically by "
f"`scripts/hacktoberfest_prep_update.py` on {today} (UTC)._"
f"`scripts/hacktoberfest_prep_update.py` on {today_iso} (UTC)._"
),
"",
f"- **Open issues:** {_fmt(open_issues)}",
f"- **Open pull requests:** {_fmt(open_prs)}",
f"- **Open PRs labelled `{AWAITING_LABEL}`:** {_fmt(awaiting)}",
(
f"- **Days until Hacktoberfest ({HACKTOBERFEST_START.isoformat()}):** "
f"{days_left}"
),
f"- **Issues to close per day to clear the backlog:** {_per_day(open_issues)}",
(
"- **Pull requests to merge or close per day to clear the backlog:** "
f"{_per_day(open_prs)}"
),
"",
(
"**Top three directories to work on** (most open pull requests "
Expand Down
Loading