pipeline.rb 8.3 KB
Newer Older
D
Douwe Maan 已提交
1
module Ci
2
  class Pipeline < ActiveRecord::Base
D
Douwe Maan 已提交
3
    extend Ci::Model
4
    include HasStatus
5
    include Importable
6
    include AfterCommitQueue
K
WIP  
Kamil Trzcinski 已提交
7

K
Kamil Trzcinski 已提交
8 9
    self.table_name = 'ci_commits'

K
Kamil Trzcinski 已提交
10
    belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
11 12
    belongs_to :user

13 14
    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
    has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
15
    has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
D
Douwe Maan 已提交
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?
D
Douwe Maan 已提交
21

22
    after_save :keep_around_commits, unless: :importing?
K
Kamil Trzcinski 已提交
23

24 25
    delegate :stages, to: :statuses

26
    state_machine :status, initial: :created do
27
      event :enqueue do
K
Kamil Trzcinski 已提交
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

52
      before_transition [:created, :pending] => :running do |pipeline|
53
        pipeline.started_at = Time.now
54 55
      end

56
      before_transition any => [:success, :failed, :canceled] do |pipeline|
57
        pipeline.finished_at = Time.now
58 59
      end

60 61 62 63
      before_transition do |pipeline|
        pipeline.update_duration
      end

64
      after_transition [:created, :pending] => :running do |pipeline|
65 66
        MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
          update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
67 68 69
      end

      after_transition any => [:success] do |pipeline|
70 71
        MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
          update_all(latest_build_finished_at: pipeline.finished_at)
72 73
      end

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

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

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

86 87
      after_transition any => [:success, :failed] do |pipeline|
        SendPipelineNotificationWorker.perform_async(pipeline.id)
88
      end
89 90
    end

91
    # ref can't be HEAD or SHA, can only be branch/tag name
92 93
    def self.latest_successful_for(ref)
      where(ref: ref).order(id: :desc).success.first
94 95
    end

D
Douwe Maan 已提交
96 97 98 99
    def self.truncate_sha(sha)
      sha[0...8]
    end

K
Kamil Trzcinski 已提交
100
    def self.stages
K
Kamil Trzcinski 已提交
101
      # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
102
      CommitStatus.where(pipeline: pluck(:id)).stages
K
Kamil Trzcinski 已提交
103 104
    end

105
    def self.total_duration
L
Lin Jen-Shin 已提交
106
      where.not(duration: nil).sum(:duration)
107 108
    end

109
    def stages_with_latest_statuses
110
      statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
111 112
    end

K
Kamil Trzcinski 已提交
113 114
    def project_id
      project.id
K
WIP  
Kamil Trzcinski 已提交
115 116
    end

L
Lin Jen-Shin 已提交
117
    # For now the only user who participates is the user who triggered
118
    def participants(_current_user = nil)
119
      [user].compact
120 121
    end

D
Douwe Maan 已提交
122
    def valid_commit_sha
123
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
124 125 126 127 128
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
129
      commit.try(:author_name)
D
Douwe Maan 已提交
130 131 132
    end

    def git_author_email
133
      commit.try(:author_email)
D
Douwe Maan 已提交
134 135 136
    end

    def git_commit_message
137
      commit.try(:message)
D
Douwe Maan 已提交
138 139
    end

140 141 142 143
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
144
    def short_sha
145
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
146 147
    end

148
    def commit
K
Kamil Trzcinski 已提交
149
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
150 151 152 153
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
154 155 156 157
    def branch?
      !tag?
    end

158 159
    def manual_actions
      builds.latest.manual_actions
160 161
    end

K
Kamil Trzcinski 已提交
162 163
    def retryable?
      builds.latest.any? do |build|
K
Kamil Trzcinski 已提交
164
        build.failed? && build.retryable?
K
Kamil Trzcinski 已提交
165 166 167
      end
    end

168 169 170 171
    def cancelable?
      builds.running_or_pending.any?
    end

K
Kamil Trzcinski 已提交
172 173 174 175
    def cancel_running
      builds.running_or_pending.each(&:cancel)
    end

176 177 178 179
    def retry_failed(user)
      builds.latest.failed.select(&:retryable?).each do |build|
        Ci::Build.retry(build, user)
      end
K
Kamil Trzcinski 已提交
180 181
    end

182
    def mark_as_processable_after_stage(stage_idx)
183
      builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
184 185
    end

K
Kamil Trzcinski 已提交
186 187 188 189 190 191 192
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
193 194 195 196
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
197 198
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
199 200 201
    end

    def coverage
202
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
203 204
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
205 206 207
      end
    end

208 209 210 211 212 213 214 215
    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

C
Connor Shea 已提交
216
    def has_warnings?
217
      builds.latest.failed_but_allowed.any?
218 219
    end

D
Douwe Maan 已提交
220
    def config_processor
221
      return nil unless ci_yaml_file
222 223 224 225 226
      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
227
        self.yaml_errors = e.message
228 229
        nil
      rescue
230
        self.yaml_errors = 'Undefined error'
231 232
        nil
      end
D
Douwe Maan 已提交
233 234
    end

K
Kamil Trzcinski 已提交
235
    def ci_yaml_file
236 237
      return @ci_yaml_file if defined?(@ci_yaml_file)

238 239 240 241
      @ci_yaml_file ||= begin
        blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
        blob.load_all_data!(project.repository)
        blob.data
242 243
      rescue
        nil
244
      end
K
Kamil Trzcinski 已提交
245 246
    end

K
Kamil Trzcinski 已提交
247 248 249 250
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
251 252 253 254 255 256 257 258 259 260 261 262 263
    # 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

264 265 266 267
    def notes
      Note.for_commit_id(sha)
    end

268 269 270
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
271

272
    def update_status
273
      with_lock do
K
Kamil Trzcinski 已提交
274
        case latest_builds_status
275 276 277 278 279 280 281
        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
282
      end
283 284
    end

285 286 287 288 289 290
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

291 292 293 294 295 296 297
    def queued_duration
      return unless started_at

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

298
    def update_duration
299 300
      return unless started_at

301
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
302 303 304
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
305 306 307
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
308 309
    end

310 311 312
    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
    def merge_requests
313 314 315
      @merge_requests ||= project.merge_requests
        .where(source_branch: self.ref)
        .select { |merge_request| merge_request.pipeline.try(:id) == self.id }
316 317
    end

318 319
    private

320
    def pipeline_data
321
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
322
    end
323

324
    def latest_builds_status
325 326 327
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
328
    end
329 330

    def keep_around_commits
331
      return unless project
332

333 334 335
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
336 337
  end
end