pipeline.rb 8.0 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_create :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

K
Kamil Trzcinski 已提交
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|
K
Kamil Trzcinski 已提交
66
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
67 68 69
      end

      after_transition any => [:success] do |pipeline|
K
Kamil Trzcinski 已提交
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

D
Douwe Maan 已提交
91 92 93 94
    def self.truncate_sha(sha)
      sha[0...8]
    end

K
Kamil Trzcinski 已提交
95
    def self.stages
K
Kamil Trzcinski 已提交
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
K
Kamil Trzcinski 已提交
98 99
    end

100
    def self.total_duration
L
Lin Jen-Shin 已提交
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

K
Kamil Trzcinski 已提交
108 109
    def project_id
      project.id
K
WIP  
Kamil Trzcinski 已提交
110 111
    end

D
Douwe Maan 已提交
112
    def valid_commit_sha
113
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
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)
D
Douwe Maan 已提交
120 121 122
    end

    def git_author_email
123
      commit.try(:author_email)
D
Douwe Maan 已提交
124 125 126
    end

    def git_commit_message
127
      commit.try(:message)
D
Douwe Maan 已提交
128 129
    end

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

D
Douwe Maan 已提交
134
    def short_sha
135
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
136 137
    end

138
    def commit
K
Kamil Trzcinski 已提交
139
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
140 141 142 143
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
144 145 146 147
    def branch?
      !tag?
    end

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

K
Kamil Trzcinski 已提交
152 153
    def retryable?
      builds.latest.any? do |build|
154
        (build.failed? || build.canceled?) && build.retryable?
K
Kamil Trzcinski 已提交
155 156 157
      end
    end

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

K
Kamil Trzcinski 已提交
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
K
Kamil Trzcinski 已提交
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

K
Kamil Trzcinski 已提交
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

K
Kamil Trzcinski 已提交
183 184 185 186
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
187 188
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
189 190 191
    end

    def coverage
192
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
193 194
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
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

C
Connor Shea 已提交
206
    def has_warnings?
207
      builds.latest.failed_but_allowed.any?
208 209
    end

D
Douwe Maan 已提交
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
D
Douwe Maan 已提交
223 224
    end

K
Kamil Trzcinski 已提交
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
K
Kamil Trzcinski 已提交
235 236
    end

K
Kamil Trzcinski 已提交
237 238 239 240
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
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
K
Kamil Trzcinski 已提交
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
K
Kamil Trzcinski 已提交
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)
K
Kamil Trzcinski 已提交
312
    end
313

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

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
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
D
Douwe Maan 已提交
326 327
  end
end