From 6a4c87d6db68e2536b90b38c1e11f9a158bd7480 Mon Sep 17 00:00:00 2001
From: Hordur Freyr Yngvason <hfyngvason@gitlab.com>
Date: Tue, 26 Mar 2024 21:22:07 +0000
Subject: [PATCH] Fix commit status API incorrectly overwriting MR head
 pipeline

The commit status API would sometimes incorrectly overwrite the merge
request head pipeline with a branch pipeline when a merge request
pipeline also existed for the same commit.

This was due to a duplicate and incorrect implementation of the required
logic. The solution is to instead fire a `Ci::PipelineCreatedEvent` _if_
an `external` pipeline is created, as this is the canonical way to
trigger the `head_pipeline_id` update. There is no need to update the
`head_pipeline_id` for pre-existing pipelines, as these would already
have fired the required events.

See https://gitlab.com/gitlab-org/gitlab/-/issues/450176

Changelog: fixed
---
 .../ci/create_commit_status_service.rb        | 13 +++-----
 spec/requests/api/commit_statuses_spec.rb     |  6 ++--
 .../ci/create_commit_status_service_spec.rb   | 32 +++++++++++++++++--
 3 files changed, 38 insertions(+), 13 deletions(-)

diff --git a/app/services/ci/create_commit_status_service.rb b/app/services/ci/create_commit_status_service.rb
index de3e7b3f7ffae..24a9130e7acf0 100644
--- a/app/services/ci/create_commit_status_service.rb
+++ b/app/services/ci/create_commit_status_service.rb
@@ -36,7 +36,6 @@ def unsafe_execute
 
       return bad_request(response.message) if response.error?
 
-      update_merge_request_head_pipeline
       response
     end
 
@@ -73,6 +72,10 @@ def create_pipeline
       ).tap do |new_pipeline|
         new_pipeline.ensure_project_iid!
         new_pipeline.save!
+
+        Gitlab::EventStore.publish(
+          Ci::PipelineCreatedEvent.new(data: { pipeline_id: new_pipeline.id })
+        )
       end
     end
 
@@ -106,14 +109,6 @@ def add_or_update_external_job
       end
     end
 
-    def update_merge_request_head_pipeline
-      return unless pipeline.latest?
-
-      ::MergeRequest
-        .from_project(project).from_source_branches(ref)
-        .update_all(head_pipeline_id: pipeline.id)
-    end
-
     def apply_job_state!(job)
       case params[:state]
       when 'pending'
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 97e4f8f16b450..24dc414710dc3 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -293,9 +293,11 @@ def create_status(commit, opts = {})
           end
 
           context 'when merge request exists for given branch' do
-            let!(:merge_request) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'develop') }
+            let!(:merge_request) do
+              create(:merge_request, source_project: project, head_pipeline_id: nil)
+            end
 
-            it 'sets head pipeline' do
+            it 'sets head pipeline', :sidekiq_inline do
               subject
 
               expect(response).to have_gitlab_http_status(:created)
diff --git a/spec/services/ci/create_commit_status_service_spec.rb b/spec/services/ci/create_commit_status_service_spec.rb
index cbdb681afe31b..67989347d4e0d 100644
--- a/spec/services/ci/create_commit_status_service_spec.rb
+++ b/spec/services/ci/create_commit_status_service_spec.rb
@@ -112,10 +112,10 @@
 
       context 'when merge request exists for given branch' do
         let!(:merge_request) do
-          create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'develop')
+          create(:merge_request, source_project: project, head_pipeline: nil)
         end
 
-        it 'sets head pipeline' do
+        it 'sets head pipeline', :sidekiq_inline do
           expect { response }
             .to change { ::Ci::Pipeline.count }.by(1)
             .and change { ::Ci::Stage.count }.by(1)
@@ -124,6 +124,34 @@
           expect(response).to be_success
           expect(merge_request.reload.head_pipeline).not_to be_nil
         end
+
+        context 'when the MR has a branch head pipeline' do
+          let!(:merge_request) do
+            create(:merge_request, :with_head_pipeline, source_project: project)
+          end
+
+          it 'adds the status to the existing pipeline' do
+            expect { response }.not_to change { ::Ci::Pipeline.count }
+            expect(response.payload[:job].pipeline_id).to eq(merge_request.head_pipeline_id)
+          end
+        end
+
+        context 'when the MR has a merged result head pipeline' do
+          let!(:merge_request) do
+            create(:merge_request, source_project: project, head_pipeline: head_pipeline)
+          end
+
+          let(:head_pipeline) { create(:ci_pipeline, :merged_result_pipeline) }
+
+          it 'creates a new branch pipeline but does not change the head pipeline' do
+            expect { response }
+              .to change { ::Ci::Pipeline.count }.by(1)
+              .and change { ::Ci::Stage.count }.by(1)
+              .and change { ::CommitStatus.count }.by(1)
+
+            expect(merge_request.reload.head_pipeline_id).to eq(head_pipeline.id)
+          end
+        end
       end
     end
 
-- 
GitLab