Repository navigation
feat: add a script to post inline review comments on a PR diff #239
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
Changes from all commits
dca3682
74014d0
5e2d6b7
7adfd1e
d847d04
f3d21d9
98e355f
7e2aab0
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,83 @@ | ||
| #!/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() |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,13 +14,13 @@ | |||||||||
|
|
||||||||||
|
|
||||||||||
|
|
||||||||||
| ## Test plan | ||||||||||
| ## How do we know this works? | ||||||||||
|
|
||||||||||
| <!-- How do we know this works? Say which kinds of verification cover the change (unit, E2E, snapshot, manual in the | ||||||||||
| app, physical device, deployed and checked live) and which part each one covers, any part none of them reaches, and | ||||||||||
| whether a test was seen to fail with the code broken or the fix reverted. CI must pass on every PR, so leave out | ||||||||||
| anything it covers: re-running its suites only counts somewhere CI can't (a Raspberry Pi, a lab PC). Don't list test | ||||||||||
| names from the diff. --> | ||||||||||
| <!-- For each part of the change, say which kind of verification pins it (unit, E2E, snapshot, manual in the app, | ||||||||||
| physical device, deployed and checked live) and what it establishes, whether a test was seen to fail with the code | ||||||||||
| broken or the fix reverted (and how it was broken), and any part nothing covers. Describe what the tests prove, never | ||||||||||
| that they ran: CI reruns every suite on every push. Re-running a suite only | ||||||||||
| counts somewhere CI can't (a Raspberry Pi, a lab PC). Don't list test names from the diff. --> | ||||||||||
|
Comment on lines
+22
to
+23
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. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
fd --hidden -e yml -e yaml . .github/workflows -0 |
while IFS= read -r -d '' file; do
printf '\n### %s\n' "$file"
cat "$file"
doneRepository: LabAutomationAndScreening/copier-base-template Length of output: 12390 🏁 Script executed: set -euo pipefail
printf '%s\n' '### .github/pull_request_template.md'
nl -ba .github/pull_request_template.md | sed -n '12,30p'
printf '%s\n' '### .github/workflows/ci.yaml'
nl -ba .github/workflows/ci.yaml | sed -n '1,12p;75,125p;205,245p'Repository: LabAutomationAndScreening/copier-base-template Length of output: 6154 Do not state that CI reruns every suite on every push. CI starts on most pushes, but Suggested prompt update- broken or the fix reverted (and how it was broken), and any part nothing covers. Describe what the tests prove, never
- that they ran: CI reruns every suite on every push. Re-running a suite only
- counts somewhere CI can't (a Raspberry Pi, a lab PC). Don't list test names from the diff. -->
+ broken or the fix reverted (and how it was broken), and any part nothing covers. Describe what the tests prove, never
+ only that they ran. CI starts on most pushes, but some suites may be skipped when a prerequisite job fails. State which
+ checks ran and what they establish. Re-running a suite only counts somewhere CI can't (a Raspberry Pi, a lab PC). Don't
+ list test names from the diff. -->📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
|
|
||||||||||
| ## Other | ||||||||||
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.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Delete the body file before writing to stdout.
If the stdout pipe closes after a successful POST,
sys.stdout.writecan raiseBrokenPipeError. The body file then remains, so a retry can post the comment again. Attempt deletion before printing the result. (docs.python.org)🤖 Prompt for AI Agents