Read about our upcoming Code of Conduct on this issue

build.rb 23.3 KB
Newer Older
1
2
# frozen_string_literal: true

Douwe Maan's avatar
Douwe Maan committed
3
module Ci
Kamil Trzcinski's avatar
Kamil Trzcinski committed
4
  class Build < CommitStatus
5
    include Ci::Processable
6
    include Ci::Metadatable
7
    include Ci::Contextable
8
    include Ci::PipelineDelegator
9
    include TokenAuthenticatable
10
    include AfterCommitQueue
11
    include ObjectStorage::BackgroundMove
Rémy Coutable's avatar
Rémy Coutable committed
12
    include Presentable
Shinya Maeda's avatar
Shinya Maeda committed
13
    include Importable
14
    include IgnorableColumn
15
    include Gitlab::Utils::StrongMemoize
16
    include Deployable
17
    include HasRef
18

19
20
21
    BuildArchivedError = Class.new(StandardError)

    ignore_column :commands
22
23
24
25
26
    ignore_column :artifacts_file
    ignore_column :artifacts_metadata
    ignore_column :artifacts_file_store
    ignore_column :artifacts_metadata_store
    ignore_column :artifacts_size
27

28
    belongs_to :project, inverse_of: :builds
29
30
    belongs_to :runner
    belongs_to :trigger_request
31
    belongs_to :erased_by, class_name: 'User'
Douwe Maan's avatar
Douwe Maan committed
32

33
    RUNNER_FEATURES = {
34
35
      upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? },
      refspecs: -> (build) { build.merge_request_ref? }
36
37
    }.freeze

38
    has_one :deployment, as: :deployable, class_name: 'Deployment'
39
    has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
40
    has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id
41

42
    has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
43
44
45
46

    Ci::JobArtifact.file_types.each do |key, value|
      has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
    end
47

48
49
50
51
52
53
    has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build

    accepts_nested_attributes_for :runner_session

    delegate :url, to: :runner_session, prefix: true, allow_nil: true
    delegate :terminal_specification, to: :runner_session, allow_nil: true
54
    delegate :gitlab_deploy_token, to: :project
55
    delegate :trigger_short_token, to: :trigger_request, allow_nil: true
Tomasz Maczukin's avatar
Tomasz Maczukin committed
56

57
    ##
58
59
60
61
62
    # Since Gitlab 11.5, deployments records started being created right after
    # `ci_builds` creation. We can look up a relevant `environment` through
    # `deployment` relation today. This is much more efficient than expanding
    # environment name with variables.
    # (See more https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22380)
63
    #
64
65
66
67
68
    # However, we have to still expand environment name if it's a stop action,
    # because `deployment` persists information for start action only.
    #
    # We will follow up this by persisting expanded name in build metadata or
    # persisting stop action in database.
69
    def persisted_environment
70
71
72
      return unless has_environment?

      strong_memoize(:persisted_environment) do
73
74
        deployment&.environment ||
          Environment.find_by(name: expanded_environment_name, project: project)
75
      end
76
77
    end

78
79
    serialize :options # rubocop:disable Cop/ActiveRecordSerialize
    serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize
Douwe Maan's avatar
Douwe Maan committed
80

Douwe Maan's avatar
Douwe Maan committed
81
82
    delegate :name, to: :project, prefix: true

Douwe Maan's avatar
Douwe Maan committed
83
    validates :coverage, numericality: true, allow_blank: true
Douwe Maan's avatar
Douwe Maan committed
84
    validates :ref, presence: true
Douwe Maan's avatar
Douwe Maan committed
85
86

    scope :unstarted, ->() { where(runner_id: nil) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
87
    scope :ignore_failures, ->() { where(allow_failure: false) }
88
    scope :with_artifacts_archive, ->() do
89
      where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
90
    end
91

92
93
94
95
    scope :with_existing_job_artifacts, ->(query) do
      where('EXISTS (?)', ::Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').merge(query))
    end

96
    scope :with_archived_trace, ->() do
97
      with_existing_job_artifacts(Ci::JobArtifact.trace)
98
99
    end

100
101
102
103
    scope :without_archived_trace, ->() do
      where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
    end

Matija Čupić's avatar
Matija Čupić committed
104
105
    scope :with_reports, ->(reports_scope) do
      with_existing_job_artifacts(reports_scope)
106
        .eager_load_job_artifacts
107
108
    end

109
110
    scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }

111
112
    scope :with_artifacts_stored_locally, -> { with_existing_job_artifacts(Ci::JobArtifact.archive.with_files_stored_locally) }
    scope :with_archived_trace_stored_locally, -> { with_existing_job_artifacts(Ci::JobArtifact.trace.with_files_stored_locally) }
113
114
    scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
    scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
115
    scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
116
117
    scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) }
    scope :scheduled_actions, ->() { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) }
118
    scope :ref_protected, -> { where(protected: true) }
119
    scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) }
120

121
122
    scope :matches_tag_ids, -> (tag_ids) do
      matcher = ::ActsAsTaggableOn::Tagging
123
        .where(taggable_type: CommitStatus.name)
124
125
126
127
128
129
130
131
132
        .where(context: 'tags')
        .where('taggable_id = ci_builds.id')
        .where.not(tag_id: tag_ids).select('1')

      where("NOT EXISTS (?)", matcher)
    end

    scope :with_any_tags, -> do
      matcher = ::ActsAsTaggableOn::Tagging
133
        .where(taggable_type: CommitStatus.name)
134
135
136
137
138
139
        .where(context: 'tags')
        .where('taggable_id = ci_builds.id').select('1')

      where("EXISTS (?)", matcher)
    end

140
141
    scope :queued_before, ->(time) { where(arel_table[:queued_at].lt(time)) }

Douwe Maan's avatar
Douwe Maan committed
142
143
    acts_as_taggable

Kamil Trzciński's avatar
Kamil Trzciński committed
144
    add_authentication_token_field :token, encrypted: :optional
145
146

    before_save :ensure_token
147
    before_destroy { unscoped_project }
Grzegorz Bizon's avatar
Grzegorz Bizon committed
148

149
    after_create unless: :importing? do |build|
150
      run_after_commit { BuildHooksWorker.perform_async(build.id) }
151
152
    end

Douwe Maan's avatar
Douwe Maan committed
153
    class << self
154
155
156
157
158
159
      # This is needed for url_for to work,
      # as the controller is JobsController
      def model_name
        ActiveModel::Name.new(self, nil, 'job')
      end

Douwe Maan's avatar
Douwe Maan committed
160
161
162
163
      def first_pending
        pending.unstarted.order('created_at ASC').first
      end

164
      def retry(build, current_user)
165
        # rubocop: disable CodeReuse/ServiceClass
166
167
168
        Ci::RetryBuildService
          .new(build.project, current_user)
          .execute(build)
169
        # rubocop: enable CodeReuse/ServiceClass
Douwe Maan's avatar
Douwe Maan committed
170
171
172
      end
    end

173
    state_machine :status do
174
175
176
177
      event :enqueue do
        transition [:created, :skipped, :manual, :scheduled] => :preparing, if: :any_unmet_prerequisites?
      end

178
179
      event :actionize do
        transition created: :manual
Kamil Trzcinski's avatar
Kamil Trzcinski committed
180
181
      end

182
183
184
185
186
187
188
189
      event :schedule do
        transition created: :scheduled
      end

      event :unschedule do
        transition scheduled: :manual
      end

190
      event :enqueue_scheduled do
191
192
193
194
        transition scheduled: :preparing, if: ->(build) do
          build.scheduled_at&.past? && build.any_unmet_prerequisites?
        end

195
        transition scheduled: :pending, if: ->(build) do
196
          build.scheduled_at&.past? && !build.any_unmet_prerequisites?
197
        end
198
199
200
      end

      before_transition scheduled: any do |build|
201
202
203
204
        build.scheduled_at = nil
      end

      before_transition created: :scheduled do |build|
Shinya Maeda's avatar
Shinya Maeda committed
205
        build.scheduled_at = build.options_scheduled_at
206
207
208
209
210
211
      end

      after_transition created: :scheduled do |build|
        build.run_after_commit do
          Ci::BuildScheduleWorker.perform_at(build.scheduled_at, build.id)
        end
212
213
      end

214
215
216
217
218
219
      after_transition any => [:preparing] do |build|
        build.run_after_commit do
          Ci::BuildPrepareWorker.perform_async(id)
        end
      end

220
221
      after_transition any => [:pending] do |build|
        build.run_after_commit do
Kim "BKC" Carlbäcker's avatar
linting    
Kim "BKC" Carlbäcker committed
222
          BuildQueueWorker.perform_async(id)
223
224
225
        end
      end

226
      after_transition pending: :running do |build|
227
228
        build.deployment&.run

229
230
231
        build.run_after_commit do
          BuildHooksWorker.perform_async(id)
        end
232
233
      end

234
      after_transition any => [:success, :failed, :canceled] do |build|
235
        build.run_after_commit do
236
          BuildFinishedWorker.perform_async(id)
237
        end
Douwe Maan's avatar
Douwe Maan committed
238
      end
239

240
      after_transition any => [:success] do |build|
241
242
        build.deployment&.succeed

243
        build.run_after_commit do
244
          BuildSuccessWorker.perform_async(id)
245
          PagesWorker.perform_async(:deploy, id) if build.pages_generator?
246
247
        end
      end
248

249
      before_transition any => [:failed] do |build|
250
        next unless build.project
251
        next unless build.deployment
252

253
254
255
256
257
258
259
        begin
          build.deployment.drop!
        rescue => e
          Gitlab::Sentry.track_exception(e, extra: { build_id: build.id })
        end

        true
260
261
262
263
      end

      after_transition any => [:failed] do |build|
        next unless build.project
264

265
        if build.retry_failure?
266
267
268
          begin
            Ci::Build.retry(build, build.user)
          rescue Gitlab::Access::AccessDeniedError => ex
Mayra Cabrera's avatar
Mayra Cabrera committed
269
            Rails.logger.error "Unable to auto-retry job #{build.id}: #{ex}" # rubocop:disable Gitlab/RailsLogger
270
          end
271
272
        end
      end
273

274
      after_transition pending: :running do |build|
275
        build.ensure_metadata.update_timeout_state
276
      end
277
278
279
280

      after_transition running: any do |build|
        Ci::BuildRunnerSession.where(build: build).delete_all
      end
281
282
283
284

      after_transition any => [:skipped, :canceled] do |build|
        build.deployment&.cancel
      end
Douwe Maan's avatar
Douwe Maan committed
285
286
    end

287
    def detailed_status(current_user)
288
289
290
      Gitlab::Ci::Status::Build::Factory
        .new(self, current_user)
        .fabricate!
Kamil Trzcinski's avatar
Kamil Trzcinski committed
291
292
    end

293
    def other_manual_actions
294
      pipeline.manual_actions.where.not(name: name)
295
296
    end

297
298
    def other_scheduled_actions
      pipeline.scheduled_actions.where.not(name: name)
299
300
    end

301
302
303
304
305
    def pages_generator?
      Gitlab.config.pages.enabled &&
        self.name == 'pages'
    end

306
307
308
309
    def runnable?
      true
    end

310
311
312
313
314
315
316
    def archived?
      return true if degenerated?

      archive_builds_older_than = Gitlab::CurrentSettings.current_application_settings.archive_builds_older_than
      archive_builds_older_than.present? && created_at < archive_builds_older_than
    end

317
    def playable?
318
      action? && !archived? && (manual? || scheduled? || retryable?)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
319
320
    end

Shinya Maeda's avatar
Shinya Maeda committed
321
    def schedulable?
322
      self.when == 'delayed' && options[:start_in].present?
Shinya Maeda's avatar
Shinya Maeda committed
323
324
    end

Shinya Maeda's avatar
Shinya Maeda committed
325
    def options_scheduled_at
Shinya Maeda's avatar
Shinya Maeda committed
326
      ChronicDuration.parse(options[:start_in])&.seconds&.from_now
Kamil Trzcinski's avatar
Kamil Trzcinski committed
327
328
    end

329
    def action?
330
      %w[manual delayed].include?(self.when)
331
332
    end

333
    # rubocop: disable CodeReuse/ServiceClass
334
    def play(current_user)
335
336
337
      Ci::PlayBuildService
        .new(project, current_user)
        .execute(self)
338
    end
339
    # rubocop: enable CodeReuse/ServiceClass
340

Kamil Trzcinski's avatar
Kamil Trzcinski committed
341
    def cancelable?
Jacopo's avatar
Jacopo committed
342
      active? || created?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
343
344
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
345
    def retryable?
Kamil Trzciński's avatar
Kamil Trzciński committed
346
      !archived? && (success? || failed? || canceled?)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
347
    end
348
349
350
351
352
353

    def retries_count
      pipeline.builds.retried.where(name: self.name).count
    end

    def retries_max
354
      normalized_retry.fetch(:max, 0)
355
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
356

357
    def retry_when
358
      normalized_retry.fetch(:when, ['always'])
359
360
    end

361
362
363
    def retry_failure?
      return false if retries_max.zero? || retries_count >= retries_max

364
      retry_when.include?('always') || retry_when.include?(failure_reason.to_s)
365
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
366

367
368
    def latest?
      !retried?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
369
370
    end

371
372
373
374
375
376
377
378
    def any_unmet_prerequisites?
      prerequisites.present?
    end

    def prerequisites
      Gitlab::Ci::Build::Prerequisite::Factory.new(self).unmet
    end

379
    def expanded_environment_name
380
381
382
      return unless has_environment?

      strong_memoize(:expanded_environment_name) do
383
384
        ExpandVariables.expand(environment, simple_variables)
      end
385
386
    end

387
    def has_environment?
388
      environment.present?
389
390
    end

391
    def starts_environment?
392
      has_environment? && self.environment_action == 'start'
393
394
395
    end

    def stops_environment?
396
      has_environment? && self.environment_action == 'stop'
397
398
399
    end

    def environment_action
400
      self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options
401
402
    end

403
404
405
406
    def has_deployment?
      !!self.deployment
    end

407
    def outdated_deployment?
408
      success? && !deployment.try(:last?)
409
    end
410

411
412
    def depends_on_builds
      # Get builds of the same type
413
      latest_builds = self.pipeline.builds.latest
414
415
416
417
418

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

419
    def triggered_by?(current_user)
Shinya Maeda's avatar
Shinya Maeda committed
420
421
422
      user == current_user
    end

423
424
425
426
    def on_stop
      options&.dig(:environment, :on_stop)
    end

427
428
429
430
    ##
    # All variables, including persisted environment variables.
    #
    def variables
431
432
433
434
435
436
437
      strong_memoize(:variables) do
        Gitlab::Ci::Variables::Collection.new
          .concat(persisted_variables)
          .concat(scoped_variables)
          .concat(persisted_environment_variables)
          .to_runner_variables
      end
438
439
    end

440
441
442
443
444
445
446
447
448
449
    CI_REGISTRY_USER = 'gitlab-ci-token'.freeze

    def persisted_variables
      Gitlab::Ci::Variables::Collection.new.tap do |variables|
        break variables unless persisted?

        variables
          .concat(pipeline.persisted_variables)
          .append(key: 'CI_JOB_ID', value: id.to_s)
          .append(key: 'CI_JOB_URL', value: Gitlab::Routing.url_helpers.project_job_url(project, self))
450
          .append(key: 'CI_JOB_TOKEN', value: token.to_s, public: false, masked: true)
451
          .append(key: 'CI_BUILD_ID', value: id.to_s)
452
          .append(key: 'CI_BUILD_TOKEN', value: token.to_s, public: false, masked: true)
453
          .append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
454
          .append(key: 'CI_REGISTRY_PASSWORD', value: token.to_s, public: false, masked: true)
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
          .append(key: 'CI_REPOSITORY_URL', value: repo_url.to_s, public: false)
          .concat(deploy_token_variables)
      end
    end

    def persisted_environment_variables
      Gitlab::Ci::Variables::Collection.new.tap do |variables|
        break variables unless persisted? && persisted_environment.present?

        variables.concat(persisted_environment.predefined_variables)

        # Here we're passing unexpanded environment_url for runner to expand,
        # and we need to make sure that CI_ENVIRONMENT_NAME and
        # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
        variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
      end
    end

    def deploy_token_variables
      Gitlab::Ci::Variables::Collection.new.tap do |variables|
        break variables unless gitlab_deploy_token

        variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.username)
478
        variables.append(key: 'CI_DEPLOY_PASSWORD', value: gitlab_deploy_token.token, public: false, masked: true)
479
      end
480
481
    end

482
483
484
485
    def features
      { trace_sections: true }
    end

486
    def merge_request
Z.J. van de Weg's avatar
Z.J. van de Weg committed
487
      return @merge_request if defined?(@merge_request)
Z.J. van de Weg's avatar
Z.J. van de Weg committed
488

489
490
      @merge_request ||=
        begin
491
          merge_requests = MergeRequest.includes(:latest_merge_request_diff)
492
493
            .where(source_branch: ref,
                   source_project: pipeline.project)
Z.J. van de Weg's avatar
Z.J. van de Weg committed
494
            .reorder(iid: :desc)
495
496

          merge_requests.find do |merge_request|
497
            merge_request.commit_shas.include?(pipeline.sha)
498
499
          end
        end
500
501
    end

Douwe Maan's avatar
Douwe Maan committed
502
    def repo_url
Kamil Trzciński's avatar
Kamil Trzciński committed
503
504
505
      return unless token

      auth = "gitlab-ci-token:#{token}@"
506
      project.http_url_to_repo.sub(%r{^https?://}) do |prefix|
507
508
        prefix + auth
      end
Douwe Maan's avatar
Douwe Maan committed
509
510
511
    end

    def allow_git_fetch
512
      project.build_allow_git_fetch
Douwe Maan's avatar
Douwe Maan committed
513
514
515
    end

    def update_coverage
516
      coverage = trace.extract_coverage(coverage_regex)
Lin Jen-Shin's avatar
Lin Jen-Shin committed
517
      update(coverage: coverage) if coverage.present?
Douwe Maan's avatar
Douwe Maan committed
518
519
    end

520
    # rubocop: disable CodeReuse/ServiceClass
521
    def parse_trace_sections!
522
      ExtractSectionsFromBuildTraceService.new(project, user).execute(self)
523
    end
524
    # rubocop: enable CodeReuse/ServiceClass
525

526
527
    def trace
      Gitlab::Ci::Trace.new(self)
528
529
    end

530
    def has_trace?
531
      trace.exist?
Tomasz Maczukin's avatar
Tomasz Maczukin committed
532
533
    end

534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
    def artifacts_file
      job_artifacts_archive&.file
    end

    def artifacts_size
      job_artifacts_archive&.size
    end

    def artifacts_metadata
      job_artifacts_metadata&.file
    end

    def artifacts?
      !artifacts_expired? && artifacts_file&.exists?
    end

    def artifacts_metadata?
      artifacts? && artifacts_metadata&.exists?
    end

554
555
    def has_job_artifacts?
      job_artifacts.any?
556
557
    end

Shinya Maeda's avatar
Shinya Maeda committed
558
559
560
561
    def has_old_trace?
      old_trace.present?
    end

562
563
    def trace=(data)
      raise NotImplementedError
Tomasz Maczukin's avatar
Tomasz Maczukin committed
564
565
    end

566
567
    def old_trace
      read_attribute(:trace)
568
569
    end

570
    def erase_old_trace!
571
      return unless has_old_trace?
Shinya Maeda's avatar
Shinya Maeda committed
572

573
      update_column(:trace, nil)
Douwe Maan's avatar
Douwe Maan committed
574
575
    end

576
577
578
579
    def needs_touch?
      Time.now - updated_at > 15.minutes.to_i
    end

Lin Jen-Shin's avatar
Lin Jen-Shin committed
580
    def valid_token?(token)
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
581
      self.token && ActiveSupport::SecurityUtils.secure_compare(token, self.token)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
582
583
    end

584
585
586
587
    def has_tags?
      tag_list.any?
    end

588
    def any_runners_online?
589
      project.any_runners? { |runner| runner.active? && runner.online? && runner.can_pick?(self) }
590
591
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
592
    def stuck?
593
594
595
      pending? && !any_runners_online?
    end

596
    def execute_hooks
597
      return unless project
598

599
      build_data = Gitlab::DataBuilder::Build.build(self)
600
601
      project.execute_hooks(build_data.dup, :job_hooks)
      project.execute_services(build_data.dup, :job_hooks)
602
603
    end

604
605
606
607
    def browsable_artifacts?
      artifacts_metadata?
    end

608
    def artifacts_metadata_entry(path, **options)
609
      artifacts_metadata.open do |metadata_stream|
610
        metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
611
          metadata_stream,
612
613
          path,
          **options)
614

615
616
        metadata.to_entry
      end
617
618
    end

619
620
621
    # and use that for `ExpireBuildInstanceArtifactsWorker`?
    def erase_erasable_artifacts!
      job_artifacts.erasable.destroy_all # rubocop: disable DestroyAll
622
623
    end

624
625
626
    def erase(opts = {})
      return false unless erasable?

627
      job_artifacts.destroy_all # rubocop: disable DestroyAll
628
629
630
631
632
      erase_trace!
      update_erased!(opts[:erased_by])
    end

    def erasable?
633
      complete? && (artifacts? || has_job_artifacts? || has_trace?)
634
635
636
637
638
639
    end

    def erased?
      !self.erased_at.nil?
    end

640
    def artifacts_expired?
641
      artifacts_expire_at && artifacts_expire_at < Time.now
642
643
    end

644
645
646
647
648
    def artifacts_expire_in
      artifacts_expire_at - Time.now if artifacts_expire_at
    end

    def artifacts_expire_in=(value)
649
650
      self.artifacts_expire_at =
        if value
651
          ChronicDuration.parse(value)&.seconds&.from_now
652
        end
653
654
    end

655
    def has_expiring_artifacts?
Z.J. van de Weg's avatar
Z.J. van de Weg committed
656
      artifacts_expire_at.present? && artifacts_expire_at > Time.now
657
658
    end

659
    def keep_artifacts!
660
      self.update(artifacts_expire_at: nil)
661
      self.job_artifacts.update_all(expire_at: nil)
662
663
    end

664
    def artifacts_file_for_type(type)
665
      job_artifacts.find_by(file_type: Ci::JobArtifact.file_types[type])&.file
666
667
    end

668
    def coverage_regex
669
      super || project.try(:build_coverage_regex)
670
671
    end

672
    def steps
Tomasz Maczukin's avatar
Tomasz Maczukin committed
673
674
      [Gitlab::Ci::Build::Step.from_commands(self),
       Gitlab::Ci::Build::Step.from_after_script(self)].compact
675
676
677
    end

    def image
678
      Gitlab::Ci::Build::Image.from_image(self)
679
680
681
    end

    def services
682
      Gitlab::Ci::Build::Image.from_services(self)
683
684
685
    end

    def cache
Matija Čupić's avatar
Matija Čupić committed
686
687
688
689
      cache = options[:cache]

      if cache && project.jobs_cache_index
        cache = cache.merge(
690
          key: "#{cache[:key]}-#{project.jobs_cache_index}")
691
      end
Matija Čupić's avatar
Matija Čupić committed
692
693

      [cache]
694
695
    end

696
    def credentials
697
      Gitlab::Ci::Build::Credentials::Factory.new(self).create!
698
699
    end

700
    def dependencies
701
702
      return [] if empty_dependencies?

703
704
      depended_jobs = depends_on_builds

705
      return depended_jobs unless options[:dependencies].present?
706

707
708
      depended_jobs.select do |job|
        options[:dependencies].include?(job.name)
709
710
711
      end
    end

712
713
714
715
    def empty_dependencies?
      options[:dependencies]&.empty?
    end

Kamil Trzciński's avatar
Kamil Trzciński committed
716
    def has_valid_build_dependencies?
Kamil Trzciński's avatar
Kamil Trzciński committed
717
      return true if Feature.enabled?('ci_disable_validates_dependencies')
718

Kamil Trzciński's avatar
Kamil Trzciński committed
719
      dependencies.all?(&:valid_dependency?)
720
721
    end

Kamil Trzciński's avatar
Kamil Trzciński committed
722
    def valid_dependency?
Shinya Maeda's avatar
Shinya Maeda committed
723
724
725
726
727
728
      return false if artifacts_expired?
      return false if erased?

      true
    end

729
730
731
732
733
734
735
736
    def runner_required_feature_names
      strong_memoize(:runner_required_feature_names) do
        RUNNER_FEATURES.select do |feature, method|
          method.call(self)
        end.keys
      end
    end

737
    def supported_runner?(features)
738
      runner_required_feature_names.all? do |feature_name|
Kamil Trzciński's avatar
Kamil Trzciński committed
739
        features&.dig(feature_name)
740
741
742
      end
    end

743
    def publishes_artifacts_reports?
744
      options&.dig(:artifacts, :reports)&.any?
745
746
    end

747
748
749
750
    def hide_secrets(trace)
      return unless trace

      trace = trace.dup
751
      Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project
Kamil Trzciński's avatar
Kamil Trzciński committed
752
      Gitlab::Ci::MaskSecret.mask!(trace, token) if token
753
754
755
      trace
    end

756
    def serializable_hash(options = {})
James Lopez's avatar
James Lopez committed
757
      super(options).merge(when: read_attribute(:when))
758
759
    end

760
761
762
763
    def has_terminal?
      running? && runner_session_url.present?
    end

764
765
    def collect_test_reports!(test_reports)
      test_reports.get_suite(group_name).tap do |test_suite|
766
        each_report(Ci::JobArtifact::TEST_REPORT_FILE_TYPES) do |file_type, blob|
767
          Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, test_suite)
768
769
770
771
        end
      end
    end

772
773
774
775
    def report_artifacts
      job_artifacts.with_reports
    end

776
777
    # Virtual deployment status depending on the environment status.
    def deployment_status
778
      return unless starts_environment?
779
780
781

      if success?
        return successful_deployment_status
782
      elsif failed?
783
784
785
786
787
788
        return :failed
      end

      :creating
    end

789
790
    private

791
    def successful_deployment_status
792
793
794
795
      if deployment&.last?
        :last
      else
        :out_of_date
796
797
798
      end
    end

799
800
801
802
    def each_report(report_types)
      job_artifacts_for_types(report_types).each do |report_artifact|
        report_artifact.each_blob do |blob|
          yield report_artifact.file_type, blob
803
804
805
806
        end
      end
    end

807
808
809
810
811
    def job_artifacts_for_types(report_types)
      # Use select to leverage cached associations and avoid N+1 queries
      job_artifacts.select { |artifact| artifact.file_type.in?(report_types) }
    end

812
    def erase_trace!
813
      trace.erase!
814
815
816
    end

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

820
    def unscoped_project
Kamil Trzciński's avatar
Kamil Trzciński committed
821
      @unscoped_project ||= Project.unscoped.find_by(id: project_id)
822
823
    end

824
    def environment_url
825
      options&.dig(:environment, :url) || persisted_environment&.external_url
826
827
    end

828
829
830
831
832
    # The format of the retry option changed in GitLab 11.5: Before it was
    # integer only, after it is a hash. New builds are created with the new
    # format, but builds created before GitLab 11.5 and saved in database still
    # have the old integer only format. This method returns the retry option
    # normalized as a hash in 11.5+ format.
833
    def normalized_retry
834
835
836
837
838
      strong_memoize(:normalized_retry) do
        value = options&.dig(:retry)
        value = value.is_a?(Integer) ? { max: value } : value.to_h
        value.with_indifferent_access
      end
839
840
    end

841
842
    def build_attributes_from_config
      return {} unless pipeline.config_processor
843

844
845
      pipeline.config_processor.build_attributes(name)
    end
Douwe Maan's avatar
Douwe Maan committed
846
847
  end
end