Setting Up PR Templates for Code Review Efficiency Jump to heading

Review bottlenecks almost always trace back to one root cause: reviewers reconstructing intent rather than validating logic. Missing ticket references, absent test evidence, and undefined breaking changes force iterative clarification rounds that inflate cycle times and stall release tagging pipelines. PR templates solve this by establishing a structured submission contract β€” the same contract that feeds automated semantic versioning and CODEOWNERS routing decisions. This page is scoped to the specific scenario where a team needs enforceable, machine-parseable PR templates wired into Git Workflow Architecture & Branching Strategies.

When to Use This Approach Jump to heading

Apply this recipe when all of the following are true:

  • Your team experiences review round-trips caused by missing context (no ticket reference, no test evidence, vague breaking-change annotations).
  • You use automated SemVer tooling (semantic-release, release-please, or similar) that reads Release Impact metadata from PR bodies or commit messages.
  • Branch protection requires at least one status check before merge, giving you a hook to enforce template compliance in CI.
  • You operate feature branch isolation or a short-lived branch model where each PR maps to a discrete unit of work.
  • You need CODEOWNERS routing to trigger correctly β€” which depends on PR metadata being present when the review request fires.

If your team uses trunk-based development with commit-level conventions enforced by commitlint and you push directly to main without PRs, this recipe does not apply β€” enforce structure at the commit layer instead.

Step-by-Step Recipe Jump to heading

Step 1 β€” Create the Template Directory and Files Jump to heading

Intent: Platform providers resolve templates from specific paths. Mislocated files silently fall back to empty forms.

# GitHub resolves single templates from .github/PULL_REQUEST_TEMPLATE.md
# Multiple templates require the directory form below
mkdir -p .github/PULL_REQUEST_TEMPLATE

# Create typed templates; names surface as options in the GitHub UI
touch .github/PULL_REQUEST_TEMPLATE/feature.md
touch .github/PULL_REQUEST_TEMPLATE/hotfix.md
touch .github/PULL_REQUEST_TEMPLATE/docs.md

Verification:

# Confirm paths are correct before committing
find .github/PULL_REQUEST_TEMPLATE -name "*.md" | sort
# Expected output:
# .github/PULL_REQUEST_TEMPLATE/docs.md
# .github/PULL_REQUEST_TEMPLATE/feature.md
# .github/PULL_REQUEST_TEMPLATE/hotfix.md

GitLab uses a different path: .gitlab/merge_request_templates/feature.md. The directory name and file placement are the only differences; the template content format is identical.

WARNING: Do not nest template files inside subdirectories of .github/PULL_REQUEST_TEMPLATE/. The GitHub parser resolves only the direct children of that directory. Files in subdirectories are silently ignored, producing empty submission forms with no error message.

Step 2 β€” Author Structured Template Content Jump to heading

Intent: Every field in the template must map to a downstream consumer β€” a reviewer, a CI script, or an automated versioning tool. Remove any field that has no consumer.

## Summary
<!-- One sentence: what this PR does and why it is needed now -->

## Type of Change
- [ ] feat: backward-compatible new feature (β†’ minor bump)
- [ ] fix: backward-compatible bug fix (β†’ patch bump)
- [ ] docs: documentation only, no runtime change
- [ ] refactor: behaviour-preserving restructure
- [ ] breaking: removes or changes a public API contract (β†’ major bump)

## Ticket
Fixes #<!-- issue number β€” required; CI will block if absent -->

## Testing
- [ ] Unit/integration tests added or updated to cover this change
- [ ] Manually verified in a branch preview environment
- [ ] No tests required (docs-only β€” explain why: )

## Security
- [ ] No credentials, tokens, or secrets introduced
- [ ] API contracts updated in the OpenAPI spec
- [ ] Security review completed (mandatory for auth, data-access, or infra changes)

## Release Impact
<!-- Exactly one of: patch | minor | major -->
<!-- This field is read by the automated SemVer pipeline β€” spelling matters -->
Release Impact: 

What changed and why: The Release Impact field is machine-readable by design. Downstream pipelines that produce cryptographically signed release tags parse this value with regex to determine the next version number without human intervention.

Verification: Open a draft PR against your protected branch. Confirm the template renders correctly in the GitHub editor. Check that ?template=hotfix.md in the URL switches the active template.

WARNING: Checklist items enforce nothing on their own. An unchecked box in a merged PR has zero operational meaning. Pair every mandatory checkbox with a CI script (Step 4) that scans the body and fails the build when the box is left unchecked β€” otherwise the template is theatre, not policy.

Step 3 β€” Enable Branch Protection Rules Jump to heading

Intent: Branch protection is the lever that makes CI status checks block the merge button. Without it, failing CI is advisory.

# Apply protection rules via the GitHub CLI
# Adjust owner/repo/branch to match your repository
gh api repos/{owner}/{repo}/branches/main/protection \
  --method PUT \
  --input - <<'EOF'
{
  "required_status_checks": {
    "strict": true,
    "contexts": ["pr-template-validation"]
  },
  "enforce_admins": true,
  "required_pull_request_reviews": {
    "dismiss_stale_reviews": true,
    "require_code_owner_reviews": true,
    "required_approving_review_count": 1
  },
  "restrictions": null
}
EOF

Verification:

# Read back the applied rules and confirm status checks are present
gh api repos/{owner}/{repo}/branches/main/protection \
  --jq '.required_status_checks.contexts'
# Expected: ["pr-template-validation"]

strict: true means the branch must be up to date with main before merging. This prevents a window where a passing PR is merged on top of a base that has since changed in a way that would fail validation.

Step 4 β€” Add CI Validation for Required Fields Jump to heading

Intent: Parse the PR body in a dedicated CI step and exit non-zero if mandatory fields are missing or malformed.

# .github/workflows/pr-template-validation.yml
name: PR Template Validation

on:
  pull_request:
    types: [opened, edited, synchronize, reopened]

jobs:
  validate:
    name: pr-template-validation
    runs-on: ubuntu-latest
    steps:
      - name: Check required fields
        env:
          PR_BODY: $
        run: |
          #!/usr/bin/env bash
          set -euo pipefail

          ERRORS=0

          # Ticket reference β€” must match "Fixes #NNN"
          if ! echo "$PR_BODY" | grep -qE 'Fixes #[0-9]+'; then
            echo "ERROR: Missing ticket reference. Add 'Fixes #NNN' to the PR body."
            ERRORS=$((ERRORS + 1))
          fi

          # Release Impact β€” must be exactly patch, minor, or major
          if ! echo "$PR_BODY" | grep -qE 'Release Impact:\s*(patch|minor|major)'; then
            echo "ERROR: Missing or invalid Release Impact. Set to patch, minor, or major."
            ERRORS=$((ERRORS + 1))
          fi

          # Summary section must not be the placeholder comment
          if echo "$PR_BODY" | grep -qF '<!-- One sentence:'; then
            echo "ERROR: Summary placeholder was not replaced."
            ERRORS=$((ERRORS + 1))
          fi

          if [ "$ERRORS" -gt 0 ]; then
            echo "PR body failed validation ($ERRORS error(s)). Fill in all required fields."
            exit 1
          fi

          echo "PR body validation passed."

Verification: Submit a draft PR with the Release Impact line blank. Confirm the pr-template-validation check appears as failed and the merge button is blocked. Then populate the field and re-push β€” the check should turn green without re-running other jobs.

Step 5 β€” Auto-Populate Fields via GitHub Actions Jump to heading

Intent: Reduce manual entry by injecting branch-derived metadata into the PR body at creation time, so contributors edit rather than author from scratch.

# .github/workflows/pr-body-inject.yml
name: PR Body Inject

on:
  pull_request:
    types: [opened]

jobs:
  inject:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v7
        with:
          github-token: $
          script: |
            const branch = context.payload.pull_request.head.ref;
            const prNumber = context.payload.pull_request.number;
            const currentBody = context.payload.pull_request.body || '';

            // Derive release impact hint from branch naming convention
            let impactHint = 'patch';
            if (branch.startsWith('feat/') || branch.startsWith('feature/')) {
              impactHint = 'minor';
            } else if (branch.startsWith('breaking/')) {
              impactHint = 'major';
            }

            // Only inject if the body still contains the placeholder
            if (currentBody.includes('Release Impact: ')) {
              const updated = currentBody.replace(
                'Release Impact: ',
                `Release Impact: ${impactHint}`
              );
              await github.rest.pulls.update({
                owner: context.repo.owner,
                repo: context.repo.repo,
                pull_number: prNumber,
                body: updated
              });
              core.info(`Injected Release Impact: ${impactHint} from branch ${branch}`);
            }

Verification: Open a PR from a branch named feat/add-oauth. The Release Impact field in the PR body should read minor before any reviewer sees the submission. Adjust the branch naming convention map to match your team’s prefix standards.

Pair this automation with commit-level enforcement. When lint-staged runs formatting checks before every commit and pre-push hooks confirm test suites pass, contributors arrive at the PR form with a clean commit set β€” the template then only needs to capture intent and scope, not fix quality issues that should have been caught earlier.

PR Template Lifecycle: Submission to Merge Jump to heading

PR Template LifecycleFlow diagram showing a PR moving from submission through template rendering, contributor fill-in, CI field validation, code owner review, and finally merge with automated SemVer bump.PR Openedbranch β†’ mainTemplateRendered+ field injectionContributorFills Fieldsticket + impactCI ValidatesBody Fieldsblocks on failfail β†’ fix fieldsMerge +SemVer Bumpauto-tag fired

Validation Checklist Jump to heading

Frequently Asked Questions Jump to heading

Can I require a specific template based on branch name? Jump to heading

Yes. A GitHub Actions workflow triggered by pull_request (type opened) reads github.event.pull_request.head.ref and uses github.rest.pulls.update to overwrite the PR body with the template content appropriate for that branch prefix. The auto-population step in Step 5 above demonstrates this pattern β€” extend it to fetch the full template file from your repository rather than just injecting a single field.

Do markdown checkboxes enforce anything by default? Jump to heading

No. Unchecked boxes in a merged PR body have no operational effect unless a CI script specifically reads the body text. To enforce a checkbox, add a grep -qF '- \[x\]' check for that specific line in the validation script from Step 4. Require the status check to pass before merge. Without this wiring, checklists are documentation, not enforcement.

How do I prevent the Release Impact field from breaking automated SemVer tools? Jump to heading

Keep the regex in your CI validation script (Release Impact:\s*(patch|minor|major)) in sync with the regex your SemVer tool uses to parse the same field. If you use semantic-release with a custom plugin that reads the PR body, test the plugin locally against a representative sample of PR bodies before enabling it in CI. Mismatched regexes cause the tool to silently default to patch, which under-bumps major releases.


  • Release Tagging & Versioning β€” the parent cluster covering semantic versioning automation, signed tags, and release branch strategies that PR template metadata feeds into.
  • Feature Branch Isolation β€” how to structure short-lived branches so each PR maps cleanly to a single template type without ambiguity.
  • Pre-Push Validation Rules β€” enforce test suite passage and lint compliance before a PR is even opened, reducing the validation burden on the template itself.
  • How to Enforce Conventional Commits with commitlint β€” when commit-level conventions are enforced, the PR template Type of Change section can be auto-derived from commit prefixes rather than entered manually.