Files
kandev/.github/scripts/claude-read-pr-file.py
2026-07-31 19:51:06 +01:00

136 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Read one UTF-8 file from the head of the PR bound by the workflow."""
import base64
import binascii
import json
import os
from pathlib import PurePosixPath
import re
import subprocess
import sys
from urllib.parse import quote
MAX_FILE_BYTES = 256 * 1024
REPOSITORY_PATTERN = re.compile(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+")
SHA_PATTERN = re.compile(r"[0-9a-f]{40}")
class PrFileError(Exception):
"""A safe, user-facing validation or retrieval error."""
def require_environment() -> tuple[str, str]:
repository = os.environ.get("GITHUB_REPOSITORY", "")
pr_number = os.environ.get("CLAUDE_REVIEW_PR_NUMBER", "")
if REPOSITORY_PATTERN.fullmatch(repository) is None:
raise PrFileError("GITHUB_REPOSITORY is not a valid owner/repository")
if re.fullmatch(r"[1-9][0-9]*", pr_number) is None:
raise PrFileError("CLAUDE_REVIEW_PR_NUMBER is not a valid pull request number")
return repository, pr_number
def validate_path(raw_path: str) -> str:
path = PurePosixPath(raw_path)
normalized_path = path.as_posix()
if (
not raw_path
or raw_path.startswith("/")
or "\\" in raw_path
or any(part in ("", ".", "..") for part in path.parts)
or any(ord(character) < 32 for character in raw_path)
or normalized_path != raw_path
):
raise PrFileError(
"path must be a relative repository path without traversal"
)
return normalized_path
def gh_api(endpoint: str) -> object:
try:
result = subprocess.run(
["gh", "api", "--method", "GET", endpoint],
check=True,
capture_output=True,
text=True,
)
return json.loads(result.stdout)
except FileNotFoundError as error:
raise PrFileError("GitHub CLI is unavailable") from error
except subprocess.CalledProcessError as error:
raise PrFileError("GitHub API request failed") from error
except json.JSONDecodeError as error:
raise PrFileError("GitHub API returned invalid JSON") from error
def get_head_sha(repository: str, pr_number: str) -> str:
response = gh_api(f"repos/{repository}/pulls/{pr_number}")
if not isinstance(response, dict):
raise PrFileError("pull request response is invalid")
head = response.get("head")
sha = head.get("sha") if isinstance(head, dict) else None
if not isinstance(sha, str) or SHA_PATTERN.fullmatch(sha) is None:
raise PrFileError("pull request head SHA is invalid")
return sha
def decode_file(response: object, requested_path: str) -> str:
if not isinstance(response, dict):
raise PrFileError("file response is invalid")
if response.get("type") != "file" or "target" in response:
raise PrFileError("requested path is not a regular file")
if response.get("path") != requested_path:
raise PrFileError("GitHub returned a different repository path")
size = response.get("size")
if not isinstance(size, int) or isinstance(size, bool) or size < 0:
raise PrFileError("file size is invalid")
if size > MAX_FILE_BYTES:
raise PrFileError("requested file exceeds the 256 KiB review limit")
if response.get("encoding") != "base64" or not isinstance(
response.get("content"), str
):
raise PrFileError("requested file has unsupported content encoding")
try:
encoded_content = "".join(response["content"].split())
raw_content = base64.b64decode(encoded_content, validate=True)
except (binascii.Error, ValueError) as error:
raise PrFileError("requested file contains invalid base64 data") from error
if len(raw_content) != size:
raise PrFileError("requested file size does not match its content")
try:
return raw_content.decode("utf-8")
except UnicodeDecodeError as error:
raise PrFileError("requested file is not valid UTF-8 text") from error
def read_pr_file(raw_path: str) -> str:
repository, pr_number = require_environment()
requested_path = validate_path(raw_path)
head_sha = get_head_sha(repository, pr_number)
encoded_path = quote(requested_path, safe="/")
endpoint = f"repos/{repository}/contents/{encoded_path}?ref={head_sha}"
return decode_file(gh_api(endpoint), requested_path)
def main() -> int:
if len(sys.argv) != 2:
print(
"usage: claude-read-pr-file.py <repository-relative-path>",
file=sys.stderr,
)
return 2
try:
sys.stdout.write(read_pr_file(sys.argv[1]))
except PrFileError as error:
print(f"error: {error}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())