fix: pick up contributors the cached API has not caught up with

GitHub's /contributors endpoint is heavily cached and can lag a merge by up
to a day. dakshverma23's commit from #129 was already linked to their account
- /commits reports it, and the commit API confirms the link - but they were
absent from the contributor wall because /contributors had not refreshed.

update-contributors.py now unions the two endpoints: /contributors for the
authoritative counts and ordering, /commits for anyone linked but not yet
surfaced. Commits authored with an unlinkable email still appear in neither,
which matches what GitHub's own contributor graph shows.

Wall goes from 13 to 14.
This commit is contained in:
Mahipal
2026-08-24 13:12:25 +02:00
parent 0161fb1c7f
commit 1b3f6b2286
2 changed files with 51 additions and 19 deletions
+2 -1
View File
@@ -419,9 +419,10 @@ This library is built by the community. Thank you to everyone who has contribute
<a href="https://github.com/shanujans" title="shanujans — 1 contribution"><img src="https://github.com/shanujans.png?size=100" width="72" height="72" alt="@shanujans"></a>
<a href="https://github.com/farhan6667" title="farhan6667 — 1 contribution"><img src="https://github.com/farhan6667.png?size=100" width="72" height="72" alt="@farhan6667"></a>
<a href="https://github.com/nyxst4ck" title="nyxst4ck — 1 contribution"><img src="https://github.com/nyxst4ck.png?size=100" width="72" height="72" alt="@nyxst4ck"></a>
<a href="https://github.com/dakshverma23" title="dakshverma23 — 1 contribution"><img src="https://github.com/dakshverma23.png?size=100" width="72" height="72" alt="@dakshverma23"></a>
</p>
<p align="center"><sub>13 contributors, ordered by contribution count · see the full <a href="https://github.com/mukul975/Anthropic-Cybersecurity-Skills/graphs/contributors">contributor graph</a></sub></p>
<p align="center"><sub>14 contributors, ordered by contribution count · see the full <a href="https://github.com/mukul975/Anthropic-Cybersecurity-Skills/graphs/contributors">contributor graph</a></sub></p>
<!-- contributors:end -->
## Community
+49 -18
View File
@@ -39,22 +39,40 @@ EXCLUDE_LOGINS = {"github-actions", "dependabot", "pull"}
AVATAR_PX = 72
def _get(path: str):
req = urllib.request.Request(f"https://api.github.com/repos/{REPO}/{path}", headers={
"Accept": "application/vnd.github+json",
"User-Agent": "update-contributors",
})
token = os.environ.get("GITHUB_TOKEN")
if token:
req.add_header("Authorization", f"Bearer {token}")
with urllib.request.urlopen(req, timeout=30) as resp:
return json.load(resp)
def _keep(login: str) -> bool:
return bool(login) and not login.endswith(EXCLUDE_SUFFIXES) and login not in EXCLUDE_LOGINS
def fetch_contributors() -> list[dict]:
"""Every non-bot contributor, most contributions first."""
"""Every non-bot contributor, most contributions first.
Two endpoints, because one of them lies. /contributors carries the
authoritative contribution counts but is heavily cached — a merge can take
up to a day to show up there. /commits is live. So anyone whose commit is
already linked to their account but has not yet surfaced in /contributors
gets picked up from the commit list instead of waiting a day to be thanked.
Commits authored with an unlinkable email (a machine hostname such as
user@HOST.localdomain) have no `author` object and are skipped by both
paths. They never appear in GitHub's own contributor graph either, so the
wall matches what GitHub itself shows.
"""
people: list[dict] = []
page = 1
while True:
url = f"https://api.github.com/repos/{REPO}/contributors?per_page=100&page={page}"
req = urllib.request.Request(url, headers={
"Accept": "application/vnd.github+json",
"User-Agent": "update-contributors",
})
token = os.environ.get("GITHUB_TOKEN")
if token:
req.add_header("Authorization", f"Bearer {token}")
with urllib.request.urlopen(req, timeout=30) as resp:
batch = json.load(resp)
batch = _get(f"contributors?per_page=100&page={page}")
if not batch:
break
people.extend(batch)
@@ -62,12 +80,25 @@ def fetch_contributors() -> list[dict]:
break
page += 1
return [
p for p in people
if p.get("type") != "Bot"
and not p.get("login", "").endswith(EXCLUDE_SUFFIXES)
and p.get("login") not in EXCLUDE_LOGINS
]
known = {p.get("login") for p in people}
ranked = [p for p in people if p.get("type") != "Bot" and _keep(p.get("login", ""))]
# Catch anyone the cached endpoint has not caught up with yet.
recent: dict[str, int] = {}
for page in (1, 2):
for commit in _get(f"commits?per_page=100&page={page}") or []:
author = commit.get("author")
if not author or author.get("type") == "Bot":
continue
login = author.get("login", "")
if login in known or not _keep(login):
continue
recent[login] = recent.get(login, 0) + 1
for login, count in sorted(recent.items(), key=lambda kv: (-kv[1], kv[0])):
ranked.append({"login": login, "contributions": count})
return ranked
def render(people: list[dict]) -> str: