BigW Consortium Gitlab

pipeline.rb 7.8 KB
Newer Older
1
module Ci
2
  class Pipeline < ActiveRecord::Base
3
    extend Ci::Model
4
    include HasStatus
5
    include Importable
Kamil Trzcinski committed
6

Kamil Trzcinski committed
7 8
    self.table_name = 'ci_commits'

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
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?
20

21
    after_save :keep_around_commits, unless: :importing?
Kamil Trzcinski committed
22

23 24
    delegate :stages, to: :statuses

25
    state_machine :status, initial: :created do
26
      event :enqueue do
Kamil Trzcinski committed
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

83 84 85 86
    def self.truncate_sha(sha)
      sha[0...8]
    end

87
    def self.stages
Kamil Trzcinski committed
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
90 91
    end

92
    def self.total_duration
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

Kamil Trzcinski committed
100 101
    def project_id
      project.id
Kamil Trzcinski committed
102 103
    end

104
    def valid_commit_sha
105
      if self.sha == Gitlab::Git::BLANK_SHA
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)
112 113 114
    end

    def git_author_email
115
      commit.try(:author_email)
116 117 118
    end

    def git_commit_message
119
      commit.try(:message)
120 121
    end

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

126
    def short_sha
127
      Ci::Pipeline.truncate_sha(sha)
128 129
    end

130
    def commit
131
      @commit ||= project.commit(sha)
132 133 134 135
    rescue
      nil
    end

136 137 138 139
    def branch?
      !tag?
    end

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

144 145
    def retryable?
      builds.latest.any? do |build|
146
        build.failed? && build.retryable?
147 148 149
      end
    end

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

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
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

Kamil Trzcinski committed
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

175 176 177 178
    def triggered?
      trigger_requests.any?
    end

179 180
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
181 182 183
    end

    def coverage
184
      coverage_array = statuses.latest.map(&:coverage).compact
185 186
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
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

Connor Shea committed
198 199
    def has_warnings?
      builds.latest.ignored.any?
200 201
    end

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
215 216
    end

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
227 228
    end

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

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 build_updated
255
      with_lock do
Kamil Trzcinski committed
256 257
        reload
        case latest_builds_status
258 259 260 261 262 263 264
        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
265
      end
266 267
    end

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

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

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

281
    def update_duration
282 283
      return unless started_at

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

    def execute_hooks
288 289 290
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
291 292
    end

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

303 304
    private

305
    def pipeline_data
306
      Gitlab::DataBuilder::Pipeline.build(self)
Kamil Trzcinski committed
307
    end
308

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

      statuses.latest.status || 'skipped'
Kamil Trzcinski committed
313
    end
314 315

    def keep_around_commits
316
      return unless project
317

318 319 320
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
321 322
  end
end