On this page
devops devopsgithubapipr-review
GitHub PR Review API - Inline Comments
How to create PR reviews with inline comments using the GitHub API via `gh` CLI.
• 4 min read
The Problem
When using gh api with -f flag and bracket notation for arrays, the JSON is
malformed:
# ❌ WRONG - causes HTTP 422 error
gh api repos/{owner}/{repo}/pulls/{PR}/reviews -X POST
-f event="COMMENT"
-f body="Review body"
-f "comments[0][path]=file.ts"
-f "comments[0][line]=123"
-f "comments[0][body]=Comment text"
# Error: "For 'properties/comments', {...} is not an array. (HTTP 422)" The -f "comments[0][path]=..." syntax does NOT create a proper JSON array.
GitHub API expects comments to be an actual array, not an object with numeric
keys.
Difficulties Encountered
- Misleading
-fflag behavior — Thegh api -fflag supports bracket notation for nested objects, socomments[0][path]looks correct but silently produces{"comments": {"0": {"path": ...}}}instead of a JSON array. No warning is emitted. - Unhelpful 422 error — The GitHub API returns “is not an array” but does not show what the malformed payload actually looked like, making it hard to diagnose without manually inspecting the serialized JSON.
- Line number validation — Even after fixing the JSON format, comments fail
silently if the
linenumber is not present in the PR diff. You must cross-reference the diff output to find valid line numbers. - Heredoc quoting subtlety — Using unquoted heredoc delimiters causes shell
expansion of backticks and
$variablesinside the JSON body, corrupting code snippets in comment text.
The Solution
Use heredoc with --input - to pipe properly formatted JSON:
cat << 'REVIEW_JSON' | gh api repos/{owner}/{repo}/pulls/{PR}/reviews -X POST --input -
{
"event": "COMMENT",
"body": "## Self Review
Key implementation points explained below.",
"comments": [
{
"path": "src/utils/calendar.ts",
"line": 244,
"side": "RIGHT",
"body": "### 📌 TZID Normalization
Explanation here..."
},
{
"path": "src/utils/calendar.ts",
"line": 307,
"side": "RIGHT",
"body": "### 📌 DST Gap Detection
Explanation here..."
}
]
}
REVIEW_JSON Key Points
Comment Structure
| Field | Required | Description |
|---|---|---|
path | Yes | File path relative to repo root |
line | Yes | Line number in the NEW version of the file |
side | Yes | "RIGHT" for new code, "LEFT" for deleted code |
body | Yes | Comment content (supports Markdown) |
Line Number Requirements
CRITICAL: The line must be a line that appears in the PR diff.
- Use lines with
+prefix (added lines) →side: "RIGHT" - Context lines (no prefix) may or may not be commentable
- Deleted lines (
-prefix) →side: "LEFT"
To find valid line numbers:
# Get the PR diff to see which lines are actually changed
gh pr diff {PR_NUMBER} -- {file_path} JSON Escaping
| Character | Escape As |
|---|---|
| Double quote | \" |
| Newline | \n |
| Backslash | \\ |
| Tab | \t (avoid, use spaces) |
Single-Quoted Heredoc
Use 'REVIEW_JSON' (single quotes) to prevent shell expansion:
# Single quotes prevent $variable and `backtick` expansion
cat << 'REVIEW_JSON' | gh api ...
{
"body": "Code: `const x = 1;`" # Backticks preserved
}
REVIEW_JSON Event Types
| Event | Description |
|---|---|
COMMENT | General comment (no approval status) |
APPROVE | Approve the PR |
REQUEST_CHANGES | Request changes before merge |
Complete Example
PR_NUMBER=644
OWNER=example-org
REPO=backend-v2
cat << 'REVIEW_JSON' | gh api repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}/reviews -X POST --input -
{
"event": "COMMENT",
"body": "## 셀프 리뷰 (Self Review) 🔍
이 PR의 핵심 구현 포인트를 설명합니다.",
"comments": [
{
"path": "src/common/utils/calendar/calendar-normalization.util.ts",
"line": 244,
"side": "RIGHT",
"body": "### 📌 TZID 정규화
**왜 이렇게 구현했는가:**
비표준 TZID를 IANA 형식으로 변환합니다."
},
{
"path": "src/common/utils/calendar/calendar-normalization.util.ts",
"line": 307,
"side": "RIGHT",
"body": "### 📌 DST Gap 감지
**문제 상황:**
DST 전환 시 존재하지 않는 시간을 dayjs가 조정합니다."
}
]
}
REVIEW_JSON Response
On success, the API returns the created review object:
{
"id": 3751032477,
"html_url": "https://github.com/org/repo/pull/644#pullrequestreview-3751032477",
"state": "COMMENTED",
"submitted_at": "2026-02-04T13:19:25Z"
} When to Use
- Automating self-review comments on your own PRs (explaining complex logic)
- CI/CD pipelines that post inline review comments (lint results, test coverage, security findings)
- Bulk-commenting on multiple files/lines in a single API call rather than clicking through the GitHub UI one by one
When NOT to Use
- Simple PR descriptions — If your notes apply to the PR as a whole, use the PR body or a single top-level comment instead of inline comments
- Non-diff lines — The API only accepts lines present in the diff; do not use this for commenting on unchanged code (use a regular issue comment)
- High-frequency automation — GitHub rate-limits API calls; posting reviews on every commit in a busy repo will hit limits quickly
- Draft PRs you will rewrite — Inline comments are tied to specific diff line numbers and become orphaned when you force-push new commits
Common Errors
| Error | Cause | Fix |
|---|---|---|
| HTTP 422 “not an array” | Using -f comments[0][...] | Use heredoc JSON |
| HTTP 422 “line not in diff” | Invalid line number | Verify line is in PR diff |
| HTTP 404 | Wrong PR number or repo | Check PR exists |
| HTTP 403 | No write access | Check permissions |