summaryrefslogtreecommitdiff
path: root/app/services/ci/create_pipeline_service.rb
blob: 884b681ff81c9f07554f37f80b11e72a45d8c7eb (plain)
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
module Ci
  class CreatePipelineService < BaseService
    attr_reader :pipeline

    def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil)
      @pipeline = Ci::Pipeline.new(
        source: source,
        project: project,
        ref: ref,
        sha: sha,
        before_sha: before_sha,
        tag: tag?,
        trigger_requests: Array(trigger_request),
        user: current_user,
        pipeline_schedule: schedule
      )

      result = validate(current_user || trigger_request.trigger.owner,
                        ignore_skip_ci: ignore_skip_ci,
                        save_on_errors: save_on_errors)

      return result if result

      begin
        Ci::Pipeline.transaction do
          pipeline.save!

          yield(pipeline) if block_given?

          Ci::CreatePipelineStagesService
            .new(project, current_user)
            .execute(pipeline)
        end
      rescue ActiveRecord::RecordInvalid => e
        return error("Failed to persist the pipeline: #{e}")
      end

      update_merge_requests_head_pipeline

      cancel_pending_pipelines if project.auto_cancel_pending_pipelines?

      pipeline_created_counter.increment(source: source)

      pipeline.tap(&:process!)
    end

    private

    def validate(triggering_user, ignore_skip_ci:, save_on_errors:)
      unless project.builds_enabled?
        return error('Pipeline is disabled')
      end

      unless allowed_to_trigger_pipeline?(triggering_user)
        if can?(triggering_user, :create_pipeline, project)
          return error("Insufficient permissions for protected ref '#{ref}'")
        else
          return error('Insufficient permissions to create a new pipeline')
        end
      end

      unless branch? || tag?
        return error('Reference not found')
      end

      unless commit
        return error('Commit not found')
      end

      unless pipeline.config_processor
        unless pipeline.ci_yaml_file
          return error("Missing #{pipeline.ci_yaml_file_path} file")
        end
        return error(pipeline.yaml_errors, save: save_on_errors)
      end

      if !ignore_skip_ci && skip_ci?
        pipeline.skip if save_on_errors
        return pipeline
      end

      unless pipeline.has_stage_seeds?
        return error('No stages / jobs for this pipeline.')
      end
    end

    def allowed_to_trigger_pipeline?(triggering_user)
      if triggering_user
        allowed_to_create?(triggering_user)
      else # legacy triggers don't have a corresponding user
        !project.protected_for?(ref)
      end
    end

    def allowed_to_create?(triggering_user)
      access = Gitlab::UserAccess.new(triggering_user, project: project)

      can?(triggering_user, :create_pipeline, project) &&
        if branch?
          access.can_update_branch?(ref)
        elsif tag?
          access.can_create_tag?(ref)
        else
          true # Allow it for now and we'll reject when we check ref existence
        end
    end

    def update_merge_requests_head_pipeline
      return unless pipeline.latest?

      MergeRequest.where(source_project: @pipeline.project, source_branch: @pipeline.ref)
        .update_all(head_pipeline_id: @pipeline.id)
    end

    def skip_ci?
      return false unless pipeline.git_commit_message
      pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
    end

    def cancel_pending_pipelines
      Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables|
        cancelables.find_each do |cancelable|
          cancelable.auto_cancel_running(pipeline)
        end
      end
    end

    def auto_cancelable_pipelines
      project.pipelines
        .where(ref: pipeline.ref)
        .where.not(id: pipeline.id)
        .where.not(sha: project.repository.sha_from_ref(pipeline.ref))
        .created_or_pending
    end

    def commit
      @commit ||= project.commit(origin_sha || origin_ref)
    end

    def sha
      commit.try(:id)
    end

    def before_sha
      params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA
    end

    def origin_sha
      params[:checkout_sha] || params[:after]
    end

    def origin_ref
      params[:ref]
    end

    def branch?
      return @is_branch if defined?(@is_branch)

      @is_branch =
        project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref)
    end

    def tag?
      return @is_tag if defined?(@is_tag)

      @is_tag =
        project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref)
    end

    def ref
      @ref ||= Gitlab::Git.ref_name(origin_ref)
    end

    def valid_sha?
      origin_sha && origin_sha != Gitlab::Git::BLANK_SHA
    end

    def error(message, save: false)
      pipeline.errors.add(:base, message)
      pipeline.drop if save
      pipeline
    end

    def pipeline_created_counter
      @pipeline_created_counter ||= Gitlab::Metrics.counter(:pipelines_created_total, "Counter of pipelines created")
    end
  end
end