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

Kamil Trzcinski's avatar
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's avatar
Kamil Trzcinski committed
22

23 24
    delegate :stages, to: :statuses

25
    state_machine :status, initial: :created do
26
      event :enqueue do
Kamil Trzcinski's avatar
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
      before_transition do |pipeline|
60 61
        pipeline.update_duration
      end
62 63 64 65

      after_transition do |pipeline, transition|
        pipeline.execute_hooks unless transition.loopback?
      end
66 67
    end

68
    # ref can't be HEAD or SHA, can only be branch/tag name
69 70
    def self.latest_successful_for(ref)
      where(ref: ref).order(id: :desc).success.first
71 72
    end

73 74 75 76
    def self.truncate_sha(sha)
      sha[0...8]
    end

77
    def self.stages
Kamil Trzcinski's avatar
Kamil Trzcinski committed
78
      # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
79
      CommitStatus.where(pipeline: pluck(:id)).stages
80 81
    end

82
    def self.total_duration
Lin Jen-Shin's avatar
Lin Jen-Shin committed
83
      where.not(duration: nil).sum(:duration)
84 85
    end

86
    def stages_with_latest_statuses
87
      statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
88 89
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
90 91
    def project_id
      project.id
Kamil Trzcinski's avatar
WIP  
Kamil Trzcinski committed
92 93
    end

94
    def valid_commit_sha
95
      if self.sha == Gitlab::Git::BLANK_SHA
96 97 98 99 100
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
101
      commit.try(:author_name)
102 103 104
    end

    def git_author_email
105
      commit.try(:author_email)
106 107 108
    end

    def git_commit_message
109
      commit.try(:message)
110 111
    end

112 113 114 115
    def git_commit_title
      commit.try(:title)
    end

116
    def short_sha
117
      Ci::Pipeline.truncate_sha(sha)
118 119
    end

120
    def commit
121
      @commit ||= project.commit(sha)
122 123 124 125
    rescue
      nil
    end

126 127 128 129
    def branch?
      !tag?
    end

130 131
    def manual_actions
      builds.latest.manual_actions
132 133
    end

134 135
    def retryable?
      builds.latest.any? do |build|
136
        build.failed? && build.retryable?
137 138 139
      end
    end

140 141 142 143
    def cancelable?
      builds.running_or_pending.any?
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
144 145 146 147
    def cancel_running
      builds.running_or_pending.each(&:cancel)
    end

148 149 150 151
    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
152 153
    end

154
    def mark_as_processable_after_stage(stage_idx)
155
      builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
156 157
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
158 159 160 161 162 163 164
    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
165 166 167 168
    def triggered?
      trigger_requests.any?
    end

169 170
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
171 172 173
    end

    def coverage
174
      coverage_array = statuses.latest.map(&:coverage).compact
175 176
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
177 178 179
      end
    end

180 181 182 183 184 185 186 187
    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
188 189
    def has_warnings?
      builds.latest.ignored.any?
190 191
    end

192
    def config_processor
193
      return nil unless ci_yaml_file
194 195 196 197 198
      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
199
        self.yaml_errors = e.message
200 201
        nil
      rescue
202
        self.yaml_errors = 'Undefined error'
203 204
        nil
      end
205 206
    end

207
    def ci_yaml_file
208 209
      return @ci_yaml_file if defined?(@ci_yaml_file)

210 211 212 213
      @ci_yaml_file ||= begin
        blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
        blob.load_all_data!(project.repository)
        blob.data
214 215
      rescue
        nil
216
      end
217 218
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
219 220 221 222
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

James Lopez's avatar
James Lopez committed
223 224 225 226 227 228 229 230 231 232 233 234 235
    # 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

236 237 238 239
    def notes
      Note.for_commit_id(sha)
    end

240 241 242
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
243

244
    def build_updated
245 246 247 248 249 250 251 252 253 254
      with_lock do
        reload
        case latest_builds_status
        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
255
      end
256 257
    end

258 259 260 261 262 263
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

264 265 266 267 268 269 270
    def queued_duration
      return unless started_at

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

271
    def update_duration
272 273
      return unless started_at

274
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
275 276 277
    end

    def execute_hooks
278 279 280
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
281 282
    end

283 284
    private

285
    def pipeline_data
286
      Gitlab::DataBuilder::Pipeline.build(self)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
287
    end
288

289
    def latest_builds_status
290 291 292
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
Kamil Trzcinski's avatar
Kamil Trzcinski committed
293
    end
294 295

    def keep_around_commits
296
      return unless project
297

298 299 300
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
301 302
  end
end