gh-cli

Drive the GitHub CLI (gh) to review a pull request — fetch the diff and context, post inline review comments anchored to specific diff lines, open conceptual conversation threads, and submit a verdict

SKILL.md

GitHub

gh CLI — PR review & commenting

Everything here drives gh via the shell. gh api repos/{owner}/{repo}/... auto-expands {owner}/{repo} from the current clone — in the URL path and in -F/--field values (not -f/--raw-field, which sends the literal string, and not inside an --input JSON body). Set GH_REPO=OWNER/REPO or pass -R OWNER/REPO when outside the repo.

0. Auth check (do this first)

gh auth status              # must be authenticated; token needs `repo` scope to post reviews
ME=$(gh api user -q .login) # your login — used for the self-PR fallback below

1. Resolve the target

PR=42
gh pr view "$PR" --json number,title,state,isDraft,author,headRefName,baseRefName,headRefOid,url,mergeable
HEAD_SHA=$(gh pr view "$PR" --json headRefOid -q .headRefOid)   # commit_id for review comments
AUTHOR=$(gh pr view "$PR" --json author -q .author.login)

2. Fetch context (read-only)

gh pr diff "$PR"                                              # unified diff (the change to review)
gh api --paginate repos/{owner}/{repo}/pulls/$PR/files        # per-file patch hunks — validate comment line numbers against these
gh pr checks "$PR"                                            # CI status (evidence, not a finding to re-derive)

# Existing feedback — read before posting so you never duplicate a point:
gh api --paginate repos/{owner}/{repo}/pulls/$PR/comments     # inline review comments
gh api --paginate repos/{owner}/{repo}/pulls/$PR/reviews      # prior reviews
gh api --paginate repos/{owner}/{repo}/issues/$PR/comments    # conversation comments

# Linked issue (acceptance criteria) — PRs link via "Closes #N":
gh pr view "$PR" --json body,closingIssuesReferences
gh issue view <N> --json number,title,body,state,labels

3. Post inline comments + verdict in ONE review (the reliable path)

Bundle every line-anchored comment and the verdict into a single POST .../pulls/<PR>/reviews call. The nested comments[] array can't be built with -f/-F, so pass raw JSON. Build it with jq (safest — it escapes bodies for you) or a quoted heredoc, then --input.

Write the static comments[] array with a quoted heredoc (so backticks and $ in bodies stay literal), then let jq inject the computed commit_id and event — never hardcode the verdict, or you'll post REQUEST_CHANGES on a self-PR (422). $EVENT and $HEAD_SHA come from steps 1 and 5; on your own PR $EVENT is COMMENT.

cat > /tmp/comments.json <<'JSON'
[
  { "path": "src/auth.ts", "line": 88, "side": "RIGHT",
    "body": "**🔴 CRITICAL — SQL injection.** userId is concatenated into the query; use a bound parameter (see suggestion)." },
  { "path": "src/api.ts", "start_line": 20, "start_side": "RIGHT", "line": 23, "side": "RIGHT",
    "body": "**🟠 MAJOR — unhandled rejection.** Wrap this block in try/catch and return a 5xx." },
  { "path": "src/old.ts", "line": 7, "side": "LEFT",
    "body": "**🟡 MINOR** — comment on a *deleted* line (LEFT = original file)." }
]
JSON
jq -n \
  --arg sha   "$HEAD_SHA" \
  --arg event "$EVENT" \
  --arg body  "## diff-reviewer verdict: $EVENT — see inline comments + threads." \
  --slurpfile comments /tmp/comments.json \
  '{commit_id:$sha, event:$event, body:$body, comments:$comments[0]}' \
  | gh api repos/{owner}/{repo}/pulls/$PR/reviews --input -

jq escapes the bodies and injects the real $HEAD_SHA/$EVENT, so the verdict always matches step 5 (and the self-PR fallback). Keep {owner}/ {repo} in the URL — they don't expand inside the JSON.

Line/side rules (getting these wrong → HTTP 422):

  • path — repo-relative file path.
  • line — the line number in the file the comment targets; for a multi-line comment it is the last line of the range.
  • side — RIGHT = added/unchanged (new file; the default), LEFT = deleted (original file). Use LEFT for removed lines.
  • Multi-line comment → also set start_line + start_side (the first line/side; start_line < line). Single-line → omit both.
  • The target line must be part of the diff (an added/changed/context line inside a hunk) — validate against pulls/$PR/files patch hunks first, or the whole call 422s.
  • event: APPROVE | REQUEST_CHANGES | COMMENT. body is required for REQUEST_CHANGES/COMMENT. Omitting event leaves the review PENDING (unsubmitted, no notification) — don't forget to submit.

Proposed fixes — suggestion blocks. When the fix is a local edit, include a suggestion block in the comment body so the author applies it in one click. The body is a JSON string, so the block is a \n-delimited fenced section like this (literal newlines shown for clarity):

**🔴 CRITICAL — SQL injection.** Use a parameterised query:

```suggestion
  const rows = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
```

In JSON that body becomes: "...Use a parameterised query:\n\n```suggestion\n const rows = ...\n```" (the suggestion replaces exactly the commented line(s)).

4. Post a conceptual (non-line) thread

For findings not tied to one line — architecture, missing tests, PR scope:

gh pr comment "$PR" --body "**🟠 MAJOR — no tests for the new auth path.** AC-3 requires a failing-login test; none added. ..."

5. Verdict & self-PR fallback

The event on the review is the verdict. But you cannot APPROVE or REQUEST_CHANGES your own PR (GitHub returns 422). Branch on it:

if [ "$ME" = "$AUTHOR" ]; then
  EVENT="COMMENT"          # always allowed on your own PR; still carries inline comments + summary
else
  EVENT="REQUEST_CHANGES"  # or APPROVE / COMMENT per the findings
fi
  • 🔴 present → REQUEST_CHANGES (or COMMENT if self-PR).
  • only 🟡/🟢 → COMMENT.
  • clean / nits only → APPROVE (non-author).

Gotchas

  • Prefer the reviews endpoint over POST .../pulls/$PR/comments — the standalone comments endpoint frequently 422s even with valid line/side.
  • {owner}/{repo} expand in the URL path and in -F/--field values (not -f/--raw-field, which is literal), and not inside --input JSON — keep them in the path.
  • position (diff-offset anchoring) is deprecated — always use line/side.
  • Pass commit_id = $HEAD_SHA; a stale SHA marks comments "outdated".
  • Throttle bulk posts — review creation sends notifications and can hit a secondary rate limit.
  • Never echo the token; repo scope is required to post (read-only → 403).
  • De-dup across runs: comments are not idempotent — a second run re-posts everything. Stamp each comment body with a <!-- diff-reviewer --> marker, and in step 2 skip any finding whose file+line already carries that marker in the fetched pulls/$PR/comments.
  • Secret in the diff? Redact the value in the comment (AKIA****) and do not put the secret in a ```suggestion block — that would republish it. Recommend rotation + history removal.
  • Staged / silent option: omit event to create the review PENDING (no notification). You can show the human the rendered review on GitHub, then submit it via POST .../pulls/$PR/reviews/$REVIEW_ID/events with the chosen event — a belt-and-suspenders complement to the in-chat preview gate.