Requiring linked issues on pull requests Jump to heading

Teams that plan work in an issue tracker want each change traceable to the issue that motivated it: to see what shipped for a feature, to audit why a change was made, to close issues automatically when work lands. In practice a third of pull requests link nothing, and the link is reconstructed later by searching titles. A check that requires a linked issue fixes that at the point where it is cheapest — when the pull request is opened — as long as it is tolerant about where the reference appears and has clear exemptions for changes that genuinely have no issue. This page builds that check for GitHub and GitLab-style keys, within pull request automation and bots.

When to use this approach Jump to heading

  • Your organisation tracks work in issues and wants pull requests linked to them.
  • Audits or release processes need to map shipped changes to tickets.
  • Issue keys already appear in some commit messages, as in enforcing issue keys in commit messages.
  • You want issues closed automatically when their pull requests merge.

Step 1 — Decide where a reference may appear Jump to heading

Be generous about location and strict about format. A reference in the title, the description or the branch name all count; what matters is that it is a real issue identifier.

# Accepted forms (adapt the key pattern to your tracker)
#   title:        "Add export scheduling (PAY-812)"
#   description:  "Fixes #431"  or  "Closes PAY-812"
#   branch:       feature/PAY-812-export-scheduling
key='([A-Z][A-Z0-9]+-[0-9]+|#[0-9]+)'
Where an issue reference can come fromThe check looks for an issue key in the pull request title, in its description, in the branch name and in commit trailers. Any one is enough. Exemption labels cover changes that genuinely have no issue.Title(PAY-812)DescriptionFixes #431Branchfeature/PAY-812-…TrailersFixes: PAY-812Exempt labelno-issuestrict about the key format, generous about where it lives

Step 2 — Check the pull request in CI Jump to heading

The check reads the title, body and head branch from the event, and commit trailers from the branch, then passes if any contain a key or the pull request carries an exemption label.

# .github/workflows/linked-issue.yml
on:
  pull_request:
    types: [opened, edited, synchronize, labeled, unlabeled, reopened]
jobs:
  linked-issue:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0, ref: "${{ github.event.pull_request.head.sha }}" }
      - env:
          TITLE: ${{ github.event.pull_request.title }}
          BODY: ${{ github.event.pull_request.body }}
          BRANCH: ${{ github.head_ref }}
          LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
        run: |
          key='([A-Z][A-Z0-9]+-[0-9]+|#[0-9]+)'
          case ",$LABELS," in *,no-issue,*|*,dependencies,*) echo "exempt by label"; exit 0 ;; esac
          trailers=$(git log --format='%(trailers:key=Fixes,valueonly)' "origin/${{ github.base_ref }}..HEAD")
          if printf '%s\n%s\n%s\n%s\n' "$TITLE" "$BODY" "$BRANCH" "$trailers" | grep -qE "$key"; then
            echo "linked issue found"; exit 0
          fi
          echo "::error::Link an issue: put a key like PAY-812 or #431 in the title, description or branch name, or add the 'no-issue' label with a reason."
          exit 1

Including edited and labeled in the trigger types means the check re-runs as soon as the author fixes the title or adds a label, without a new push.

Step 3 — Validate that the issue exists Jump to heading

A pattern match accepts PAY-99999 even if no such issue exists. For GitHub issues, check through the API; for external trackers, call their API with a read-only token. Keep this step optional and fast, and fail open if the tracker is unavailable, so a tracker outage does not block all merges.

for n in $(printf '%s\n' "$TITLE $BODY" | grep -oE '#[0-9]+' | tr -d '#' | sort -u); do
  gh api "repos/$GITHUB_REPOSITORY/issues/$n" --jq '.state' >/dev/null 2>&1 \
    || echo "::warning::#$n does not exist in this repository"
done
Outcomes of the linked-issue checkIf an exemption label is present, the check passes. If a key is found and the issue exists, it passes. If a key is found but the tracker cannot confirm it, it warns and passes. If no key is found anywhere, it fails with instructions.What does the pull request carry?exemption labelPassreason in labelkey foundPasswarn if not foundnothingFailhow to linkfail open on tracker outages — the check should never be the outage

Step 4 — Close issues automatically on merge Jump to heading

Linking is half the value; closing is the other half. Closing keywords such as Fixes #431 in the description close GitHub issues when the pull request merges into the default branch. For external trackers, a workflow on merge can transition the issue.

on:
  pull_request:
    types: [closed]
jobs:
  transition:
    if: github.event.pull_request.merged
    runs-on: ubuntu-latest
    steps:
      - env: { TEXT: "${{ github.event.pull_request.title }} ${{ github.event.pull_request.body }}" }
        run: |
          for k in $(printf '%s' "$TEXT" | grep -oE '[A-Z][A-Z0-9]+-[0-9]+' | sort -u); do
            curl -fsS -m 5 -X POST "$TRACKER_URL/issues/$k/transitions" \
              -H "Authorization: Bearer $TRACKER_TOKEN" -d '{"to":"done"}' || echo "could not transition $k"
          done

Step 5 — Keep exemptions visible and rare Jump to heading

Some changes have no issue: typo fixes, dependency bumps from bots, urgent incident reverts. An exemption label makes them explicit. Report how often it is used, so it does not become the default.

gh pr list --state merged --limit 200 --json labels \
  --jq '[.[] | select(any(.labels[]; .name=="no-issue"))] | length' |
  xargs -I{} echo "{} of last 200 merged PRs used no-issue"
Share of pull requests linked to issuesAn illustrative team before and after introducing the check. Before, about two thirds of pull requests linked an issue. After, almost all did, with a small, stable share using the no-issue exemption for typos, reverts and bot updates.share of merged pull requests (illustrative)linked, before64%unlinked, before36%linked, after93%exempt, after7%a stable exemption share is healthy; a rising one means the label is becoming a habit

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Should the check be required? Jump to heading

Once the team is used to it, yes. Start it as non-required for a couple of weeks so people see the message and fix their habits, then add it to the required checks.

What about commits pushed directly to main? Jump to heading

Direct pushes do not go through pull requests, so the check never sees them. Protect main so changes arrive by pull request, or enforce keys in commit messages server-side as in enforcing commit message policy in pre-receive.

Can the branch name be generated from the issue automatically? Jump to heading

Many trackers and forges can create a branch from an issue with the key in its name. Encouraging that makes the check pass without anyone thinking about it.