Posting external commit status changes merge request's "head_pipeline" from detached to non-detached
Summary
If a commit/merge request has both detached and non-detached pipeline when using /projects/:id/statuses/:sha
to add external build status it, the associated pipeline will change from the detached pipeline to the non-detached (which contains the external build status).
This is related to #14064 (closed) but don't think it is a duplicate.
Steps to reproduce
Create a project with the following gitlab-ci.yml file:
stages:
- test
always-failing-on-mr:
stage: test
only:
refs:
- merge_requests
script:
- "false"
always-fine-on-branches:
stage: test
only:
refs:
- branches
script:
- "true"
Create a new commit on another branch. Create a merge request for merging that branch.
Let the defined CI jobs finish.
See that it is the failing merge-request pipeline that is shown in merge request ui.
Post external build status on the commit.
Notice how the merge request now shows the non-merge-request pipeline instead.
Example Project
Not a project, but the a bunch of API calls to automate it:
import requests
import random
import subprocess
GITLAB_HOST = "http://127.0.0.1:3000"
ACCESS_TOKEN = 'XYZXYZ'
CI_CONFIG = """
stages:
- test
always-failing-on-mr:
stage: test
only:
refs:
- merge_requests
script:
- "true || false"
always-fine-on-branches:
stage: test
only:
refs:
- branches
script:
- "true"
"""
## CREATE NEW PROJECT
r = requests.post(GITLAB_HOST + "/api/v4/projects",
headers={'Authorization': 'bearer %s' % ACCESS_TOKEN},
json={
'name': 'test-project-%d' % random.randint(1000, 2000),
'auto_devops_enabled': False,
'only_allow_merge_if_pipeline_succeeds': True,
})
assert r.status_code == 201
res = r.json()
print(res)
projid = res['id']
regtoken = res['runners_token']
## create initial commit
r = requests.post(GITLAB_HOST + "/api/v4/projects/%d/repository/commits" % projid,
headers={'Authorization': 'bearer %s' % ACCESS_TOKEN},
json={
'branch': 'master',
'startsha': '0000000000000000000000000000000000000000',
'commit_message': 'Initial commit',
'actions': [
{
'action': 'create',
'file_path': 'README.md',
'content': 'Yeah, read me now!'
},
{
'action': 'create',
'file_path': '.gitlab-ci.yml',
'content': CI_CONFIG
},
],
})
print(r.json())
## Create new commit on new branch
r = requests.post(GITLAB_HOST + "/api/v4/projects/%d/repository/commits" % projid,
headers={'Authorization': 'bearer %s' % ACCESS_TOKEN},
json={
'branch': 'master-update',
'start_branch': 'master',
'commit_message': 'Update',
'actions': [
{
'action': 'create',
'file_path': 'Test',
'content': 'test'
},
],
})
print(r.json())
MR_SHA = r.json()['id']
## Create merge request
r = requests.post(GITLAB_HOST + "/api/v4/projects/%d/merge_requests" % projid,
headers={'Authorization': 'bearer %s' % ACCESS_TOKEN},
json={
'source_branch': 'master-update',
'target_branch': 'master',
'title': 'A new merge request',
})
print(r.json())
print("Here is the registration token in case you don't have shared runners: %s" % regtoken)
print("press enter when pipeline have finished")
raw_input()
r = requests.post(GITLAB_HOST + "/api/v4/projects/%d/statuses/%s" % (projid, MR_SHA),
headers={'Authorization': 'bearer %s' % ACCESS_TOKEN},
json={
'state': 'success',
#'state': 'failed',
'ref': 'master-update',
'name': 'my-external-status',
'description': 'External status',
})
print(r.json())
What is the current bug behavior?
Which pipeline that is the "primary pipeline" associated with the merge request that is shown on the merge request changes when external commit status is posted.
What is the expected correct behavior?
I'm not 100% sure, but it shouldn't change like that at least.
The exact semantics of pipelines in merge request when having both "only: merge_requests" and "only: branches" is a bit unclear from documentation.
Now only the ones in the detached pipeline (i.e "only: merge_requests") will prevent merging if they fail and only_allow_merge_if_pipeline_succeeds is set.
Relevant logs and/or screenshots
How the merge request looks before posting external commit status:
How the merge request looks after posting external commit status:
Possible fixes
I believe the problem is caused by this section in lib/api/commit_statuses.rb
MergeRequest.where(source_project: @project, source_branch: ref)
.update_all(head_pipeline_id: pipeline.id) if pipeline.latest?
which unconditionally updates head_pipeline
if it is marked as latest
. Something that doesn't seem quite right.
I get rid of this problem by instead of setting head_pipeline_id directly call update_head_pipeline on the merge request which has knowledge of different types of pipelines.
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -117,8 +117,9 @@ module API
render_api_error!('invalid state', 400)
end
- MergeRequest.where(source_project: @project, source_branch: ref)
- .update_all(head_pipeline_id: pipeline.id) if pipeline.latest?
+ MergeRequest.where(source_project: @project, source_branch: ref).each do |mr|
+ mr.update_head_pipeline
+ end
present status, with: Entities::CommitStatus
rescue StateMachines::InvalidTransition => e
But this still doesn't solve what to do when there are multiple associated pipelines with an merge request. If the detached is successful but the "latest" isn't, I would expect merges to be prevented.