Files
antigravity-skills-reference/tools/scripts/sync_contributors.py

164 lines
5.4 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
from _project_paths import find_repo_root
from update_readme import configure_utf8_output, load_metadata
CONTRIBUTOR_SECTION_START = "We officially thank the following contributors for their help in making this repository awesome!\n\n"
SPECIAL_LINK_OVERRIDES = {
"Copilot": "https://github.com/apps/copilot-swe-agent",
"github-actions[bot]": "https://github.com/apps/github-actions",
"copilot-swe-agent[bot]": "https://github.com/apps/copilot-swe-agent",
}
def parse_existing_contributor_links(content: str) -> dict[str, str]:
links: dict[str, str] = {}
pattern = re.compile(r"^- \[@(?P<label>.+?)\]\((?P<url>https://github\.com/.+?)\)$")
for line in content.splitlines():
match = pattern.match(line.strip())
if not match:
continue
links[match.group("label")] = match.group("url")
return links
def parse_existing_contributor_order(content: str) -> list[str]:
order: list[str] = []
seen: set[str] = set()
pattern = re.compile(r"^- \[@(?P<label>.+?)\]\((?P<url>https://github\.com/.+?)\)$")
for line in content.splitlines():
match = pattern.match(line.strip())
if not match:
continue
label = match.group("label")
if label in seen:
continue
seen.add(label)
order.append(label)
return order
def parse_contributors_response(payload: list[dict]) -> list[str]:
contributors: list[str] = []
seen: set[str] = set()
for entry in payload:
login = entry.get("login")
if not isinstance(login, str) or not login or login in seen:
continue
seen.add(login)
contributors.append(login)
return contributors
def order_contributors_for_render(contributors: list[str], existing_order: list[str]) -> list[str]:
contributor_set = set(contributors)
ordered_existing = [login for login in existing_order if login in contributor_set]
new_contributors = sorted(
(login for login in contributors if login not in existing_order),
key=lambda login: login.casefold(),
)
return ordered_existing + new_contributors
def infer_contributor_url(login: str, existing_links: dict[str, str]) -> str:
if login in existing_links:
return existing_links[login]
if login in SPECIAL_LINK_OVERRIDES:
return SPECIAL_LINK_OVERRIDES[login]
if login.endswith("[bot]"):
app_name = login[: -len("[bot]")]
return f"https://github.com/apps/{app_name}"
return f"https://github.com/{login}"
def render_contributor_lines(contributors: list[str], existing_links: dict[str, str]) -> str:
lines = []
for login in contributors:
url = infer_contributor_url(login, existing_links)
lines.append(f"- [@{login}]({url})")
return "\n".join(lines)
def update_repo_contributors_section(content: str, contributors: list[str]) -> str:
existing_links = parse_existing_contributor_links(content)
ordered_contributors = order_contributors_for_render(
contributors,
parse_existing_contributor_order(content),
)
rendered_list = render_contributor_lines(ordered_contributors, existing_links)
if CONTRIBUTOR_SECTION_START not in content or "\n## " not in content:
raise ValueError("README.md does not contain the expected Repo Contributors section structure.")
start_index = content.index(CONTRIBUTOR_SECTION_START) + len(CONTRIBUTOR_SECTION_START)
end_index = content.index("\n## ", start_index)
return f"{content[:start_index]}{rendered_list}\n{content[end_index:]}"
def fetch_contributors(repo: str) -> list[str]:
result = subprocess.run(
[
"gh",
"api",
f"repos/{repo}/contributors?per_page=100",
"--paginate",
"--slurp",
],
check=True,
capture_output=True,
text=True,
)
payload = json.loads(result.stdout)
flat_entries: list[dict] = []
for page in payload:
if isinstance(page, list):
flat_entries.extend(entry for entry in page if isinstance(entry, dict))
return parse_contributors_response(flat_entries)
def sync_contributors(base_dir: str | Path, dry_run: bool = False) -> bool:
root = Path(base_dir)
metadata = load_metadata(str(root))
contributors = fetch_contributors(metadata["repo"])
readme_path = root / "README.md"
original = readme_path.read_text(encoding="utf-8")
updated = update_repo_contributors_section(original, contributors)
if updated == original:
return False
if dry_run:
print(f"[dry-run] Would update contributors in {readme_path}")
return True
readme_path.write_text(updated, encoding="utf-8", newline="\n")
print(f"✅ Updated contributors in {readme_path}")
return True
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Synchronize the README Repo Contributors section.")
parser.add_argument("--dry-run", action="store_true", help="Preview contributor changes without writing files.")
return parser.parse_args()
def main() -> int:
args = parse_args()
root = find_repo_root(__file__)
sync_contributors(root, dry_run=args.dry_run)
return 0
if __name__ == "__main__":
configure_utf8_output()
sys.exit(main())