Read about our upcoming Code of Conduct on this issue

This instance was upgraded to Heptapod 0.28.0 today

build.rb 24.5 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 Gitlab::Utils::StrongMemoize
15
    include HasRef
16

17
18
    BuildArchivedError = Class.new(StandardError)

19
20
21
22
23
24
25
26
    self.ignored_columns += %i[
      artifacts_file
      artifacts_file_store
      artifacts_metadata
      artifacts_metadata_store
      artifacts_size
      commands
    ]
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
Kamil Trzciński's avatar
Kamil Trzciński committed
41
    has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build
42

43
    has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
Matija Čupić's avatar
Matija Čupić committed
44
    has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id
45
    has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id
46
47
48
49

    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
50

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

    accepts_nested_attributes_for :runner_session
Matija Čupić's avatar
Matija Čupić committed
54
    accepts_nested_attributes_for :job_variables
Kamil Trzciński's avatar
Kamil Trzciński committed
55
    accepts_nested_attributes_for :needs
56
57
58

    delegate :url, to: :runner_session, prefix: true, allow_nil: true
    delegate :terminal_specification, to: :runner_session, allow_nil: true
59
    delegate :gitlab_deploy_token, to: :project
60
    delegate :trigger_short_token, to: :trigger_request, allow_nil: true
Tomasz Maczukin's avatar
Tomasz Maczukin committed
61

62
    ##
63
64
65
66
    # 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.
67
    # (See more https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22380)
68
    #
69
70
71
72
73
    # 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.
74
    def persisted_environment
75
76
77
      return unless has_environment?

      strong_memoize(:persisted_environment) do
78
79
        deployment&.environment ||
          Environment.find_by(name: expanded_environment_name, project: project)
80
      end
81
82
    end

83
84
    serialize :options # rubocop:disable Cop/ActiveRecordSerialize
    serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize
Douwe Maan's avatar
Douwe Maan committed
85

Douwe Maan's avatar
Douwe Maan committed
86
87
    delegate :name, to: :project, prefix: true

Douwe Maan's avatar
Douwe Maan committed
88
    validates :coverage, numericality: true, allow_blank: true
Douwe Maan's avatar
Douwe Maan committed
89
    validates :ref, presence: true
Douwe Maan's avatar
Douwe Maan committed
90

91
92
93
94
95
    scope :not_interruptible, -> do
      joins(:metadata).where('ci_builds_metadata.id NOT IN (?)',
        Ci::BuildMetadata.scoped_build.with_interruptible.select(:id))
    end

Douwe Maan's avatar
Douwe Maan committed
96
    scope :unstarted, ->() { where(runner_id: nil) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
97
    scope :ignore_failures, ->() { where(allow_failure: false) }
98
    scope :with_artifacts_archive, ->() do
99
      where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
100
    end
101

102
103
104
105
    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

106
    scope :with_archived_trace, ->() do
107
      with_existing_job_artifacts(Ci::JobArtifact.trace)
108
109
    end

110
111
112
113
    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
114
115
    scope :with_reports, ->(reports_scope) do
      with_existing_job_artifacts(reports_scope)
116
        .eager_load_job_artifacts
117
118
    end

119
120
    scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }

121
122
    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) }
123
    scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
124
125
    scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) }
    scope :scheduled_actions, ->() { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) }
126
    scope :ref_protected, -> { where(protected: true) }
127
    scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) }
128
129
    scope :with_stale_live_trace, -> { with_live_trace.finished_before(12.hours.ago) }
    scope :finished_before, -> (date) { finished.where('finished_at < ?', date) }
130

131
132
133
134
135
136
    scope :with_secure_reports_from_options, -> (job_type) { where('options like :job_type', job_type: "%:artifacts:%:reports:%:#{job_type}:%") }

    scope :with_secure_reports_from_config_options, -> (job_types) do
      joins(:metadata).where("ci_builds_metadata.config_options -> 'artifacts' -> 'reports' ?| array[:job_types]", job_types: job_types)
    end

137
138
    scope :matches_tag_ids, -> (tag_ids) do
      matcher = ::ActsAsTaggableOn::Tagging
139
        .where(taggable_type: CommitStatus.name)
140
141
142
143
144
145
146
147
148
        .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
149
        .where(taggable_type: CommitStatus.name)
150
151
152
153
154
155
        .where(context: 'tags')
        .where('taggable_id = ci_builds.id').select('1')

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

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

Douwe Maan's avatar
Douwe Maan committed
158
159
    acts_as_taggable

Kamil Trzciński's avatar
Kamil Trzciński committed
160
    add_authentication_token_field :token, encrypted: :optional
161
162

    before_save :ensure_token
163
    before_destroy { unscoped_project }
Grzegorz Bizon's avatar
Grzegorz Bizon committed
164

165
    after_create unless: :importing? do |build|
166
      run_after_commit { BuildHooksWorker.perform_async(build.id) }
167
168
    end

Douwe Maan's avatar
Douwe Maan committed
169
    class << self
170
171
172
173
174
175
      # 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
176
177
178
179
      def first_pending
        pending.unstarted.order('created_at ASC').first
      end

180
      def retry(build, current_user)
181
        # rubocop: disable CodeReuse/ServiceClass
182
183
184
        Ci::RetryBuildService
          .new(build.project, current_user)
          .execute(build)
185
        # rubocop: enable CodeReuse/ServiceClass
Douwe Maan's avatar
Douwe Maan committed
186
187
188
      end
    end

189
    state_machine :status do
190
191
192
193
      event :enqueue do
        transition [:created, :skipped, :manual, :scheduled] => :preparing, if: :any_unmet_prerequisites?
      end

194
195
      event :actionize do
        transition created: :manual
Kamil Trzcinski's avatar
Kamil Trzcinski committed
196
197
      end

198
199
200
201
202
203
204
205
      event :schedule do
        transition created: :scheduled
      end

      event :unschedule do
        transition scheduled: :manual
      end

206
      event :enqueue_scheduled do
207
208
209
210
        transition scheduled: :preparing, if: ->(build) do
          build.scheduled_at&.past? && build.any_unmet_prerequisites?
        end

211
        transition scheduled: :pending, if: ->(build) do
212
          build.scheduled_at&.past? && !build.any_unmet_prerequisites?
213
        end
214
215
216
      end

      before_transition scheduled: any do |build|
217
218
219
220
        build.scheduled_at = nil
      end

      before_transition created: :scheduled do |build|
Shinya Maeda's avatar
Shinya Maeda committed
221
        build.scheduled_at = build.options_scheduled_at
222
223
224
225
226
227
      end

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

230
231
232
233
234
235
      after_transition any => [:preparing] do |build|
        build.run_after_commit do
          Ci::BuildPrepareWorker.perform_async(id)
        end
      end

236
237
      after_transition any => [:pending] do |build|
        build.run_after_commit do
Kim "BKC" Carlbäcker's avatar
linting    
Kim "BKC" Carlbäcker committed
238
          BuildQueueWorker.perform_async(id)
239
240
241
        end
      end

242
      after_transition pending: :running do |build|
243
        build.pipeline.persistent_ref.create
244
245
        build.deployment&.run

246
247
248
        build.run_after_commit do
          BuildHooksWorker.perform_async(id)
        end
249
250
      end

251
      after_transition any => [:success, :failed, :canceled] do |build|
252
        build.run_after_commit do
253
          BuildFinishedWorker.perform_async(id)
254
        end
Douwe Maan's avatar
Douwe Maan committed
255
      end
256

257
      after_transition any => [:success] do |build|
258
259
        build.deployment&.succeed

260
        build.run_after_commit do
261
          BuildSuccessWorker.perform_async(id)
262
          PagesWorker.perform_async(:deploy, id) if build.pages_generator?
263
264
        end
      end
265

266
      before_transition any => [:failed] do |build|
267
        next unless build.project
268
        next unless build.deployment
269

270
271
272
273
274
275
276
        begin
          build.deployment.drop!
        rescue => e
          Gitlab::Sentry.track_exception(e, extra: { build_id: build.id })
        end

        true
277
278
279
280
      end

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

282
        if build.retry_failure?
283
284
285
          begin
            Ci::Build.retry(build, build.user)
          rescue Gitlab::Access::AccessDeniedError => ex
Mayra Cabrera's avatar
Mayra Cabrera committed
286
            Rails.logger.error "Unable to auto-retry job #{build.id}: #{ex}" # rubocop:disable Gitlab/RailsLogger
287
          end
288
289
        end
      end
290

291
      after_transition pending: :running do |build|
292
        build.ensure_metadata.update_timeout_state
293
      end
294
295
296
297

      after_transition running: any do |build|
        Ci::BuildRunnerSession.where(build: build).delete_all
      end
298
299
300
301

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

304
    def detailed_status(current_user)
305
306
307
      Gitlab::Ci::Status::Build::Factory
        .new(self, current_user)
        .fabricate!
Kamil Trzcinski's avatar
Kamil Trzcinski committed
308
309
    end

310
    def other_manual_actions
311
      pipeline.manual_actions.where.not(name: name)
312
313
    end

314
315
    def other_scheduled_actions
      pipeline.scheduled_actions.where.not(name: name)
316
317
    end

318
319
320
321
322
    def pages_generator?
      Gitlab.config.pages.enabled &&
        self.name == 'pages'
    end

323
324
325
326
    def runnable?
      true
    end

327
328
329
330
331
332
333
    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

334
    def playable?
335
      action? && !archived? && (manual? || scheduled? || retryable?)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
336
337
    end

Shinya Maeda's avatar
Shinya Maeda committed
338
    def schedulable?
339
      self.when == 'delayed' && options[:start_in].present?
Shinya Maeda's avatar
Shinya Maeda committed
340
341
    end

Shinya Maeda's avatar
Shinya Maeda committed
342
    def options_scheduled_at
Shinya Maeda's avatar
Shinya Maeda committed
343
      ChronicDuration.parse(options[:start_in])&.seconds&.from_now
Kamil Trzcinski's avatar
Kamil Trzcinski committed
344
345
    end

346
    def action?
347
      %w[manual delayed].include?(self.when)
348
349
    end

350
    # rubocop: disable CodeReuse/ServiceClass
Matija Čupić's avatar
Matija Čupić committed
351
    def play(current_user, job_variables_attributes = nil)
352
353
      Ci::PlayBuildService
        .new(project, current_user)
Matija Čupić's avatar
Matija Čupić committed
354
        .execute(self, job_variables_attributes)
355
    end
356
    # rubocop: enable CodeReuse/ServiceClass
357

Kamil Trzcinski's avatar
Kamil Trzcinski committed
358
    def cancelable?
Jacopo's avatar
Jacopo committed
359
      active? || created?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
360
361
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
362
    def retryable?
Kamil Trzciński's avatar
Kamil Trzciński committed
363
      !archived? && (success? || failed? || canceled?)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
364
    end
365
366
367
368
369
370

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

    def retries_max
371
      normalized_retry.fetch(:max, 0)
372
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
373

374
    def retry_when
375
      normalized_retry.fetch(:when, ['always'])
376
377
    end

378
379
380
    def retry_failure?
      return false if retries_max.zero? || retries_count >= retries_max

381
      retry_when.include?('always') || retry_when.include?(failure_reason.to_s)
382
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
383

384
385
    def latest?
      !retried?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
386
387
    end

388
389
390
391
392
393
394
395
    def any_unmet_prerequisites?
      prerequisites.present?
    end

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

396
    def expanded_environment_name
397
398
399
      return unless has_environment?

      strong_memoize(:expanded_environment_name) do
400
        ExpandVariables.expand(environment, -> { simple_variables })
401
      end
402
403
    end

404
    def has_environment?
405
      environment.present?
406
407
    end

408
    def starts_environment?
409
      has_environment? && self.environment_action == 'start'
410
411
412
    end

    def stops_environment?
413
      has_environment? && self.environment_action == 'stop'
414
415
416
    end

    def environment_action
417
      self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options
418
419
420
    end

    def outdated_deployment?
421
      success? && !deployment.try(:last?)
422
    end
423

424
425
    def depends_on_builds
      # Get builds of the same type
426
      latest_builds = self.pipeline.builds.latest
427
428
429
430
431

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

432
    def triggered_by?(current_user)
Shinya Maeda's avatar
Shinya Maeda committed
433
434
435
      user == current_user
    end

436
437
438
439
    def on_stop
      options&.dig(:environment, :on_stop)
    end

440
441
442
443
    ##
    # All variables, including persisted environment variables.
    #
    def variables
444
445
446
447
      strong_memoize(:variables) do
        Gitlab::Ci::Variables::Collection.new
          .concat(persisted_variables)
          .concat(scoped_variables)
Matija Čupić's avatar
Matija Čupić committed
448
          .concat(job_variables)
449
450
451
          .concat(persisted_environment_variables)
          .to_runner_variables
      end
452
453
    end

454
    CI_REGISTRY_USER = 'gitlab-ci-token'
455
456
457
458
459
460
461
462
463

    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))
464
          .append(key: 'CI_JOB_TOKEN', value: token.to_s, public: false, masked: true)
465
          .append(key: 'CI_BUILD_ID', value: id.to_s)
466
          .append(key: 'CI_BUILD_TOKEN', value: token.to_s, public: false, masked: true)
467
          .append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
468
          .append(key: 'CI_REGISTRY_PASSWORD', value: token.to_s, public: false, masked: true)
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
          .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)
492
        variables.append(key: 'CI_DEPLOY_PASSWORD', value: gitlab_deploy_token.token, public: false, masked: true)
493
      end
494
495
    end

496
497
498
499
    def features
      { trace_sections: true }
    end

500
    def merge_request
Z.J. van de Weg's avatar
Z.J. van de Weg committed
501
      return @merge_request if defined?(@merge_request)
Z.J. van de Weg's avatar
Z.J. van de Weg committed
502

503
504
      @merge_request ||=
        begin
505
          merge_requests = MergeRequest.includes(:latest_merge_request_diff)
506
507
            .where(source_branch: ref,
                   source_project: pipeline.project)
Z.J. van de Weg's avatar
Z.J. van de Weg committed
508
            .reorder(iid: :desc)
509
510

          merge_requests.find do |merge_request|
511
            merge_request.commit_shas.include?(pipeline.sha)
512
513
          end
        end
514
515
    end

Douwe Maan's avatar
Douwe Maan committed
516
    def repo_url
Kamil Trzciński's avatar
Kamil Trzciński committed
517
518
519
      return unless token

      auth = "gitlab-ci-token:#{token}@"
520
      project.http_url_to_repo.sub(%r{^https?://}) do |prefix|
521
522
        prefix + auth
      end
Douwe Maan's avatar
Douwe Maan committed
523
524
525
    end

    def allow_git_fetch
526
      project.build_allow_git_fetch
Douwe Maan's avatar
Douwe Maan committed
527
528
529
    end

    def update_coverage
530
      coverage = trace.extract_coverage(coverage_regex)
Lin Jen-Shin's avatar
Lin Jen-Shin committed
531
      update(coverage: coverage) if coverage.present?
Douwe Maan's avatar
Douwe Maan committed
532
533
    end

534
    # rubocop: disable CodeReuse/ServiceClass
535
    def parse_trace_sections!
536
      ExtractSectionsFromBuildTraceService.new(project, user).execute(self)
537
    end
538
    # rubocop: enable CodeReuse/ServiceClass
539

540
541
    def trace
      Gitlab::Ci::Trace.new(self)
542
543
    end

544
    def has_trace?
545
      trace.exist?
Tomasz Maczukin's avatar
Tomasz Maczukin committed
546
547
    end

548
549
550
551
552
553
554
555
    def has_live_trace?
      trace.live_trace_exist?
    end

    def has_archived_trace?
      trace.archived_trace_exist?
    end

556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
    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

576
577
    def has_job_artifacts?
      job_artifacts.any?
578
579
    end

Shinya Maeda's avatar
Shinya Maeda committed
580
581
582
583
    def has_old_trace?
      old_trace.present?
    end

584
585
    def trace=(data)
      raise NotImplementedError
Tomasz Maczukin's avatar
Tomasz Maczukin committed
586
587
    end

588
589
    def old_trace
      read_attribute(:trace)
590
591
    end

592
    def erase_old_trace!
593
      return unless has_old_trace?
Shinya Maeda's avatar
Shinya Maeda committed
594

595
      update_column(:trace, nil)
Douwe Maan's avatar
Douwe Maan committed
596
597
    end

598
599
600
601
    def needs_touch?
      Time.now - updated_at > 15.minutes.to_i
    end

Lin Jen-Shin's avatar
Lin Jen-Shin committed
602
    def valid_token?(token)
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
603
      self.token && ActiveSupport::SecurityUtils.secure_compare(token, self.token)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
604
605
    end

606
607
608
609
    def has_tags?
      tag_list.any?
    end

610
    def any_runners_online?
611
      project.any_runners? { |runner| runner.active? && runner.online? && runner.can_pick?(self) }
612
613
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
614
    def stuck?
615
616
617
      pending? && !any_runners_online?
    end

618
    def execute_hooks
619
      return unless project
620

621
      build_data = Gitlab::DataBuilder::Build.build(self)
622
623
      project.execute_hooks(build_data.dup, :job_hooks)
      project.execute_services(build_data.dup, :job_hooks)
624
625
    end

626
627
628
629
    def browsable_artifacts?
      artifacts_metadata?
    end

630
    def artifacts_metadata_entry(path, **options)
631
      artifacts_metadata.open do |metadata_stream|
632
        metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
633
          metadata_stream,
634
635
          path,
          **options)
636

637
638
        metadata.to_entry
      end
639
640
    end

641
642
643
    # and use that for `ExpireBuildInstanceArtifactsWorker`?
    def erase_erasable_artifacts!
      job_artifacts.erasable.destroy_all # rubocop: disable DestroyAll
644
645
    end

646
647
648
    def erase(opts = {})
      return false unless erasable?

649
      job_artifacts.destroy_all # rubocop: disable DestroyAll
650
651
652
653
654
      erase_trace!
      update_erased!(opts[:erased_by])
    end

    def erasable?
655
      complete? && (artifacts? || has_job_artifacts? || has_trace?)
656
657
658
659
660
661
    end

    def erased?
      !self.erased_at.nil?
    end

662
    def artifacts_expired?
663
      artifacts_expire_at && artifacts_expire_at < Time.now
664
665
    end

666
667
668
669
670
    def artifacts_expire_in
      artifacts_expire_at - Time.now if artifacts_expire_at
    end

    def artifacts_expire_in=(value)
671
672
      self.artifacts_expire_at =
        if value
673
          ChronicDuration.parse(value)&.seconds&.from_now
674
        end
675
676
    end

677
    def has_expiring_artifacts?
Z.J. van de Weg's avatar
Z.J. van de Weg committed
678
      artifacts_expire_at.present? && artifacts_expire_at > Time.now
679
680
    end

681
    def keep_artifacts!
682
      self.update(artifacts_expire_at: nil)
683
      self.job_artifacts.update_all(expire_at: nil)
684
685
    end

686
    def artifacts_file_for_type(type)
687
      job_artifacts.find_by(file_type: Ci::JobArtifact.file_types[type])&.file
688
689
    end

690
    def coverage_regex
691
      super || project.try(:build_coverage_regex)
692
693
    end

694
    def steps
Tomasz Maczukin's avatar
Tomasz Maczukin committed
695
696
      [Gitlab::Ci::Build::Step.from_commands(self),
       Gitlab::Ci::Build::Step.from_after_script(self)].compact
697
698
699
    end

    def image
700
      Gitlab::Ci::Build::Image.from_image(self)
701
702
703
    end

    def services
704
      Gitlab::Ci::Build::Image.from_services(self)
705
706
707
    end

    def cache
Matija Čupić's avatar
Matija Čupić committed
708
709
710
711
      cache = options[:cache]

      if cache && project.jobs_cache_index
        cache = cache.merge(
712
          key: "#{cache[:key]}-#{project.jobs_cache_index}")
713
      end
Matija Čupić's avatar
Matija Čupić committed
714
715

      [cache]
716
717
    end

718
    def credentials
719
      Gitlab::Ci::Build::Credentials::Factory.new(self).create!
720
721
    end

722
    def dependencies
723
724
      return [] if empty_dependencies?

725
726
      depended_jobs = depends_on_builds

Kamil Trzciński's avatar
Kamil Trzciński committed
727
      # find all jobs that are needed
728
      if Feature.enabled?(:ci_dag_support, project, default_enabled: true) && needs.exists?
Kamil Trzciński's avatar
Kamil Trzciński committed
729
        depended_jobs = depended_jobs.where(name: needs.select(:name))
Kamil Trzciński's avatar
Kamil Trzciński committed
730
      end
731

Kamil Trzciński's avatar
Kamil Trzciński committed
732
733
734
      # find all jobs that are dependent on
      if options[:dependencies].present?
        depended_jobs = depended_jobs.where(name: options[:dependencies])
735
      end
Kamil Trzciński's avatar
Kamil Trzciński committed
736
737

      depended_jobs
738
739
    end

740
741
742
743
    def empty_dependencies?
      options[:dependencies]&.empty?
    end

Kamil Trzciński's avatar
Kamil Trzciński committed
744
    def has_valid_build_dependencies?
Kamil Trzciński's avatar
Kamil Trzciński committed
745
      return true if Feature.enabled?('ci_disable_validates_dependencies')
746

Kamil Trzciński's avatar
Kamil Trzciński committed
747
      dependencies.all?(&:valid_dependency?)
748
749
    end

Kamil Trzciński's avatar
Kamil Trzciński committed
750
    def valid_dependency?
Shinya Maeda's avatar
Shinya Maeda committed
751
752
753
754
755
756
      return false if artifacts_expired?
      return false if erased?

      true
    end

757
758
759
760
761
762
763
764
    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

765
    def supported_runner?(features)
766
      runner_required_feature_names.all? do |feature_name|
Kamil Trzciński's avatar
Kamil Trzciński committed
767
        features&.dig(feature_name)
768
769
770
      end
    end

771
    def publishes_artifacts_reports?
772
      options&.dig(:artifacts, :reports)&.any?
773
774
    end

775
776
777
778
    def hide_secrets(trace)
      return unless trace

      trace = trace.dup
779
      Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project
Kamil Trzciński's avatar
Kamil Trzciński committed
780
      Gitlab::Ci::MaskSecret.mask!(trace, token) if token
781
782
783
      trace
    end

784
    def serializable_hash(options = {})
James Lopez's avatar
James Lopez committed
785
      super(options).merge(when: read_attribute(:when))
786
787
    end

788
789
790
791
    def has_terminal?
      running? && runner_session_url.present?
    end

792
793
    def collect_test_reports!(test_reports)
      test_reports.get_suite(group_name).tap do |test_suite|
794
        each_report(Ci::JobArtifact::TEST_REPORT_FILE_TYPES) do |file_type, blob|
795
          Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, test_suite)
796
797
798
799
        end
      end
    end

800
801
802
803
    def report_artifacts
      job_artifacts.with_reports
    end

804
805
    # Virtual deployment status depending on the environment status.
    def deployment_status
806
      return unless starts_environment?
807
808
809

      if success?
        return successful_deployment_status
810
      elsif failed?
811
812
813
814
815
816
        return :failed
      end

      :creating
    end

817
818
    private

819
    def successful_deployment_status
820
821
822
823
      if deployment&.last?
        :last
      else
        :out_of_date
824
825
826
      end
    end

827
828
829
830
    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
831
832
833
834
        end
      end
    end

835
836
837
838
839
    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

840
    def erase_trace!
841
      trace.erase!
842
843
844
    end

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

848
    def unscoped_project
Kamil Trzciński's avatar
Kamil Trzciński committed
849
      @unscoped_project ||= Project.unscoped.find_by(id: project_id)
850
851
    end

852
    def environment_url
853
      options&.dig(:environment, :url) || persisted_environment&.external_url
854
855
    end

856
857
858
859
860
    # 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.
861
    def normalized_retry
862
863
864
865
866
      strong_memoize(:normalized_retry) do
        value = options&.dig(:retry)
        value = value.is_a?(Integer) ? { max: value } : value.to_h
        value.with_indifferent_access
      end
867
868
    end

869
870
    def build_attributes_from_config
      return {} unless pipeline.config_processor
871

872
873
      pipeline.config_processor.build_attributes(name)
    end
Douwe Maan's avatar
Douwe Maan committed
874
875
  end
end
876
877

Ci::Build.prepend_if_ee('EE::Ci::Build')