github-workflows/skills/gh-cli-patterns/SKILL.md
Canonical reference for all gh CLI command shapes used by skills in this plugin. Defines the placeholder convention, allowed --json fields, GraphQL fallback rules, -f/-F/--raw-field flag semantics, the PR-readiness gate, code-scanning alert query, review-thread fetch/count/resolve mutations, and heredoc bodies. Prevents Unknown JSON field errors and divergent query shapes.
npx skillsauth add jacobpevans/claude-code-plugins gh-cli-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Two visually distinct notations — never mix them up:
| Notation | Meaning | Example |
|---|---|---|
| $varName | GraphQL variable name — keep as literal text in the query body | $prNumber |
| <UPPER_NAME> | Shell template — replace before running | <PR_NUMBER> |
Standard replacements:
<OWNER> → $(gh repo view --json owner --jq '.owner.login')
<REPO> → $(gh repo view --json name --jq '.name')
<PR_NUMBER> → $(gh pr view --json number --jq '.number') (integer)
<THREAD_ID> → PRRT_* node ID from the fetch-threads query (string)
<DATABASE_ID> → numeric comment ID from the fetch-threads query
gh pr view --json — REST-OnlyreviewThreads is not a valid --json field — it is GraphQL-only. Any
gh pr view --json reviewThreads call fails with Unknown JSON field: "reviewThreads".
Other GraphQL-only fields: inline thread structure, resolution status, full
mergeStateStatus enum.
Rule: if the field isn't returned by gh pr view --json (no value), use gh api graphql.
| Operation | Use |
|---|---|
| Fetch unresolved threads | GraphQL — see Canonical Review-Thread Queries |
| Verify thread resolution count | GraphQL — see Canonical Review-Thread Queries |
| Resolve a thread | GraphQL — resolveReviewThread mutation |
| Reply to a thread | GraphQL (addPullRequestReviewThreadReply) or REST (simpler for markdown/special chars) |
| Reply to a PR-level comment | REST repos/<OWNER>/<REPO>/issues/<PR_NUMBER>/comments |
| PR state fields (state, mergeable, mergeStateStatus, etc.) | gh pr view --json if listed; else GraphQL |
| Flag | Use |
|---|---|
| -f key=value | String — for the -f query='...' GraphQL body and string variables |
| -F key=value | Auto-typed — for Int! and Boolean! GraphQL variables |
| --raw-field 'key=value' | Literal string, no $var expansion — for queries using inline <PLACEHOLDER> substitution |
Never interpolate shell $VARS inside a GraphQL query string. Declare typed variables
with -f/-F instead.
Use first: 100 (never first: 25 or last: 100). Always include pageInfo.
Replace <OWNER>, <REPO>, <PR_NUMBER> before running (see Placeholder Convention above).
gh api graphql -f query='
query($owner:String!,$repo:String!,$prNumber:Int!){
repository(owner:$owner,name:$repo){
pullRequest(number:$prNumber){
state mergeable mergeStateStatus isDraft reviewDecision
commits(last:1){nodes{commit{statusCheckRollup{state}}}}
reviewThreads(first:100){nodes{isResolved} pageInfo{hasNextPage}}
}
}
}' -f owner=<OWNER> -f repo=<REPO> -F prNumber=<PR_NUMBER>
Inside the -f query='...' body, $owner/$repo/$prNumber are GraphQL variable names —
keep them literal. After the closing ', -f owner=<OWNER> etc. bind values — replace the
<ANGLE_BRACKET> placeholders with actual strings.
Required values — abort if any fail:
| Field | Required | Abort message |
|---|---|---|
| state | OPEN | "PR is not open" |
| mergeable | MERGEABLE | "PR has git conflicts" |
| mergeStateStatus | CLEAN or HAS_HOOKS | "PR blocked: {value}" |
| isDraft | false | "PR is a draft" |
| reviewDecision | APPROVED or null | "Review decision: {value}" |
| statusCheckRollup.state | SUCCESS | "CI: {state}" |
| All reviewThreads.isResolved | true | "Unresolved threads" |
| reviewThreads.pageInfo.hasNextPage | false | ">100 threads — paginate" |
NOT-ready
mergeStateStatusvalues:BEHIND,BLOCKED,DIRTY,DRAFT,UNKNOWN,UNSTABLE.
Replace <OWNER>, <REPO> before running.
gh api 'repos/<OWNER>/<REPO>/code-scanning/alerts?state=open&per_page=100' \
--jq 'length' || echo "0"
per_page=100 covers realistic alert counts. || echo "0" handles disabled code-scanning (404).
Must return 0; otherwise invoke /resolve-codeql fix.
Replace <OWNER>, <REPO>, <PR_NUMBER> using inline literal substitution before running
(uses --raw-field — no -f/-F variable binding).
Fetch unresolved threads (id = PRRT_* node ID for mutations, databaseId = numeric ID for REST replies):
gh api graphql --raw-field 'query=query {
repository(owner: "<OWNER>", name: "<REPO>") {
pullRequest(number: <PR_NUMBER>) {
reviewThreads(first: 100) {
nodes {
id isResolved path line startLine
comments(first: 100) {
nodes { id databaseId body author { login } createdAt }
}
}
}
}
}
}'
Count unresolved (must equal 0 before merging; checks overflow):
gh api graphql --raw-field 'query=query {
repository(owner: "<OWNER>", name: "<REPO>") {
pullRequest(number: <PR_NUMBER>) {
reviewThreads(first: 100) { nodes { isResolved } pageInfo { hasNextPage } }
}
}
}' --jq '{unresolved: ([.data.repository.pullRequest.reviewThreads.nodes[]
| select(.isResolved == false)] | length),
overflow: .data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage}'
Must return {"unresolved": 0, "overflow": false}. Non-zero unresolved or overflow: true
means threads remain.
| Operation | Correct | WRONG — do not use |
|---|---|---|
| Reply | addPullRequestReviewThreadReply | addPullRequestReviewComment (creates new comment, not a reply) |
| Resolve | resolveReviewThread | resolvePullRequestReviewThread (does not exist) |
Replace <THREAD_ID> and <DATABASE_ID> before running.
# Reply via GraphQL (use REST below for markdown/special characters)
gh api graphql --raw-field 'query=mutation {
addPullRequestReviewThreadReply(
input: {pullRequestReviewThreadId: "<THREAD_ID>", body: "reply text"}
) { comment { id body } }
}'
# Reply via REST (simpler for markdown and special characters)
gh api repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/comments/<DATABASE_ID>/replies -f body="reply text"
# Resolve
gh api graphql --raw-field 'query=mutation {
resolveReviewThread(input: {threadId: "<THREAD_ID>"}) { thread { id isResolved } }
}'
Failure guide: stale <THREAD_ID> → re-fetch threads; permission error → gh auth status;
wrong mutation name → check table above.
Single authoritative format for all PR status output. Reference this section from any skill that emits a summary — do NOT define local output formats in individual skills.
{Title}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ https://github.com/<OWNER>/<REPO>/pull/42 Ready for review
🟡 https://github.com/<OWNER>/<REPO>/pull/43 CI pending
🔴 https://github.com/<OWNER>/<REPO>/pull/44 Conflicts | 3 open comments
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
All Open PRs — <OWNER>/<REPO>
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ https://github.com/<OWNER>/<REPO>/pull/42 Ready for review
🟡 https://github.com/<OWNER>/<REPO>/pull/43 CI pending
🔴 https://github.com/<OWNER>/<REPO>/pull/44 Conflicts | 3 open comments
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Ready to merge (1):
/squash-merge-pr 42 (<OWNER>/<REPO>)
Blocked — needs human (2):
https://github.com/<OWNER>/<REPO>/pull/43 — CI pending
https://github.com/<OWNER>/<REPO>/pull/44 — Conflicts | 3 open comments
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Title by invocation:
/ship → Ship Summary/finalize-pr (single PR or current branch) → PR Status/finalize-pr all or /finalize-pr org → Finalization Summary| Emoji | Condition |
|-------|-----------|
| ✅ | mergeable == MERGEABLE AND mergeStateStatus == CLEAN or HAS_HOOKS, no unresolved threads |
| 🟡 | mergeStateStatus == BEHIND/UNKNOWN/UNSTABLE, reviewDecision == REVIEW_REQUIRED, or mergeable == UNKNOWN (GitHub computing) |
| 🔴 | mergeable == CONFLICTING, mergeStateStatus == BLOCKED/DIRTY/DRAFT, reviewDecision == CHANGES_REQUESTED, unresolved threads, isDraft == true, or CI failed |
Append after the URL, separated by |. Omit when no issues exist ("Ready for review" suffices).
| Tag | Section 1 trigger | Section 2 trigger |
|-----|-------------------|-------------------|
| Conflicts | mergeable == CONFLICTING | mergeable == CONFLICTING |
| Computing | mergeable == UNKNOWN | mergeable == UNKNOWN |
| N open comments | Unresolved thread count from Phase 3 gate | (not available — omit count) |
| CHANGES_REQUESTED | reviewDecision == CHANGES_REQUESTED | reviewDecision == CHANGES_REQUESTED |
| Review required | reviewDecision == REVIEW_REQUIRED | reviewDecision == REVIEW_REQUIRED |
| CI pending | statusCheckRollup.state != SUCCESS | mergeStateStatus == UNKNOWN/UNSTABLE |
| CI failed | CI checks terminal failure | mergeStateStatus == BLOCKED (when other fields clean) |
| Draft | isDraft == true | isDraft == true |
| Behind main | mergeStateStatus == BEHIND | mergeStateStatus == BEHIND |
Fetch PR URL (for Section 1 — current PRs):
gh pr view <PR_NUMBER> --json url --jq '.url'
Fetch all open PRs (for Section 2 — one GraphQL call per affected repo):
gh pr list --json does NOT support mergeStateStatus — use GraphQL instead:
gh api graphql -f query='
query($owner:String!,$repo:String!){
repository(owner:$owner,name:$repo){
pullRequests(states:OPEN,first:50){
nodes{
number url title mergeable reviewDecision mergeStateStatus isDraft
commits(last:1){nodes{commit{statusCheckRollup{state}}}}
}
}
}
}' -f owner=<OWNER> -f repo=<REPO>
For org-wide mode, run once per repo from Phase 1 discovery, replacing <OWNER>/<REPO>.
| Invocation | Affected repos for Section 2 |
|-----------|------------------------------|
| /ship | Current repo |
| /finalize-pr (single or all) | Current repo |
| /finalize-pr org | All repos with open PRs from Phase 1 discovery |
All modes: /squash-merge-pr <NUMBER> — run from the worktree of the target repo.
The repo context is shown as a label, not a flag (the skill has no --repo argument):
Ready to merge (1):
/squash-merge-pr 42 (JacobPEvans/claude-code-plugins)
For org-wide mode, note the target repo so the user knows which worktree to navigate to.
gh pr edit <PR_NUMBER> --body "$(cat <<'EOF'
body content here
EOF
)"
Same pattern for gh pr create, gh pr comment, gh issue comment. Never use --body-file.
documentation
Use when editing GitHub Actions workflow files (.github/workflows/*.yml) in JacobPEvans repos. Documents when to target self-hosted RunsOn runners vs GitHub-hosted runners, the v3 label catalog used across the org, the required github.run_id segment, and the GitHub App allowlist prereq.
testing
Check PR merge readiness, sync local repo, cleanup stale worktrees; optional cross-repo sweep and stale-branch prune modes
tools
Local rebase-merge workflow for pull requests with signed commits
tools
Analyze current Claude Code session token usage via Splunk. Shows per-model, per-tool, and subagent token breakdown with cache efficiency metrics.