# Review Pr

> Review a pull request, address inline reviewer comments, post replies, and apply fixes. Use when asked to review PR feedback, respond to comments, or fix issues raised in a PR.

- Skill: `canonical/review-pr` (Agent Skill)
- Install (CLI): `npx skillmds@latest add canonical/review-pr`
- Raw SKILL.md: https://api.skillmd.com/api/skills/canonical/review-pr/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: canonical (https://skillmd.com/u/canonical)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/canonical/review-pr

---


# Task: review and address PR feedback

## Goal

Read all reviewer comments on a pull request — including resolved/outdated ones and
suppressed comments buried in Copilot review bodies (see Step 1a) — apply any
necessary fixes to the code, post a reply to each comment explaining what was done,
and resolve the threads where possible.

## AI disclaimer

**Every comment you post must end with the following disclaimer on its own line:**

```
> *This comment was posted by an AI assistant (GitHub Copilot) on behalf of the pull request author.*
```

This applies to all PR comment types: inline replies, general PR comments, and
review submission bodies.

---

## Step 1 — Read the PR

```bash
# Get PR title, author, description
# Note: avoid --jq for complex filters — it silently exits 1 in this environment.
# Pipe to python3 instead for reliable parsing.
gh api repos/canonical/charm-integration-testing/pulls/<number> \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Title: {d[\"title\"]}\nAuthor: {d[\"user\"][\"login\"]}\nBranch: {d[\"head\"][\"ref\"]}\n\nBody:\n{d[\"body\"]}')"

# Read all inline review comments (--paginate ensures more than 100 comments are read)
gh api --paginate "repos/canonical/charm-integration-testing/pulls/<number>/comments?per_page=100" \
  | python3 -c "
import sys, json
for c in json.load(sys.stdin):
    print(f\"{c['path']}:{c.get('line','?')} [{c['user']['login']}] (id:{c['id']})\")
    print(c['body'])
    print('---')
"

# Read general (issue-style) comments
gh api --paginate "repos/canonical/charm-integration-testing/issues/<number>/comments?per_page=100" \
  | python3 -c "
import sys, json
for c in json.load(sys.stdin):
    print(f\"[{c['user']['login']}] (id:{c['id']})\")
    print(c['body'])
    print('---')
"
```

Read the review instructions file at `.github/instructions/python-pr-review.instructions.md`
before evaluating any Python changes.

### Step 1a — Read Copilot review bodies, including suppressed comments

Copilot's automated reviews (`copilot-pull-request-reviewer[bot]`) post a review body with
a status header and, in `<details>` blocks, comments it decided not to post inline
("suppressed comments"). These are only visible in the review body, not in the
`/pulls/<number>/comments` inline-comments list, so they are easy to miss.

```bash
gh api --paginate "repos/canonical/charm-integration-testing/pulls/<number>/reviews?per_page=100" \
  | python3 -c "
import sys, json, re
for r in json.load(sys.stdin):
    if r['user']['login'] != 'copilot-pull-request-reviewer[bot]':
        continue
    body = r['body'] or ''
    header = re.search(r'^###\s*(.+)', body, re.M)
    print(f\"review id:{r['id']}  status: {header.group(1) if header else '(no header)'}\")
    m = re.search(r'Suppressed comments \((\d+)\)(.*?)(?:\n- \*\*Files reviewed|</details>|\Z)', body, re.S)
    if m and int(m.group(1)) > 0:
        print('  suppressed comments:')
        print(m.group(2).strip())
    print('---')
"
```

Status headers seen in this repo (and their meaning):

- **🟢 Approval recommended (or no header)** — no changes requested. Note that the
  script above prints its own literal `(no header)` when the `### ...` regex finds
  nothing; that is the script's placeholder text, not a status Copilot itself emits —
  treat it the same as "Approval recommended".
- **🟡 Changes recommended** — Copilot posted at least one actionable inline comment;
  these show up in the normal `/comments` list from Step 1.
- **🔵 Needs a closer look** — Copilot flagged a concern but suppressed the inline
  comment (often because confidence was lower or it judged the point secondary).
  The detail is only in the "Suppressed comments" block, not in `/comments`.

**Treat suppressed comments as real feedback to triage**, not noise: read the quoted
file/line and text, decide whether it's a valid issue, and if so fix it exactly as you
would an inline comment. Do not skip a review just because its comments were suppressed
or because the header is "Needs a closer look" rather than "Changes recommended".

---

## Step 2 — Read the changed files

Check out the PR branch and read the relevant source files before making any changes:

```bash
# Find the branch name
gh api repos/canonical/charm-integration-testing/pulls/<number> \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['head']['ref'])"

git fetch origin <branch>
git checkout <branch>
```

---

## Step 3 — Apply fixes

Make targeted fixes in the source files. Follow the repository coding conventions.
Do not modify files outside the scope of the comments being addressed.

**Important — credential-string redaction:** The sandbox tools (`view`, `cat`,
and the `edit` tool's `old_str` matching) redact strings that look like
credentials (e.g. any value adjacent to the word `password`), replacing them
with `******`. If the file you are editing contains such strings, the `edit`
tool's `old_str` will not match. Use `python3` file I/O to read and rewrite
those files directly instead.

After editing, re-run any validation commands the user has previously requested in this session (e.g. lint, unit tests, integration tests) and fix any failures before committing. Only then commit:

```bash
git add <files>
git commit -m "fix: <short description>

<detail of what was fixed and why>

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>"
```

---

## Step 4 — Post replies

**Reply to each inline comment individually.** Do not post a single comprehensive PR comment in place of individual replies.

Reply to each inline comment using the `gh api` tool with stdin input:

```bash
gh api -X POST repos/canonical/charm-integration-testing/pulls/comments/<comment_id>/replies --input /dev/stdin <<EOF
{
  "body": "<your reply>\n\n> *This comment was posted by an AI assistant (GitHub Copilot) on behalf of the pull request author.*"
}
EOF
```

Verify each reply by checking the returned `id` field is non-empty in the JSON output.

For general PR comments, `curl` works fine:

```bash
curl -s -X POST \
  -H "Authorization: token ${GH_TOKEN}" \
  -H "Content-Type: application/json" \
  "https://api.github.com/repos/canonical/charm-integration-testing/issues/<number>/comments" \
  -d '{"body": "<your reply>\n\n> *This comment was posted by an AI assistant (GitHub Copilot) on behalf of the pull request author.*"}'
```

**Suppressed comments have no `comment_id`** (they were never posted as a standalone
inline comment), so they cannot be replied to via the inline-replies endpoint. Address
each one with a single general PR comment instead, quoting the file/line so the reviewer
can match it to the review body:

```bash
curl -s -X POST \
  -H "Authorization: token ${GH_TOKEN}" \
  -H "Content-Type: application/json" \
  "https://api.github.com/repos/canonical/charm-integration-testing/issues/<number>/comments" \
  -d '{"body": "Re: suppressed comment on `<path>:<line>` (review <review_id>):\n\n<your reply>\n\n> *This comment was posted by an AI assistant (GitHub Copilot) on behalf of the pull request author.*"}'
```

---

## Step 5 — Resolve threads (optional)

Resolve each review thread using the GraphQL API:

```bash
# First, get the node IDs for all open threads
curl -s -X POST \
  -H "Authorization: token ${GH_TOKEN}" \
  -H "Content-Type: application/json" \
  "https://api.github.com/graphql" \
  -d '{
    "query": "{ repository(owner: \"canonical\", name: \"charm-integration-testing\") { pullRequest(number: <number>) { reviewThreads(first: 20) { nodes { id isResolved comments(first: 1) { nodes { databaseId } } } } } } }"
  }' | python3 -c "
import sys, json
d = json.load(sys.stdin)
threads = d['data']['repository']['pullRequest']['reviewThreads']['nodes']
for t in threads:
    cid = t['comments']['nodes'][0]['databaseId'] if t['comments']['nodes'] else None
    print(t['id'], t['isResolved'], cid)
"

# Resolve each open thread
curl -s -X POST \
  -H "Authorization: token ${GH_TOKEN}" \
  -H "Content-Type: application/json" \
  "https://api.github.com/graphql" \
  -d '{"query": "mutation { resolveReviewThread(input: {threadId: \"<thread_node_id>\"}) { thread { id isResolved } } }"}'
```

**Note:** `resolveReviewThread` requires `pull_requests=write` on the token.
If it returns `FORBIDDEN`, the token lacks this scope — skip silently and note
that threads need manual resolution.

---

## Step 6 — Push (if applicable)

Only push if you have confirmed write access to the branch:

```bash
git push origin <branch>
```

If push is blocked (SSH key missing, token scope insufficient), commit locally
and report the commit SHA so a maintainer can cherry-pick or force-push.

---

## Summary output

After completing all steps, report:

- Which comments were addressed and how, including any suppressed comments found in
  Copilot review bodies (list the review status header, e.g. "Needs a closer look")
- Which files were changed (with a one-line description per file)
- Which threads were resolved vs. left open (and why)
- The commit SHA (if a commit was made)

