GitHub Pull Requests Complete Guide: Reviews, CI/CD & Best Practices
Pull requests are the backbone of modern software development on GitHub. They are the mechanism through which code gets reviewed, tested, and merged, and they shape how teams collaborate, maintain quality, and ship software. Whether you are opening your first PR or trying to improve your team's review culture, this guide covers every aspect of the pull request workflow: from writing effective descriptions to configuring branch protection, integrating CI/CD pipelines, choosing merge strategies, and automating PR operations with GitHub Actions and the GitHub CLI.
This guide reflects GitHub's feature set as of early 2026, including the latest improvements to code review, merge queues, and repository rulesets.
Table of Contents
- What Is a Pull Request?
- The Pull Request Workflow
- Creating Effective Pull Requests
- Writing Great PR Descriptions
- Code Review Best Practices
- PR Review Comments and Suggestions
- Draft Pull Requests
- PR Templates and CODEOWNERS
- Branch Protection Rules
- CI/CD Integration with PRs
- Merge Strategies
- Handling Merge Conflicts in PRs
- GitHub CLI for PR Management
- PR Automation with GitHub Actions
- Conventional Commits and PR Titles
- Stacked PRs and Large PR Strategies
- PR Metrics and Team Velocity
- Frequently Asked Questions
What Is a Pull Request?
A pull request is a proposal to merge a set of changes from one branch into another. On GitHub, a PR provides a structured place to describe your changes, discuss the implementation with teammates, run automated checks, and ultimately integrate the code into the target branch. The term "pull request" comes from the idea that you are requesting the maintainer to pull your changes into their branch.
Pull requests are more than a merge mechanism. They serve as:
- Code review platforms — Reviewers examine the diff, leave comments, and approve or request changes
- Documentation — The PR description and discussion form a permanent record of why a change was made
- Quality gates — CI/CD pipelines, linters, and tests run automatically before code is merged
- Knowledge sharing — Team members learn about parts of the codebase they do not usually work in
The Pull Request Workflow
The standard GitHub PR workflow follows these steps:
- Create a branch from the target branch (usually
main) - Make commits on your feature branch
- Push the branch to the remote repository
- Open a pull request against the target branch
- Automated checks run (CI, linters, tests)
- Code review by one or more teammates
- Address feedback with additional commits or amendments
- Approval from required reviewers
- Merge the pull request
- Delete the branch (cleanup)
# Typical PR workflow from the command line
git checkout -b feature/add-user-search main
# ... make changes ...
git add -A
git commit -m "feat: add user search with fuzzy matching"
git push -u origin feature/add-user-search
# Then open PR on GitHub or use: gh pr create
For a deeper look at branching models, see our Git Branching Strategies Guide.
Creating Effective Pull Requests
The quality of a pull request is determined before you write a single line of code. Keep these principles in mind:
Keep PRs Small and Focused
Research consistently shows that smaller pull requests get reviewed faster, receive better feedback, and have fewer defects. A study by SmartBear found that review quality drops sharply after 400 lines of changes. Aim for PRs that change fewer than 300 lines when possible.
- One logical change per PR (a feature, a bug fix, a refactor)
- Separate refactoring from functional changes
- Split large features into incremental PRs that each stand alone
- Move config or dependency changes into their own PRs
Write Meaningful Commit Messages
Even if you plan to squash-merge, good commit messages help reviewers understand the progression of your work:
# Good: explains what and why
git commit -m "feat: add fuzzy search to user lookup
Use Levenshtein distance for matching. This handles
typos in the admin dashboard search, which support
reported as the #2 complaint in Q4."
# Bad: describes what the diff already shows
git commit -m "updated search.js"
Self-Review Before Opening
Before requesting a review, go through your own diff on GitHub. You will catch debug statements, accidental whitespace changes, and missing tests that you would be embarrassed to have a reviewer point out.
Writing Great PR Descriptions
The PR description is the first thing a reviewer reads. A good description reduces review time and prevents misunderstandings. Here is a template that works well for most teams:
## What
One-line summary of the change.
## Why
Context: link to the issue, user story, or bug report.
Explain the motivation behind this approach.
## How
Describe the implementation approach. Mention key design
decisions and any alternatives you considered.
## Testing
- [ ] Unit tests added/updated
- [ ] Manual testing steps:
1. Go to /admin/users
2. Type a misspelled name in the search box
3. Verify fuzzy results appear
## Screenshots
(For UI changes, include before/after screenshots)
## Related
- Closes #142
- Depends on #138
- See also: #130 (design doc)
GitHub automatically links Closes #142 to the referenced issue and will close it when the PR merges. Use Fixes, Resolves, or Closes followed by the issue number.
Code Review Best Practices
Code review is the highest-leverage activity in a pull request. Done well, it catches bugs, spreads knowledge, and improves design. Done poorly, it becomes a bottleneck that demoralizes the team.
For Reviewers
- Review promptly. Aim to start a review within 4 hours of being requested. Stale PRs kill momentum.
- Read the description first. Understand the context before looking at code.
- Focus on correctness, design, and maintainability rather than style preferences (use linters for style).
- Ask questions instead of making demands. "What happens if this list is empty?" is better than "Handle the empty case."
- Distinguish blocking concerns from nitpicks. Prefix optional suggestions with "nit:" so the author knows what must be addressed.
- Approve when good enough. Do not hold a PR hostage for perfection. If the code is correct and maintainable, approve it.
For Authors
- Respond to every comment, even if just with a thumbs-up emoji, so the reviewer knows you saw it.
- Do not take feedback personally. The review is about the code, not about you.
- Explain the why, not just the what. If you disagree with a suggestion, explain your reasoning.
- Push follow-up commits rather than force-pushing over reviewed code, so reviewers can see what changed since their review.
PR Review Comments and Suggestions
GitHub provides several ways to leave feedback on a pull request:
Single-Line and Multi-Line Comments
Click the + button next to any line in the diff to leave a comment. Drag to select multiple lines for a multi-line comment. These comments are anchored to specific code, making them precise and actionable.
Suggested Changes
Reviewers can propose exact code changes that the author can accept with a single click:
```suggestion
const MAX_RETRIES = 3;
for (let i = 0; i < MAX_RETRIES; i++) {
```
The author sees a "Commit suggestion" button and can batch multiple suggestions into a single commit. This is one of the most efficient review features on GitHub because it eliminates back-and-forth.
Review States
- Comment — General feedback without explicit approval or rejection
- Approve — The code looks good and is ready to merge
- Request changes — Blocking issues must be addressed before merge
Always submit your comments as a review (click "Start a review" or "Finish your review") rather than posting them individually. Batched reviews send a single notification instead of spamming the author.
Draft Pull Requests
Draft PRs signal that your code is not ready for formal review. They are useful for:
- Getting early architectural feedback before you finish the implementation
- Running CI/CD pipelines against work-in-progress code
- Making your work visible to the team without requesting reviews
- Preventing accidental merges of incomplete work
# Create a draft PR from the command line
gh pr create --draft --title "feat: user search" --body "WIP: implementing fuzzy search"
# Convert a draft to ready for review
gh pr ready
Draft PRs cannot be merged until they are marked as ready. Some teams adopt the practice of opening a draft PR immediately after creating a branch, converting it to ready only when all checks pass and the code is complete.
PR Templates and CODEOWNERS
Pull Request Templates
Create a file at .github/pull_request_template.md to provide a default description for every new PR in your repository:
## What does this PR do?
## Why is this change needed?
## How was this tested?
- [ ] Unit tests
- [ ] Integration tests
- [ ] Manual testing
## Checklist
- [ ] Code follows project style guidelines
- [ ] Self-review completed
- [ ] Documentation updated (if applicable)
- [ ] No breaking changes (or migration guide included)
For repositories with different types of changes, you can create multiple templates in .github/PULL_REQUEST_TEMPLATE/ and select them via URL query parameters.
CODEOWNERS
The CODEOWNERS file automatically assigns reviewers based on which files a PR modifies. Place it at .github/CODEOWNERS or the repository root:
# .github/CODEOWNERS
# Default owners for everything
* @myorg/core-team
# Frontend code requires frontend team review
/src/components/ @myorg/frontend
/src/styles/ @myorg/frontend
*.css @myorg/frontend
# API changes require backend team review
/src/api/ @myorg/backend
/src/middleware/ @myorg/backend
# Infrastructure changes require DevOps review
Dockerfile @myorg/devops
docker-compose.yml @myorg/devops
.github/workflows/ @myorg/devops
# Security-sensitive files require security team
/src/auth/ @myorg/security @myorg/backend
When combined with branch protection rules that require CODEOWNERS approval, this ensures the right people always review the right code.
Branch Protection Rules
Branch protection rules prevent direct pushes to important branches and enforce quality gates before merging. Configure them in Settings → Branches → Branch protection rules.
Essential Protection Settings
- Require pull request reviews before merging — Set the minimum number of approvals (1 or 2 for most teams)
- Dismiss stale reviews — When new commits are pushed, previous approvals are invalidated
- Require review from CODEOWNERS — Ensures domain experts approve changes to their areas
- Require status checks to pass — CI must be green before merge is allowed
- Require branches to be up to date — The PR branch must be current with the target branch
- Require signed commits — All commits must have GPG or SSH signatures
- Restrict who can push — Only specific users or teams can push to the branch
- Include administrators — Even admins must follow the rules (disable only for emergencies)
Repository Rulesets
GitHub's newer repository rulesets provide more granular control than classic branch protection. They support targeting by branch name patterns, tag patterns, and can be applied at the organization level. Rulesets also support bypass lists, allowing specific roles or teams to bypass certain rules for emergencies without disabling them entirely.
CI/CD Integration with PRs
Pull requests are the natural integration point for continuous integration. Every push to a PR branch triggers automated checks that validate the code before a human reviewer even looks at it.
GitHub Actions Workflow for PRs
# .github/workflows/pr-checks.yml
name: PR Checks
on:
pull_request:
branches: [main, develop]
types: [opened, synchronize, reopened]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm test -- --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
The pull_request trigger runs workflows in the context of the PR, which means they have read-only access to the base repository. This is a security feature that prevents malicious PRs from exfiltrating secrets. For workflows that need write access, use pull_request_target with caution.
For a comprehensive look at GitHub Actions, see our GitHub Actions CI/CD Complete Guide.
Required Status Checks
After your CI workflow runs for the first time, you can make it a required status check in branch protection. This means the PR cannot be merged unless the specified checks pass. Configure this in Settings → Branches → Require status checks → search for your job names (lint, test, build).
Merge Strategies: Merge Commit, Squash, and Rebase
GitHub offers three ways to merge a pull request. Each produces a different commit history.
Merge Commit (Create a Merge Commit)
This is the default. It creates a merge commit that ties the feature branch history into the target branch. All individual commits are preserved.
# Result on main:
# a - b - c - M (main, M is the merge commit)
# \ /
# d - e (feature branch commits)
Best for: Teams that want to preserve full history and see exactly what happened on each branch. Works well with Git Flow.
Squash and Merge
Compresses all commits from the PR into a single commit on the target branch. The individual commits are discarded from the main branch history.
# Result on main:
# a - b - c - S (main, S contains all changes from d + e)
# Feature branch commits d, e are not on main
Best for: Most teams. Keeps main clean with one commit per feature or fix. Each commit on main corresponds to one PR, making git log and git bisect straightforward. This is the most popular strategy in 2026.
Rebase and Merge
Replays each commit from the PR individually onto the target branch. No merge commit is created. Commit hashes change because each commit gets a new parent.
# Result on main:
# a - b - c - d' - e' (main, d' and e' are rebased versions)
Best for: Teams that want linear history but also want to preserve individual commits. Requires clean, atomic commits on the feature branch. For more on rebase, see our Git Rebase Complete Guide.
Choosing Your Strategy
You can enforce a single merge strategy in repository settings (Settings → General → Pull Requests). Many teams allow only squash merge on main and merge commits on release branches.
Handling Merge Conflicts in PRs
Merge conflicts happen when the PR branch and the target branch have both modified the same lines. GitHub shows a warning banner and blocks the merge button until conflicts are resolved.
Resolving via the GitHub Web Editor
For simple conflicts, GitHub provides a browser-based conflict editor. Click "Resolve conflicts" on the PR page, edit the conflict markers, and commit directly.
Resolving Locally with Merge
# Fetch the latest target branch
git fetch origin main
# Merge main into your feature branch
git checkout feature/user-search
git merge origin/main
# Resolve conflicts in your editor, then:
git add .
git commit -m "resolve merge conflicts with main"
git push
Resolving Locally with Rebase
# Rebase your branch onto the latest main
git fetch origin main
git checkout feature/user-search
git rebase origin/main
# Resolve each conflict as it comes up:
# Edit the file, then:
git add .
git rebase --continue
# After all conflicts are resolved:
git push --force-with-lease
For a deep dive into conflict resolution, see our Git Merge Conflicts Guide.
Preventing Conflicts
- Keep PRs small and short-lived
- Rebase or merge main into your branch regularly
- Coordinate with teammates when modifying the same files
- Use CODEOWNERS to make file ownership explicit
GitHub CLI (gh) for PR Management
The GitHub CLI (gh) lets you manage pull requests entirely from the terminal. It is the fastest way to create, review, and merge PRs without leaving your editor.
Creating Pull Requests
# Create a PR interactively
gh pr create
# Create with title and body
gh pr create --title "feat: add user search" \
--body "Implements fuzzy search for the admin dashboard. Closes #142."
# Create a draft PR
gh pr create --draft --title "WIP: search feature"
# Create and assign reviewers
gh pr create --title "feat: search" --reviewer alice,bob
# Create targeting a specific base branch
gh pr create --base develop --title "feat: search"
Reviewing Pull Requests
# List open PRs
gh pr list
# View a specific PR
gh pr view 42
# Check out a PR locally for testing
gh pr checkout 42
# View the diff
gh pr diff 42
# Approve a PR
gh pr review 42 --approve
# Request changes
gh pr review 42 --request-changes --body "Need error handling for empty input"
# Leave a comment
gh pr review 42 --comment --body "Looks good overall, one suggestion below"
Merging and Closing
# Merge with default strategy
gh pr merge 42
# Squash merge
gh pr merge 42 --squash
# Rebase merge
gh pr merge 42 --rebase
# Merge and delete the branch
gh pr merge 42 --squash --delete-branch
# Close without merging
gh pr close 42
Advanced gh Commands
# List PRs that need your review
gh pr list --search "review-requested:@me"
# List PRs by a specific author
gh pr list --author alice
# View PR checks/status
gh pr checks 42
# Re-request review after addressing feedback
gh pr edit 42 --add-reviewer alice
# Add labels
gh pr edit 42 --add-label "ready-for-review,frontend"
PR Automation with GitHub Actions
GitHub Actions can automate repetitive PR tasks, reducing manual work for maintainers and contributors.
Auto-Label PRs by File Path
# .github/workflows/label-pr.yml
name: Label PR
on:
pull_request:
types: [opened, synchronize]
jobs:
label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/labeler@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Reads label config from .github/labeler.yml
# .github/labeler.yml
frontend:
- changed-files:
- any-glob-to-any-file: ['src/components/**', '*.css', '*.tsx']
backend:
- changed-files:
- any-glob-to-any-file: ['src/api/**', 'src/middleware/**']
docs:
- changed-files:
- any-glob-to-any-file: ['docs/**', '*.md']
ci:
- changed-files:
- any-glob-to-any-file: ['.github/workflows/**']
Auto-Assign Reviewers
# .github/workflows/assign-reviewers.yml
name: Assign Reviewers
on:
pull_request:
types: [opened, ready_for_review]
jobs:
assign:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: kentaro-m/auto-assign-action@v2
with:
configuration-path: '.github/auto-assign.yml'
PR Size Check
# .github/workflows/pr-size.yml
name: PR Size Check
on:
pull_request:
types: [opened, synchronize]
jobs:
size-check:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check PR size
run: |
ADDITIONS=$(gh pr view ${{ github.event.pull_request.number }} --json additions -q '.additions')
DELETIONS=$(gh pr view ${{ github.event.pull_request.number }} --json deletions -q '.deletions')
TOTAL=$((ADDITIONS + DELETIONS))
echo "PR size: +$ADDITIONS -$DELETIONS ($TOTAL total lines)"
if [ "$TOTAL" -gt 500 ]; then
gh pr comment ${{ github.event.pull_request.number }} \
--body "**Warning**: This PR changes $TOTAL lines. Consider splitting into smaller PRs for easier review."
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
For more automation patterns with Git, see our Git Hooks Complete Guide.
Conventional Commits and PR Titles
The Conventional Commits specification provides a structured format for commit messages and PR titles that enables automated changelog generation and semantic versioning.
Format
<type>(<scope>): <description>
# Examples:
feat(auth): add OAuth2 login with Google
fix(api): handle null response from user service
docs(readme): update installation instructions
refactor(search): extract fuzzy matching into utility
test(cart): add integration tests for checkout flow
chore(deps): upgrade express from 4.18 to 4.21
ci(deploy): add staging environment workflow
perf(db): add index on users.email column
Common Types
feat— A new feature (triggers a minor version bump)fix— A bug fix (triggers a patch version bump)docs— Documentation changes onlyrefactor— Code change that neither fixes a bug nor adds a featuretest— Adding or updating testschore— Maintenance tasks (dependencies, configs)ci— CI/CD configuration changesperf— Performance improvements
When using squash merge, the PR title becomes the commit message on main. Enforce conventional PR titles with a GitHub Action that validates the title format before the PR can be merged.
Stacked PRs and Large PR Strategies
When a feature is too large for a single PR, break it into a series of dependent PRs that build on each other. This approach is called "stacked PRs" or "PR chains."
How Stacking Works
# PR 1: Base infrastructure (targets main)
git checkout -b feat/search-models main
# Add database models and migrations
git push -u origin feat/search-models
gh pr create --base main --title "feat(search): add search models and migrations"
# PR 2: API layer (targets PR 1 branch)
git checkout -b feat/search-api feat/search-models
# Add API endpoints
git push -u origin feat/search-api
gh pr create --base feat/search-models --title "feat(search): add search API endpoints"
# PR 3: Frontend (targets PR 2 branch)
git checkout -b feat/search-ui feat/search-api
# Add UI components
git push -u origin feat/search-ui
gh pr create --base feat/search-api --title "feat(search): add search UI components"
Merge from the bottom up: merge PR 1 into main first, then retarget PR 2 to main, merge it, and so on. GitHub automatically retargets downstream PRs when their base branch is merged.
Tools for Managing Stacked PRs
- ghstack — Facebook's tool for managing stacked diffs
- spr — Stacked PRs for GitHub, maintains a clean stack automatically
- graphite — Commercial tool with a polished UI for stacked PRs
- git-town — Manages branch hierarchies and syncs stacks
Other Strategies for Large Changes
- Feature flags — Merge incomplete code behind a flag so it does not affect users
- Branch by abstraction — Introduce an abstraction layer, swap the implementation behind it in stages
- Parallel PRs — If parts of the feature are independent, open separate PRs that all target
main
PR Metrics and Team Velocity
Tracking pull request metrics helps identify bottlenecks in your development process and improve team throughput.
Key Metrics to Track
- Time to first review — How long before a reviewer starts looking at a PR (target: under 4 hours)
- Time to merge — Total time from PR creation to merge (target: under 24 hours for normal PRs)
- Review cycles — How many rounds of review before approval (target: 1-2 cycles)
- PR size — Lines changed per PR (target: under 400 lines)
- Review depth — Number of comments per PR (too few may indicate rubber-stamping)
- Rework rate — Percentage of PRs that require changes after review
Querying PR Metrics with GitHub CLI
# List merged PRs from the last 30 days with timing data
gh pr list --state merged --limit 50 --json number,title,createdAt,mergedAt,additions,deletions \
| jq '.[] | {number, title, days_open: (((.mergedAt | fromdateiso8601) - (.createdAt | fromdateiso8601)) / 86400 | floor), size: (.additions + .deletions)}'
# Average PR size
gh pr list --state merged --limit 100 --json additions,deletions \
| jq '[.[] | .additions + .deletions] | add / length | floor'
# PRs currently waiting for review (older than 24 hours)
gh pr list --search "review:required created:<$(date -d '1 day ago' +%Y-%m-%d)" \
--json number,title,createdAt
Improving PR Velocity
- Set up Slack or Teams notifications for PR reviews
- Establish team review SLAs (e.g., first review within 4 hours)
- Rotate review duties so no one person is a bottleneck
- Use auto-assign to distribute reviews evenly
- Keep PRs small so they are quick to review
- Write thorough PR descriptions so reviewers do not need to ask questions
Frequently Asked Questions
What is the difference between squash merge, rebase merge, and merge commit on GitHub?
A merge commit preserves every individual commit from the feature branch and adds a merge commit on top, keeping full history. Squash merge compresses all commits into a single commit on the target branch, giving a clean history but losing per-commit detail. Rebase merge replays each commit individually onto the target branch without a merge commit, creating a linear history. Most teams use squash merge for feature branches to keep main clean, and reserve merge commits for long-lived release branches.
How many reviewers should a pull request have?
For most teams, one required reviewer is the minimum and two reviewers is ideal for production code. Research from Microsoft and Google shows that the first reviewer catches the majority of issues, with diminishing returns after the second reviewer. Configure branch protection rules to require at least one approving review before merge. For critical systems like authentication or payment processing, require two or more approvals and use CODEOWNERS to route reviews to domain experts.
Should I use draft pull requests or regular pull requests for work in progress?
Use draft pull requests for any work that is not ready for formal review. Drafts signal to teammates that the code is still evolving and prevents accidental merges. They are useful for getting early design feedback, running CI checks before the code is complete, and showing progress on long-running features. Convert the draft to a regular PR by clicking "Ready for review" when you want formal code review. Some teams open draft PRs immediately after creating a branch to make all work visible.
How do I resolve merge conflicts in a GitHub pull request?
You have three options. First, use GitHub's web editor for simple conflicts by clicking the "Resolve conflicts" button on the PR page. Second, resolve locally by running git fetch origin, git checkout your-branch, git merge origin/main, fixing the conflicts in your editor, then git add and git commit. Third, rebase locally with git rebase origin/main, resolve conflicts during rebase, and force-push with git push --force-with-lease. The local approach gives you your full editor and test suite, which is better for complex conflicts.
What should I include in a pull request description?
A good PR description includes: a one-line summary of what changed, the motivation or context explaining why the change is needed, a description of the approach taken and any alternatives considered, testing instructions or evidence that the change works, screenshots or recordings for UI changes, links to related issues or tickets, and a checklist of any deployment steps needed. Use a PR template to enforce this structure across your team so that reviewers always have the context they need.
Continue Learning
This guide is part of our Git and GitHub deep-dive series. Explore the related guides to build a complete understanding of collaborative development workflows:
- Git Rebase: The Complete Guide — Interactive rebase, squash commits, and rebase workflows
- Git Branching Strategies Guide — Git Flow, GitHub Flow, and Trunk-Based Development
- Git Merge Conflicts Guide — Resolve conflicts confidently in any scenario
- GitHub Revert Pull Request Guide — Undo merged PRs safely, including squash/rebase rollback edge cases
- GitHub Revert Button Missing Fix Guide — Diagnose missing Revert buttons and apply safe rollback commands
- GitHub Protected Branch Revert Guide — Safe rollback workflow for branch rules and merge queue repos
- GitHub Actions CI/CD Complete Guide — Automate builds, tests, and deployments
- Git Hooks Complete Guide — Pre-commit, pre-push, and server-side hooks