#!/usr/bin/env python3
"""Regenerate the Community Contributors wall in the READMEs.
Fetches the repository's contributors straight from the GitHub API (the same
source, and same commit-count order, as the sidebar / contributors graph) and
appends publicly recognized contributions that are not represented in the
commit graph. It then rewrites the block between the ``CONTRIBUTORS`` markers
in each README.
Unlike a third-party image service (e.g. contrib.rocks, which calls the GitHub
API anonymously, gets rate-limited for this repo, and rendered only a single
avatar), this runs with the repo's own token and sees every attributed
contributor.
GitHub markdown strips inline CSS, so a plain ``
`` is always square, and a
committed SVG that embeds the avatar as a ``data:`` URI is blocked by
raw.githubusercontent's content-security-policy. So each avatar is cropped to a
circle with transparent corners and committed as a small PNG under
``.github/contributors/``; a round raster image renders everywhere, and each is
wrapped in a link to the person's GitHub profile.
Run by the daily local automation; runnable manually for a preview with
``GITHUB_TOKEN`` set or a ``gh auth`` session. Requires Pillow.
"""
from __future__ import annotations
import io
import json
import os
import re
import subprocess
import sys
import urllib.request
REPO = os.environ.get("GITHUB_REPOSITORY", "PKU-YuanGroup/OpenAI4S")
READMES = ("README.md", "README_zh.md")
AVATAR_DIR = os.path.join(".github", "contributors")
START = ""
END = ""
SRC = 256 # source crop resolution for a crisp circle
DISPLAY = 64 # rendered avatar size in px, close to the original wall
# Bots and the automated co-author identity are not community members. The
# ``noreply@anthropic.com`` co-author has no GitHub account (it shows as an
# unlinked grey avatar in the sidebar) and the /contributors API omits it, so
# it is naturally excluded here too.
EXCLUDE = {"github-actions[bot]", "dependabot[bot]", "actions-user"}
# Publicly accepted contributions that are not represented in the commit graph.
# Keep this list limited to GitHub logins whose recognition is already public.
#
# `difficulttopickaname` is a maintainer (CODEOWNERS: /openai4s/server/,
# /tests/browser_smoke.mjs) whose commits ARE in the history -- authored as
# `minhan.tang ` -- but the /contributors API returns
# them with no linked account, so the graph cannot see them. The durable fix is
# on their side: adding that address to their GitHub account retroactively
# links every past commit. This entry carries the recognition until then, and
# `include_recognized_contributors` drops it automatically the moment the API
# starts returning the login, so nothing has to be cleaned up afterwards.
RECOGNIZED_CONTRIBUTORS = ("EQSTLab", "difficulttopickaname")
_UA = {"User-Agent": "openai4s-contributors-script"}
def _token() -> str | None:
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
if token:
return token
try: # local convenience only
out = subprocess.run(
["gh", "auth", "token"], capture_output=True, text=True, timeout=10
)
return out.stdout.strip() or None
except Exception: # noqa: BLE001
return None
def _get(url: str, token: str | None) -> bytes:
req = urllib.request.Request(url, headers=dict(_UA))
if token:
req.add_header("Authorization", f"Bearer {token}")
with urllib.request.urlopen(req, timeout=30) as resp:
return resp.read()
def fetch_contributors(token: str | None) -> list[dict]:
people: list[dict] = []
page = 1
while True:
url = (
f"https://api.github.com/repos/{REPO}/contributors"
f"?per_page=100&page={page}"
)
batch = json.loads(_get(url, token))
if not isinstance(batch, list) or not batch:
break
people.extend(batch)
if len(batch) < 100:
break
page += 1
seen: set[str] = set()
kept: list[dict] = []
for c in people:
login = c.get("login")
if c.get("type") != "User" or not login or login in EXCLUDE or login in seen:
continue
seen.add(login)
kept.append(c)
# Stable sort by commit count desc == GitHub's default contributor order.
kept.sort(key=lambda c: c.get("contributions", 0), reverse=True)
return kept
def include_recognized_contributors(people: list[dict]) -> list[dict]:
"""Append public non-commit contributors without duplicating API users.
The same two guards `fetch_contributors` applies to an API row apply here:
a hand-maintained list is not a reason to render a bot or an organization
account inside a wall that is otherwise restricted to human users.
"""
seen = {person["login"].casefold() for person in people}
return people + [
{"login": login}
for login in RECOGNIZED_CONTRIBUTORS
if login.casefold() not in seen and login not in EXCLUDE
]
def _circular_png(raw: bytes) -> bytes:
from PIL import Image, ImageDraw, ImageOps
im = ImageOps.fit(
Image.open(io.BytesIO(raw)).convert("RGBA"),
(SRC, SRC),
method=Image.LANCZOS,
)
mask = Image.new("L", (SRC, SRC), 0)
ImageDraw.Draw(mask).ellipse((0, 0, SRC - 1, SRC - 1), fill=255)
im.putalpha(mask)
out = io.BytesIO()
im.save(out, format="PNG", optimize=True)
return out.getvalue()
def _avatar_name(login: str) -> str:
return f"{login}.png"
def write_avatars(people: list[dict], token: str | None) -> tuple[set[str], int]:
"""Refresh the avatars, prune the departed, and say what was written.
Returns the logins that have a usable committed PNG *and* the number this
run actually produced: "a file with that name exists" and "I refreshed it"
are different facts, and only the second one says the run worked.
"""
os.makedirs(AVATAR_DIR, exist_ok=True)
written = 0
for c in people:
login = c["login"]
avatar_url = c.get("avatar_url")
url = avatar_url or f"https://github.com/{login}.png"
url += ("&" if "?" in url else "?") + "s=256"
try:
# Only the API's own avatar_url is fetched authenticated. The
# github.com/.png form 302s to avatars.githubusercontent.com,
# and urllib copies every header except content-length/content-type
# across a redirect -- so sending the token here would hand it to a
# host that never asked for it.
png = _circular_png(_get(url, token if avatar_url else None))
except Exception as exc: # noqa: BLE001
print(f" avatar failed for {login}: {exc}", file=sys.stderr)
continue
with open(os.path.join(AVATAR_DIR, _avatar_name(login)), "wb") as f:
f.write(png)
written += 1
# Drop old identities and legacy SVGs, but keep a current contributor's
# committed PNG when a transient refresh fails. Compared case-insensitively
# because a login whose casing drifts from the committed filename writes
# through to the existing inode under the OLD name on a case-preserving
# filesystem, and an exact-match prune then deletes the file just written.
current = {_avatar_name(person["login"]).casefold() for person in people}
for name in os.listdir(AVATAR_DIR):
if name.casefold() not in current and name.endswith((".png", ".svg")):
os.remove(os.path.join(AVATAR_DIR, name))
# Derived from what survived on disk, spelled exactly as render() will spell
# it, so a local
is never emitted for a name that is not there.
on_disk = set(os.listdir(AVATAR_DIR))
have_png = {
person["login"] for person in people if _avatar_name(person["login"]) in on_disk
}
return have_png, written
def render(people: list[dict], have_png: set[str]) -> str:
rows = []
for c in people:
login = c["login"]
src = (
f"{AVATAR_DIR.replace(os.sep, '/')}/{_avatar_name(login)}"
if login in have_png
else f"https://github.com/{login}.png"
)
rows.append(
f''
f'
'
)
return "\n".join(rows)
def update_readme(path: str, block: str) -> bool:
with open(path, encoding="utf-8") as fh:
text = fh.read()
if START not in text or END not in text:
print(f"markers not found in {path}", file=sys.stderr)
return False
replacement = f"{START}\n{block}\n{END}"
updated = re.sub(
re.escape(START) + r".*?" + re.escape(END),
lambda _m: replacement,
text,
flags=re.DOTALL,
)
if updated == text:
return False
with open(path, "w", encoding="utf-8") as fh:
fh.write(updated)
return True
def main() -> int:
token = _token()
people = fetch_contributors(token)
if not people:
print("no contributors fetched (rate limit or auth?)", file=sys.stderr)
return 1
people = include_recognized_contributors(people)
have_png, written = write_avatars(people, token)
block = render(people, have_png)
changed = [p for p in READMES if os.path.exists(p) and update_readme(p, block)]
print(
f"{len(people)} contributors: "
+ ", ".join(c["login"] for c in people)
+ f"\ncircular pngs: {written} refreshed, {len(have_png)} linked locally"
+ f"; readmes updated: {changed or 'none'}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())