Read about our upcoming Code of Conduct on this issue

pipeline.rb 7.87 KB
Newer Older
Douwe Maan's avatar
Douwe Maan committed
1
module Ci
2
  class Pipeline < ActiveRecord::Base
Douwe Maan's avatar
Douwe Maan committed
3
    extend Ci::Model
4
    include HasStatus
5
    include Importable
6
    include AfterCommitQueue
Kamil Trzcinski's avatar
WIP    
Kamil Trzcinski committed
7

Kamil Trzcinski's avatar
Kamil Trzcinski committed
8
9
    self.table_name = 'ci_commits'

10
    belongs_to :project, foreign_key: :gl_project_id
11
12
    belongs_to :user

13
    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
14
15
    has_many :builds, foreign_key: :commit_id
    has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
Douwe Maan's avatar
Douwe Maan committed
16

17
18
19
20
    validates_presence_of :sha, unless: :importing?
    validates_presence_of :ref, unless: :importing?
    validates_presence_of :status, unless: :importing?
    validate :valid_commit_sha, unless: :importing?
Douwe Maan's avatar
Douwe Maan committed
21

22
    after_create :keep_around_commits, unless: :importing?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
23

24
25
    delegate :stages, to: :statuses

26
    state_machine :status, initial: :created do
27
      event :enqueue do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
28
        transition created: :pending
29
        transition [:success, :failed, :canceled, :skipped] => :running
30
31
32
      end

      event :run do
33
        transition any => :running
34
35
      end

36
37
38
39
40
41
42
43
      event :skip do
        transition any => :skipped
      end

      event :drop do
        transition any => :failed
      end

44
45
46
47
48
49
      event :succeed do
        transition any => :success
      end

      event :cancel do
        transition any => :canceled
50
51
      end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
52
53
54
55
      # IMPORTANT
      # Do not add any operations to this state_machine
      # Create a separate worker for each new operation

56
      before_transition [:created, :pending] => :running do |pipeline|
57
        pipeline.started_at = Time.now
58
59
      end

60
      before_transition any => [:success, :failed, :canceled] do |pipeline|
61
        pipeline.finished_at = Time.now
62
63
64
        pipeline.update_duration
      end

65
      after_transition [:created, :pending] => :running do |pipeline|
Kamil Trzcinski's avatar
Kamil Trzcinski committed
66
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
67
68
69
      end

      after_transition any => [:success] do |pipeline|
Kamil Trzcinski's avatar
Kamil Trzcinski committed
70
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
71
72
      end

73
      after_transition [:created, :pending, :running] => :success do |pipeline|
74
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
75
      end
76
77

      after_transition do |pipeline, transition|
78
79
80
81
82
        next if transition.loopback?

        pipeline.run_after_commit do
          PipelineHooksWorker.perform_async(id)
        end
83
      end
84
85
    end

86
    # ref can't be HEAD or SHA, can only be branch/tag name
87
88
    def self.latest_successful_for(ref)
      where(ref: ref).order(id: :desc).success.first
89
90
    end

Douwe Maan's avatar
Douwe Maan committed
91
92
93
94
    def self.truncate_sha(sha)
      sha[0...8]
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
95
    def self.stages
Kamil Trzcinski's avatar
Kamil Trzcinski committed
96
      # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
97
      CommitStatus.where(pipeline: pluck(:id)).stages
Kamil Trzcinski's avatar
Kamil Trzcinski committed
98
99
    end

100
    def self.total_duration
Lin Jen-Shin's avatar
Lin Jen-Shin committed
101
      where.not(duration: nil).sum(:duration)
102
103
    end

104
    def stages_with_latest_statuses
105
      statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
106
107
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
108
109
    def project_id
      project.id
Kamil Trzcinski's avatar
WIP    
Kamil Trzcinski committed
110
111
    end

Douwe Maan's avatar
Douwe Maan committed
112
    def valid_commit_sha
113
      if self.sha == Gitlab::Git::BLANK_SHA
Douwe Maan's avatar
Douwe Maan committed
114
115
116
117
118
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
119
      commit.try(:author_name)
Douwe Maan's avatar
Douwe Maan committed
120
121
122
    end

    def git_author_email
123
      commit.try(:author_email)
Douwe Maan's avatar
Douwe Maan committed
124
125
126
    end

    def git_commit_message
127
      commit.try(:message)
Douwe Maan's avatar
Douwe Maan committed
128
129
    end

130
131
132
133
    def git_commit_title
      commit.try(:title)
    end

Douwe Maan's avatar
Douwe Maan committed
134
    def short_sha
135
      Ci::Pipeline.truncate_sha(sha)
Douwe Maan's avatar
Douwe Maan committed
136
137
    end

138
    def commit
139
      @commit ||= project.commit(sha)
Douwe Maan's avatar
Douwe Maan committed
140
141
142
143
    rescue
      nil
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
144
145
146
147
    def branch?
      !tag?
    end

148
149
    def manual_actions
      builds.latest.manual_actions
150
151
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
152
153
    def retryable?
      builds.latest.any? do |build|
154
        (build.failed? || build.canceled?) && build.retryable?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
155
156
157
      end
    end

158
159
160
161
    def cancelable?
      builds.running_or_pending.any?
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
162
163
164
165
    def cancel_running
      builds.running_or_pending.each(&:cancel)
    end

166
167
168
169
    def retry_failed(user)
      builds.latest.failed.select(&:retryable?).each do |build|
        Ci::Build.retry(build, user)
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
170
171
    end

172
    def mark_as_processable_after_stage(stage_idx)
173
      builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
174
175
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
176
177
178
179
180
181
182
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
183
184
185
186
    def triggered?
      trigger_requests.any?
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
187
188
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
Douwe Maan's avatar
Douwe Maan committed
189
190
191
    end

    def coverage
192
      coverage_array = statuses.latest.map(&:coverage).compact
193
194
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
Douwe Maan's avatar
Douwe Maan committed
195
196
197
      end
    end

198
199
200
201
202
203
204
205
    def config_builds_attributes
      return [] unless config_processor

      config_processor.
        builds_for_ref(ref, tag?, trigger_requests.first).
        sort_by { |build| build[:stage_idx] }
    end

Connor Shea's avatar
Connor Shea committed
206
    def has_warnings?
207
      builds.latest.failed_but_allowed.any?
208
209
    end

Douwe Maan's avatar
Douwe Maan committed
210
    def config_processor
211
      return nil unless ci_yaml_file
212
213
214
215
216
      return @config_processor if defined?(@config_processor)

      @config_processor ||= begin
        Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
      rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
217
        self.yaml_errors = e.message
218
219
        nil
      rescue
220
        self.yaml_errors = 'Undefined error'
221
222
        nil
      end
Douwe Maan's avatar
Douwe Maan committed
223
224
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
225
    def ci_yaml_file
226
227
      return @ci_yaml_file if defined?(@ci_yaml_file)

228
229
230
231
      @ci_yaml_file ||= begin
        blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
        blob.load_all_data!(project.repository)
        blob.data
232
233
      rescue
        nil
234
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
235
236
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
237
238
239
240
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

James Lopez's avatar
James Lopez committed
241
242
243
244
245
246
247
248
249
250
251
252
253
    # Manually set the notes for a Ci::Pipeline
    # There is no ActiveRecord relation between Ci::Pipeline and notes
    # as they are related to a commit sha. This method helps importing
    # them using the +Gitlab::ImportExport::RelationFactory+ class.
    def notes=(notes)
      notes.each do |note|
        note[:id] = nil
        note[:commit_id] = sha
        note[:noteable_id] = self['id']
        note.save!
      end
    end

254
255
256
257
    def notes
      Note.for_commit_id(sha)
    end

258
259
260
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
261

262
    def update_status
263
      with_lock do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
264
        case latest_builds_status
265
266
267
268
269
270
271
        when 'pending' then enqueue
        when 'running' then run
        when 'success' then succeed
        when 'failed' then drop
        when 'canceled' then cancel
        when 'skipped' then skip
        end
272
      end
273
274
    end

275
276
277
278
279
280
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

281
282
283
284
285
286
287
    def queued_duration
      return unless started_at

      seconds = (started_at - created_at).to_i
      seconds unless seconds.zero?
    end

288
    def update_duration
289
290
      return unless started_at

291
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
292
293
294
    end

    def execute_hooks
Kamil Trzcinski's avatar
Kamil Trzcinski committed
295
296
297
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
298
299
    end

300
301
302
    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
    def merge_requests
303
304
305
      @merge_requests ||= project.merge_requests
        .where(source_branch: self.ref)
        .select { |merge_request| merge_request.pipeline.try(:id) == self.id }
306
307
    end

308
309
    private

310
    def pipeline_data
311
      Gitlab::DataBuilder::Pipeline.build(self)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
312
    end
313

314
    def latest_builds_status
315
316
317
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
Kamil Trzcinski's avatar
Kamil Trzcinski committed
318
    end
319
320

    def keep_around_commits
321
      return unless project
322

323
324
325
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
Douwe Maan's avatar
Douwe Maan committed
326
327
  end
end