# Github

> Use when opening or updating PRs (title, description, labels, reviewers), adding or updating comments, or managing reviews via the gh CLI. For CI/Actions, use the github-ci skill instead.

- Skill: `ikea-port-poc/github` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add ikea-port-poc/github`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ikea-port-poc/github/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: IKEA-Port-POC (https://skillmd.com/u/ikea-port-poc)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ikea-port-poc/github

---


# 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](#repositories-with-pr-title-enforcement) for format
> and version impact.

```bash
# 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

```bash
# 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

```bash
# 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](https://docs.gitstream.cm/). 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:**

```bash
# 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 change
- `TICKET` — project key + number; valid prefixes vary by repo and are declared
  in the `titleRegex` (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](https://github.com/thenativeweb/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`.

```bash
# 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.

```bash
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

```bash
# 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

```bash
# 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.
>
> ```bash
> # 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 with `after:`.

#### Bash (macOS/Linux)

```bash
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)

```powershell
$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.

```bash
# 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

```bash
# 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:

```bash
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 |

