
Fusion Github Review Resolution
- 2 installs
- 9 repo stars
- Updated August 4, 2026
- equinor/fusion-framework
This is a copy of fusion-github-review-resolution by equinor - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks.
About
fusion-github-review-resolution is a Claude Code skill in the AI & Agent Building category.
- fusion-github-review-resolution
- AI & Agent Building
- AI-coding skill
Fusion Github Review Resolution by the numbers
- 2 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/equinor/fusion-framework --skill fusion-github-review-resolutionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 9 |
| Last updated | August 4, 2026 |
| Repository | equinor/fusion-framework ↗ |
What it does
Helps with ai & agent building tasks.
Files
Resolve GitHub Review Comments
When to use
Use this skill when a pull request has unresolved inline review comments and you need a repeatable, auditable closure workflow.
Typical triggers (skill should activate on all of these):
URL patterns — activate immediately:
https://github.com/<owner>/<repo>/pull/<number>#pullrequestreview-<id>https://github.com/<owner>/<repo>/pull/<number>(when context implies review work)
Explicit user requests:
- "Fix the review comments"
- "Address the review feedback on this PR"
- "There are unresolved review comments — fix them"
- "Resolve the conversations on this PR"
- "Handle all unresolved comments on this PR"
- "For each review comment: fix, test, commit, reply, resolve"
- "Close the open review threads"
- "Check this review and resolve the conversation when fixed"
- "Resolve outstanding review feedback"
- "The PR has changes requested — fix it"
Implicit / agent-detected:
- A PR is in "changes requested" state and the agent is asked to work on it
- A PR has unresolved review threads and the user asks to improve or merge the PR
- An agent working on a PR detects open review conversations
When not to use
Do not use this skill when:
- no unresolved review comments exist,
- the request is only to summarize or describe feedback without making code changes,
- all targeted threads are already resolved or outdated and acknowledged,
- the branch/worktree is intentionally read-only.
Required inputs
Collect before execution:
- repository owner/name,
- pull request number or URL,
- optional review id to scope comments (for example
pullrequestreview-<id>), - branch/worktree decision,
- required validation commands for the repository.
When a review URL is provided (github.com/<owner>/<repo>/pull/<number>#pullrequestreview-<id>),auto-extract owner, repo, PR number, and review id from it.
Only branch/worktree choice and validation commands still need confirming.
Optional context:
- linked issue reference (for example
equinor/fusion-core-tasks#432), - commit granularity preference when comments overlap the same file.
Instructions
Follow this phase order unless the user explicitly asks for a different sequence: fetch → analyze → fix → validate → push → reply → resolve → verify. Do not interleave GitHub thread mutations with code-editing retries.
1. Ask whether to use a dedicated git worktree
- Ask this before any other workflow questions.
- If yes, use/create the worktree and continue there.
2. Gather unresolved comments and create working tracker
- If a review URL with
#pullrequestreview-<id>was provided, parse owner, repo, PR number, and review id from it before fetching. - Copy or open `assets/review-resolution-checklist.md`—this becomes your working document. Fill in the context section and update the comment tracking table as you work through each thread.
- Fetch review threads for the PR and filter unresolved threads.
- If a specific review id or review URL is provided, limit to comments from that review.
- Within the targeted review, collect all comments associated with that review id (do not include replies from other reviews unless explicitly requested).
- Build a working list with: thread id, comment id, parent review id, file path, original comment body, and all subsequent replies in that thread (including contributor replies).
- Read the full reply chain for each thread — contributors may have added clarifications, constraints, or additional context that must be taken into account when deciding how to resolve the comment.
- Capture a baseline list of targeted thread ids and unresolved-thread count before any GitHub mutation.
3. Understand, research, and judge each comment
- Read the referenced file(s) and nearby logic.
- Decide whether the feedback is correct, partially correct, outdated, or incorrect against the current code, requirements, and surrounding context.
- Reviewers are not automatically correct; do not make code churn just to satisfy a comment that is stale or wrong.
- If the feedback is correct, verify root cause and identify the smallest safe fix.
- If the feedback is clearly incorrect or outdated, prepare a concise evidence-based reply instead of changing code unnecessarily.
- If uncertain, inspect adjacent tests/usages before editing.
- If doubt remains after local research, ask the user before making code changes or mutating that review thread.
4. Fix, check, commit (per comment)
- Apply focused code/doc changes only for comments you judged valid or partially valid.
- Run targeted checks first, then required repo checks.
- Create one commit per comment when practical.
- If two comments require one inseparable change, use one commit and map both comments to that commit in replies.
- For comments you decline, record the reasoning in the tracker so the eventual reply is explicit and auditable.
5. Push once after all fixes
- After all comment-related commits are created, push branch updates once.
6. Reply and resolve each review comment
- Before any thread mutation, prefer structured tooling in this order:
1. dedicated GitHub MCP review-thread reply/resolve tools exposed in the current client session, 2. the bundled GraphQL assets or bundled scripts/resolve-review-comments.sh, 3. never ad hoc temporary Python scripts, one-off batch helpers, or blind gh api retry loops.
- Prepare exactly one planned reply per targeted thread from the checklist/tracker before posting anything.
- Re-fetch the current thread state before retrying if a reply/resolve attempt errors, times out, or returns an uncertain result.
- For each thread, these two steps are mandatory and must happen together in order unless the thread is still uncertain and waiting for user input:
1. Post a reply on the thread: either describe what changed and include the commit hash(es), or explain why no code change was made because the comment is incorrect/outdated. 2. Resolve the thread immediately after the reply is posted — never before.
- Post at most one reply attempt per thread per run.
- If an equivalent agent-authored reply already exists, do not post another reply; reuse it and only resolve if the thread is still unresolved.
- If a different agent-authored reply already exists, stop and inspect manually rather than stacking another comment.
- Do not resolve a thread that is still uncertain; escalate it to the user first.
- Never resolve a thread without a reply. Never post a reply without then resolving the thread.
- Keep replies specific: name the file/line changed and the commit, not just "fixed".
7. Verify closure state
- Re-check review threads and confirm no targeted unresolved threads remain.
- Confirm the targeted unresolved-thread count dropped to zero and no duplicate agent replies were created during this run.
- Re-check latest CI status if the workflow expects green checks.
8. Ask whether to request a new review from the original review author
- After fixes are pushed and threads are resolved, ask if the user wants to request a new review from the author of the review comments.
- If yes, request review from that reviewer username and report that the request was sent.
9. Optional scripted execution
- Use
scripts/get-review-comments.shto fetch matching review comments (including sub-comments associated with the review id). - Results are limited to the first 100 review threads and first 100 comments per thread.
- Example test:
skills/.experimental/fusion-github-review-resolution/scripts/get-review-comments.sh --owner equinor --repo fusion-skills --pr 27 --review-id 3837647674- Use
--include-outdatedwhen you need comments from outdated matching threads. - Use
scripts/resolve-review-comments.shto reply+resolve matching threads with a dry-run-first duplicate-reply guard. - Keep default dry-run behavior; use
--applyonly after fixes are committed and pushed. - By default the script refuses to add another authenticated-user reply to a thread that already contains one; use
--allow-additional-replyonly after manual inspection. - Example dry-run:
skills/.experimental/fusion-github-review-resolution/scripts/resolve-review-comments.sh --owner equinor --repo fusion-skills --pr 27 --review-id 3837647674 --include-resolved- Example apply:
skills/.experimental/fusion-github-review-resolution/scripts/resolve-review-comments.sh --owner equinor --repo fusion-skills --pr 27 --review-id 3837647674 --apply --message "Addressed in <commit>: <what changed>."
Tooling map (MCP vs GraphQL)
Use GitHub MCP tools for high-level PR operations and any dedicated review-thread mutations the current client exposes. Use GraphQL for thread-level review operations when MCP coverage is missing.
| Workflow action | Preferred tool | Notes |
|---|---|---|
| Request reviewer / update PR metadata | mcp_github_update_pull_request | Works for collaborator reviewers and standard PR updates. |
| Create or submit PR review | mcp_github_pull_request_review_write | Handles pending review lifecycle actions. |
| Add general PR comment | mcp_github_add_issue_comment | Adds issue-style comment to PR conversation, not inline thread reply. |
| List review threads and comments | assets/pull-request-review-threads.graphql | Use with gh api graphql -f query=@assets/pull-request-review-threads.graphql for thread-level context. |
| Count unresolved threads for specific review id | assets/unresolved-thread-count-for-review.graphql | Post-process response (for example with jq) to filter by review id and unresolved state. |
| Reply to a review thread | Dedicated MCP review-thread reply tool, otherwise assets/add-pull-request-review-thread-reply.graphql | Prefer the MCP tool when available; otherwise use the bundled thread-scoped mutation instead of ad hoc scripts. |
| Resolve a review thread | Dedicated MCP review-thread resolve tool, otherwise assets/resolve-review-thread.graphql | Use the matching structured tool for the current client/session. ⚠️ GraphQL note: resolveReviewThread uses threadId, not pullRequestReviewThreadId. |
| List PR reviews (review URL/id lookup support) | assets/pull-request-reviews.graphql | Useful when starting from review URL context. |
Pro tip: See each .graphql file in assets for complete mutation/query syntax and parameter names.GraphQL cost awareness
Review-resolution workflows make multiple GraphQL mutation calls (reply + resolve per thread). Be conservative:
- Mutations cost 5 secondary-limit points each (vs 1 for read queries). Budget accordingly when processing many threads.
- Pause at least 1 second between consecutive mutation calls to avoid secondary rate limits.
- Keep
first/lastconnection arguments small (preferfirst: 100only when you need all threads in a single page). - If a secondary rate-limit error or
retry-afterheader is returned, stop processing and respect the indicated wait before retrying. - Always prefer a dedicated MCP review-thread tool over raw GraphQL when the client exposes one.
Expected output
Return a concise report containing:
- comments processed count,
- disposition summary (fixed, declined with rationale, escalated to user),
- files changed,
- commit list (hash + message),
- validation commands run and outcomes,
- confirmation of push,
- reply/resolve confirmation per thread,
- completed checklist location,
- any remaining unresolved threads or blockers.
Linked issue usage
When an issue is provided (for example equinor/fusion-core-tasks#432):
- mention the issue in progress/final summaries,
- keep implementation aligned with issue scope,
- avoid expanding to unrelated PR automation.
Safety & constraints
- Never expose secrets or tokens in logs/replies.
- Prefer argv-based process execution over shell-interpolated command strings.
- Keep diffs minimal and scoped to review feedback.
- Do not assume review feedback is correct; reason about it against the code and requirements first.
- If a comment remains ambiguous after research, ask the user instead of guessing.
- Do not resolve a thread without posting a concrete fix reply.
- Do not claim checks passed unless commands were actually run.
- Do not force-push; use regular commits and a single push after all fixes.
- If a comment is outdated but still unresolved, either:
- resolve with a clear explanation and commit reference, or
- leave unresolved and report why.
- In scripted mode, keep default dry-run behavior and require explicit
--applyfor mutations.
mutation AddPullRequestReviewThreadReply($threadId: ID!, $body: String!) {
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) {
comment {
url
}
}
}
query PullRequestReviewThreads($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviewThreads(first: 100) {
nodes {
id
isResolved
isOutdated
path
line
comments(first: 100) {
nodes {
databaseId
body
url
author {
login
}
createdAt
pullRequestReview {
databaseId
state
author {
login
}
}
}
}
}
}
}
}
}
query PullRequestReviews($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviews(first: 100) {
nodes {
databaseId
state
author {
login
}
submittedAt
url
}
}
}
}
}
mutation ResolveReviewThread($threadId: ID!) {
resolveReviewThread(input: { threadId: $threadId }) {
thread {
id
isResolved
}
}
}
Review Resolution Checklist
Use this checklist while processing unresolved review comments on a pull request.
Quick workflow
- [ ] Gather unresolved threads and map context
- [ ] Capture targeted unresolved-thread baseline
- [ ] Research and understand each comment
- [ ] Classify each comment (valid, partial, incorrect/outdated, uncertain)
- [ ] Apply focused fixes per comment
- [ ] Run validation checks
- [ ] Push all commits once
- [ ] Reply and resolve each thread
- [ ] Verify no unresolved threads remain
- [ ] Request re-review (optional)
Context
| Field | Value |
|---|---|
| Repository | |
| Pull request # | |
| Review ID (optional) | |
| Linked issue (optional) | |
| Base branch | |
| Working branch/worktree |
Validation commands
Targeted checks (run early, per-fix):
(paste commands here)Required full checks (run before push):
(paste commands here)Comment tracking
Note: Add more rows as needed. Each row = one review thread to resolve.
| Thread ID | File | Line(s) | Summary of feedback | Research notes | Disposition | Fix summary / rationale | Commit hash | Reply posted | Resolved |
|---|---|---|---|---|---|---|---|---|---|
| ☐ | ☐ | ||||||||
| ☐ | ☐ | ||||||||
| ☐ | ☐ |
Pre-mutation checks
- [ ] Baseline unresolved thread count captured
- [ ] Each targeted thread checked for existing agent replies
- [ ] Any uncertain comments escalated to the user before reply/resolve
- [ ] Reply text drafted once per thread before mutation
- [ ] Any failed/uncertain mutation attempts re-checked before retrying
Pre-push checklist
- [ ] Every tracked comment has a corresponding fix
- [ ] All fixes run and pass targeted checks
- [ ] Required full repository checks complete and pass
- [ ] Each fix has a commit with clear message
- [ ] Branch ready to push (no uncommitted changes)
Post-push / reply + resolve checklist
- [ ] Every comment has a reply with commit reference(s)
- [ ] Declined comments have evidence-based rationale replies
- [ ] Every reply is followed immediately by thread resolution
- [ ] No duplicate agent replies were posted during this run
- [ ] No unresolved threads remain (verify with GraphQL query)
- [ ] Any outdated threads addressed or documented
Final summary
Processed: ___ comments / ___ threads
Commits created:
Any remaining blockers:
query UnresolvedThreadCountForReview($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviewThreads(first: 100) {
nodes {
id
isResolved
comments(first: 1) {
nodes {
pullRequestReview {
databaseId
}
}
}
}
}
}
}
}
Changelog
0.1.4 - 2026-03-11
patch
- #76 `3efc478` - Add GraphQL cost awareness section to review-resolution skill to enforce conservative mutation pacing and secondary rate-limit handling.
- Document per-mutation secondary cost (5 points) and per-query cost (1 point)
- Require at least 1-second pause between consecutive GraphQL mutation calls
- Require respect for
retry-afterheaders before retrying on rate-limit errors
resolves equinor/fusion-core-tasks#535
0.1.3 - 2026-03-06
patch
- #58 `22b4d66` - Tighten the experimental review-resolution workflow so agents follow a deterministic fetch → analyze → fix → validate → push → reply → resolve sequence and avoid ad hoc mutation scripts.
- Prefer structured review-thread tools when the client exposes them, otherwise use the bundled GraphQL assets or shell helper.
- Guard
resolve-review-comments.shagainst duplicate authenticated-user replies by default and use the thread-scoped GraphQL reply mutation. - Track mutation baselines and retry checks in the skill checklist so retries re-fetch state before posting again.
- Require the skill to judge whether review feedback is actually correct, reply with rationale when it is not, and ask the user when the comment remains ambiguous.
resolves equinor/fusion-skills#57
0.1.2 - 2026-03-05
patch
- #55 `2d346c8` - Add required ownership metadata (
metadata.owner,metadata.status) to all skills. Owner is set to@equinor/fusion-core(repository default) and status is set according to skill lifecycle (activefor production skills,experimentalfor early-stage skills). Sponsor metadata was considered but is not required for MVP.
resolves equinor/fusion-core-tasks#474
0.1.1 - 2026-03-03
patch
- #40 `cd68535` - Add one-operation-per-file GraphQL assets for review-thread workflows and document the MCP-vs-GraphQL tooling map in the experimental
fusion-github-review-resolutionskill.
0.1.0 - 2026-03-02
minor
- #33 `c8b513c` - Add experimental
fusion-github-review-resolutionskill with a deterministic workflow for unresolved PR review comments, including review-url trigger guidance, per-comment remediation process, and checklist support.
Add companion bash helpers:
skills/.experimental/fusion-github-review-resolution/scripts/get-review-comments.shskills/.experimental/fusion-github-review-resolution/scripts/resolve-review-comments.sh
The scripts support review-id scoped collection and safe dry-run-first resolution flows using gh.
resolves equinor/fusion-core-tasks#432
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
skills/.experimental/fusion-github-review-resolution/scripts/get-review-comments.sh \
--owner <owner> --repo <repo> --pr <number> --review-id <number> [--include-resolved] [--include-outdated]
Description:
Lists review-thread comments for a specific PR review id, including any sub-comments
that are associated with that review id. Default output is unresolved + non-outdated
threads only.
Note: Uses GraphQL queries limited to the first 100 review threads and first 100
comments per thread; results may be incomplete on very large pull requests.
Examples:
skills/.experimental/fusion-github-review-resolution/scripts/get-review-comments.sh \
--owner equinor --repo fusion-skills --pr 27 --review-id 3837647674
skills/.experimental/fusion-github-review-resolution/scripts/get-review-comments.sh \
--owner equinor --repo fusion-skills --pr 27 --review-id 3837647674 --include-outdated
EOF
}
OWNER=""
REPO=""
PR_NUMBER=""
REVIEW_ID=""
INCLUDE_RESOLVED="false"
INCLUDE_OUTDATED="false"
require_command() {
local name="$1"
local hint="$2"
if ! command -v "$name" >/dev/null 2>&1; then
echo "ERROR: '$name' is required. $hint" >&2
exit 1
fi
}
require_arg() {
local flag="$1"
local value="$2"
local hint="$3"
if [[ -z "$value" || "$value" == -* ]]; then
echo "ERROR: $flag requires $hint." >&2
usage
exit 1
fi
}
validate_owner() {
local value="$1"
if ! [[ "$value" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,37}[A-Za-z0-9])?$ ]]; then
echo "ERROR: --owner must match GitHub owner naming rules (1-39 chars, alphanumeric or single hyphens, no leading/trailing hyphen)." >&2
exit 1
fi
if [[ "$value" =~ -- ]]; then
echo "ERROR: --owner cannot contain consecutive hyphens." >&2
exit 1
fi
}
validate_repo() {
local value="$1"
if ! [[ "$value" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "ERROR: --repo must contain only letters, numbers, dots, underscores, or hyphens." >&2
exit 1
fi
if (( ${#value} > 100 )); then
echo "ERROR: --repo must be 100 characters or fewer." >&2
exit 1
fi
}
while [[ $# -gt 0 ]]; do
case "$1" in
--owner)
require_arg "--owner" "${2:-}" "an owner value"
OWNER="$2"
shift 2
;;
--repo)
require_arg "--repo" "${2:-}" "a repository value"
REPO="$2"
shift 2
;;
--pr)
require_arg "--pr" "${2:-}" "a pull request number"
PR_NUMBER="$2"
shift 2
;;
--review-id)
require_arg "--review-id" "${2:-}" "a review id value"
REVIEW_ID="$2"
shift 2
;;
--include-resolved)
INCLUDE_RESOLVED="true"
shift
;;
--include-outdated)
INCLUDE_OUTDATED="true"
shift
;;
--help|-h)
usage
exit 0
;;
*)
echo "ERROR: Unknown argument: $1" >&2
usage
exit 1
;;
esac
done
if [[ -z "$OWNER" || -z "$REPO" || -z "$PR_NUMBER" || -z "$REVIEW_ID" ]]; then
echo "ERROR: Missing required arguments." >&2
usage
exit 1
fi
validate_owner "$OWNER"
validate_repo "$REPO"
if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
echo "ERROR: --pr must be a positive integer." >&2
exit 1
fi
if ! [[ "$REVIEW_ID" =~ ^[1-9][0-9]*$ ]]; then
echo "ERROR: --review-id must be a positive integer." >&2
exit 1
fi
require_command "gh" "Install GitHub CLI and authenticate with 'gh auth login'."
require_command "jq" "Install jq to parse JSON output."
if ! gh auth status >/dev/null 2>&1; then
echo "ERROR: GitHub CLI is not authenticated. Run 'gh auth login'." >&2
exit 1
fi
# shellcheck disable=SC2016
QUERY='query($owner:String!,$repo:String!,$number:Int!){repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100){nodes{id isResolved isOutdated path line comments(first:100){nodes{databaseId body url author{login}createdAt pullRequestReview{databaseId state author{login}}}}}}}}}'
# shellcheck disable=SC2209
JSON_OUTPUT="$({
GH_PAGER=cat gh api graphql \
-f "query=$QUERY" \
-F "owner=$OWNER" \
-F "repo=$REPO" \
-F "number=$PR_NUMBER"
} )"
if [[ -z "$JSON_OUTPUT" ]]; then
echo "ERROR: Empty response from GitHub API." >&2
exit 1
fi
if printf '%s\n' "$JSON_OUTPUT" | jq -e '.errors | length > 0' >/dev/null 2>&1; then
echo "ERROR: GitHub GraphQL errors: $(printf '%s\n' "$JSON_OUTPUT" | jq -c '.errors')" >&2
exit 1
fi
if printf '%s\n' "$JSON_OUTPUT" | jq -e '.data == null' >/dev/null 2>&1; then
echo "ERROR: GitHub GraphQL response missing data." >&2
exit 1
fi
printf '%s\n' "$JSON_OUTPUT" | jq \
--argjson reviewId "$REVIEW_ID" \
--arg includeResolved "$INCLUDE_RESOLVED" \
--arg includeOutdated "$INCLUDE_OUTDATED" '
.data.repository.pullRequest.reviewThreads.nodes
| map(
. as $thread
| {
threadId: $thread.id,
isResolved: $thread.isResolved,
isOutdated: $thread.isOutdated,
path: $thread.path,
line: $thread.line,
comments: (
$thread.comments.nodes
| map(select(.pullRequestReview.databaseId == $reviewId))
)
}
| select(.comments | length > 0)
| if $includeResolved == "true" then . else select(.isResolved == false) end
| if $includeOutdated == "true" then . else select(.isOutdated == false) end
)
| if length == 0 then
{ summary: "No matching review comments found for the requested filters." }
else
{
summary: ("Found " + (length|tostring) + " matching thread(s)."),
threads: .
}
end
'
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
skills/.experimental/fusion-github-review-resolution/scripts/resolve-review-comments.sh \
--owner <owner> --repo <repo> --pr <number> --review-id <number> \
[--message <text> | --message-file <path>] [--apply] [--include-resolved] [--include-outdated] \
[--allow-additional-reply]
Description:
Resolves matching review threads for a given pull request review id.
Note: Uses GraphQL queries limited to the first 100 review threads and first 100
comments per thread; results may be incomplete on very large pull requests.
Safety:
- Default mode is dry-run and performs no GitHub mutations.
- --apply is required to post replies and resolve threads.
- --message or --message-file is required with --apply.
- By default the script fails closed when the authenticated user already replied on a target thread.
Use --allow-additional-reply only after manual inspection.
Examples:
Dry-run:
skills/.experimental/fusion-github-review-resolution/scripts/resolve-review-comments.sh \
--owner equinor --repo fusion-skills --pr 27 --review-id 3837647674 --include-resolved
Apply:
skills/.experimental/fusion-github-review-resolution/scripts/resolve-review-comments.sh \
--owner equinor --repo fusion-skills --pr 27 --review-id 3837647674 \
--apply --message "Addressed in <commit>: <what changed>."
EOF
}
OWNER=""
REPO=""
PR_NUMBER=""
REVIEW_ID=""
MESSAGE=""
APPLY="false"
INCLUDE_RESOLVED="false"
INCLUDE_OUTDATED="false"
MESSAGE_FROM_INLINE="false"
MESSAGE_FROM_FILE="false"
ALLOW_ADDITIONAL_REPLY="false"
VIEWER_LOGIN=""
require_command() {
local name="$1"
local hint="$2"
if ! command -v "$name" >/dev/null 2>&1; then
echo "ERROR: '$name' is required. $hint" >&2
exit 1
fi
}
require_arg() {
local flag="$1"
local value="$2"
local hint="$3"
if [[ -z "$value" || "$value" == -* ]]; then
echo "ERROR: $flag requires $hint." >&2
usage
exit 1
fi
}
validate_owner() {
local value="$1"
if ! [[ "$value" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,37}[A-Za-z0-9])?$ ]]; then
echo "ERROR: --owner must match GitHub owner naming rules (1-39 chars, alphanumeric or single hyphens, no leading/trailing hyphen)." >&2
exit 1
fi
if [[ "$value" =~ -- ]]; then
echo "ERROR: --owner cannot contain consecutive hyphens." >&2
exit 1
fi
}
validate_repo() {
local value="$1"
if ! [[ "$value" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "ERROR: --repo must contain only letters, numbers, dots, underscores, or hyphens." >&2
exit 1
fi
if (( ${#value} > 100 )); then
echo "ERROR: --repo must be 100 characters or fewer." >&2
exit 1
fi
}
while [[ $# -gt 0 ]]; do
case "$1" in
--owner)
require_arg "--owner" "${2:-}" "an owner value"
OWNER="$2"
shift 2
;;
--repo)
require_arg "--repo" "${2:-}" "a repository value"
REPO="$2"
shift 2
;;
--pr)
require_arg "--pr" "${2:-}" "a pull request number"
PR_NUMBER="$2"
shift 2
;;
--review-id)
require_arg "--review-id" "${2:-}" "a review id value"
REVIEW_ID="$2"
shift 2
;;
--message)
require_arg "--message" "${2:-}" "a message value"
MESSAGE="$2"
MESSAGE_FROM_INLINE="true"
shift 2
;;
--message-file)
require_arg "--message-file" "${2:-}" "a path argument"
MESSAGE_FILE="$2"
if [[ ! -r "$MESSAGE_FILE" ]]; then
echo "ERROR: Message file '$MESSAGE_FILE' does not exist or is not readable" >&2
exit 1
fi
MESSAGE="$(cat "$MESSAGE_FILE")"
MESSAGE_FROM_FILE="true"
shift 2
;;
--apply)
APPLY="true"
shift
;;
--include-resolved)
INCLUDE_RESOLVED="true"
shift
;;
--include-outdated)
INCLUDE_OUTDATED="true"
shift
;;
--allow-additional-reply)
ALLOW_ADDITIONAL_REPLY="true"
shift
;;
--help|-h)
usage
exit 0
;;
*)
echo "ERROR: Unknown argument: $1" >&2
usage
exit 1
;;
esac
done
if [[ -z "$OWNER" || -z "$REPO" || -z "$PR_NUMBER" || -z "$REVIEW_ID" ]]; then
echo "ERROR: Missing required arguments." >&2
usage
exit 1
fi
validate_owner "$OWNER"
validate_repo "$REPO"
if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
echo "ERROR: --pr must be a positive integer." >&2
exit 1
fi
if ! [[ "$REVIEW_ID" =~ ^[1-9][0-9]*$ ]]; then
echo "ERROR: --review-id must be a positive integer." >&2
exit 1
fi
if [[ "$MESSAGE_FROM_INLINE" == "true" && "$MESSAGE_FROM_FILE" == "true" ]]; then
echo "ERROR: Use only one of --message or --message-file." >&2
exit 1
fi
if [[ "$APPLY" == "true" && ! "$MESSAGE" =~ [^[:space:]] ]]; then
echo "ERROR: --message or --message-file is required when --apply is set." >&2
exit 1
fi
require_command "gh" "Install GitHub CLI and authenticate with 'gh auth login'."
require_command "jq" "Install jq to parse JSON output."
if ! gh auth status >/dev/null 2>&1; then
echo "ERROR: GitHub CLI is not authenticated. Run 'gh auth login'." >&2
exit 1
fi
export GH_PAGER=cat
VIEWER_LOGIN="$(gh api user --jq '.login')"
if [[ -z "$VIEWER_LOGIN" ]]; then
echo "ERROR: Failed to determine the authenticated GitHub user." >&2
exit 1
fi
# shellcheck disable=SC2016
QUERY='query($owner:String!,$repo:String!,$number:Int!){repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100){nodes{id isResolved isOutdated path line comments(first:100){nodes{databaseId url body author{login} pullRequestReview{databaseId}}}}}}}}'
# shellcheck disable=SC2209
JSON_OUTPUT="$({
GH_PAGER=cat gh api graphql \
-f "query=$QUERY" \
-F "owner=$OWNER" \
-F "repo=$REPO" \
-F "number=$PR_NUMBER"
} )"
if [[ -z "$JSON_OUTPUT" ]]; then
echo "ERROR: Empty response from GitHub API." >&2
exit 1
fi
if printf '%s\n' "$JSON_OUTPUT" | jq -e '.errors | length > 0' >/dev/null 2>&1; then
echo "ERROR: GitHub GraphQL errors: $(printf '%s\n' "$JSON_OUTPUT" | jq -c '.errors')" >&2
exit 1
fi
if printf '%s\n' "$JSON_OUTPUT" | jq -e '.data == null' >/dev/null 2>&1; then
echo "ERROR: GitHub GraphQL response missing data." >&2
exit 1
fi
TARGETS_JSON="$(printf '%s\n' "$JSON_OUTPUT" | jq -c \
--argjson reviewId "$REVIEW_ID" \
--arg includeResolved "$INCLUDE_RESOLVED" \
--arg includeOutdated "$INCLUDE_OUTDATED" \
--arg viewerLogin "$VIEWER_LOGIN" \
--arg message "$MESSAGE" '
.data.repository.pullRequest.reviewThreads.nodes
| map(
. as $thread
| {
threadId: $thread.id,
isResolved: $thread.isResolved,
isOutdated: $thread.isOutdated,
path: ($thread.path // "<unknown>"),
line: $thread.line,
commentIds: ($thread.comments.nodes | map(.databaseId)),
matchingReviewCommentIds: (
$thread.comments.nodes
| map(select(.pullRequestReview.databaseId == $reviewId) | .databaseId)
),
viewerReplyIds: (
$thread.comments.nodes
| map(
select(
(.author.login // "") == $viewerLogin
and ((.pullRequestReview.databaseId // 0) != $reviewId)
)
| .databaseId
)
),
hasMatchingViewerReply: (
$thread.comments.nodes
| map(
select(
(.author.login // "") == $viewerLogin
and ((.pullRequestReview.databaseId // 0) != $reviewId)
and ((.body // "") == $message)
)
)
| length > 0
)
}
| select(.matchingReviewCommentIds | length > 0)
| if $includeResolved == "true" then . else select(.isResolved == false) end
| if $includeOutdated == "true" then . else select(.isOutdated == false) end
)
')"
TARGET_COUNT="$(printf '%s\n' "$TARGETS_JSON" | jq 'length')"
if [[ "$TARGET_COUNT" == "0" ]]; then
echo "No matching review threads found for review $REVIEW_ID with current filters."
exit 0
fi
echo "Found $TARGET_COUNT matching thread(s):"
printf '%s\n' "$TARGETS_JSON" | jq -r '
to_entries[]
| "\(.key + 1). \(.value.threadId) \(.value.path)\(if .value.line then ":\(.value.line)" else "" end) comments=\(.value.matchingReviewCommentIds | map(tostring) | join(",")) viewerReplies=\(.value.viewerReplyIds | length)\(if .value.hasMatchingViewerReply then " [matching-reply]" else "" end)\(if .value.isResolved then " [resolved]" else "" end)\(if .value.isOutdated then " [outdated]" else "" end)"
'
if [[ "$APPLY" != "true" ]]; then
echo "Dry-run only. Re-run with --apply to post replies and resolve threads."
exit 0
fi
while IFS= read -r row; do
THREAD_ID="$(printf '%s\n' "$row" | jq -r '.threadId')"
IS_RESOLVED="$(printf '%s\n' "$row" | jq -r '.isResolved')"
VIEWER_REPLY_COUNT="$(printf '%s\n' "$row" | jq -r '.viewerReplyIds | length')"
HAS_MATCHING_VIEWER_REPLY="$(printf '%s\n' "$row" | jq -r '.hasMatchingViewerReply')"
if [[ "$IS_RESOLVED" == "true" ]]; then
echo "Thread $THREAD_ID is already resolved; skipping reply and resolve mutations."
continue
fi
# Guard against duplicate thread replies on retries. If the authenticated user already
# replied, fail closed unless the exact message is already present or the operator opts in.
if [[ "$HAS_MATCHING_VIEWER_REPLY" == "true" ]]; then
echo "Skipping reply for thread $THREAD_ID; identical reply already exists from $VIEWER_LOGIN."
elif [[ "$VIEWER_REPLY_COUNT" != "0" && "$ALLOW_ADDITIONAL_REPLY" != "true" ]]; then
echo "ERROR: Thread $THREAD_ID already has $VIEWER_REPLY_COUNT reply/replies from $VIEWER_LOGIN. Re-run with --allow-additional-reply only after manual inspection." >&2
exit 1
else
# Use the thread-scoped GraphQL mutation so replies stay attached to the review thread.
# shellcheck disable=SC2209,SC2016
ADD_REPLY_OUTPUT="$({
GH_PAGER=cat gh api graphql \
-f 'query=mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{url}}}' \
-F "threadId=$THREAD_ID" \
-f "body=$MESSAGE"
} )"
if [[ -z "$ADD_REPLY_OUTPUT" ]]; then
echo "ERROR: Empty reply mutation response for thread $THREAD_ID." >&2
exit 1
fi
if printf '%s\n' "$ADD_REPLY_OUTPUT" | jq -e '.errors | length > 0' >/dev/null 2>&1; then
echo "ERROR: Reply mutation errors for thread $THREAD_ID: $(printf '%s\n' "$ADD_REPLY_OUTPUT" | jq -c '.errors')" >&2
exit 1
fi
REPLY_URL="$(printf '%s\n' "$ADD_REPLY_OUTPUT" | jq -r '.data.addPullRequestReviewThreadReply.comment.url // empty')"
if [[ -z "$REPLY_URL" ]]; then
echo "ERROR: Reply mutation for thread $THREAD_ID did not return a comment URL." >&2
exit 1
fi
echo "Posted reply for thread $THREAD_ID: $REPLY_URL"
fi
# shellcheck disable=SC2209,SC2016
MUTATION_OUTPUT="$({
GH_PAGER=cat gh api graphql \
-f 'query=mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{id isResolved}}}' \
-F "threadId=$THREAD_ID"
} )"
if [[ -z "$MUTATION_OUTPUT" ]]; then
echo "ERROR: Empty mutation response for thread $THREAD_ID." >&2
exit 1
fi
if printf '%s\n' "$MUTATION_OUTPUT" | jq -e '.errors | length > 0' >/dev/null 2>&1; then
echo "ERROR: GraphQL mutation errors for thread $THREAD_ID: $(printf '%s\n' "$MUTATION_OUTPUT" | jq -c '.errors')" >&2
exit 1
fi
if ! printf '%s\n' "$MUTATION_OUTPUT" | jq -e '.data.resolveReviewThread.thread.isResolved == true' >/dev/null 2>&1; then
echo "ERROR: Thread $THREAD_ID was not resolved." >&2
exit 1
fi
echo "Resolved thread $THREAD_ID."
done < <(printf '%s\n' "$TARGETS_JSON" | jq -c '.[]')
echo "Completed: processed $TARGET_COUNT matching thread(s)."