brandonwie.dev
EN / KR
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 -f flag behavior — The gh api -f flag supports bracket notation for nested objects, so comments[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 line number 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 $variables inside 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

FieldRequiredDescription
pathYesFile path relative to repo root
lineYesLine number in the NEW version of the file
sideYes"RIGHT" for new code, "LEFT" for deleted code
bodyYesComment 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

CharacterEscape 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

EventDescription
COMMENTGeneral comment (no approval status)
APPROVEApprove the PR
REQUEST_CHANGESRequest 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

ErrorCauseFix
HTTP 422 “not an array”Using -f comments[0][...]Use heredoc JSON
HTTP 422 “line not in diff”Invalid line numberVerify line is in PR diff
HTTP 404Wrong PR number or repoCheck PR exists
HTTP 403No write accessCheck permissions

Comments

enko