Set first deployed to production for tag deployments
What does this MR do and why?
A project that deploys production from a tag pipeline never gets "Merge request first deployed to production" on its merge requests. Value stream stages that end on that event stay empty, including the default Staging stage. DORA lead time still has a value.
When a deployment succeeds, GitLab sets the event on merge requests whose target branch equals the deployment's ref. A tag is never a target branch, so a tag deployment matches no merge request. DORA lead time reads the merge requests that LinkMergeRequestsService links to the deployment, so the two disagree on one deployment.
Closes #606520
flowchart TD
A[Production deployment succeeds] --> B{Deployed from a tag?}
B -- no --> C[Take merge requests that target the ref and merged since the previous deployment]
B -- yes --> D[Link the merge requests new to this deployment]
D --> E[Take the linked merge requests]
C --> F[Set the event on those that do not have it yet]
E --> F
classDef changed fill:#fff3cd,stroke:#b58900,color:#333
class D,E changedWhat changed
- A tag deployment to a production environment links its merge requests from the deployed commits, as DORA does, and sets the event on the linked ones.
Deployments::UpdateEnvironmentServicedoes the linking andDeployment#update_merge_request_metrics!picks the linked merge requests.Deployments::LinkMergeRequestWorkerruns the same service for the deployment. Its inserts skip rows already linked. - With no previous deployment,
Deployments::LinkMergeRequestsServicelinks the merged merge requests of the default branch when it contains the tag commit. It looked for a branch named like the tag and found none. As for a branch deployment, this first link takes every merged merge request it finds. - DORA lead time for an environment's first tag deployment counts the merge requests this link finds. Before, that deployment linked none.
- Branch deployments take the same path as before.
- The value stream analytics page said production environments are found by name patterns. The code reads the deployment tier, so the page now says that and what a tag deployment counts.
Queries
A tag deployment replaces the target branch and previous deployment conditions of the existing update with a lookup of the deployment's linked merge requests by the primary key of deployment_merge_requests:
UPDATE "merge_request_metrics" SET "first_deployed_to_production_at" = '2026-10-19 03:25:52.882238'
WHERE "merge_request_metrics"."merge_request_id" IN (
SELECT "merge_requests"."id" FROM "merge_requests"
INNER JOIN "merge_request_metrics" ON "merge_request_metrics"."merge_request_id" = "merge_requests"."id"
WHERE "merge_requests"."target_project_id" = 7
AND "merge_request_metrics"."target_project_id" = 7
AND "merge_request_metrics"."first_deployed_to_production_at" IS NULL
AND (merge_request_metrics.merged_at <= '2026-10-19 03:25:52.882238')
AND "merge_requests"."id" IN (
SELECT "deployment_merge_requests"."merge_request_id" FROM "deployment_merge_requests"
WHERE "deployment_merge_requests"."deployment_id" = 19))
AND "merge_request_metrics"."first_deployed_to_production_at" IS NULLThe plan below is from Database Lab (gitlab-production-main) for a gitlab-org/gitlab deployment with 9 linked merge requests. The deployment_merge_requests primary key drives the lookup. The update took 135 ms on a cold clone and 0.2 ms with the pages cached. None of the 9 matched first_deployed_to_production_at IS NULL, so the update touched no rows.
Plan
Update on merge_request_metrics (cost=7247.44..7250.36 rows=0 width=0)
-> Nested Loop (cost=7247.44..7250.36 rows=1 width=32)
Join Filter: (merge_request_metrics_1.merge_request_id = merge_request_metrics.merge_request_id)
-> HashAggregate (cost=7246.86..7246.87 rows=1 width=38)
Group Key: merge_requests.id
-> Nested Loop (cost=1.72..7246.86 rows=1 width=38)
-> Nested Loop (cost=1.15..7243.38 rows=1 width=28)
-> Index Scan using deployment_merge_requests_pkey on deployment_merge_requests (cost=0.57..1481.01 rows=1604 width=14)
Index Cond: (deployment_id = 1626295851)
-> Index Scan using merge_requests_pkey on merge_requests (cost=0.57..3.59 rows=1 width=14)
Index Cond: (id = deployment_merge_requests.merge_request_id)
Filter: (target_project_id = 278964)
-> Index Scan using unique_merge_request_metrics_by_merge_request_id on merge_request_metrics merge_request_metrics_1 (cost=0.57..3.48 rows=1 width=10)
Index Cond: (merge_request_id = merge_requests.id)
Filter: ((first_deployed_to_production_at IS NULL) AND (target_project_id = 278964) AND (merged_at <= now()))
-> Index Scan using unique_merge_request_metrics_by_merge_request_id on merge_request_metrics (cost=0.57..3.47 rows=1 width=10)
Index Cond: (merge_request_id = merge_requests.id)
Filter: (first_deployed_to_production_at IS NULL)References
How to set up and validate locally
On a project with a main branch, add a job that runs only for tags and deploys to a production environment:
deploy:
rules:
- if: $CI_COMMIT_TAG
environment:
name: production
script:
- echo deploy- Merge a merge request into
main, create the tagv1.0.0frommainand wait for the tag pipeline to deploy. - Merge a second merge request, create the tag
v1.1.0, merge a third merge request and wait for the second deployment. - Read
first_deployed_to_production_atfromGET /projects/:id/merge_requests/:iidfor each merge request after each deployment.
| Merge request | Before | With this change |
|---|---|---|
Merged before v1.0.0 |
null |
set when v1.0.0 deploys |
Merged before v1.1.0, after v1.0.0 |
null |
set when v1.1.0 deploys |
Merged before v1.0.0, read after v1.1.0 |
null |
unchanged since v1.0.0 |
Merged after v1.1.0 was created |
null |
null |
A first deployment from a tag on a branch that is ahead of main leaves the event null. A deployment from main sets the event as before. An environment named live counts as production, and production with deployment_tier: staging does not.
Run the specs:
bundle exec rspec spec/models/deployment_spec.rb spec/services/deploymentsMR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
An AI coding assistant wrote this change. It ran the changed spec files and the other specs that touch deployments, merge request metrics and value stream stage events, RuboCop, and a mutation pass over the changed lines on test VMs. It ran Vale and markdownlint on the changed page. It ran the tag, branch and environment scenarios above on a GitLab 19.5.0 test instance with this change loaded, and the query plan on Database Lab.