From gh-sdlc
Pull request standards and merge strategies (part of gh-sdlc). Provides formatting rules for PRs — only applies when PRs are actually being created as part of the /gh-sdlc workflow. Does NOT auto-activate on its own.
How this skill is triggered — by the user, by Claude, or both
Slash command
/gh-sdlc:pr-policyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Pull requests are the unit of code integration. Every PR represents atomic, reviewable work that advances a single issue.**
Pull requests are the unit of code integration. Every PR represents atomic, reviewable work that advances a single issue.
PR titles use the same format as commit messages — the issue reference prefix followed by an imperative description:
gh-<issue>: <imperative description>
Examples:
gh-6: initialize `uv` with `discord.py` dependenciesgh-8: implement command registration systemgh-15: add OAuth2 token refresh logicUse inline codeblocks (backtick wrapping) for filenames, flags, tool/package names, and directory paths in titles. See commit-policy for the full codeblock usage guide.
Hotfix format (no issue):
hotfix: critical issue description
When creating a PR, apply all metadata in one command. Always use --body-file to pass the PR body — never inline markdown in --body "..." because backticks, code blocks, and special characters get mangled by shell interpretation.
When the title contains backtick-wrapped names, use single quotes with raw backticks — never backslash-escape them inside single quotes (see commit-policy for details).
# Write body to temp file (markdown is preserved exactly)
cat > /tmp/pr-body.md <<'EOF'
## Changes
Brief technical summary.
Closes #issue-number
## Testing
- [ ] Tests pass
## Checklist
- [ ] Self-reviewed diff
EOF
gh pr create \
--title "gh-<issue>: <imperative description>" \
--body-file /tmp/pr-body.md \
--label "existing-label" \
--project "Project Name" \
--milestone "vX.Y" \
--reviewer <username> \
--assignee "@me"
rm /tmp/pr-body.md
Why --body-file? The --body flag passes through shell expansion, which corrupts backticks (`), code fences (```), and $ in markdown. Writing to a file first with a single-quoted heredoc (<<'EOF') preserves content exactly.
Label rules:
Reviewer/assignee rules:
--assignee "@me") unless user explicitly opted out--reviewer <username>) when one is designated. Users can opt out of reviewer assignment. Do not self-assign as reviewer. Note: @me is not supported for --reviewer — use the actual username.## Changes
Brief technical summary of implementation approach.
Closes #issue-number
## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests pass
- [ ] Manual testing completed
## Checklist
- [ ] Code follows project style guide
- [ ] Documentation updated
- [ ] No debugging artifacts
- [ ] Commit history is clean
- [ ] Self-reviewed diff
## Reviewer Notes
Context for architectural decisions and tradeoffs.
The Closes #N line goes directly in the Changes section — not in a separate heading. This creates the Development sidebar link automatically when the PR targets the default branch, giving a clean linked-issue appearance rather than a verbose "Issue Reference" section.
PRs are project artifacts just like issues. They should be tracked on the project board with full metadata.
After creating a PR, add it to the project board and set fields:
# Add PR to project
PR_URL=$(gh pr view <number> --json url -q .url)
ITEM_ID=$(gh project item-add <project-number> --owner "@me" --url "$PR_URL" --format json --jq '.id')
# Set status to "In Review"
gh project item-edit --id "$ITEM_ID" --field-id "$STATUS_FIELD_ID" --project-id "$PROJECT_ID" \
--single-select-option-id "$IN_REVIEW_OPTION_ID"
Every PR must have:
--assignee "@me")The Development sidebar shows linked issues with a status icon — this is the proper way to associate PRs with issues (not a body section).
When PR targets the default branch (main):
Include Closes #N in the PR body (inside the Changes section). GitHub automatically creates the Development sidebar link and will auto-close the issue on merge.
When PR targets a non-default branch (child → parent):
Closing keywords are ignored by GitHub when the PR doesn't target the default branch — no link is created and merging has no effect on issues. Instead, reference the issue with Part of #N or For #N in the body (no closing keyword). The Development sidebar link must be created manually via the GitHub UI, or accepted as absent for intermediate PRs.
Supported closing keywords: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved (case-insensitive, optional colon).
Check boxes as work completes. After each task in a checklist is done, update the issue or PR body to replace - [ ] with - [x]. This applies to both PR checklists and issue acceptance criteria.
Fetch the current body, apply sed replacements, write to a temp file, and update via --body-file:
# Tick a checkbox on a PR
gh pr view <number> --json body -q .body \
| sed 's/- \[ \] Unit tests added/- [x] Unit tests added/' \
> /tmp/updated-body.md
gh pr edit <number> --body-file /tmp/updated-body.md
# Tick a checkbox on an issue
gh issue view <number> --json body -q .body \
| sed 's/- \[ \] Criterion one/- [x] Criterion one/' \
> /tmp/updated-body.md
gh issue edit <number> --body-file /tmp/updated-body.md
For multiple checkboxes, chain the sed replacements:
gh pr view <number> --json body -q .body \
| sed 's/- \[ \] Unit tests added/- [x] Unit tests added/' \
| sed 's/- \[ \] Self-reviewed diff/- [x] Self-reviewed diff/' \
> /tmp/updated-body.md
gh pr edit <number> --body-file /tmp/updated-body.md
Rules:
Always prefer rebase merge to maintain a clean linear history where every commit on main is meaningful and passes tests independently.
Use for:
Requirements:
(#pr) suffix (e.g., gh-11: add OAuth2 config (#20)). This is mandatory — every commit on main must reference the PR it came from.# Amend commits to add (#pr), then force push
git rebase -i HEAD~N # reword each commit to append "(#<pr-number>)"
git push --force-with-lease
gh pr merge <number> --rebase --delete-branch
Use only when the branch has broken intermediary commits that cannot be cleaned up:
Squash commit message format:
gh-<issue>: <imperative description> (#pr)
<Why this change was needed — one sentence>
- Key change 1
- Key change 2
- Key change 3
Co-authored-by: Reviewer Name <email>
gh pr merge <number> --squash --delete-branch
Rule of thumb: If you followed the fixup workflow during development and rebased before marking ready, you should never need squash. Squash is a fallback for messy history, not the default.
Prohibited. Creates non-linear history and pollutes git log.
Exception: Merging long-lived release branches back to main.
| Type | Pattern | Example |
|---|---|---|
| Feature | feature/issue-number-description | feature/23-role-permissions |
| Child feature | feature/parent-number/child-number-description | feature/1/6-uv-setup |
| Bugfix | bugfix/issue-number-description | bugfix/56-null-pointer |
| Hotfix | hotfix/description | hotfix/redis-connection-leak |
mainmain (squash only if intermediaries are broken)For parent issue #1 with children #2, #3:
main
├── feature/1-parent-issue
├── feature/1/2-child-issue-one
└── feature/1/3-child-issue-two
Rule: When child issues exist, ALWAYS create corresponding sub-branches. Every child issue gets its own feature/parent/child-description branch. Sub-branches mirror sub-issues — if you decomposed the issue, decompose the branch.
Merge order: Children → Parent → Main
Acceptable scenarios:
# Interactive rebase to clean last 5 commits
git rebase -i HEAD~5
# Rebase onto latest main
git fetch origin main
git rebase origin/main
# Force push cleaned history
git push --force-with-lease
After PR approval: No force pushes. Use additional commits.
Standard cleanup before marking PR ready:
| Operation | Usage |
|---|---|
pick | Keep commit as-is |
reword | Change commit message |
edit | Modify commit content |
squash | Combine with previous, keep both messages |
fixup | Combine with previous, discard message |
drop | Remove commit entirely |
git add -p file.py
Choose hunks that belong together for focused, atomic commits.
| Lines Changed | Recommendation |
|---|---|
| < 200 | Ideal |
| 200-500 | Acceptable |
| 500-1000 | Consider splitting |
| > 1000 | Must split |
Exceptions: Generated code, mechanical refactors, initial scaffolding, documentation overhauls.
Verify: code correctness, edge case handling, test coverage, performance implications, security considerations, API design coherence, commit message quality.
Small changes (typos, minor logic):
git commit --fixup=target-commit-hash
Substantial changes (architectural):
git commit -m "gh-23: refactor permission cache invalidation"
Before re-requesting review, squash all fixups:
git rebase -i --autosquash origin/<target-branch>
git push --force-with-lease
git fetch origin <target-branch>
git rebase origin/<target-branch>
# Resolve conflicts in files
git add resolved-file.py
git rebase --continue
git push --force-with-lease
Never merge main into feature branch. Always rebase.
hotfix/ branch from mainhotfix: prefixgit revert <commit-hash>
Revert PR format:
Revert "gh-23: add OAuth2 support"
This reverts commit a1b2c3d due to [reason].
Root cause under investigation in #45.
Use draft PRs for:
Convert to ready when: all criteria met, history cleaned, self-review complete.
npx claudepluginhub achxy/dotclaude --plugin gh-sdlcGuides Git branch management and GitHub PR workflows using main-branch development, modern git switch/restore commands, and MCP tools for creating, listing, and updating PRs.
Creates or updates a GitHub PR or GitLab MR using Conventional PR format with intent, summary, changes, rationale, and test plan. Detects workspace, branch, and default branch automatically.
Creates, refreshes, and rewrites PR titles and descriptions following Sentry conventions. Requires GitHub CLI.