diff --git a/.github/scripts/request_review.py b/.github/scripts/request_review.py deleted file mode 100644 index d799e7c58..000000000 --- a/.github/scripts/request_review.py +++ /dev/null @@ -1,189 +0,0 @@ -# /// script -# requires-python = ">=3.9" -# dependencies = [ -# "githubkit", -# "loguru", -# ] -# /// - -from __future__ import annotations - -import asyncio -import os -import re -import sys -from collections.abc import Iterator -from contextlib import contextmanager -from itertools import chain - -from githubkit import GitHub -from githubkit.exception import RequestFailed -from loguru import logger - -ORG_NAME = "ghostty-org" -REPO_NAME = "ghostty" -ALLOWED_PARENT_TEAM = "localization" -LOCALIZATION_TEAM_NAME_PATTERN = re.compile(r"[a-z]{2}_[A-Z]{2}") -LEVEL_MAP = {"DEBUG": "DBG", "WARNING": "WRN", "ERROR": "ERR"} - -logger.remove() -logger.add( - sys.stderr, - format=lambda record: ( - "{time:YYYY-MM-DD HH:mm:ss.SSS} | " - f"{LEVEL_MAP[record['level'].name]} | " - "{function}:{line} - " - "{message}\n" - ), - backtrace=True, - diagnose=True, -) - - -@contextmanager -def log_fail(message: str, *, die: bool = True) -> Iterator[None]: - try: - yield - except RequestFailed as exc: - logger.error(message) - logger.error(exc) - logger.error(exc.response.raw_response.json()) - if die: - sys.exit(1) - - -gh = GitHub(os.environ["GITHUB_TOKEN"]) - -with log_fail("Invalid token"): - # Do the simplest request as a test - gh.rest.rate_limit.get() - - -async def fetch_and_parse_codeowners() -> dict[str, str]: - logger.debug("Fetching CODEOWNERS file...") - with log_fail("Failed to fetch CODEOWNERS file"): - content = ( - await gh.rest.repos.async_get_content( - ORG_NAME, - REPO_NAME, - "CODEOWNERS", - headers={"Accept": "application/vnd.github.raw+json"}, - ) - ).text - - logger.debug("Parsing CODEOWNERS file...") - codeowners: dict[str, str] = {} - for line in content.splitlines(): - if not line or line.lstrip().startswith("#"): - continue - - # This assumes that all entries only list one owner - # and that this owner is a team (ghostty-org/foobar) - path, owner = line.split() - path = path.lstrip("/") - owner = owner.removeprefix(f"@{ORG_NAME}/") - - if not is_localization_team(owner): - logger.debug(f"Skipping non-l11n codeowner {owner!r} for {path}") - continue - - codeowners[path] = owner - logger.debug(f"Found codeowner {owner!r} for {path}") - return codeowners - - -async def get_team_members(team_name: str) -> list[str]: - logger.debug(f"Fetching team {team_name!r}...") - with log_fail(f"Failed to fetch team {team_name!r}"): - team = (await gh.rest.teams.async_get_by_name(ORG_NAME, team_name)).parsed_data - - if team.parent and team.parent.slug == ALLOWED_PARENT_TEAM: - logger.debug(f"Fetching team {team_name!r} members...") - with log_fail(f"Failed to fetch team {team_name!r} members"): - resp = await gh.rest.teams.async_list_members_in_org(ORG_NAME, team_name) - members = [m.login for m in resp.parsed_data] - logger.debug(f"Team {team_name!r} members: {', '.join(members)}") - return members - - logger.warning(f"Team {team_name} does not have a {ALLOWED_PARENT_TEAM!r} parent") - return [] - - -async def get_changed_files(pr_number: int) -> list[str]: - logger.debug("Gathering changed files...") - with log_fail("Failed to gather changed files"): - diff_entries = ( - await gh.rest.pulls.async_list_files( - ORG_NAME, - REPO_NAME, - pr_number, - per_page=3000, - headers={"Accept": "application/vnd.github+json"}, - ) - ).parsed_data - return [d.filename for d in diff_entries] - - -async def request_review(pr_number: int, user: str, pr_author: str) -> None: - if user == pr_author: - logger.debug(f"Skipping review request for {user!r} (is PR author)") - logger.debug(f"Requesting review from {user!r}...") - with log_fail(f"Failed to request review from {user}", die=False): - await gh.rest.pulls.async_request_reviewers( - ORG_NAME, - REPO_NAME, - pr_number, - headers={"Accept": "application/vnd.github+json"}, - data={"reviewers": [user]}, - ) - - -def is_localization_team(team_name: str) -> bool: - return LOCALIZATION_TEAM_NAME_PATTERN.fullmatch(team_name) is not None - - -async def get_pr_author(pr_number: int) -> str: - logger.debug("Fetching PR author...") - with log_fail("Failed to fetch PR author"): - resp = await gh.rest.pulls.async_get(ORG_NAME, REPO_NAME, pr_number) - pr_author = resp.parsed_data.user.login - logger.debug(f"Found author: {pr_author!r}") - return pr_author - - -async def main() -> None: - logger.debug("Reading PR number...") - pr_number = int(os.environ["PR_NUMBER"]) - logger.debug(f"Starting review request process for PR #{pr_number}...") - - changed_files = await get_changed_files(pr_number) - logger.debug(f"Changed files: {', '.join(map(repr, changed_files))}") - - pr_author = await get_pr_author(pr_number) - codeowners = await fetch_and_parse_codeowners() - - found_owners = set[str]() - for file in changed_files: - logger.debug(f"Finding owner for {file!r}...") - for path, owner in codeowners.items(): - if file.startswith(path): - logger.debug(f"Found owner: {owner!r}") - break - else: - logger.debug("No owner found") - continue - found_owners.add(owner) - - member_lists = await asyncio.gather( - *(get_team_members(owner) for owner in found_owners) - ) - await asyncio.gather( - *( - request_review(pr_number, user, pr_author) - for user in chain.from_iterable(member_lists) - ) - ) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml deleted file mode 100644 index 9abe0b5e2..000000000 --- a/.github/workflows/review.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Request Review - -on: - pull_request: - types: - - opened - - synchronize - -env: - PY_COLORS: 1 - -jobs: - review: - runs-on: namespace-profile-ghostty-xsm - steps: - - uses: actions/checkout@v4 - - - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 - with: - path: | - /nix - /zig - - - uses: cachix/install-nix-action@v30 - with: - nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 - with: - name: ghostty - authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - - name: Request Localization Review - env: - GITHUB_TOKEN: ${{ secrets.GH_REVIEW_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: nix develop -c uv run .github/scripts/request_review.py