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

Kamil Trzcinski's avatar
Kamil Trzcinski committed
6 7
    self.table_name = 'ci_commits'

8
    belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
9 10
    belongs_to :user

11 12
    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
    has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
13
    has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
14

15
    validates_presence_of :sha
16
    validates_presence_of :ref
17
    validates_presence_of :status
18 19
    validate :valid_commit_sha

20
    after_save :keep_around_commits
Kamil Trzcinski's avatar
Kamil Trzcinski committed
21

22 23
    delegate :stages, to: :statuses

24
    state_machine :status, initial: :created do
25
      event :queue do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
26
        transition created: :pending
27
        transition any - [:created, :pending] => :running
28 29 30
      end

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

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

      event :drop do
        transition any => :failed
      end

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

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

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

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

58
      before_transition do |pipeline|
59 60
        pipeline.update_duration
      end
61 62 63 64

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

67
    # ref can't be HEAD or SHA, can only be branch/tag name
68
    scope :latest_successful_for, ->(ref = default_branch) do
69
      where(ref: ref).success.order(id: :desc).limit(1)
70 71
    end

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

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

Kamil Trzcinski's avatar
Kamil Trzcinski committed
81 82
    def project_id
      project.id
Kamil Trzcinski's avatar
WIP  
Kamil Trzcinski committed
83 84
    end

85
    def valid_commit_sha
86
      if self.sha == Gitlab::Git::BLANK_SHA
87 88 89 90 91
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
92
      commit.try(:author_name)
93 94 95
    end

    def git_author_email
96
      commit.try(:author_email)
97 98 99
    end

    def git_commit_message
100
      commit.try(:message)
101 102
    end

103 104 105 106
    def git_commit_title
      commit.try(:title)
    end

107
    def short_sha
108
      Ci::Pipeline.truncate_sha(sha)
109 110
    end

111
    def commit
112
      @commit ||= project.commit(sha)
113 114 115 116
    rescue
      nil
    end

117 118 119 120
    def branch?
      !tag?
    end

121 122
    def manual_actions
      builds.latest.manual_actions
123 124
    end

125 126
    def retryable?
      builds.latest.any? do |build|
127
        build.failed? && build.retryable?
128 129 130
      end
    end

131 132 133 134
    def cancelable?
      builds.running_or_pending.any?
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
135 136 137 138
    def cancel_running
      builds.running_or_pending.each(&:cancel)
    end

139 140 141 142
    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
143 144
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
145 146 147 148 149 150 151
    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
152 153 154 155
    def triggered?
      trigger_requests.any?
    end

156 157
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
158 159 160
    end

    def coverage
161
      coverage_array = statuses.latest.map(&:coverage).compact
162 163
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
164 165 166
      end
    end

167 168 169 170 171 172 173 174
    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
175 176
    def has_warnings?
      builds.latest.ignored.any?
177 178
    end

179
    def config_processor
180
      return nil unless ci_yaml_file
181 182 183 184 185
      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
186
        self.yaml_errors = e.message
187 188
        nil
      rescue
189
        self.yaml_errors = 'Undefined error'
190 191
        nil
      end
192 193
    end

194
    def ci_yaml_file
195 196
      return @ci_yaml_file if defined?(@ci_yaml_file)

197 198 199 200
      @ci_yaml_file ||= begin
        blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
        blob.load_all_data!(project.repository)
        blob.data
201 202
      rescue
        nil
203
      end
204 205
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
206 207 208 209
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

James Lopez's avatar
James Lopez committed
210 211 212 213 214 215 216 217 218 219 220 221 222
    # 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

223 224 225 226
    def notes
      Note.for_commit_id(sha)
    end

227 228 229
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
230

231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
    def build_updated
      case latest_builds_status
      when 'pending'
        queue
      when 'running'
        run
      when 'success'
        succeed
      when 'failed'
        drop
      when 'canceled'
        cancel
      when 'skipped'
        skip
      end
246 247
    end

248 249 250 251 252 253
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

254
    def update_duration
255
      self.duration = statuses.latest.duration
256 257 258 259
    end

    def execute_hooks
      project.execute_hooks(pipeline_data, :pipeline_hooks)
260 261 262
      project.execute_services(pipeline_data, :pipeline_hooks)
    end

263 264
    private

265
    def pipeline_data
266
      Gitlab::DataBuilder::Pipeline.build(self)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
267
    end
268

269
    def latest_builds_status
270 271 272 273 274
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
    end

275
    def keep_around_commits
276
      return unless project
277

278 279 280
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
281 282
  end
end