pipeline.rb 7.8 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
K
WIP  
Kamil Trzcinski 已提交
6

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

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

12 13
    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
    has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
14
    has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
D
Douwe Maan 已提交
15

16 17 18 19
    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 已提交
20

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

23 24
    delegate :stages, to: :statuses

25
    state_machine :status, initial: :created do
26
      event :enqueue do
K
Kamil Trzcinski 已提交
27
        transition created: :pending
28
        transition [:success, :failed, :canceled, :skipped] => :running
29 30 31
      end

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

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

      event :drop do
        transition any => :failed
      end

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

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

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

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

59
      after_transition [:created, :pending] => :running do |pipeline|
60 61
        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)
62 63 64
      end

      after_transition any => [:success] do |pipeline|
65 66
        MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
          update_all(latest_build_finished_at: pipeline.finished_at)
67 68
      end

69
      before_transition do |pipeline|
70 71
        pipeline.update_duration
      end
72 73 74 75

      after_transition do |pipeline, transition|
        pipeline.execute_hooks unless transition.loopback?
      end
76 77
    end

78
    # ref can't be HEAD or SHA, can only be branch/tag name
79 80
    def self.latest_successful_for(ref)
      where(ref: ref).order(id: :desc).success.first
81 82
    end

D
Douwe Maan 已提交
83 84 85 86
    def self.truncate_sha(sha)
      sha[0...8]
    end

K
Kamil Trzcinski 已提交
87
    def self.stages
K
Kamil Trzcinski 已提交
88
      # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
89
      CommitStatus.where(pipeline: pluck(:id)).stages
K
Kamil Trzcinski 已提交
90 91
    end

92
    def self.total_duration
L
Lin Jen-Shin 已提交
93
      where.not(duration: nil).sum(:duration)
94 95
    end

96
    def stages_with_latest_statuses
97
      statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
98 99
    end

K
Kamil Trzcinski 已提交
100 101
    def project_id
      project.id
K
WIP  
Kamil Trzcinski 已提交
102 103
    end

D
Douwe Maan 已提交
104
    def valid_commit_sha
105
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
106 107 108 109 110
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
111
      commit.try(:author_name)
D
Douwe Maan 已提交
112 113 114
    end

    def git_author_email
115
      commit.try(:author_email)
D
Douwe Maan 已提交
116 117 118
    end

    def git_commit_message
119
      commit.try(:message)
D
Douwe Maan 已提交
120 121
    end

122 123 124 125
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
126
    def short_sha
127
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
128 129
    end

130
    def commit
K
Kamil Trzcinski 已提交
131
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
132 133 134 135
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
136 137 138 139
    def branch?
      !tag?
    end

140 141
    def manual_actions
      builds.latest.manual_actions
142 143
    end

K
Kamil Trzcinski 已提交
144 145
    def retryable?
      builds.latest.any? do |build|
K
Kamil Trzcinski 已提交
146
        build.failed? && build.retryable?
K
Kamil Trzcinski 已提交
147 148 149
      end
    end

150 151 152 153
    def cancelable?
      builds.running_or_pending.any?
    end

K
Kamil Trzcinski 已提交
154 155 156 157
    def cancel_running
      builds.running_or_pending.each(&:cancel)
    end

158 159 160 161
    def retry_failed(user)
      builds.latest.failed.select(&:retryable?).each do |build|
        Ci::Build.retry(build, user)
      end
K
Kamil Trzcinski 已提交
162 163
    end

164
    def mark_as_processable_after_stage(stage_idx)
165
      builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
166 167
    end

K
Kamil Trzcinski 已提交
168 169 170 171 172 173 174
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
175 176 177 178
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
179 180
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
181 182 183
    end

    def coverage
184
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
185 186
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
187 188 189
      end
    end

190 191 192 193 194 195 196 197
    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 已提交
198
    def has_warnings?
199
      builds.latest.failed_but_allowed.any?
200 201
    end

D
Douwe Maan 已提交
202
    def config_processor
203
      return nil unless ci_yaml_file
204 205 206 207 208
      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
209
        self.yaml_errors = e.message
210 211
        nil
      rescue
212
        self.yaml_errors = 'Undefined error'
213 214
        nil
      end
D
Douwe Maan 已提交
215 216
    end

K
Kamil Trzcinski 已提交
217
    def ci_yaml_file
218 219
      return @ci_yaml_file if defined?(@ci_yaml_file)

220 221 222 223
      @ci_yaml_file ||= begin
        blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
        blob.load_all_data!(project.repository)
        blob.data
224 225
      rescue
        nil
226
      end
K
Kamil Trzcinski 已提交
227 228
    end

K
Kamil Trzcinski 已提交
229 230 231 232
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
233 234 235 236 237 238 239 240 241 242 243 244 245
    # 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

246 247 248 249
    def notes
      Note.for_commit_id(sha)
    end

250 251 252
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
253

254
    def update_status
255
      with_lock do
K
Kamil Trzcinski 已提交
256
        case latest_builds_status
257 258 259 260 261 262 263
        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
264
      end
265 266
    end

267 268 269 270 271 272
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

273 274 275 276 277 278 279
    def queued_duration
      return unless started_at

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

280
    def update_duration
281 282
      return unless started_at

283
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
284 285 286
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
287 288 289
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
290 291
    end

292 293 294
    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
    def merge_requests
295 296 297 298 299
      @merge_requests ||=
        begin
          project.merge_requests.where(source_branch: self.ref).
            select { |merge_request| merge_request.pipeline.try(:id) == self.id }
        end
300 301
    end

302 303
    private

304
    def pipeline_data
305
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
306
    end
307

308
    def latest_builds_status
309 310 311
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
312
    end
313 314

    def keep_around_commits
315
      return unless project
316

317 318 319
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
320 321
  end
end