← The ADLC library
Field guides · 2

Linking GitHub work to code, reliably

Closing keywords, the development sidebar and Projects automation do more than most teams use. Here is how the link is really made, where it silently fails, and how to measure your own linking rate.

Your issues and your code live in the same product, so linking them should be free. It mostly is. The reason a third of your closed issues have no pull request attached is not that GitHub cannot do it, it is that the link is made by a convention nobody enforces, and when the convention is missed nothing anywhere complains.

Good news first: if you track work in GitHub Issues and Projects, the native tooling covers this problem end to end. You do not need to buy anything. You do need to understand which of the several linking mechanisms you are actually relying on, because they behave differently.

Three mechanisms that get constantly conflated

There are three separate mechanisms, and people conflate them constantly.

Closing keywords. A phrase such as Closes #123, Fixes #123 or Resolves #123 creates a real, structured link between a pull request and an issue, and closes the issue when the pull request merges into the default branch. This is the strongest of the three because it is not just a mention: GitHub records it as a relationship, shows it in the issue’s timeline, and acts on it at merge time.

Two details that account for most of the confusion:

  • The keyword is read from the pull request description and from commit messages. A keyword that exists only in the pull request title is the classic reason a link never appears. Test this in a scratch repository rather than trusting anyone’s recollection, including mine, because it is the single most commonly misremembered fact about GitHub.
  • Cross-repository behaviour is not the same as same-repository behaviour, and it has changed over time. If your issues live in one repository and your code in another, prove the closing behaviour works in your own organisation before you build a process on it.

Plain references. A bare #123, or owner/repo#123 across repositories, produces a cross-reference in the issue timeline. It is a breadcrumb, not a relationship. It will not close anything, it will not appear as a linked pull request, and it will not be picked up by anything reading structured links. Perfectly useful for humans, useless for reporting.

The development sidebar. You can attach a pull request to an issue directly in the interface, and you can create a branch from an issue, which pre-links the two without anybody typing a keyword. Branch-from-issue is the most underused feature in this whole area. It makes the correct behaviour the path of least resistance, which is the only enforcement mechanism that ever holds.

On top of that, Projects can automate the status transition. A project can move an item when a linked pull request opens, when review is requested, and when it merges. This is where the link starts paying for itself, because the board stops needing to be dragged by hand. Where these automations are configured has moved around as Projects has evolved, so find them in your project’s settings rather than following an old screenshot.

The mechanism has no error state

The mechanism has no error state. Nothing fails when a keyword is absent, so the gap is invisible until someone asks a question that needs the data.

The specific failure conditions:

Squash merges plus keyword-in-commit. If the keyword lived only in a commit message and the merge is squashed, what lands on the trunk is the generated squash message. If your team edits that message, the keyword can disappear at the last moment, in a dialog nobody screenshots.

Bot pull requests. Dependency and security updates merge in volume and reference no issue. They belong in the denominator only if you decide they do, and most teams forget to exclude them and then panic at the ratio.

Fork-based contributions. Outside contributors frequently do not have context on your issue conventions, and a maintainer merging in a hurry will not add the keyword afterwards.

Multi-issue changes. One pull request that resolves three issues needs three keywords. Two of them get forgotten roughly every time.

Issues that live somewhere else. Plenty of organisations track engineering work in GitHub Issues and product work in another system entirely. GitHub’s linking says nothing about the second system, and the gap between the two is exactly where the reporting question lands.

Closing keywordStructured link, closes the issue, visible to the API. This is the one to standardise on.
Branch created from issueLink exists before any text is typed. Highest compliance, lowest effort.
Manual sidebar linkReliable, but depends on someone remembering after the fact.
Plain #123 mentionHuman breadcrumb only. Counts for nothing in a report.
Teams believe they are using the first and are usually relying on the last.

Measure before you intervene

Measure before you intervene. The GitHub CLI will give you the raw material without any custom tooling:

gh pr list --state merged --limit 500 --json number,title,body,author

Count how many bodies contain a closing keyword followed by an issue reference, and exclude your bot authors from the denominator. That ratio, per repository, is your linking rate. It takes ten minutes and it is the only number in this article that matters.

For the structured version rather than the text version, GitHub’s GraphQL API exposes, per pull request, the issues that pull request will close on merge. That is the correct field to build a real report on, because it reflects the relationship GitHub actually recorded rather than what somebody typed. Look up the current field name in the schema explorer rather than copying it from documentation of uncertain vintage.

Then change the default path, not the rules. In order of how much they return:

  1. Create branches from issues. One click, link guaranteed, nothing to remember. If you change one habit, change this one.
  2. Put the keyword in the pull request template. A template with Closes # already on a line by itself converts a memory task into a fill-in-the-blank. .github/pull_request_template.md is the file, and it is a five-minute change with a disproportionate effect.
  3. Make it a required check. A workflow that inspects the pull request body for a closing keyword and fails when it is absent, with an allowlist for bots and a label such as no-issue for the genuine exceptions. The escape hatch is not a weakness, it is what stops people inventing fake issues to get past the gate.
  4. Automate the board. Wire the project so a linked pull request moves the item. Once the link changes what the board shows, developers maintain it because it saves them a drag, not because a policy says so.

That is the whole solution. For teams tracking work in GitHub, there is no gap here worth spending money on, and anyone telling you otherwise is selling something.

Where this breaks down

Closing keywords conflate merging with being done. Merged to trunk is not released, and for teams with a real staging path the auto-close lies about state. The fix is to stop using the keyword’s closing behaviour and use the link only, moving status from deployment events instead.

Required checks generate placeholder issues. Gate the pull request on an issue and you will get one-line issues created thirty seconds before the pull request. That is compliance, not traceability, and it is the honest cost of enforcement.

The ratio flatters repositories with low volume. A repository with nine merges a quarter and one bot update reads as 89 percent and means nothing. Look at absolute counts alongside the ratio.

None of this proves the change matched the issue. A linked pull request tells you a keyword was typed. Whether the code does what the issue asked is a different question entirely, and no linking convention will ever answer it.

The takeaway

GitHub already has everything needed to link work to code. The reason it does not happen is that the mechanism is silent when it fails and optional when it succeeds, so the fix is to make the linked path the easy path and to make the unlinked path noisy.

If you take one thing into next week: add Closes # to your pull request template in your busiest repository. It is a one-line change and it will move your linking rate further than any policy you could write.