Repository navigation
Copier update: copier-base-template v0.2.0 (review-comment script, nested-folder Biome settings) #242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Copier update: copier-base-template v0.2.0 (review-comment script, nested-folder Biome settings) #242
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| #!/usr/bin/env python3 | ||
| """Post a new inline review comment on a PR diff via the GitHub API. | ||
|
|
||
| Usage: | ||
| # Comment on one line of the new file: | ||
| post-review-comment.py --pr <number> --path <repo-relative path> --line <n> --body-file <path> | ||
|
|
||
| # Comment spanning a range of lines (start and end must be in the same diff hunk): | ||
| post-review-comment.py --pr <number> --path <repo-relative path> --start-line <n> --line <m> --body-file <path> | ||
|
|
||
| # Comment on a removed line (the old file's numbering): | ||
| post-review-comment.py --pr <number> --path <repo-relative path> --line <n> --side LEFT --body-file <path> | ||
|
|
||
| Line numbers are those of the file at --commit-id (default: the PR's current head) for --side RIGHT, or of the base | ||
| file for --side LEFT. Owner and repo are derived from the git remote automatically. | ||
|
|
||
| This script exists so that gh api calls are covered by the skills allow-list | ||
| rather than requiring manual approval each time. | ||
| """ | ||
|
|
||
| import argparse | ||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| sys.path.insert(0, str(Path(__file__).parent)) | ||
| from utils import owner_repo_from_remote | ||
| from utils import post_via_gh_api | ||
| from utils import run_cmd | ||
|
|
||
|
|
||
| def pr_head_sha(*, owner: str, repo: str, pr: int) -> str: | ||
| try: | ||
| result = run_cmd( | ||
| ["gh", "api", f"repos/{owner}/{repo}/pulls/{pr}", "--jq", ".head.sha"], | ||
| timeout=30, | ||
| timeout_msg="gh api timed out while reading the PR head commit.", | ||
| ) | ||
| except subprocess.CalledProcessError as e: | ||
| _ = sys.stderr.write(f"gh api error reading PR #{pr}: {e.stderr}\n") | ||
| sys.exit(1) | ||
| return result.stdout.strip() | ||
|
|
||
|
|
||
| def main() -> None: | ||
| parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | ||
| _ = parser.add_argument("--pr", type=int, required=True, help="PR number") | ||
| _ = parser.add_argument("--path", required=True, help="File path relative to the repo root") | ||
| _ = parser.add_argument("--line", type=int, required=True, help="Line the comment ends on") | ||
| _ = parser.add_argument("--start-line", type=int, help="First line of a multi-line comment") | ||
| _ = parser.add_argument("--side", choices=["RIGHT", "LEFT"], default="RIGHT", help="RIGHT = new file, LEFT = old") | ||
| _ = parser.add_argument("--start-side", choices=["RIGHT", "LEFT"], help="Defaults to --side") | ||
| _ = parser.add_argument("--commit-id", help="Commit the line numbers refer to (default: the PR's head)") | ||
| _ = parser.add_argument("--body-file", required=True, type=Path) | ||
| args = parser.parse_args() | ||
|
|
||
| if args.start_line is not None and args.start_line >= args.line: | ||
| _ = sys.stderr.write(f"--start-line ({args.start_line}) must be less than --line ({args.line})\n") | ||
| sys.exit(1) | ||
|
|
||
| owner, repo = owner_repo_from_remote() | ||
|
|
||
| commit_id = args.commit_id | ||
| if commit_id is None: | ||
| commit_id = pr_head_sha(owner=owner, repo=repo, pr=args.pr) | ||
|
|
||
| string_fields = {"commit_id": commit_id, "path": args.path, "side": args.side} | ||
| int_fields = {"line": args.line} | ||
| if args.start_line is not None: | ||
| string_fields["start_side"] = args.side if args.start_side is None else args.start_side | ||
| int_fields["start_line"] = args.start_line | ||
|
|
||
| post_via_gh_api( | ||
| endpoint=f"repos/{owner}/{repo}/pulls/{args.pr}/comments", | ||
| body_file=args.body_file, | ||
| string_fields=string_fields, | ||
| int_fields=int_fields, | ||
| timeout_msg="gh api timed out while posting the review comment.", | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
|
|
||
| # ============== WARNING ============================================================================== | ||
| # File is managed by copier template: gh:LabAutomationAndScreening/copier-base-template.git | ||
| # See .config/.copier-managed-files.json for details. | ||
| # | ||
| # You are welcome to make changes to this file in your repo if they are custom to your project, | ||
| # but if the change should be shared with other projects, please backport it to the template repo. | ||
| # ===================================================================================================== | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,9 +5,12 @@ | |
| # You are welcome to make changes to this file in your repo if they are custom to your project, | ||
| # but if the change should be shared with other projects, please backport it to the template repo. | ||
| # ===================================================================================================== | ||
| import json | ||
| import re | ||
| import subprocess | ||
| import sys | ||
| from collections.abc import Mapping | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| def run_cmd( | ||
|
|
@@ -48,3 +51,55 @@ def owner_repo_from_remote() -> tuple[str, str]: | |
| _ = sys.stderr.write(f"Cannot parse GitHub owner/repo from remote: {url}\n") | ||
| sys.exit(1) | ||
| return match.group(1), match.group(2) | ||
|
|
||
|
|
||
| def _read_body_file(body_file: Path) -> str: | ||
| try: | ||
| return body_file.read_text(encoding="utf-8") | ||
| except OSError as e: | ||
| _ = sys.stderr.write(f"Unable to read body file {body_file}: {e}\n") | ||
| sys.exit(1) | ||
|
|
||
|
|
||
| def post_via_gh_api( | ||
| *, | ||
| endpoint: str, | ||
| body_file: Path, | ||
| string_fields: Mapping[str, str] | None = None, | ||
| int_fields: Mapping[str, int] | None = None, | ||
| timeout_msg: str, | ||
| ) -> None: | ||
| """POST a comment to a GitHub API endpoint with gh api, print its URL, then delete the body file it came from. | ||
|
|
||
| The comment's `body` field is read from body_file. string_fields are sent as strings (--raw-field) and int_fields as | ||
| JSON numbers (--field), which is what GitHub expects for values like line numbers. | ||
|
|
||
| Exits with status 1 if gh api fails. Once gh api succeeds the comment exists, so an unparsable response still counts | ||
| as posted and the body file is still deleted (otherwise a retry would post a duplicate). A failure to delete the body | ||
| file only warns. | ||
| """ | ||
| body = _read_body_file(body_file) | ||
| cmd = ["gh", "api", endpoint, "--method", "POST", "--raw-field", f"body={body}"] | ||
| if string_fields is not None: | ||
| for key, value in string_fields.items(): | ||
| cmd += ["--raw-field", f"{key}={value}"] | ||
| if int_fields is not None: | ||
| for key, value in int_fields.items(): | ||
| cmd += ["--field", f"{key}={value}"] | ||
|
|
||
| try: | ||
| result = run_cmd(cmd, timeout=30, timeout_msg=timeout_msg) | ||
| except subprocess.CalledProcessError as e: | ||
| _ = sys.stderr.write(f"gh api error: {e.stderr}\n") | ||
| sys.exit(1) | ||
|
|
||
| try: | ||
| url = json.loads(result.stdout).get("html_url", "(no URL in response)") | ||
| except json.JSONDecodeError: | ||
| url = "(unparsable response)" | ||
|
Comment on lines
+97
to
+99
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Replace
📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| _ = sys.stdout.write(f"Posted: {url}\n") | ||
|
|
||
| try: | ||
| body_file.unlink() | ||
| except OSError as e: | ||
| _ = sys.stderr.write(f"Warning: post succeeded but could not delete body file {body_file}: {e}\n") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Replace the ternary with an explicit
if/else. The coding guideline forbids ternaries in non-test code because they hide branches fromcoverage.py..claude/skills/address-pr-comments/post-review-comment.py#L70-L70: useif args.start_side is None:andelse:blocks that assignstring_fields["start_side"].template/.claude/skills/address-pr-comments/post-review-comment.py#L70-L70: apply the same change.📍 Affects 2 files
.claude/skills/address-pr-comments/post-review-comment.py#L70-L70(this comment)template/.claude/skills/address-pr-comments/post-review-comment.py#L70-L70🤖 Prompt for AI Agents
Source: Coding guidelines