Pull requests
Read when: naming a branch, opening a PR, writing or updating its description, answering review comments, or merging. Owns: branch names, PR titles and bodies, linking issues, draft vs ready, review replies, updating a PR, merge methods and the merge message. See also: the commit format in ../SKILL.md; the spec in spec.md.
Open, push or merge only when the owner asks. Checked against GitHub's behaviour on 2026-11-03; other hosts (GitLab merge requests, Bitbucket) work the same way with different button names.
TL;DR
- Branch
<type>/<scope>-<short-description>with the PR title's type and scope, cut from an up-to-date main branch. - Title = a Conventional Commits subject. It becomes the squash commit, so it carries the type, the scope and a real description.
- Body: what and why, how it was checked, what is left out, breaking changes,
Closes #N. - One reason per PR. Small enough to review in one sitting.
- Fill the repo's PR template when there is one.
- Answer every review thread: fix and say what changed, or say why not. Resolve only what you fixed.
- After review, add commits; force-push only your own branch that nobody else has checked out. Never force-push or rebase the main branch.
- Merge with the method the repo uses; with squash, rewrite the message before you click.
Before opening
git fetch origin
git log --oneline origin/main..HEAD # exactly the commits you mean to send
git diff --stat origin/main...HEAD # no stray files
- Run the repo's own checks (lint, type check, tests, build). CI should confirm, not discover.
- Look for a template:
.github/pull_request_template.md,.github/PULL_REQUEST_TEMPLATE.md,.github/PULL_REQUEST_TEMPLATE/*.md,PULL_REQUEST_TEMPLATE.mdordocs/PULL_REQUEST_TEMPLATE.md. - Read the last few merged PRs: title style, body headings, whether they squash.
- Check the subject and the branch name agree:
s=$(git log -1 --format=%s)
echo "$s" | grep -Eq '^(feat|fix|refactor|perf|test|style|ci|build|docs|chore|revert)(\([a-z0-9-]+\))?!?: [a-z]' \
|| echo "subject: $s"
b=$(git branch --show-current)
p=$(echo "$s" | sed -nE 's/^([a-z]+)\(([a-z0-9-]+)\)!?: .*/\1\/\2-/p')
[ -z "$p" ] || [ "${b#"$p"}" != "$b" ] || echo "branch: $b, expected $p…"
Branch names
Name the branch <type>/<scope>-<short-description>, taking the type and scope from the PR title, so the
branch, the title and the squash commit all agree. Lowercase kebab-case; the description is two to five
words.
feat/invoices-csv-export # feat(invoices): add CSV export
fix/cart-total-rounding # fix(cart): round totals once, after discounts
feat/invoice-export # no scope
feat/412-invoice-export # an issue number in place of the scope
feat-invoices-export # no slash after the type
- Leave issue numbers out unless the repo requires them;
Closes #Nin the body links the issue. - A tool-generated name (
<tool>/<random-slug>) is renamed before the first push:git branch -m feat/invoices-csv-export. - Don't rename the branch of an open PR just to fix its name: the squash commit takes the PR title.
Title
Same rules as a commit subject (format): type(scope): description,
imperative, lowercase start, no trailing period, under ~72 characters.
feat(export): add CSV export for invoices
fix(cart): round totals once, after discounts
docs: explain pagination cursors
Not Update README, WIP, Fixes, or the branch name. The type follows the most important change: a
feature that also fixes a bug on the way is feat, and the fix is mentioned in the body (or split out).
Put ! in the title when the PR breaks something (spec.md, rules 11 to 13).
Body
Write it for a reviewer who has not seen the conversation, and for someone reading the history a year later. Without a template:
## Summary
What changes, in two to four bullets a reviewer can check against the diff.
## Why
The problem, the constraint, and the alternative you rejected.
## How it was checked
- Commands run and their result (lint, types, tests, build).
- Manual checks: the page, the flow, both themes, the phone width.
## Not in this PR
What you left out on purpose, and where it goes next.
## Breaking changes
Only when there are any: what breaks and what a user must do.
Closes #123
- Leave out sections that would say "none". A two-line change can have a two-line body.
Closes,FixesorResolvesfollowed by#Ncloses the issue when the PR merges into the default branch.Refs #Nlinks without closing.- No file-by-file list: the diff shows it. No session narration ("first I tried…").
- Screenshots for visual changes, before and after, with demo data only.
- Mark it draft while it is not ready for review; mark it ready when checks pass and the body is final.
Size and scope
- One reason to change per PR, the same test as a commit: could it be reverted alone?
- A change that spans unrelated areas is two PRs. If it cannot be split, scope it to the main area.
- A refactor and the feature on top of it can be two PRs, the refactor first.
- Unrelated fixes you noticed go in their own PR or a note, not this one.
- Generated files (lockfiles, snapshots) belong with the change that generated them.
Review comments
On your own PR:
- Answer every thread. Either "Fixed in
<sha>: <what changed>" or why it stays as it is. A reviewer should not have to diff to find out. - Resolve a thread only after its fix is pushed. Leave the reviewer's open questions for them to resolve.
- Re-request review after pushing changes for a "changes requested" review.
- Treat bot findings as claims: reproduce or trace them before changing code.
When you review, label each comment so the author knows what blocks the merge (Conventional Comments style):
issue: this drops the last page when the count is a multiple of 20.
suggestion (non-blocking): `match` would read better than the if-chain here.
question: is the 30s timeout from the provider's docs?
nit (non-blocking): trailing space.
praise: the retry test covers exactly the case that bit us last month.
Updating a PR
- Push new commits for review feedback. Force-pushing rewrites the history reviewers already read and breaks the checkout of anyone who pulled the branch: do it only on your own branch, and say so.
- To catch up with the main branch, merge it in, or rebase if the branch is yours alone and the repo rebases.
- If the scope changed, update the title and body. They are the record, not the first draft.
- If the PR is already merged, follow-up work is a new PR from the latest main branch, never more commits on the merged branch.
Merging
| Method | What lands on the main branch | What you must get right |
|---|---|---|
| Squash | one commit: the PR title (GitHub appends (#N)) and the body you write in the merge box |
the title and the body |
| Merge commit | every commit plus Merge pull request #N from … |
every commit on the branch |
| Rebase | every commit, replayed | every commit on the branch |
Use the method the repo uses; the log shows it ((#N) at the end of subjects means squash).
With squash, GitHub fills the merge body with the list of the branch's commit subjects. Replace it:
fix(cart): round totals once, after discounts (#57)
Each line rounded its own discount, so a 3-item cart could be a cent off
the invoice. Totals now round once, at the end, the way the invoice does.
Closes #51
Co-authored-by: Name <address>
- Keep the attribution footers (
Co-authored-by,Signed-off-by) the branch carried; drop the rest. - Merge only when checks are green on the latest commit and required reviews are in.
- Delete the branch after merging unless the repo keeps them.