1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
# frozen_string_literal: true
module Ci
class CreateCommitStatusService < BaseService
include ::Gitlab::ExclusiveLeaseHelpers
include ::Gitlab::Utils::StrongMemoize
include ::Services::ReturnServiceResponses
delegate :sha, to: :commit
def execute(optional_commit_status_params:)
in_lock(pipeline_lock_key, **pipeline_lock_params) do
@optional_commit_status_params = optional_commit_status_params
unsafe_execute
end
end
private
attr_reader :pipeline, :stage, :commit_status, :optional_commit_status_params
def unsafe_execute
return not_found('Commit') if commit.blank?
return bad_request('State is required') if params[:state].blank?
return not_found('References for commit') if ref.blank?
@pipeline = first_matching_pipeline || create_pipeline
return forbidden unless ::Ability.allowed?(current_user, :update_pipeline, pipeline)
@stage = find_or_create_external_stage
@commit_status = find_or_build_external_commit_status
return bad_request(commit_status.errors.messages) if commit_status.invalid?
response = add_or_update_external_job
return bad_request(response.message) if response.error?
update_merge_request_head_pipeline
response
end
def ref
params[:ref] || first_matching_pipeline&.ref ||
repository.branch_names_contains(sha).first
end
strong_memoize_attr :ref
def commit
project.commit(params[:sha])
end
strong_memoize_attr :commit
def first_matching_pipeline
pipelines = project.ci_pipelines.newest_first(sha: sha)
pipelines = pipelines.for_ref(params[:ref]) if params[:ref]
pipelines = pipelines.id_in(params[:pipeline_id]) if params[:pipeline_id]
pipelines.first
end
strong_memoize_attr :first_matching_pipeline
def name
params[:name] || params[:context] || 'default'
end
def create_pipeline
project.ci_pipelines.build(
source: :external,
sha: sha,
ref: ref,
user: current_user,
protected: project.protected_for?(ref)
).tap do |new_pipeline|
new_pipeline.ensure_project_iid!
new_pipeline.save!
end
end
def find_or_create_external_stage
pipeline.stages.safe_find_or_create_by!(name: 'external') do |stage| # rubocop:disable Performance/ActiveRecordSubtransactionMethods
stage.position = ::GenericCommitStatus::EXTERNAL_STAGE_IDX
stage.project = project
end
end
def find_or_build_external_commit_status
::GenericCommitStatus.running_or_pending.find_or_initialize_by( # rubocop:disable CodeReuse/ActiveRecord
project: project,
pipeline: pipeline,
name: name,
ref: ref,
user: current_user,
protected: project.protected_for?(ref),
ci_stage: stage,
stage_idx: stage.position,
stage: 'external'
).tap do |new_commit_status|
new_commit_status.assign_attributes(optional_commit_status_params)
end
end
def add_or_update_external_job
::Ci::Pipelines::AddJobService.new(pipeline).execute!(commit_status) do |job|
apply_job_state!(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'
job.enqueue!
when 'running'
job.enqueue
job.run!
when 'success'
job.success!
when 'failed'
job.drop!(:api_failure)
when 'canceled'
job.cancel!
else
raise('invalid state')
end
end
def pipeline_lock_key
"api:commit_statuses:project:#{project.id}:sha:#{params[:sha]}"
end
def pipeline_lock_params
{
ttl: 5.seconds,
sleep_sec: 0.1.seconds,
retries: 20
}
end
def not_found(message)
error("404 #{message} Not Found", :not_found)
end
def bad_request(message)
error(message, :bad_request)
end
def forbidden
error("403 Forbidden", :forbidden)
end
end
end
|