Files
antigravity-skills-reference/tools/scripts/sync_contributors.py
sickn33 694721223c feat(repo): Add contributor sync and consistency audits
Add maintainer automation for repo-state hygiene so contributor acknowledgements, count-sensitive docs, and GitHub About metadata stay aligned from the same workflow.

Cover the new scripts with regression tests and wire them into the local test suite to keep future maintenance changes from drifting silently.
2026-03-21 10:48:00 +01:00

134 lines
4.3 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_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 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)
rendered_list = render_contributor_lines(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())