Linking GitLab work to code, reliably
GitLab links issues to merge requests better than any other combination on the market. Here is how the mechanism works, the four ways it still slips, and how to measure the gap.
You are trying to answer a simple question: which issues actually shipped in the last release. You have the merge requests, you have the issues, and the mapping between them is roughly 70 percent complete, which is the worst possible number because it is high enough to look usable and low enough to be wrong.
I will say this plainly at the top, because it is true and because you will not hear it often from a vendor: if your issues and your code both live in GitLab, GitLab solves this problem completely on its own. There is nothing to buy. What there is, is a set of mechanisms that most teams use only partly, and a measurement nobody takes.
Real relationships, not text that happens to match
Issues and merge requests are objects in the same project, so the links between them are real relationships rather than text matches that happen to have worked.
Closing patterns. A phrase such as Closes #123 in a merge request description creates a structured relationship and closes the issue when the merge request merges into the default branch. GitLab shows the affected issues on the merge request itself before you merge, which is a small thing that matters a lot: it is the only feedback in this whole area that appears before the mistake rather than after it. If the merge request does not show a closing issue, the link is not there, and you can see that while you still care.
The set of accepted closing words is configurable at the instance level in self-managed installations, so if a team insists Implements should count, that is a conversation with your administrator rather than an impossibility.
Plain references. A bare #123, or group/project#123 across projects, creates a cross-reference note on the issue. It is a breadcrumb for humans. It does not create the closing relationship and it will not appear in anything you query for structured links. Teams confuse the two constantly and then wonder why the report is short.
Create branch and merge request from the issue. From the issue itself you can spin up a branch, or a branch and a draft merge request together, and GitLab pre-fills the closing pattern for you. This is the single highest-value habit available here, because the link is created before any human has a chance to forget it, and the generated branch name carries the issue number, which means the reference survives even if somebody rewrites the description later.
Related issues and linked items. Beyond closing, GitLab can record that two items block or relate to each other. Useful for dependency mapping, not a substitute for the closing relationship when you are trying to answer what shipped.
Merge request templates and description templates, committed into the repository, so the closing line is present in every new merge request as a prompt rather than a memory test.
The API. GitLab exposes, for a given merge request, the issues that merge request will close. That is the correct source for a linking-rate report, because it reflects the relationship GitLab recorded rather than a regular expression over text you hope is right. Check the current endpoint in your instance’s API documentation, since the surface has grown across versions.
What most teams do
- Create the branch in a terminal
- Reference the issue with a bare number, if at all
- Close issues by hand at the end of the sprint
- Never look at the ratio
What the tool is built for
- Branch and draft merge request created from the issue
- Closing pattern pre-filled and visible before merge
- Issue closes on merge, automatically
- Ratio queried from the API, monthly
Four conditions account for nearly all the misses
Four conditions, and only four, account for nearly all of the missing 30 percent.
The work started in a terminal. A branch cut locally and pushed with -o merge_request.create or through the returned link produces a merge request with a blank description and no closing pattern. Everything downstream follows from that one moment.
The issue lives in another project. Cross-project closing works with the full reference, but it is typed by hand far more often than it is generated, and typed things get skipped. Group-level work tracked in one project while the code sits in ten is the most common shape of this problem, and it is exactly the shape that senior people ask questions about.
Squash rewrote the history. The closing pattern lives on the merge request description, so squashing is safe for the relationship. But if your team relied on the pattern being in commit messages, squash collapses them into one generated message, and what survives depends on what the merger typed in that dialog.
Bots and non-issue work. Dependency updates, security patches, generated files. These merge without issues and they belong in the denominator only if you decide they do.
Notice what is not on this list: GitLab itself failing. The mechanism is sound. The gap is entirely in where work starts and whether anyone counts.
Get the number out of the API
Get the number. Pull merged merge requests for a project over the last quarter through the API, and for each one ask GitLab which issues it closes. The ratio of merge requests with at least one closing issue, excluding your service accounts, is your linking rate. Do it per project. Aggregated across an estate it hides exactly the project that is broken.
Then, cheapest first:
- Move the start of work into the issue. Make “create branch from the issue” the documented first step, not an alternative to the terminal. This is a habit change with no configuration and it removes the dominant failure mode outright.
- Commit a merge request description template containing the closing line as a placeholder. It costs one file and converts recall into completion.
- Add a pipeline job that checks it. A job that queries the merge request for closing issues and fails when there are none, allowlisting bot accounts and a documented exception label. Make it a required job on protected branches only, so experimentation is not taxed.
- For cross-project work, standardise the full reference and put it in the template as a comment, because the full form is the part people get wrong.
That is the whole thing. If your issues are in GitLab and your code is in GitLab, you now have complete, structured traceability using features you are already paying for, and you should not let anyone sell you a layer on top of it.
Where this breaks down
Auto-close is not auto-done. Merging to the default branch is not the same as being live, and for teams with a real staging pipeline the auto-closed issue is a lie by a few days. If that gap matters to you, use the link and disable the closing behaviour, driving status from deployment events instead.
A required pipeline job punishes the wrong people. The person who forgot the closing line is usually mid-incident. Build the exception label before you build the check, and make sure using it is not socially expensive.
Cross-project references are fragile by nature. Move a project between groups and the full references in old descriptions are historical text. GitLab handles the redirect for navigation; your regular expressions will not.
Linking rate is hygiene, not quality. A merge request that closes an issue proves a relationship was recorded. Whether the code satisfies what the issue asked for is a separate question, and no amount of linking discipline will answer it.
The takeaway
GitLab is the rare case where the native answer is the complete answer. The missing links are not a tooling gap, they are a consequence of work starting in a terminal instead of in an issue, and of nobody ever measuring the result.
If you take one thing into next week: pick your busiest project, query the merged merge requests for their closing issues, and put the ratio next to the team’s sprint completion percentage. If those two numbers disagree, one of them is fiction.