
Pr Reviewer
- 9 installs
- 33 repo stars
- Updated April 26, 2026
- bighardperson/computer-science-skills-collection
Pr-reviewer is a skill that runs automated GitHub pull-request review with diff analysis, lint integration, and structured reports.
About
Pr-reviewer is an automated code-review skill for GitHub pull requests. It analyzes diffs for security issues, error-handling gaps, risky calls, style problems, and missing test coverage, integrates golangci-lint and ruff, and posts structured reports. It supports Go, Python, and JavaScript/TypeScript and requires the authenticated gh CLI.
- Automated GitHub PR review with diff analysis and lint integration
- Flags security, error handling, risk, style, TODOs, and test gaps
- Smart re-review tracks HEAD SHA; supports Go, Python, JS/TS
Pr Reviewer by the numbers
- 9 all-time installs (skills.sh)
- Ranked #826 of 1,354 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
pr-reviewer capabilities & compatibility
- Capabilities
- code review · security audit · testing
- Works with
- github
- Use cases
- code review · security audit · testing
What pr-reviewer says it does
Automated GitHub PR code review with diff analysis, lint integration, and structured reports.
Tracks HEAD SHA per PR. Only re-reviews when new commits are pushed.
npx skills add https://github.com/bighardperson/computer-science-skills-collection --skill pr-reviewerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 9 |
|---|---|
| repo stars | ★ 33 |
| Last updated | April 26, 2026 |
| Repository | bighardperson/computer-science-skills-collection ↗ |
What it does
Automatically review GitHub pull request diffs for security, error handling, style, and test-coverage issues.
Who is it for?
Automated review of GitHub PR diffs across Go, Python, and JavaScript/TypeScript
Skip if: Repos not on GitHub or without an authenticated gh CLI
When should I use this skill?
Reviewing pull requests for security, error handling, style, or test coverage
What you get
A structured markdown review report with a per-PR verdict and findings by category.
- markdown PR review report
- per-PR verdict
By the numbers
- 6 check categories
- 3 supported languages: Go, Python, JS/TS
- 4-level summary verdict
Files
PR Reviewer
Automated code review for GitHub pull requests. Analyzes diffs for security issues, error handling gaps, style problems, and test coverage.
Prerequisites
ghCLI installed and authenticated (gh auth status)- Repository access (read at minimum, write for posting comments)
- Optional:
golangci-lintfor Go linting,rufffor Python linting
Quick Start
# Review all open PRs in current repo
scripts/github/pr-reviewer.sh check
# Review a specific PR
scripts/github/pr-reviewer.sh review 42
# Post review as GitHub comment
scripts/github/pr-reviewer.sh post 42
# Check status of all open PRs
scripts/github/pr-reviewer.sh status
# List unreviewed PRs (useful for heartbeat/cron integration)
scripts/github/pr-reviewer.sh list-unreviewedConfiguration
Set these environment variables or the script auto-detects from the current git repo:
PR_REVIEW_REPO— GitHub repo inowner/repoformat (default: detected fromgh repo view)PR_REVIEW_DIR— Local checkout path for lint (default: git root of cwd)PR_REVIEW_STATE— State file path (default:./data/pr-reviews.json)PR_REVIEW_OUTDIR— Report output directory (default:./data/pr-reviews/)
Directories Written
- `PR_REVIEW_STATE` (default:
./data/pr-reviews.json) — Tracks reviewed PRs and their HEAD SHAs - `PR_REVIEW_OUTDIR` (default:
./data/pr-reviews/) — Markdown review reports
What It Checks
| Category | Icon | Examples |
|---|---|---|
| Security | 🔴 | Hardcoded credentials, AWS keys, secrets in code |
| Error Handling | 🟡 | Discarded errors (Go _ :=), bare except: (Python), unchecked Close() |
| Risk | 🟠 | panic() calls, process.exit() |
| Style | 🔵 | fmt.Print/print()/console.log in prod, very long lines |
| TODOs | 📝 | TODO, FIXME, HACK, XXX markers |
| Test Coverage | 📊 | Source files changed without corresponding test changes |
Smart Re-Review
Tracks HEAD SHA per PR. Only re-reviews when new commits are pushed. Use review <PR#> to force re-review.
Report Format
Reports are saved as markdown files in the output directory. Each report includes:
- PR metadata (author, branch, changes)
- Commit list
- Changed file categorization by language/type
- Automated diff findings with file, line, category, and context
- Test coverage analysis
- Local lint results (when repo is checked out locally)
- Summary verdict: 🔴 SECURITY / 🟡 NEEDS ATTENTION / 🔵 MINOR NOTES / ✅ LOOKS GOOD
Heartbeat/Cron Integration
Add to a periodic check (heartbeat, cron job, or CI):
UNREVIEWED=$(scripts/github/pr-reviewer.sh list-unreviewed)
if [ -n "$UNREVIEWED" ]; then
scripts/github/pr-reviewer.sh check
fiExtending
The analysis patterns in the script are organized by language. Add new patterns by appending to the relevant pattern list in the analyze_diff() function:
# Add a new Go pattern
go_patterns.append((r'^\+.*os\.Exit\(', 'RISK', 'Direct os.Exit() — consider returning error')){
"ownerId": "kn701kzxq5tkxw9mypbmspqw5580cxew",
"slug": "pr-reviewer",
"version": "1.0.1",
"publishedAt": 1771529167274
}{
"slug": "pr-reviewer",
"name": "pr-reviewer",
"version": "1.0.1",
"installedAt": 1776152362999,
"source": "skillhub"
}#!/usr/bin/env bash
# pr-review.sh — Automated GitHub PR code review
#
# Fetches open PRs, analyzes diffs for security/error/style issues,
# runs local lint, and generates structured review reports.
#
# Usage:
# pr-review.sh check # Review all unreviewed open PRs
# pr-review.sh review <PR#> # Review a specific PR (force re-review)
# pr-review.sh post <PR#> # Post review as a GitHub PR comment
# pr-review.sh status # Show review state for all open PRs
# pr-review.sh list-unreviewed # List PRs needing review (for automation)
#
# Environment:
# PR_REVIEW_REPO — owner/repo (default: auto-detect from gh)
# PR_REVIEW_DIR — local checkout for lint (default: git root)
# PR_REVIEW_STATE — state file (default: ./data/pr-reviews.json)
# PR_REVIEW_OUTDIR — reports dir (default: ./data/pr-reviews/)
set -euo pipefail
# ── Configuration ────────────────────────────────────────────────────────────
REPO="${PR_REVIEW_REPO:-}"
LOCAL_DIR="${PR_REVIEW_DIR:-}"
STATE_FILE="${PR_REVIEW_STATE:-./data/pr-reviews.json}"
REVIEWS_DIR="${PR_REVIEW_OUTDIR:-./data/pr-reviews}"
# Auto-detect repo if not set
if [ -z "$REPO" ]; then
REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
if [ -z "$REPO" ]; then
echo "Error: Could not detect repo. Set PR_REVIEW_REPO=owner/repo" >&2
exit 1
fi
fi
# Auto-detect local dir if not set
if [ -z "$LOCAL_DIR" ]; then
LOCAL_DIR=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
fi
mkdir -p "$REVIEWS_DIR"
[ -f "$STATE_FILE" ] || echo '{}' > "$STATE_FILE"
# ── Helpers ──────────────────────────────────────────────────────────────────
log() { echo "[pr-review] $*" >&2; }
get_open_prs() {
gh pr list --repo "$REPO" --state open \
--json number,title,author,createdAt,headRefName,additions,deletions,changedFiles,labels,baseRefName \
2>/dev/null || echo '[]'
}
get_pr_diff() {
gh pr diff "$1" --repo "$REPO" 2>/dev/null || echo ""
}
get_pr_files() {
gh pr view "$1" --repo "$REPO" --json files --jq '.files[].path' 2>/dev/null || echo ""
}
get_pr_commits() {
gh pr view "$1" --repo "$REPO" --json commits \
--jq '.commits[] | "\(.oid[:8]) \(.messageHeadline)"' 2>/dev/null || echo ""
}
is_reviewed() {
local pr_num="$1"
local head_sha
head_sha=$(gh pr view "$pr_num" --repo "$REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
python3 -c "
import json
with open('$STATE_FILE') as f:
state = json.load(f)
pr = state.get(str($pr_num), {})
if pr.get('head_sha') == '$head_sha' and pr.get('status') == 'reviewed':
print('yes')
else:
print('no')
" 2>/dev/null || echo "no"
}
update_state() {
python3 -c "
import json, time
with open('$STATE_FILE') as f:
state = json.load(f)
state[str($1)] = {
'head_sha': '$2',
'status': '$3',
'reviewed_at': int(time.time()),
'report': '$REVIEWS_DIR/${1}.md'
}
with open('$STATE_FILE', 'w') as f:
json.dump(state, f, indent=2)
"
}
# ── Analysis ─────────────────────────────────────────────────────────────────
categorize_files() {
python3 -c "
import sys, json
from collections import defaultdict
cats = defaultdict(list)
for line in sys.stdin:
f = line.strip()
if not f: continue
ext = f.rsplit('.', 1)[-1] if '.' in f else ''
if ext == 'go':
cats['go'].append(f)
elif ext == 'py':
cats['python'].append(f)
elif ext in ('ts', 'tsx', 'js', 'jsx'):
cats['frontend'].append(f)
elif ext in ('yml', 'yaml', 'toml', 'json', 'env'):
cats['config'].append(f)
elif ext in ('md', 'txt', 'rst'):
cats['docs'].append(f)
elif ext == 'sql':
cats['sql'].append(f)
elif 'Dockerfile' in f or f == 'docker-compose.yml':
cats['docker'].append(f)
elif f.startswith('.github/'):
cats['ci'].append(f)
else:
cats['other'].append(f)
print(json.dumps(dict(cats)))
"
}
analyze_diff() {
python3 << 'PYEOF'
import sys, json, re
diff_text = sys.stdin.read()
findings = []
secret_patterns = [
(r'(?i)(password|passwd|secret|api[_-]?key|token|auth)\s*[:=]\s*["\x27][^"\x27]{8,}["\x27]', 'SECURITY', 'Possible hardcoded credential/secret'),
(r'(?i)AWS[_A-Z]*KEY\s*[:=]', 'SECURITY', 'Possible hardcoded AWS key'),
(r'(?i)-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY', 'SECURITY', 'Private key in source code'),
]
go_patterns = [
(r'^\+.*,\s*_\s*:?=.*\(', 'ERROR_HANDLING', 'Discarded error return value (Go)'),
(r'^\+.*\.Close\(\)\s*$', 'ERROR_HANDLING', 'Unchecked Close() — consider defer with error check'),
(r'^\+.*panic\(', 'RISK', 'Direct panic() call — consider returning error'),
(r'^\+.*fmt\.Print', 'STYLE', 'fmt.Print in production code — use structured logging'),
(r'^\+.*os\.Exit\(', 'RISK', 'Direct os.Exit() — consider returning error'),
]
python_patterns = [
(r'^\+.*except\s*:', 'ERROR_HANDLING', 'Bare except clause — catches everything including SystemExit'),
(r'^\+.*except Exception:', 'ERROR_HANDLING', 'Broad except Exception — consider specific errors'),
(r'^\+.*print\(', 'STYLE', 'print() in production code — use logging module'),
(r'^\+.*# type: ignore', 'TYPING', 'Type ignore comment — document why'),
]
js_patterns = [
(r'^\+.*console\.log\(', 'STYLE', 'console.log in production code'),
(r'^\+.*debugger', 'STYLE', 'Debugger statement — remove before merge'),
(r'^\+.*process\.exit\(', 'RISK', 'Direct process.exit() — consider throwing'),
(r'^\+.*eval\(', 'SECURITY', 'eval() usage — potential code injection'),
(r'^\+.*any\b', 'TYPING', 'TypeScript `any` type — consider specific type'),
]
general_patterns = [
(r'^\+.*TODO', 'TODO', 'TODO marker — track or address'),
(r'^\+.*FIXME', 'TODO', 'FIXME marker — should address before merge'),
(r'^\+.*HACK', 'TODO', 'HACK marker — needs cleanup'),
(r'^\+.*XXX', 'TODO', 'XXX marker — needs attention'),
(r'^\+.{200,}', 'STYLE', 'Very long line (>200 chars)'),
]
lines = diff_text.split('\n')
current_file = None
line_num = 0
for line in lines:
m = re.match(r'^\+\+\+ b/(.*)', line)
if m:
current_file = m.group(1)
continue
m = re.match(r'^@@ -\d+(?:,\d+)? \+(\d+)', line)
if m:
line_num = int(m.group(1))
continue
if line.startswith('+') and not line.startswith('+++'):
line_num += 1
all_p = secret_patterns + general_patterns
if current_file:
if current_file.endswith('.go'):
all_p += go_patterns
elif current_file.endswith('.py'):
all_p += python_patterns
elif current_file.endswith(('.js', '.jsx', '.ts', '.tsx')):
all_p += js_patterns
for pat, cat, msg in all_p:
if re.search(pat, line):
findings.append({
'file': current_file or 'unknown',
'line': line_num,
'category': cat,
'message': msg,
'context': line[1:].strip()[:120]
})
elif not line.startswith('-'):
line_num += 1
print(json.dumps(findings))
PYEOF
}
run_local_lint() {
local files="$1"
local results=""
if [ -z "$LOCAL_DIR" ] || [ ! -d "$LOCAL_DIR" ]; then
return
fi
# Go lint
local go_files
go_files=$(echo "$files" | grep '\.go$' || true)
if [ -n "$go_files" ] && command -v golangci-lint &>/dev/null; then
local go_dirs
go_dirs=$(echo "$go_files" | sed -n 's|\(.*\)/[^/]*\.go$|\1|p' | sort -u)
for dir in $go_dirs; do
local full="$LOCAL_DIR/$dir"
if [ -d "$full" ]; then
local out
out=$(cd "$full" && golangci-lint run --timeout 2m --new-from-rev=HEAD~1 2>&1 || true)
if [ -n "$out" ]; then
results="${results}\n### golangci-lint ($dir)\n\`\`\`\n${out}\n\`\`\`\n"
fi
fi
done
fi
# Python lint
local py_files
py_files=$(echo "$files" | grep '\.py$' || true)
if [ -n "$py_files" ] && command -v ruff &>/dev/null; then
local py_paths
py_paths=$(echo "$py_files" | sed "s|^|$LOCAL_DIR/|" | tr '\n' ' ')
local ruff_cfg=""
[ -f "$LOCAL_DIR/pyproject.toml" ] && ruff_cfg="--config $LOCAL_DIR/pyproject.toml"
local out
out=$(ruff check $ruff_cfg $py_paths 2>&1 || true)
if [ -n "$out" ] && ! echo "$out" | grep -q "^All checks passed"; then
results="${results}\n### ruff\n\`\`\`\n${out}\n\`\`\`\n"
fi
fi
echo -e "$results"
}
check_test_coverage() {
python3 -c "
import sys
files = '''$1'''.strip().split('\n')
src, tests = [], []
for f in files:
f = f.strip()
if not f: continue
if f.endswith('_test.go') or 'test_' in f.split('/')[-1] or f.endswith('_test.py') or f.endswith('.test.ts') or f.endswith('.test.js') or f.endswith('.spec.ts') or f.endswith('.spec.js'):
tests.append(f)
elif f.endswith(('.go', '.py', '.ts', '.tsx', '.js', '.jsx')):
src.append(f)
missing = []
for s in src:
has_test = False
s_dir = '/'.join(s.split('/')[:-1])
s_name = s.split('/')[-1].rsplit('.', 1)[0]
for t in tests:
t_dir = '/'.join(t.split('/')[:-1])
if t_dir == s_dir or f'test_{s_name}' in t or f'{s_name}_test' in t or f'{s_name}.test' in t or f'{s_name}.spec' in t:
has_test = True
break
skip = any(k in s for k in ['__init__', 'main.go', 'main.py', 'config', 'types', 'models', 'schema', 'index.ts', 'index.js'])
if not has_test and not skip:
missing.append(s)
if missing:
print('Files without corresponding test changes:')
for f in missing:
print(f' - {f}')
else:
print('Test coverage looks adequate for changed files.')
"
}
# ── Report Generation ────────────────────────────────────────────────────────
generate_report() {
local pr_num="$1"
local force="${2:-false}"
if [ "$force" != "true" ]; then
local reviewed
reviewed=$(is_reviewed "$pr_num")
if [ "$reviewed" = "yes" ]; then
log "PR #$pr_num already reviewed at current HEAD. Use 'review' to force."
return 0
fi
fi
log "Reviewing PR #$pr_num..."
local pr_info
pr_info=$(gh pr view "$pr_num" --repo "$REPO" \
--json title,author,headRefName,headRefOid,baseRefName,additions,deletions,body,createdAt,labels 2>/dev/null)
local title author branch head_sha additions deletions body created
title=$(echo "$pr_info" | python3 -c "import json,sys; print(json.load(sys.stdin)['title'])")
author=$(echo "$pr_info" | python3 -c "import json,sys; print(json.load(sys.stdin)['author']['login'])")
branch=$(echo "$pr_info" | python3 -c "import json,sys; print(json.load(sys.stdin)['headRefName'])")
head_sha=$(echo "$pr_info" | python3 -c "import json,sys; print(json.load(sys.stdin)['headRefOid'])")
additions=$(echo "$pr_info" | python3 -c "import json,sys; print(json.load(sys.stdin)['additions'])")
deletions=$(echo "$pr_info" | python3 -c "import json,sys; print(json.load(sys.stdin)['deletions'])")
body=$(echo "$pr_info" | python3 -c "import json,sys; print(json.load(sys.stdin).get('body','') or '')")
created=$(echo "$pr_info" | python3 -c "import json,sys; print(json.load(sys.stdin)['createdAt'])")
local files diff commits
files=$(get_pr_files "$pr_num")
diff=$(get_pr_diff "$pr_num")
commits=$(get_pr_commits "$pr_num")
local categories
categories=$(echo "$files" | categorize_files)
local findings_file
findings_file=$(mktemp)
echo "$diff" | analyze_diff > "$findings_file"
trap "rm -f '$findings_file'" EXIT
local test_coverage
test_coverage=$(check_test_coverage "$files")
local lint_results=""
lint_results=$(run_local_lint "$files" 2>/dev/null || echo "")
local findings_summary
findings_summary=$(python3 -c "
import json
from collections import Counter
with open('$findings_file') as f:
findings = json.load(f)
counts = Counter(f['category'] for f in findings)
if counts:
for cat, count in sorted(counts.items()):
icon = {'SECURITY':'🔴','ERROR_HANDLING':'🟡','RISK':'🟠','STYLE':'🔵','TODO':'📝','TYPING':'🟣'}.get(cat,'⚪')
print(f'{icon} {cat}: {count}')
else:
print('✅ No issues found in diff analysis')
")
local report_file="$REVIEWS_DIR/${pr_num}.md"
cat > "$report_file" << REPORT
# PR #${pr_num} Review: ${title}
**Author:** ${author}
**Branch:** \`${branch}\`
**HEAD:** \`${head_sha:0:8}\`
**Created:** ${created}
**Changes:** +${additions} / -${deletions}
**Reviewed:** $(date -Iseconds)
## Description
${body:-_No description provided._}
## Commits
\`\`\`
${commits}
\`\`\`
## Changed Files
$(echo "$categories" | python3 -c "
import json, sys
cats = json.load(sys.stdin)
icons = {'go':'🔷','python':'🐍','frontend':'🌐','ci':'⚙️','config':'📦','docs':'📝','docker':'🐳','sql':'💾','other':'📄'}
for cat, files in sorted(cats.items()):
print(f'### {icons.get(cat,\"📄\")} {cat.title()} ({len(files)} files)')
for f in files:
print(f'- \`{f}\`')
print()
")
## Automated Analysis
### Diff Findings
${findings_summary}
$(python3 -c "
import json
with open('$findings_file') as f:
findings = json.load(f)
if findings:
print('| File | Line | Category | Finding | Context |')
print('|------|------|----------|---------|---------|')
for f in findings[:50]:
ctx = f['context'].replace('|', '\\\\|')
print(f'| \`{f[\"file\"].split(\"/\")[-1]}\` | {f[\"line\"]} | {f[\"category\"]} | {f[\"message\"]} | \`{ctx[:60]}\` |')
")
### Test Coverage
${test_coverage}
$(if [ -n "$lint_results" ]; then echo "### Local Lint Results"; echo ""; echo "$lint_results"; else echo "### Local Lint"; echo ""; echo "_Skipped (repo not checked out locally or linters not found)._"; fi)
## Summary
$(python3 -c "
import json
with open('$findings_file') as f:
findings = json.load(f)
sec = [f for f in findings if f['category'] == 'SECURITY']
err = [f for f in findings if f['category'] in ('ERROR_HANDLING', 'RISK')]
sty = [f for f in findings if f['category'] in ('STYLE', 'TODO', 'TYPING')]
if sec:
print('🔴 **SECURITY CONCERNS** — Review security findings before merging.')
elif err:
print('🟡 **NEEDS ATTENTION** — Error handling / risk items to review.')
elif sty:
print('🔵 **MINOR STYLE NOTES** — Looks good overall, minor suggestions above.')
else:
print('✅ **LOOKS GOOD** — No automated issues found. Ready for human review.')
")
---
_Automated PR review • $(date '+%Y-%m-%d %H:%M')_
REPORT
update_state "$pr_num" "$head_sha" "reviewed"
log "Report saved to $report_file"
echo "$report_file"
}
# ── Commands ─────────────────────────────────────────────────────────────────
cmd_check() {
local prs
prs=$(get_open_prs)
local count
count=$(echo "$prs" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))")
if [ "$count" = "0" ]; then
log "No open PRs."
echo '{"reviewed": 0, "skipped": 0, "total": 0}'
return 0
fi
local reviewed=0 skipped=0
local pr_nums
pr_nums=$(echo "$prs" | python3 -c "import json,sys; [print(p['number']) for p in json.load(sys.stdin)]")
while IFS= read -r pr_num; do
[ -z "$pr_num" ] && continue
local already
already=$(is_reviewed "$pr_num")
if [ "$already" = "yes" ]; then
skipped=$((skipped + 1))
log "PR #$pr_num: already reviewed, skipping."
else
generate_report "$pr_num" "false"
reviewed=$((reviewed + 1))
fi
done <<< "$pr_nums"
echo "{\"reviewed\": $reviewed, \"skipped\": $skipped, \"total\": $count}"
}
cmd_review() {
local pr_num="${1:?PR number required}"
generate_report "$pr_num" "true"
}
cmd_post() {
local pr_num="${1:?PR number required}"
local report_file="$REVIEWS_DIR/${pr_num}.md"
if [ ! -f "$report_file" ]; then
log "No review report for PR #$pr_num. Run 'review $pr_num' first."
exit 1
fi
gh pr comment "$pr_num" --repo "$REPO" --body-file "$report_file"
log "Review posted to PR #$pr_num"
}
cmd_status() {
local prs
prs=$(get_open_prs)
python3 -c "
import json, sys, time
prs = json.loads(sys.stdin.read())
try:
with open('$STATE_FILE') as f:
state = json.load(f)
except:
state = {}
if not prs:
print('No open PRs.')
sys.exit(0)
print(f'Open PRs: {len(prs)}')
print()
for pr in prs:
num = str(pr['number'])
s = state.get(num, {})
status = s.get('status', 'unreviewed')
icon = '✅' if status == 'reviewed' else '⏳'
print(f'{icon} PR #{num}: {pr[\"title\"]} ({pr[\"author\"][\"login\"]})')
print(f' +{pr[\"additions\"]}/-{pr[\"deletions\"]} | {pr[\"headRefName\"]}')
if s:
age = int(time.time()) - s.get('reviewed_at', 0)
print(f' Reviewed {age // 3600}h ago | SHA: {s.get(\"head_sha\", \"?\")[:8]}')
print()
" <<< "$prs"
}
cmd_list_unreviewed() {
local prs
prs=$(get_open_prs)
local pr_nums
pr_nums=$(echo "$prs" | python3 -c "import json,sys; [print(p['number']) for p in json.load(sys.stdin)]" 2>/dev/null)
while IFS= read -r pr_num; do
[ -z "$pr_num" ] && continue
local already
already=$(is_reviewed "$pr_num")
if [ "$already" != "yes" ]; then
echo "$pr_num"
fi
done <<< "$pr_nums"
}
# ── Main ─────────────────────────────────────────────────────────────────────
case "${1:-}" in
check) cmd_check ;;
review) cmd_review "${2:-}" ;;
post) cmd_post "${2:-}" ;;
status) cmd_status ;;
list-unreviewed) cmd_list_unreviewed ;;
*)
echo "Usage: pr-review.sh {check|review <PR#>|post <PR#>|status|list-unreviewed}"
echo ""
echo "Environment:"
echo " PR_REVIEW_REPO owner/repo (default: auto-detect)"
echo " PR_REVIEW_DIR local checkout for lint (default: git root)"
echo " PR_REVIEW_STATE state file (default: ./data/pr-reviews.json)"
echo " PR_REVIEW_OUTDIR reports dir (default: ./data/pr-reviews/)"
exit 1
;;
esac
Related skills
FAQ
Which languages does pr-reviewer support?
Go, Python, and JavaScript/TypeScript, with golangci-lint and ruff integration.
Does it re-review on every run?
No. It tracks HEAD SHA per PR and only re-reviews when new commits are pushed.