GitHub Skill
The gh CLI is installed and authenticated against github.com. All commands
accept -R OWNER/REPO to target a specific
repository. When working inside a git repo, -R can be omitted and gh
infers the remote automatically.
CI / GitHub Actions: For diagnosing CI failures, reading build logs, or rerunning workflows, load the github-ci skill instead.
Commit Hygiene
Only commit files that are directly related to the current change. Before committing, review the staged files and unstage anything unrelated (formatting changes in other files, unrelated refactors, editor artifacts, etc.). If unrelated changes exist in the working tree, leave them uncommitted — do not bundle them into the same commit or PR.
Commit messages on feature branches don't affect versioning. Most repos squash-merge using the PR title as the final commit message. The PR title (not individual commits) drives automated semantic versioning. See PR Title Enforcement for format and version impact.
# Review what's staged before committing
git diff --cached --name-only
# Unstage an unrelated file
git reset HEAD path/to/unrelated-file
# Stage only specific files
git add path/to/related-file1 path/to/related-file2
Pull Requests
List PRs
# Interactive table (default)
gh pr list -R OWNER/REPO
# JSON — useful for scripting
gh pr list -R OWNER/REPO \
--json number,title,headRefName,state,isDraft \
--jq '.[]'
# Filter by state / author / label
gh pr list -R OWNER/REPO --state open --author "@me"
gh pr list -R OWNER/REPO --label bug
View a PR
# Human-readable summary
gh pr view 46 -R OWNER/REPO
# JSON (selected fields — not exhaustive, add more as needed)
gh pr view 46 -R OWNER/REPO \
--json number,title,body,state,author,headRefName,baseRefName,\
reviewRequests,labels,isDraft,url
# Just the body (to read the current description)
gh pr view 46 -R OWNER/REPO --json body --jq '.body'
Repositories with PR Title Enforcement
Some repositories enforce PR title conventions via automation tools like
gitStream. The rule typically lives in
.cm/semantic-commits.cm at the repo root. A non-compliant title triggers an
automatic request-changes review that blocks merge.
Before creating or updating a PR title, check whether the target repo enforces title rules:
# Fetch and decode the rule file.
# For PRs targeting a non-default branch, append ?ref=<base-branch>:
# repos/OWNER/REPO/contents/.cm/semantic-commits.cm?ref=develop
RESPONSE=$(gh api repos/OWNER/REPO/contents/.cm/semantic-commits.cm \
--jq '.content' 2>&1) || {
if echo "$RESPONSE" | grep -q "Not Found"; then
echo "No title enforcement in this repo"
exit 0
fi
echo "ERROR: $RESPONSE" >&2
exit 1
}
CM=$(echo "$RESPONSE" | base64 -d)
# Inspect the titleRegex to see the allowed pattern and ticket prefixes
echo "$CM" | grep 'titleRegex:'
If the file exists, the title must follow:
type(scope)!: TICKET: description
Where:
type— one of:build,chore,ci,docs,feat,fix,perf,refactor,revert,style,test(scope)— optional, single word, e.g.(api),(checkout)!— optional, marks a breaking changeTICKET— project key + number; valid prefixes vary by repo and are declared in thetitleRegex(e.g.PROJ-123)description— free text describing the change
Version impact
Repos that squash-merge (commit title = PR title) use the type to determine
the next semantic version automatically via
get-next-version:
| Type | Version bump | Example |
|---|---|---|
feat |
minor (1.0.0 → 1.1.0) | New capability |
fix, perf |
patch (1.0.0 → 1.0.1) | Bug fix, performance improvement |
! (any type) |
major (1.0.0 → 2.0.0) | Breaking change |
chore, docs, ci, test, refactor, build, style, revert |
no release | Internal changes |
Choose the type carefully — it controls whether a release is triggered and
what kind of version bump it produces. The description part of the title also
ends up in the changelog (generated by JReleaser with the conventional-commits
preset), so write it as a clear, user-facing summary of the change.
Examples:
feat(api): PROJ-123: add retry logic to HTTP client
fix: PROJ-456: resolve null pointer in order processing
chore(ci): PROJ-789: upgrade Node to 22
docs!: PROJ-42: overhaul onboarding guide
Exemptions (gitStream skips the check for these):
- Draft PRs — safe to open a draft with a placeholder title, fix before marking ready
- Docs-only PRs (all files match
\.md$) — in some repos only
Create a PR
Ensure the branch is pushed before creating. Pass --title and --body-file
to skip interactive prompts. Use mktemp for the body file to avoid
stale/shared temp file issues.
Note: Examples below use
feat: ...titles (no ticket prefix). If the repo enforces title rules, use the full format:feat: PROJ-123: description.
# Push the branch first to avoid interactive prompts
git push -u origin HEAD
# Write body to a temp file, then create PR
BODY=$(mktemp)
trap 'rm -f "$BODY"' EXIT
cat > "$BODY" << 'ENDOFBODY'
## Summary
- Added exponential back-off to the HTTP client
- Closes #42
ENDOFBODY
gh pr create -R OWNER/REPO \
--head "$(git branch --show-current)" \
--base main \
--title "feat: add retry logic" \
--body-file "$BODY"
# With draft, reviewers, labels
gh pr create -R OWNER/REPO \
--title "feat: add retry logic" \
--body-file "$BODY" \
--base main \
--draft \
--reviewer alice,bob \
--label enhancement
# Short bodies without special characters can use --body directly
gh pr create -R OWNER/REPO \
--title "fix: typo" \
--body "Fixed typo in README"
# --fill autofills title and body from commit messages
gh pr create -R OWNER/REPO --fill
Update a PR description
Use mktemp for multi-line bodies — avoids shell quoting issues and
stale temp file reuse.
BODY=$(mktemp)
trap 'rm -f "$BODY"' EXIT
# Replace the description entirely
cat > "$BODY" << 'ENDOFBODY'
## Summary
- Refactored CheckGhAuth into toolchain package
- Updated selfupdate to delegate to toolchain.CheckGhAuth
## Changes
- internal/toolchain/install.go — exported CheckGhAuth
- internal/selfupdate/selfupdate.go — removed duplicate, calls toolchain
ENDOFBODY
gh pr edit 46 -R OWNER/REPO --body-file "$BODY"
# Append a section to the existing body (handles null/empty body safely)
if ! CURRENT=$(gh pr view 46 -R OWNER/REPO --json body --jq '.body // ""'); then
echo "Failed to fetch PR body" >&2
exit 1
fi
printf '%s\n\n## Latest update\n- Tests passing on all platforms' \
"$CURRENT" > "$BODY"
gh pr edit 46 -R OWNER/REPO --body-file "$BODY"
# Update title only
gh pr edit 46 -R OWNER/REPO --title "refactor: deduplicate CheckGhAuth"
# Change base branch
gh pr edit 46 -R OWNER/REPO --base develop
Comments
# Add a comment
gh pr comment 46 -R OWNER/REPO --body "LGTM — all checks passing"
# Edit the most recent comment you posted
gh pr comment 46 -R OWNER/REPO --edit-last --body "Updated: LGTM after rebase"
# Upsert pattern — edit last comment, or create one if none exists yet.
# WARNING: --edit-last targets YOUR most recent comment on the PR, which may
# be unrelated. For reliable upserts, use a hidden HTML marker in the body
# (e.g. <!-- bot:ci-status -->) and find/update that specific comment via API.
gh pr comment 46 -R OWNER/REPO \
--edit-last --create-if-none \
--body "## CI Status\n✓ All checks passed at $(date -u +%H:%M UTC)"
# Delete your last comment (--yes skips the confirmation prompt)
gh pr comment 46 -R OWNER/REPO --delete-last --yes
Reviews
# Approve
gh pr review 46 -R OWNER/REPO --approve --body "Looks good"
# Request changes
gh pr review 46 -R OWNER/REPO --request-changes \
--body "Please add tests for the error path"
# Leave a review comment (no verdict)
gh pr review 46 -R OWNER/REPO --comment \
--body "Consider extracting this helper"
Resolve review threads (without posting comments)
Use GraphQL to resolve inline review threads directly.
Copilot review threads: When addressing feedback from Copilot (
copilot-pull-request-reviewer), never post replies on the thread. Push the code fix and resolve the thread silently — keeps the review flow clean and avoids bot-thread noise.# Resolve a single thread by ID (no comment posted) gh api graphql -F id="PRRT_xxx" -f query='mutation($id:ID!) { resolveReviewThread(input:{threadId:$id}) { thread { id isResolved } } }'
Pagination: The query below fetches up to 100 threads. For PRs with more, add
pageInfo { hasNextPage endCursor }and loop withafter:.
Bash (macOS/Linux)
PR=46
OWNER=OWNER
REPO_NAME=REPO
QUERY_THREADS='query($owner:String!, $repo:String!, $number:Int!) {
repository(owner:$owner, name:$repo) {
pullRequest(number:$number) {
reviewThreads(first:100) {
nodes {
id
isResolved
comments(first:1) { nodes { url author { login } } }
}
}
}
}
}'
RESOLVE_THREAD='mutation($id:ID!) {
resolveReviewThread(input:{threadId:$id}) {
thread { id isResolved }
}
}'
# List unresolved threads
JQ_FILTER='.data.repository.pullRequest.reviewThreads.nodes[]
| select(.isResolved == false)
| {id, url: .comments.nodes[0].url, author: .comments.nodes[0].author.login}'
gh api graphql \
-F owner="$OWNER" -F repo="$REPO_NAME" -F number=$PR \
-f query="$QUERY_THREADS" \
--jq "$JQ_FILTER"
# Resolve one thread by ID
THREAD_ID=PRRT_xxx
gh api graphql -F id="$THREAD_ID" -f query="$RESOLVE_THREAD"
# Resolve all unresolved threads on the PR
UNRESOLVED_FILTER='.data.repository.pullRequest.reviewThreads.nodes[]
| select(.isResolved == false) | .id'
for id in $(gh api graphql \
-F owner="$OWNER" -F repo="$REPO_NAME" -F number=$PR \
-f query="$QUERY_THREADS" --jq "$UNRESOLVED_FILTER"); do
if gh api graphql -F id="$id" -f query="$RESOLVE_THREAD" >/dev/null; then
echo "resolved $id"
else
echo "FAILED to resolve $id" >&2
fi
done
PowerShell (Windows)
$PR = 46
$Owner = "OWNER"
$RepoName = "REPO"
$queryThreads = @'
query($owner:String!, $repo:String!, $number:Int!) {
repository(owner:$owner, name:$repo) {
pullRequest(number:$number) {
reviewThreads(first:100) {
nodes {
id
isResolved
comments(first:1) { nodes { url author { login } } }
}
}
}
}
}
'@
$resolveThread = @'
mutation($id:ID!) {
resolveReviewThread(input:{threadId:$id}) {
thread { id isResolved }
}
}
'@
$jqFilter = '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)'
# List unresolved threads with comment URLs
gh api graphql -F owner="$Owner" -F repo="$RepoName" -F number=$PR `
-f query="$queryThreads" `
--jq "$jqFilter | {id, url: .comments.nodes[0].url, author: .comments.nodes[0].author.login}"
# Resolve one thread by ID
$ThreadId = "PRRT_xxx"
gh api graphql -F id="$ThreadId" -f query="$resolveThread"
# Resolve all unresolved threads on the PR
$threadIds = gh api graphql -F owner="$Owner" -F repo="$RepoName" -F number=$PR `
-f query="$queryThreads" --jq "$jqFilter | .id"
foreach ($id in $threadIds) {
$result = gh api graphql -F id="$id" -f query="$resolveThread" 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "resolved $id"
} else {
Write-Error "FAILED to resolve ${id}: $result"
}
}
PR check status
Quick way to see whether CI is passing on a PR. For deeper CI investigation (reading logs, rerunning failed jobs), use the github-ci skill.
# Show status of all checks for the PR
gh pr checks 46 -R OWNER/REPO
# Wait until all checks finish, then print result
gh pr checks 46 -R OWNER/REPO --watch
Other PR operations
# Mark as ready for review (un-draft)
gh pr ready 46 -R OWNER/REPO
# Add/remove labels and reviewers
gh pr edit 46 -R OWNER/REPO \
--add-label "needs-review" --remove-label "wip"
gh pr edit 46 -R OWNER/REPO \
--add-reviewer alice --remove-reviewer bob
# Merge (squash by default if repo is configured that way)
gh pr merge 46 -R OWNER/REPO --squash --delete-branch
# Close without merging
gh pr close 46 -R OWNER/REPO
Workflow for Keeping a PR Description Up-to-Date
After completing a significant piece of work on a branch, update the PR description to reflect what was actually done:
PR=<pr-number>
REPO=OWNER/REPO
BODY=$(mktemp)
trap 'rm -f "$BODY"' EXIT
cat > "$BODY" << 'ENDOFBODY'
## Summary
- Refactored CheckGhAuth into toolchain package
- selfupdate now delegates to toolchain.CheckGhAuth
- All tests passing on linux/macos/windows
## Changes
- `internal/toolchain/install.go` — exported CheckGhAuth with 10s timeout
- `internal/selfupdate/selfupdate.go` — removed duplicate, calls toolchain
- `internal/selfupdate/selfupdate_test.go` — updated assertion to use HasPrefix
Closes #45
ENDOFBODY
gh pr edit $PR -R $REPO --body-file "$BODY"
Error Handling
| Situation | What to do |
|---|---|
gh auth status shows not logged in |
Run gh auth login |
403 Resource not accessible |
Check gh auth status -t for scopes. Could be: missing scope (fix with gh auth refresh -s <scope>), SSO not authorized (visit the URL in the error), org/repo permissions, or fork token restrictions. |
Could not resolve to a PullRequest |
PR number is wrong or -R is missing/incorrect |