build.rb 10.8 KB
Newer Older
1 2
# == Schema Information
#
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
3
# Table name: ci_builds
4 5 6 7 8 9 10 11 12 13 14
#
#  id                 :integer          not null, primary key
#  project_id         :integer
#  status             :string(255)
#  finished_at        :datetime
#  trace              :text
#  created_at         :datetime
#  updated_at         :datetime
#  started_at         :datetime
#  runner_id          :integer
#  coverage           :float
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
15
#  commit_id          :integer
16 17 18
#  commands           :text
#  job_id             :integer
#  name               :string(255)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
19
#  deploy             :boolean          default(FALSE)
20 21 22 23
#  options            :text
#  allow_failure      :boolean          default(FALSE), not null
#  stage              :string(255)
#  trigger_request_id :integer
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
24 25 26 27 28 29 30 31
#  stage_idx          :integer
#  tag                :boolean
#  ref                :string(255)
#  user_id            :integer
#  type               :string(255)
#  target_url         :string(255)
#  description        :string(255)
#  artifacts_file     :text
Stan Hu's avatar
Stan Hu committed
32
#  gl_project_id      :integer
33
#  artifacts_metadata :text
34 35
#  erased_by_id       :integer
#  erased_at          :datetime
36 37 38
#

module Ci
39
  class Build < CommitStatus
40 41
    belongs_to :runner, class_name: 'Ci::Runner'
    belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
42
    belongs_to :erased_by, class_name: 'User'
43 44 45 46

    serialize :options

    validates :coverage, numericality: true, allow_blank: true
47
    validates_presence_of :ref
48 49

    scope :unstarted, ->() { where(runner_id: nil) }
50
    scope :ignore_failures, ->() { where(allow_failure: false) }
51

52
    mount_uploader :artifacts_file, ArtifactUploader
53
    mount_uploader :artifacts_metadata, ArtifactUploader
54

55 56
    acts_as_taggable

57 58
    before_destroy { project }

59
    after_create :execute_hooks
60 61 62 63 64 65 66 67 68 69 70 71

    class << self
      def last_month
        where('created_at > ?', Date.today - 1.month)
      end

      def first_pending
        pending.unstarted.order('created_at ASC').first
      end

      def create_from(build)
        new_build = build.dup
72
        new_build.status = 'pending'
73
        new_build.runner_id = nil
74
        new_build.trigger_request_id = nil
75 76 77 78
        new_build.save
      end

      def retry(build)
79
        new_build = Ci::Build.new(status: 'pending')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
80 81
        new_build.ref = build.ref
        new_build.tag = build.tag
82 83 84
        new_build.options = build.options
        new_build.commands = build.commands
        new_build.tag_list = build.tag_list
85
        new_build.gl_project_id = build.gl_project_id
86 87 88 89
        new_build.commit_id = build.commit_id
        new_build.name = build.name
        new_build.allow_failure = build.allow_failure
        new_build.stage = build.stage
90
        new_build.stage_idx = build.stage_idx
91 92 93 94 95 96 97
        new_build.trigger_request = build.trigger_request
        new_build.save
        new_build
      end
    end

    state_machine :status, initial: :pending do
98
      after_transition pending: :running do |build|
99 100 101
        build.execute_hooks
      end

102 103 104 105 106
      # We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed
      around_transition any => [:success, :failed, :canceled] do |build, block|
        block.call
        build.commit.create_next_builds(build) if build.commit
      end
107

108
      after_transition any => [:success, :failed, :canceled] do |build|
109
        build.update_coverage
110
        build.execute_hooks
111 112 113
      end
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
114
    def retryable?
115
      project.builds_enabled? && commands.present?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
116 117 118
    end

    def retried?
119
      !self.commit.statuses.latest.include?(self)
120 121 122 123
    end

    def retry
      Ci::Build.retry(self)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
124 125
    end

126 127
    def depends_on_builds
      # Get builds of the same type
128
      latest_builds = self.commit.builds.latest
129 130 131 132 133

      # Return builds from previous stages
      latest_builds.where('stage_idx < ?', stage_idx)
    end

134 135
    def trace_html
      html = Ci::Ansi2html::convert(trace) if trace.present?
136
      html || ''
137 138 139
    end

    def timeout
140
      project.build_timeout
141 142 143
    end

    def variables
144
      predefined_variables + yaml_variables + project_variables + trigger_variables
145 146
    end

147 148 149 150 151 152 153 154 155 156
    def merge_request
      merge_requests = MergeRequest.includes(:merge_request_diff)
                                   .where(source_branch: ref, source_project_id: commit.gl_project_id)
                                   .reorder(iid: :asc)

      merge_requests.find do |merge_request|
        merge_request.commits.any? { |ci| ci.id == commit.sha }
      end
    end

157
    def project_id
Kamil Trzcinski's avatar
Kamil Trzcinski committed
158
      commit.project.id
159 160 161 162 163 164 165
    end

    def project_name
      project.name
    end

    def repo_url
166 167 168 169
      auth = "gitlab-ci-token:#{token}@"
      project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
        prefix + auth
      end
170 171 172
    end

    def allow_git_fetch
173
      project.build_allow_git_fetch
174 175 176
    end

    def update_coverage
177
      return unless project
178 179 180
      coverage_regex = project.build_coverage_regex
      return unless coverage_regex
      coverage = extract_coverage(trace, coverage_regex)
181 182 183 184 185 186 187 188

      if coverage.is_a? Numeric
        update_attributes(coverage: coverage)
      end
    end

    def extract_coverage(text, regex)
      begin
Jared Szechy's avatar
Jared Szechy committed
189 190
        matches = text.scan(Regexp.new(regex)).last
        matches = matches.last if matches.kind_of?(Array)
191 192 193 194 195
        coverage = matches.gsub(/\d+(\.\d+)?/).first

        if coverage.present?
          coverage.to_f
        end
196
      rescue
197 198 199 200 201
        # if bad regex or something goes wrong we dont want to interrupt transition
        # so we just silentrly ignore error for now
      end
    end

202 203
    def has_trace?
      raw_trace.present?
204 205
    end

206
    def raw_trace
207
      if File.file?(path_to_trace)
208
        File.read(path_to_trace)
209
      elsif project.ci_id && File.file?(old_path_to_trace)
210 211
        # Temporary fix for build trace data integrity
        File.read(old_path_to_trace)
212 213 214 215 216
      else
        # backward compatibility
        read_attribute :trace
      end
    end
217 218 219

    def trace
      trace = raw_trace
220
      if project && trace.present? && project.runners_token.present?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
221
        trace.gsub(project.runners_token, 'xxxxxx')
222 223 224 225
      else
        trace
      end
    end
226

Tomasz Maczukin's avatar
Tomasz Maczukin committed
227
    def trace_length
228 229
      if raw_trace
        raw_trace.length
Tomasz Maczukin's avatar
Tomasz Maczukin committed
230
      else
231
        0
Tomasz Maczukin's avatar
Tomasz Maczukin committed
232 233 234
      end
    end

235
    def trace=(trace)
236 237 238 239 240
      recreate_trace_dir
      File.write(path_to_trace, trace)
    end

    def recreate_trace_dir
241 242
      unless Dir.exists?(dir_to_trace)
        FileUtils.mkdir_p(dir_to_trace)
243
      end
244 245
    end
    private :recreate_trace_dir
246

247
    def append_trace(trace_part, offset)
248 249
      recreate_trace_dir

250
      File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
251 252 253
      File.open(path_to_trace, 'a') do |f|
        f.write(trace_part)
      end
254 255 256 257
    end

    def dir_to_trace
      File.join(
Valery Sizov's avatar
Valery Sizov committed
258
        Settings.gitlab_ci.builds_path,
259 260 261 262 263 264 265 266 267
        created_at.utc.strftime("%Y_%m"),
        project.id.to_s
      )
    end

    def path_to_trace
      "#{dir_to_trace}/#{id}.log"
    end

268 269 270
    ##
    # Deprecated
    #
271 272 273
    # This is a hotfix for CI build data integrity, see #4246
    # Should be removed in 8.4, after CI files migration has been done.
    #
274 275 276 277 278 279 280 281 282 283 284
    def old_dir_to_trace
      File.join(
        Settings.gitlab_ci.builds_path,
        created_at.utc.strftime("%Y_%m"),
        project.ci_id.to_s
      )
    end

    ##
    # Deprecated
    #
285 286 287
    # This is a hotfix for CI build data integrity, see #4246
    # Should be removed in 8.4, after CI files migration has been done.
    #
288 289 290 291
    def old_path_to_trace
      "#{old_dir_to_trace}/#{id}.log"
    end

292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
    ##
    # Deprecated
    #
    # This contains a hotfix for CI build data integrity, see #4246
    #
    # This method is used by `ArtifactUploader` to create a store_dir.
    # Warning: Uploader uses it after AND before file has been stored.
    #
    # This method returns old path to artifacts only if it already exists.
    #
    def artifacts_path
      old = File.join(created_at.utc.strftime('%Y_%m'),
                      project.ci_id.to_s,
                      id.to_s)

      old_store = File.join(ArtifactUploader.artifacts_path, old)
      return old if project.ci_id && File.directory?(old_store)

      File.join(
        created_at.utc.strftime('%Y_%m'),
        project.id.to_s,
        id.to_s
      )
    end

317
    def token
Kamil Trzcinski's avatar
Kamil Trzcinski committed
318
      project.runners_token
319 320 321
    end

    def valid_token? token
Kamil Trzcinski's avatar
Kamil Trzcinski committed
322
      project.valid_runners_token? token
323 324
    end

325 326 327 328 329 330 331 332
    def can_be_served?(runner)
      (tag_list - runner.tag_list).empty?
    end

    def any_runners_online?
      project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
    end

333
    def stuck?
334 335 336
      pending? && !any_runners_online?
    end

337
    def execute_hooks
338
      return unless project
339
      build_data = Gitlab::BuildDataBuilder.build(self)
340 341
      project.execute_hooks(build_data.dup, :build_hooks)
      project.execute_services(build_data.dup, :build_hooks)
342 343
    end

344 345 346 347
    def artifacts?
      artifacts_file.exists?
    end

348
    def artifacts_metadata?
349
      artifacts? && artifacts_metadata.exists?
350 351
    end

352 353
    def artifacts_metadata_entry(path, **options)
      Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry
354 355
    end

356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
    def erase(opts = {})
      return false unless erasable?

      remove_artifacts_file!
      remove_artifacts_metadata!
      erase_trace!
      update_erased!(opts[:erased_by])
    end

    def erasable?
      complete? && (artifacts? || has_trace?)
    end

    def erased?
      !self.erased_at.nil?
    end

    private

    def erase_trace!
      self.trace = nil
    end

    def update_erased!(user = nil)
      self.update(erased_by: user, erased_at: Time.now)
    end

383
    def yaml_variables
384 385 386 387
      global_yaml_variables + job_yaml_variables
    end

    def global_yaml_variables
388
      if commit.config_processor
389
        commit.config_processor.global_variables.map do |key, value|
390 391 392 393 394 395 396
          { key: key, value: value, public: true }
        end
      else
        []
      end
    end

397
    def job_yaml_variables
398 399 400 401 402 403
      if commit.config_processor
        commit.config_processor.job_variables(name).map do |key, value|
          { key: key, value: value, public: true }
        end
      else
        []
404 405 406
      end
    end

407
    def project_variables
408
      project.variables.map do |variable|
409 410 411 412 413 414 415 416 417 418 419 420 421
        { key: variable.key, value: variable.value, public: false }
      end
    end

    def trigger_variables
      if trigger_request && trigger_request.variables
        trigger_request.variables.map do |key, value|
          { key: key, value: value, public: false }
        end
      else
        []
      end
    end
422 423 424

    def predefined_variables
      variables = []
425
      variables << { key: :CI_BUILD_TAG, value: ref, public: true } if tag?
426 427 428 429 430
      variables << { key: :CI_BUILD_NAME, value: name, public: true }
      variables << { key: :CI_BUILD_STAGE, value: stage, public: true }
      variables << { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } if trigger_request
      variables
    end
431 432
  end
end