PHASE 4 OF 14

Pull Requests at Depth

PR templates, bulk review suggestions, linked issues, label automation, stacked PRs, PR size discipline, the GitHub CLI for daily PR work, and the metrics that tell you whether your review process is actually working

Pull Requests PR Templates Stacked PRs gh CLI Cycle Time Label Automation
4.1

PR Templates: Single, Multi-Template & the config.yml

A PR template pre-fills the description field when a PR is opened. A good template reduces reviewer confusion, cuts back-and-forth, and encodes your team's "definition of ready" directly into the PR workflow.

Single template

Create .github/PULL_REQUEST_TEMPLATE.md — it auto-fills every new PR description.

## What does this PR do? <!-- One paragraph summary. Link to the design doc or ticket. --> ## Type of change - [ ] Bug fix (non-breaking) - [ ] New feature (non-breaking) - [ ] Breaking change - [ ] Refactor / no behaviour change - [ ] Documentation only ## How was this tested? <!-- Unit tests / integration tests / manual steps / screenshots --> ## Checklist - [ ] Tests added or updated - [ ] Docs updated (if user-facing) - [ ] No secrets or credentials in code - [ ] Linked to issue: Closes #<issue-number> ## Screenshots (if UI change) <!-- Before / After -->

Multiple templates

Create a directory .github/PULL_REQUEST_TEMPLATE/ with multiple .md files. GitHub does not auto-select one — the author appends a query param to the PR URL to choose:

# Directory layout .github/PULL_REQUEST_TEMPLATE/ feature.md bugfix.md hotfix.md refactor.md # URL to open a PR with the hotfix template pre-filled: https://github.com/org/repo/compare/main...fix/my-branch?template=hotfix.md

Automate the URL selection in your branch naming conventions — a branch named hotfix/* could be linked from your runbook to the hotfix-template URL.

Issue template config.yml

The .github/ISSUE_TEMPLATE/config.yml file controls what happens when a user clicks "New issue." You can disable blank issues (forcing a template choice) and add external links for support or security reports:

# .github/ISSUE_TEMPLATE/config.yml blank_issues_enabled: false contact_links: - name: Security vulnerability url: https://github.com/org/repo/security/advisories/new about: Report a security issue privately - name: Community support url: https://github.com/org/repo/discussions about: Ask questions and get help
YAML ISSUE TEMPLATES
Prefer YAML issue templates (bug_report.yml) over Markdown ones (bug_report.md). YAML templates render as structured forms with required fields, dropdowns, and checkboxes — they're much harder for reporters to skip past than a free-text Markdown template.
4.2

Review Anatomy: Suggestions, Bulk Apply & Thread Tracking

GitHub's review interface has several features that senior reviewers under-use. Knowing them makes reviews both faster and more actionable.

Leaving a suggestion

In the diff view, click the + next to a line, then click the "Insert a suggestion" icon (or use the keyboard shortcut while writing the comment). The suggestion renders as an inline diff the author can apply with one click — no back-and-forth explaining what to type.

```suggestion const retryCount = options.retryCount ?? 3; ``` This defaults to 3 retries if the caller doesn't specify — safer than 0.

Applying suggestions in bulk

When a reviewer leaves multiple suggestions across a PR, the author can apply all of them in a single commit:

  1. On each suggestion, click "Add suggestion to batch" instead of "Commit suggestion"
  2. Once all desired suggestions are batched, a counter appears in the top toolbar
  3. Click "Commit suggestions" — one commit applies all of them

This keeps the commit history clean instead of producing one micro-commit per suggestion.

Review states

StateWhat it signalsBlocks merge?
CommentFeedback without an explicit verdict — questions, suggestions, observationsNo
ApproveThe reviewer is happy for this to merge (subject to other requirements)No — enables merge
Request changesThe reviewer has blocking concerns that must be addressed before mergeYes — counts as a required dismissal until addressed and re-reviewed
APPROVAL + COMMENTS ANTI-PATTERN
Approving a PR while leaving unresolved block: or issue: comments is ambiguous — does the author need to address them before merging? Use "Request changes" for anything that must be fixed. Use "Approve" only when you're genuinely satisfied. This is a cultural norm to establish explicitly.

Resolved thread tracking

Review threads can be marked "Resolved" by the PR author once addressed. But resolved threads are hidden by default — reviewers who want to verify fixes must click "Show resolved" to re-examine them. Two practices help:

  • Authors should not self-resolve threads that the reviewer marked as blocking. Let the reviewer resolve after re-checking — or at minimum, reply with the commit SHA where it was fixed so the reviewer can verify quickly.
  • Use the "Files changed" tab's unresolved thread count as a merge readiness signal. Zero unresolved threads + required reviews = safe to merge.
4.3

Linked Issues: Closing Keywords & Projects Automation

GitHub's closing keywords let a PR automatically close one or more issues when it merges. This keeps issue trackers tidy without manual cleanup.

Closing keywords

# In PR title, description, or any commit message: Closes #42 Fixes #42 ← most common Resolves #42 Fix #42 ← also works (case-insensitive) # Close multiple issues: Fixes #42, fixes #43, closes #44 # Cross-repo close: Fixes org/other-repo#17

The issue closes only when the PR merges to the default branch. Merging to a non-default branch (e.g. release/v2.4) does not trigger the close.

Linked issues in GitHub Projects v2

When a PR is linked to an issue (via closing keyword or manual link in the sidebar), and the issue is in a GitHub Project, you can automate status transitions:

# Projects v2 workflow automation — no YAML needed, configured in UI: Issue added to project → Set Status = "Todo" PR opened for issue → Set Status = "In Progress" PR merged → Set Status = "Done" + close issue PR closed without merge → Set Status = "Todo" (revert)

Set this up under your Project's Settings → Workflows. It gives you a lightweight Kanban board that stays accurate automatically — no manual status updates needed.

CROSS-TEAM LINKING
If your issues live in a separate repo from your code (common when a product team owns the issue tracker), use the cross-repo closing syntax: Fixes org/product-tracker#42. The PR in the code repo will display the linked issue and close it on merge.
4.4

Label Automation: Auto-Labeler & Label Taxonomy

Labels are only useful if they're applied consistently. Manual labelling drifts — automate it.

Auto-labeler with actions/labeler

The actions/labeler action reads a config file and applies labels based on which files changed in a PR:

# .github/labeler.yml area/payments: - changed-files: - any-glob-to-any-file: 'services/payments/**' area/auth: - changed-files: - any-glob-to-any-file: 'services/auth/**' area/frontend: - changed-files: - any-glob-to-any-file: - '**/*.tsx' - '**/*.css' infra: - changed-files: - any-glob-to-any-file: - '**/*.tf' - 'infra/**' dependencies: - changed-files: - any-glob-to-any-file: - '**/package.json' - '**/pom.xml' - '**/go.sum' size/large: # covered separately by PR size action - changed-files: - any-glob-to-any-file: '**'
# .github/workflows/labeler.yml on: pull_request: permissions: contents: read pull-requests: write jobs: label: runs-on: ubuntu-latest steps: - uses: actions/labeler@v5 with: repo-token: ${{ secrets.GITHUB_TOKEN }} configuration-path: .github/labeler.yml sync-labels: true # remove labels when files no longer match

Label taxonomy for a mature team

CategoryLabelsPurpose
Areaarea/payments, area/auth, area/frontendRoute notifications to the right team; filter PR dashboards by domain
Typebug, feature, refactor, docs, dependenciesChangelog generation; sprint categorisation
Sizesize/xs, size/s, size/m, size/l, size/xlReview SLA enforcement; flag oversized PRs
Statusblocked, needs-review, on-hold, ready-to-mergeTriage and queue management
Prioritypriority/critical, priority/high, priority/lowOn-call severity; release scheduling
Backportbackport release/v2.4Trigger the backport action (Phase 3)
SYNCING LABELS ACROSS REPOS
Use the EndBug/label-sync action against a .github/labels.yml source of truth in your .github org repo. Run it on a schedule or on push to propagate label changes to all repos in the org automatically.
4.5

Stacked PRs: The Pattern, Tooling & Trade-offs

A stacked PR (also called a "PR stack" or "dependent PR chain") is a series of PRs where each one targets the previous branch rather than main directly. The stack is merged bottom-up — the base PR merges first, then each layer re-targets main and merges in turn.

Why stacked PRs exist

A large feature often has natural layers: data model changes, service logic, API layer, UI. Shipping them as one 1,500-line PR makes review nearly impossible. Stacking lets you ship each layer independently while continuing work on the layers above.

Feature: new payment method main └──► feat/payment-db-schema PR #1 ← reviewer can focus on schema only └──► feat/payment-service PR #2 ← builds on #1, reviewer sees service logic └──► feat/payment-api PR #3 ← REST endpoints only └──► feat/payment-ui PR #4 ← UI only Merge order: #1 → main, then #2 re-targets main → merge, etc.

Manual stacking workflow

# Create the base layer $ git checkout -b feat/payment-db-schema main # ... make DB changes ... $ gh pr create --base main --title "feat: payment method DB schema" # Create layer 2 on top of layer 1 $ git checkout -b feat/payment-service feat/payment-db-schema # ... make service changes ... $ gh pr create --base feat/payment-db-schema --title "feat: payment service logic" # When PR #1 merges to main, re-target PR #2 $ gh pr edit 2 --base main

Tooling: graphite, gh-stack, spr

ToolWhat it automatesNotes
Graphite (graphite.dev)Full stack management: create, update, rebase, merge; web dashboard; Slack notificationsSaaS, free tier available; most polished UX; GitHub integration via GitHub App
spr (github.com/ejoffe/spr)CLI: stacks PRs from individual commits on a single branch; auto-rebases stack when base mergesOpen source; commit-based stacking (different philosophy from branch stacking)
gh-stackgh extension: visualises and manages branch-based stacksLightweight; no external service; manual rebase required

Trade-offs

BenefitCost
Reviewers see focused, smaller diffsIf the base PR needs significant changes, all dependent PRs need rebasing
Unblocks development — work continues while reviews happenComplex to manage without tooling; easy to lose track of stack state
Smaller PRs merge faster; feedback loop is tighterRequires discipline: each layer must be independently deployable or feature-flagged
STACK DISCIPLINE
Only stack PRs when each layer can be merged independently without breaking the application. If PR #2 can't be deployed without PR #1, they should either stay stacked (fine) or be a single PR. Never use stacking to hide a large PR — the stack total size is the same.
4.6

PR Size Discipline: Splitting Strategy & Size Signals

Research consistently shows that PR review quality degrades sharply above ~400 lines of diff. Reviewers shift from finding bugs to pattern-matching for shape, and approve without truly understanding the change.

Size thresholds

XS <50 lines S 50–200 lines M 200–400 lines L 400+ lines — discuss before submitting

These are change lines (git diff --stat additions + deletions), not file count. A PR touching 20 files but changing 3 lines each is fine. A PR touching one file with 600 changes needs splitting.

Automated size labelling

# .github/workflows/pr-size.yml on: pull_request: types: [opened, synchronize] jobs: size-label: runs-on: ubuntu-latest permissions: pull-requests: write steps: - uses: codelytv/pr-size-labeler@v1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} xs_label: size/xs xs_max_size: 50 s_label: size/s s_max_size: 200 m_label: size/m m_max_size: 400 l_label: size/l l_max_size: 1000 xl_label: size/xl fail_if_xl: false # warn but don't block (or set true to force splitting) exclude_files: | *.lock **/generated/** **/*.min.js # exclude auto-generated and vendored files

Splitting strategies

SituationHow to split
Feature with schema + logic + UIStack three PRs (see 4.5): schema first, then service, then UI
Refactor + new feature mixed togetherPR 1: pure refactor (no behaviour change). PR 2: new feature on top of the clean code. Easier to review both.
Large test file additionsAcceptable to have tests in a follow-up PR if the implementation PR is already large — but link them explicitly
Dependency update + dependent code changePR 1: dependency bump. PR 2: code changes that use the new API. The first is trivially reviewable.
Config/infra change + application changeAlways separate — different reviewers, different risk profiles
THE RULE
A PR should answer one question: "What changed and why?" If the description needs the word "and" to connect two distinct concepts, it should probably be two PRs.
4.7

Protected Branches + Required Reviews: Dismissal & Team Approval

We covered branch protection settings in Phase 3. Here we focus on the review-specific behaviours that trips teams up in practice.

Dismiss stale reviews on new push

When enabled, any new commit pushed to the PR after an approval dismisses all approvals — the reviewer must re-approve. This is the correct default for production branches: an approval on commit A is not an approval on commit A + B.

The only time to disable this: late-stage PRs where only trivial changes (typos, rebase commits) are expected. In that case, ask the reviewer to explicitly re-approve rather than disabling the protection globally.

Team-level approvals

When CODEOWNERS references a team (e.g. @acme/payments-team), GitHub requires at least one member of that team to approve — not just any reviewer. If the PR touches files owned by two teams, one approval from each team is required.

TEAM MEMBERSHIP MATTERS
If a CODEOWNER team has zero members in GitHub, the required review is unsatisfiable — the PR can never merge. This happens after team restructuring when CODEOWNERS isn't updated. Audit CODEOWNERS after any team rename or reorganisation.

Dismissal restrictions

By default, anyone with write access can dismiss a pull request review. Use "Restrict who can dismiss pull request reviews" to limit this to specific users or teams. This prevents:

  • An author dismissing a "Request changes" review from the reviewer and merging anyway
  • A well-meaning teammate dismissing a review to unblock a PR they don't own

Approvals from outside the required count

Additional approvals beyond the minimum required count still appear in the PR — they're visible social signal even though they're not mechanically required. Encourage reviewers who aren't CODEOWNERS to still leave substantive approvals or comments. Their input has value even if the system doesn't require it for merge.

4.8

GitHub CLI for Daily PR Workflows

The gh CLI brings GitHub's PR workflow into the terminal. Senior developers who live in the terminal find it dramatically faster than context-switching to the browser for routine PR operations.

Creating PRs

# Interactive — prompts for title, body, reviewers, labels $ gh pr create # Non-interactive — everything on one line $ gh pr create \ --title "feat: add payment retry logic" \ --body "Closes #42. Adds exponential backoff retry for failed payments." \ --base main \ --reviewer alice,@acme/payments-team \ --label "area/payments,size/m" \ --assignee @me # Open as draft $ gh pr create --draft --title "WIP: payment retry" # Fill body from a file (good for complex templates) $ gh pr create --body-file .github/pr-body.md

Reviewing PRs

# List PRs waiting for your review $ gh pr list --search "review-requested:@me state:open" # Check out a PR branch locally to run/test it $ gh pr checkout 247 # View PR diff in the terminal $ gh pr diff 247 # Leave a review comment $ gh pr review 247 --comment --body "nit: rename this variable for clarity" # Approve $ gh pr review 247 --approve --body "LGTM — good edge case handling" # Request changes $ gh pr review 247 --request-changes --body "block: the retry loop can deadlock — see comment"

Merging and managing PRs

# Merge (uses repo default merge method) $ gh pr merge 247 # Merge with explicit method $ gh pr merge 247 --squash --subject "feat: payment retry (#247)" # Enable auto-merge (merges when CI passes + reviews approved) $ gh pr merge 247 --auto --squash # Close without merging $ gh pr close 247 # Reopen a closed PR $ gh pr reopen 247 # Add a label $ gh pr edit 247 --add-label "ready-to-merge" # Change reviewers $ gh pr edit 247 --add-reviewer bob --remove-reviewer alice

The PR dashboard alias

# Add this alias to ~/.config/gh/config.yml or run once: $ gh alias set prs 'pr list --search "state:open involves:@me" --json number,title,author,reviewDecision,statusCheckRollup --jq ".[] | [.number, .author.login, .title[:60], .reviewDecision] | @tsv"' $ gh prs 247 alice feat: add payment retry logic REVIEW_REQUIRED 251 bob fix: timeout on checkout APPROVED 255 carol refactor: extract payment utils CHANGES_REQUESTED

Useful one-liners for leads

# All open PRs older than 3 days (stale review candidates) $ gh pr list --json number,title,createdAt \ --jq '.[] | select(.createdAt < (now - 259200 | strftime("%Y-%m-%dT%H:%M:%SZ"))) | [.number, .title] | @tsv' # PRs waiting on your team's review $ gh pr list --search "team-review-requested:acme/payments-team state:open" # Count open PRs per author (workload distribution) $ gh pr list --json author --jq '[.[].author.login] | group_by(.) | map({user: .[0], count: length}) | sort_by(-.count)[]'
4.9

PR Metrics: Cycle Time, Time-to-Review & Thoroughness Signals

You can't improve what you don't measure. These are the metrics that actually predict team velocity and code quality — not lines written or PRs merged per day.

Cycle Time

First commit → merged to main

The most important flow metric. High cycle time means long-lived branches, integration risk, and slow feedback. Target: <2 days for most teams.

Time to First Review

PR opened → first substantive review

Measures reviewer responsiveness. Long TFR kills momentum and encourages developers to context-switch away, forgetting the PR. Target: <4 hours.

Review Iterations

Number of review rounds before merge

1–2 rounds is healthy. 5+ rounds indicates unclear requirements, PR too large, or a reviewer/author communication problem.

PR Size (median)

Lines changed per PR (median, not mean)

Use median to ignore outliers. If your median is creeping above 300, your review quality is degrading even if nobody notices.

Extracting metrics via GitHub GraphQL

# GraphQL query — PR cycle time for the last 30 PRs merged to main query { repository(owner: "acme", name: "platform") { pullRequests( states: MERGED orderBy: {field: UPDATED_AT, direction: DESC} first: 30 ) { nodes { number title createdAt mergedAt additions deletions reviews(first: 1, states: [APPROVED, CHANGES_REQUESTED]) { nodes { submittedAt } } } } } }
# Run via gh CLI and compute cycle time in hours $ gh api graphql -f query=' query { repository(owner:"acme", name:"platform") { pullRequests(states:MERGED, first:30, orderBy:{field:UPDATED_AT,direction:DESC}) { nodes { number createdAt mergedAt additions deletions } } } }' \ --jq '.data.repository.pullRequests.nodes | map({ number: .number, cycleHours: ((.mergedAt | fromdateiso8601) - (.createdAt | fromdateiso8601)) / 3600 | round, linesChanged: (.additions + .deletions) })'

Review thoroughness signals (qualitative)

Metrics tools can't measure review quality directly, but these proxy signals are reliable:

SignalWhat it suggests
High approval rate with zero commentsRubber-stamping — reviews aren't substantive. Either PRs are too small and trivial, or reviewers are approving without reading.
All comments are nits/styleReviewers are focusing on surface rather than logic — consider automated formatting to free reviewer attention for substance.
Bugs found post-merge that were visible in the diffReview quality problem — reviewers may be overwhelmed or PRs may be too large to review properly.
Long review threads that could have been a conversationMissing async communication norms — a 15-comment thread on a design question should have been a 5-minute Slack discussion first.
PRs approved quickly by a single reviewer every timeReview load is concentrated — bus factor risk and likely rubber-stamping under time pressure.
LEAD ACTION
Run a monthly 15-minute retro on three randomly sampled PRs from the previous month. Read them as a team: was the description clear? Was the review substantive? Could it have been split? This ritual surfaces patterns that metrics miss.

Up Next — Phase 5: GitHub Actions Foundations

Workflow YAML anatomy, every trigger type, contexts and expressions, secrets vs variables, caching strategies, service containers, and debugging failing workflows.

Continue to Phase 5 → Back to Hub