1# frozen_string_literal: true
2
3module Ci
4  class Build < Ci::Processable
5    include Ci::Metadatable
6    include Ci::Contextable
7    include TokenAuthenticatable
8    include AfterCommitQueue
9    include ObjectStorage::BackgroundMove
10    include Presentable
11    include Importable
12    include Ci::HasRef
13    extend ::Gitlab::Utils::Override
14
15    BuildArchivedError = Class.new(StandardError)
16
17    belongs_to :project, inverse_of: :builds
18    belongs_to :runner
19    belongs_to :trigger_request
20    belongs_to :erased_by, class_name: 'User'
21    belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
22
23    RUNNER_FEATURES = {
24      upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? },
25      refspecs: -> (build) { build.merge_request_ref? },
26      artifacts_exclude: -> (build) { build.supports_artifacts_exclude? },
27      multi_build_steps: -> (build) { build.multi_build_steps? },
28      return_exit_code: -> (build) { build.exit_codes_defined? }
29    }.freeze
30
31    DEFAULT_RETRIES = {
32      scheduler_failure: 2
33    }.freeze
34
35    DEGRADATION_THRESHOLD_VARIABLE_NAME = 'DEGRADATION_THRESHOLD'
36    RUNNERS_STATUS_CACHE_EXPIRATION = 1.minute
37
38    has_one :deployment, as: :deployable, class_name: 'Deployment'
39    has_one :pending_state, class_name: 'Ci::BuildPendingState', inverse_of: :build
40    has_one :queuing_entry, class_name: 'Ci::PendingBuild', foreign_key: :build_id
41    has_one :runtime_metadata, class_name: 'Ci::RunningBuild', foreign_key: :build_id
42    has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build
43    has_many :report_results, class_name: 'Ci::BuildReportResult', inverse_of: :build
44
45    # Projects::DestroyService destroys Ci::Pipelines, which use_fast_destroy on :job_artifacts
46    # before we delete builds. By doing this, the relation should be empty and not fire any
47    # DELETE queries when the Ci::Build is destroyed. The next step is to remove `dependent: :destroy`.
48    # Details: https://gitlab.com/gitlab-org/gitlab/-/issues/24644#note_689472685
49    has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
50    has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id
51    has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id
52
53    has_many :pages_deployments, inverse_of: :ci_build
54
55    Ci::JobArtifact.file_types.each do |key, value|
56      has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
57    end
58
59    has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build
60    has_one :trace_metadata, class_name: 'Ci::BuildTraceMetadata', inverse_of: :build
61
62    has_many :terraform_state_versions, class_name: 'Terraform::StateVersion', inverse_of: :build, foreign_key: :ci_build_id
63
64    accepts_nested_attributes_for :runner_session, update_only: true
65    accepts_nested_attributes_for :job_variables
66
67    delegate :url, to: :runner_session, prefix: true, allow_nil: true
68    delegate :terminal_specification, to: :runner_session, allow_nil: true
69    delegate :service_specification, to: :runner_session, allow_nil: true
70    delegate :gitlab_deploy_token, to: :project
71    delegate :trigger_short_token, to: :trigger_request, allow_nil: true
72
73    ##
74    # Since Gitlab 11.5, deployments records started being created right after
75    # `ci_builds` creation. We can look up a relevant `environment` through
76    # `deployment` relation today.
77    # (See more https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22380)
78    #
79    # Since Gitlab 12.9, we started persisting the expanded environment name to
80    # avoid repeated variables expansion in `action: stop` builds as well.
81    def persisted_environment
82      return unless has_environment?
83
84      strong_memoize(:persisted_environment) do
85        # This code path has caused N+1s in the past, since environments are only indirectly
86        # associated to builds and pipelines; see https://gitlab.com/gitlab-org/gitlab/-/issues/326445
87        # We therefore batch-load them to prevent dormant N+1s until we found a proper solution.
88        BatchLoader.for(expanded_environment_name).batch(key: project_id) do |names, loader, args|
89          Environment.where(name: names, project: args[:key]).find_each do |environment|
90            loader.call(environment.name, environment)
91          end
92        end
93      end
94    end
95
96    def persisted_environment=(environment)
97      strong_memoize(:persisted_environment) { environment }
98    end
99
100    serialize :options # rubocop:disable Cop/ActiveRecordSerialize
101    serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize
102
103    delegate :name, to: :project, prefix: true
104
105    validates :coverage, numericality: true, allow_blank: true
106    validates :ref, presence: true
107
108    scope :not_interruptible, -> do
109      joins(:metadata).where.not('ci_builds_metadata.id' => Ci::BuildMetadata.scoped_build.with_interruptible.select(:id))
110    end
111
112    scope :unstarted, -> { where(runner_id: nil) }
113    scope :with_downloadable_artifacts, -> do
114      where('EXISTS (?)',
115        Ci::JobArtifact.select(1)
116          .where('ci_builds.id = ci_job_artifacts.job_id')
117          .where(file_type: Ci::JobArtifact::DOWNLOADABLE_TYPES)
118      )
119    end
120
121    scope :in_pipelines, ->(pipelines) do
122      where(pipeline: pipelines)
123    end
124
125    scope :with_existing_job_artifacts, ->(query) do
126      where('EXISTS (?)', ::Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').merge(query))
127    end
128
129    scope :without_archived_trace, -> do
130      where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
131    end
132
133    scope :with_reports, ->(reports_scope) do
134      with_existing_job_artifacts(reports_scope)
135        .eager_load_job_artifacts
136    end
137
138    scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }
139    scope :eager_load_tags, -> { includes(:tags) }
140
141    scope :eager_load_everything, -> do
142      includes(
143        [
144          { pipeline: [:project, :user] },
145          :job_artifacts_archive,
146          :metadata,
147          :trigger_request,
148          :project,
149          :user,
150          :tags
151        ]
152      )
153    end
154
155    scope :with_exposed_artifacts, -> do
156      joins(:metadata).merge(Ci::BuildMetadata.with_exposed_artifacts)
157        .includes(:metadata, :job_artifacts_metadata)
158    end
159
160    scope :with_project_and_metadata, -> do
161      if Feature.enabled?(:non_public_artifacts, type: :development)
162        joins(:metadata).includes(:metadata).preload(:project)
163      end
164    end
165
166    scope :with_artifacts_not_expired, -> { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.current) }
167    scope :with_expired_artifacts, -> { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.current) }
168    scope :with_pipeline_locked_artifacts, -> { joins(:pipeline).where('pipeline.locked': Ci::Pipeline.lockeds[:artifacts_locked]) }
169    scope :last_month, -> { where('created_at > ?', Date.today - 1.month) }
170    scope :manual_actions, -> { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) }
171    scope :scheduled_actions, -> { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) }
172    scope :ref_protected, -> { where(protected: true) }
173    scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) }
174    scope :with_stale_live_trace, -> { with_live_trace.finished_before(12.hours.ago) }
175    scope :finished_before, -> (date) { finished.where('finished_at < ?', date) }
176    scope :license_management_jobs, -> { where(name: %i(license_management license_scanning)) } # handle license rename https://gitlab.com/gitlab-org/gitlab/issues/8911
177
178    scope :with_secure_reports_from_config_options, -> (job_types) do
179      joins(:metadata).where("ci_builds_metadata.config_options -> 'artifacts' -> 'reports' ?| array[:job_types]", job_types: job_types)
180    end
181
182    scope :queued_before, ->(time) { where(arel_table[:queued_at].lt(time)) }
183
184    scope :preload_project_and_pipeline_project, -> do
185      preload(Ci::Pipeline::PROJECT_ROUTE_AND_NAMESPACE_ROUTE,
186              pipeline: Ci::Pipeline::PROJECT_ROUTE_AND_NAMESPACE_ROUTE)
187    end
188
189    scope :with_coverage, -> { where.not(coverage: nil) }
190    scope :without_coverage, -> { where(coverage: nil) }
191    scope :with_coverage_regex, -> { where.not(coverage_regex: nil) }
192
193    acts_as_taggable
194
195    add_authentication_token_field :token, encrypted: :required
196
197    before_save :ensure_token
198
199    after_save :stick_build_if_status_changed
200
201    after_create unless: :importing? do |build|
202      run_after_commit { BuildHooksWorker.perform_async(build.id) }
203    end
204
205    class << self
206      # This is needed for url_for to work,
207      # as the controller is JobsController
208      def model_name
209        ActiveModel::Name.new(self, nil, 'job')
210      end
211
212      def first_pending
213        pending.unstarted.order('created_at ASC').first
214      end
215
216      def retry(build, current_user)
217        # rubocop: disable CodeReuse/ServiceClass
218        Ci::RetryBuildService
219          .new(build.project, current_user)
220          .execute(build)
221        # rubocop: enable CodeReuse/ServiceClass
222      end
223
224      def with_preloads
225        preload(:job_artifacts_archive, :job_artifacts, :tags, project: [:namespace])
226      end
227    end
228
229    state_machine :status do
230      event :enqueue do
231        transition [:created, :skipped, :manual, :scheduled] => :preparing, if: :any_unmet_prerequisites?
232      end
233
234      event :enqueue_scheduled do
235        transition scheduled: :preparing, if: :any_unmet_prerequisites?
236        transition scheduled: :pending
237      end
238
239      event :enqueue_preparing do
240        transition preparing: :pending
241      end
242
243      event :actionize do
244        transition created: :manual
245      end
246
247      event :schedule do
248        transition created: :scheduled
249      end
250
251      event :unschedule do
252        transition scheduled: :manual
253      end
254
255      before_transition on: :enqueue_scheduled do |build|
256        build.scheduled_at.nil? || build.scheduled_at.past? # If false is returned, it stops the transition
257      end
258
259      before_transition scheduled: any do |build|
260        build.scheduled_at = nil
261      end
262
263      before_transition created: :scheduled do |build|
264        build.scheduled_at = build.options_scheduled_at
265      end
266
267      before_transition on: :enqueue_preparing do |build|
268        !build.any_unmet_prerequisites? # If false is returned, it stops the transition
269      end
270
271      after_transition created: :scheduled do |build|
272        build.run_after_commit do
273          Ci::BuildScheduleWorker.perform_at(build.scheduled_at, build.id)
274        end
275      end
276
277      after_transition any => [:preparing] do |build|
278        build.run_after_commit do
279          Ci::BuildPrepareWorker.perform_async(id)
280        end
281      end
282
283      # rubocop:disable CodeReuse/ServiceClass
284      after_transition any => [:pending] do |build, transition|
285        Ci::UpdateBuildQueueService.new.push(build, transition)
286
287        build.run_after_commit do
288          BuildQueueWorker.perform_async(id)
289          BuildHooksWorker.perform_async(id)
290        end
291      end
292
293      after_transition pending: any do |build, transition|
294        Ci::UpdateBuildQueueService.new.pop(build, transition)
295      end
296
297      after_transition any => [:running] do |build, transition|
298        Ci::UpdateBuildQueueService.new.track(build, transition)
299      end
300
301      after_transition running: any do |build, transition|
302        Ci::UpdateBuildQueueService.new.untrack(build, transition)
303
304        Ci::BuildRunnerSession.where(build: build).delete_all
305      end
306
307      # rubocop:enable CodeReuse/ServiceClass
308      #
309      after_transition pending: :running do |build|
310        build.ensure_metadata.update_timeout_state
311      end
312
313      after_transition pending: :running do |build|
314        build.run_after_commit do
315          build.pipeline.persistent_ref.create
316
317          BuildHooksWorker.perform_async(id)
318        end
319      end
320
321      after_transition any => [:success, :failed, :canceled] do |build|
322        build.run_after_commit do
323          build.run_status_commit_hooks!
324
325          if Feature.enabled?(:ci_build_finished_worker_namespace_changed, build.project, default_enabled: :yaml)
326            Ci::BuildFinishedWorker.perform_async(id)
327          else
328            ::BuildFinishedWorker.perform_async(id)
329          end
330        end
331      end
332
333      after_transition any => [:success] do |build|
334        build.run_after_commit do
335          BuildSuccessWorker.perform_async(id)
336          PagesWorker.perform_async(:deploy, id) if build.pages_generator?
337        end
338      end
339
340      after_transition any => [:failed] do |build|
341        next unless build.project
342
343        if build.auto_retry_allowed?
344          begin
345            Ci::Build.retry(build, build.user)
346          rescue Gitlab::Access::AccessDeniedError => ex
347            Gitlab::AppLogger.error "Unable to auto-retry job #{build.id}: #{ex}"
348          end
349        end
350      end
351
352      # Synchronize Deployment Status
353      # Please note that the data integirty is not assured because we can't use
354      # a database transaction due to DB decomposition.
355      after_transition do |build, transition|
356        next if transition.loopback?
357        next unless build.project
358
359        build.run_after_commit do
360          build.deployment&.sync_status_with(build)
361        end
362      end
363    end
364
365    def self.build_matchers(project)
366      unique_params = [
367        :protected,
368        Arel.sql("(#{arel_tag_names_array.to_sql})")
369      ]
370
371      group(*unique_params).pluck('array_agg(id)', *unique_params).map do |values|
372        Gitlab::Ci::Matching::BuildMatcher.new({
373          build_ids: values[0],
374          protected: values[1],
375          tag_list: values[2],
376          project: project
377        })
378      end
379    end
380
381    def build_matcher
382      strong_memoize(:build_matcher) do
383        Gitlab::Ci::Matching::BuildMatcher.new({
384          protected: protected?,
385          tag_list: tag_list,
386          build_ids: [id],
387          project: project
388        })
389      end
390    end
391
392    def auto_retry_allowed?
393      auto_retry.allowed?
394    end
395
396    def detailed_status(current_user)
397      Gitlab::Ci::Status::Build::Factory
398        .new(self.present, current_user)
399        .fabricate!
400    end
401
402    def other_manual_actions
403      pipeline.manual_actions.reject { |action| action.name == self.name }
404    end
405
406    def other_scheduled_actions
407      pipeline.scheduled_actions.reject { |action| action.name == self.name }
408    end
409
410    def pages_generator?
411      Gitlab.config.pages.enabled &&
412        self.name == 'pages'
413    end
414
415    def runnable?
416      true
417    end
418
419    def archived?
420      return true if degenerated?
421
422      archive_builds_older_than = Gitlab::CurrentSettings.current_application_settings.archive_builds_older_than
423      archive_builds_older_than.present? && created_at < archive_builds_older_than
424    end
425
426    def playable?
427      action? && !archived? && (manual? || scheduled? || retryable?)
428    end
429
430    def schedulable?
431      self.when == 'delayed' && options[:start_in].present?
432    end
433
434    def options_scheduled_at
435      ChronicDuration.parse(options[:start_in])&.seconds&.from_now
436    end
437
438    def action?
439      %w[manual delayed].include?(self.when)
440    end
441
442    # rubocop: disable CodeReuse/ServiceClass
443    def play(current_user, job_variables_attributes = nil)
444      Ci::PlayBuildService
445        .new(project, current_user)
446        .execute(self, job_variables_attributes)
447    end
448    # rubocop: enable CodeReuse/ServiceClass
449
450    def cancelable?
451      active? || created?
452    end
453
454    def retryable?
455      return false if retried? || archived? || deployment_rejected?
456
457      success? || failed? || canceled?
458    end
459
460    def retries_count
461      pipeline.builds.retried.where(name: self.name).count
462    end
463
464    override :all_met_to_become_pending?
465    def all_met_to_become_pending?
466      super && !any_unmet_prerequisites?
467    end
468
469    def any_unmet_prerequisites?
470      prerequisites.present?
471    end
472
473    def prerequisites
474      Gitlab::Ci::Build::Prerequisite::Factory.new(self).unmet
475    end
476
477    def expanded_environment_name
478      return unless has_environment?
479
480      strong_memoize(:expanded_environment_name) do
481        # We're using a persisted expanded environment name in order to avoid
482        # variable expansion per request.
483        if metadata&.expanded_environment_name.present?
484          metadata.expanded_environment_name
485        else
486          ExpandVariables.expand(environment, -> { simple_variables })
487        end
488      end
489    end
490
491    def expanded_kubernetes_namespace
492      return unless has_environment?
493
494      namespace = options.dig(:environment, :kubernetes, :namespace)
495
496      if namespace.present?
497        strong_memoize(:expanded_kubernetes_namespace) do
498          ExpandVariables.expand(namespace, -> { simple_variables })
499        end
500      end
501    end
502
503    def has_environment?
504      environment.present?
505    end
506
507    def starts_environment?
508      has_environment? && self.environment_action == 'start'
509    end
510
511    def stops_environment?
512      has_environment? && self.environment_action == 'stop'
513    end
514
515    def environment_action
516      self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options
517    end
518
519    def environment_deployment_tier
520      self.options.dig(:environment, :deployment_tier) if self.options
521    end
522
523    def outdated_deployment?
524      success? && !deployment.try(:last?)
525    end
526
527    def triggered_by?(current_user)
528      user == current_user
529    end
530
531    def on_stop
532      options&.dig(:environment, :on_stop)
533    end
534
535    ##
536    # All variables, including persisted environment variables.
537    #
538    def variables
539      strong_memoize(:variables) do
540        Gitlab::Ci::Variables::Collection.new
541          .concat(persisted_variables)
542          .concat(dependency_proxy_variables)
543          .concat(job_jwt_variables)
544          .concat(scoped_variables)
545          .concat(job_variables)
546          .concat(persisted_environment_variables)
547      end
548    end
549
550    def persisted_variables
551      Gitlab::Ci::Variables::Collection.new.tap do |variables|
552        break variables unless persisted?
553
554        variables
555          .concat(pipeline.persisted_variables)
556          .append(key: 'CI_JOB_ID', value: id.to_s)
557          .append(key: 'CI_JOB_URL', value: Gitlab::Routing.url_helpers.project_job_url(project, self))
558          .append(key: 'CI_JOB_TOKEN', value: token.to_s, public: false, masked: true)
559          .append(key: 'CI_JOB_STARTED_AT', value: started_at&.iso8601)
560          .append(key: 'CI_BUILD_ID', value: id.to_s)
561          .append(key: 'CI_BUILD_TOKEN', value: token.to_s, public: false, masked: true)
562          .append(key: 'CI_REGISTRY_USER', value: ::Gitlab::Auth::CI_JOB_USER)
563          .append(key: 'CI_REGISTRY_PASSWORD', value: token.to_s, public: false, masked: true)
564          .append(key: 'CI_REPOSITORY_URL', value: repo_url.to_s, public: false)
565          .concat(deploy_token_variables)
566      end
567    end
568
569    def persisted_environment_variables
570      Gitlab::Ci::Variables::Collection.new.tap do |variables|
571        break variables unless persisted? && persisted_environment.present?
572
573        variables.concat(persisted_environment.predefined_variables)
574
575        variables.append(key: 'CI_ENVIRONMENT_ACTION', value: environment_action)
576
577        # Here we're passing unexpanded environment_url for runner to expand,
578        # and we need to make sure that CI_ENVIRONMENT_NAME and
579        # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
580        variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
581      end
582    end
583
584    def deploy_token_variables
585      Gitlab::Ci::Variables::Collection.new.tap do |variables|
586        break variables unless gitlab_deploy_token
587
588        variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.username)
589        variables.append(key: 'CI_DEPLOY_PASSWORD', value: gitlab_deploy_token.token, public: false, masked: true)
590      end
591    end
592
593    def dependency_proxy_variables
594      Gitlab::Ci::Variables::Collection.new.tap do |variables|
595        break variables unless Gitlab.config.dependency_proxy.enabled
596
597        variables.append(key: 'CI_DEPENDENCY_PROXY_USER', value: ::Gitlab::Auth::CI_JOB_USER)
598        variables.append(key: 'CI_DEPENDENCY_PROXY_PASSWORD', value: token.to_s, public: false, masked: true)
599      end
600    end
601
602    def features
603      {
604        trace_sections: true,
605        failure_reasons: self.class.failure_reasons.keys
606      }
607    end
608
609    def merge_request
610      strong_memoize(:merge_request) do
611        pipeline.all_merge_requests.order(iid: :asc).first
612      end
613    end
614
615    def repo_url
616      return unless token
617
618      auth = "#{::Gitlab::Auth::CI_JOB_USER}:#{token}@"
619      project.http_url_to_repo.sub(%r{^https?://}) do |prefix|
620        prefix + auth
621      end
622    end
623
624    def allow_git_fetch
625      project.build_allow_git_fetch
626    end
627
628    def update_coverage
629      coverage = trace.extract_coverage(coverage_regex)
630      update(coverage: coverage) if coverage.present?
631    end
632
633    def trace
634      Gitlab::Ci::Trace.new(self)
635    end
636
637    def has_trace?
638      trace.exist?
639    end
640
641    def has_live_trace?
642      trace.live_trace_exist?
643    end
644
645    def has_archived_trace?
646      trace.archived_trace_exist?
647    end
648
649    def artifacts_file
650      job_artifacts_archive&.file
651    end
652
653    def artifacts_size
654      job_artifacts_archive&.size
655    end
656
657    def artifacts_metadata
658      job_artifacts_metadata&.file
659    end
660
661    def artifacts?
662      !artifacts_expired? && artifacts_file&.exists?
663    end
664
665    def locked_artifacts?
666      pipeline.artifacts_locked? && artifacts_file&.exists?
667    end
668
669    # This method is similar to #artifacts? but it includes the artifacts
670    # locking mechanics. A new method was created to prevent breaking existing
671    # behavior and avoid introducing N+1s.
672    def available_artifacts?
673      (!artifacts_expired? || pipeline.artifacts_locked?) && job_artifacts_archive&.exists?
674    end
675
676    def artifacts_metadata?
677      artifacts? && artifacts_metadata&.exists?
678    end
679
680    def has_job_artifacts?
681      job_artifacts.any?
682    end
683
684    def has_test_reports?
685      job_artifacts.test_reports.exists?
686    end
687
688    def has_old_trace?
689      old_trace.present?
690    end
691
692    def trace=(data)
693      raise NotImplementedError
694    end
695
696    def old_trace
697      read_attribute(:trace)
698    end
699
700    def erase_old_trace!
701      return unless has_old_trace?
702
703      update_column(:trace, nil)
704    end
705
706    def ensure_trace_metadata!
707      Ci::BuildTraceMetadata.find_or_upsert_for!(id)
708    end
709
710    def artifacts_expose_as
711      options.dig(:artifacts, :expose_as)
712    end
713
714    def artifacts_paths
715      options.dig(:artifacts, :paths)
716    end
717
718    def needs_touch?
719      Time.current - updated_at > 15.minutes.to_i
720    end
721
722    def valid_token?(token)
723      self.token && ActiveSupport::SecurityUtils.secure_compare(token, self.token)
724    end
725
726    # acts_as_taggable uses this method create/remove tags with contexts
727    # defined by taggings and to get those contexts it executes a query.
728    # We don't use any other contexts except `tags`, so we don't need it.
729    override :custom_contexts
730    def custom_contexts
731      []
732    end
733
734    def tag_list
735      if tags.loaded?
736        tags.map(&:name)
737      else
738        super
739      end
740    end
741
742    def has_tags?
743      tag_list.any?
744    end
745
746    def any_runners_online?
747      cache_for_online_runners do
748        project.any_online_runners? { |runner| runner.match_build_if_online?(self) }
749      end
750    end
751
752    def any_runners_available?
753      cache_for_available_runners do
754        ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937') do
755          project.active_runners.exists?
756        end
757      end
758    end
759
760    def stuck?
761      pending? && !any_runners_online?
762    end
763
764    def execute_hooks
765      return unless project
766
767      project.execute_hooks(build_data.dup, :job_hooks) if project.has_active_hooks?(:job_hooks)
768      project.execute_integrations(build_data.dup, :job_hooks) if project.has_active_integrations?(:job_hooks)
769    end
770
771    def browsable_artifacts?
772      artifacts_metadata?
773    end
774
775    def artifacts_public?
776      return true unless Feature.enabled?(:non_public_artifacts, type: :development)
777
778      artifacts_public = options.dig(:artifacts, :public)
779
780      return true if artifacts_public.nil? # Default artifacts:public to true
781
782      options.dig(:artifacts, :public)
783    end
784
785    def artifacts_metadata_entry(path, **options)
786      artifacts_metadata.open do |metadata_stream|
787        metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
788          metadata_stream,
789          path,
790          **options)
791
792        metadata.to_entry
793      end
794    end
795
796    # and use that for `ExpireBuildInstanceArtifactsWorker`?
797    def erase_erasable_artifacts!
798      job_artifacts.erasable.destroy_all # rubocop: disable Cop/DestroyAll
799    end
800
801    def erase(opts = {})
802      return false unless erasable?
803
804      job_artifacts.destroy_all # rubocop: disable Cop/DestroyAll
805      erase_trace!
806      update_erased!(opts[:erased_by])
807    end
808
809    def erasable?
810      complete? && (artifacts? || has_job_artifacts? || has_trace?)
811    end
812
813    def erased?
814      !self.erased_at.nil?
815    end
816
817    def artifacts_expired?
818      artifacts_expire_at && artifacts_expire_at < Time.current
819    end
820
821    def artifacts_expire_in
822      artifacts_expire_at - Time.current if artifacts_expire_at
823    end
824
825    def artifacts_expire_in=(value)
826      self.artifacts_expire_at =
827        if value
828          ChronicDuration.parse(value)&.seconds&.from_now
829        end
830    end
831
832    def has_expired_locked_archive_artifacts?
833      locked_artifacts? &&
834        artifacts_expire_at.present? && artifacts_expire_at < Time.current
835    end
836
837    def has_expiring_archive_artifacts?
838      has_expiring_artifacts? && job_artifacts_archive.present?
839    end
840
841    def self.keep_artifacts!
842      update_all(artifacts_expire_at: nil)
843      Ci::JobArtifact.where(job: self.select(:id)).update_all(expire_at: nil)
844    end
845
846    def keep_artifacts!
847      self.update(artifacts_expire_at: nil)
848      self.job_artifacts.update_all(expire_at: nil)
849    end
850
851    def artifacts_file_for_type(type)
852      file_types = Ci::JobArtifact.associated_file_types_for(type)
853      file_types_ids = file_types&.map { |file_type| Ci::JobArtifact.file_types[file_type] }
854      job_artifacts.find_by(file_type: file_types_ids)&.file
855    end
856
857    def coverage_regex
858      super || project.try(:build_coverage_regex)
859    end
860
861    def steps
862      [Gitlab::Ci::Build::Step.from_commands(self),
863       Gitlab::Ci::Build::Step.from_release(self),
864       Gitlab::Ci::Build::Step.from_after_script(self)].compact
865    end
866
867    def image
868      Gitlab::Ci::Build::Image.from_image(self)
869    end
870
871    def services
872      Gitlab::Ci::Build::Image.from_services(self)
873    end
874
875    def cache
876      cache = Array.wrap(options[:cache])
877
878      if project.jobs_cache_index
879        cache = cache.map do |single_cache|
880          single_cache.merge(key: "#{single_cache[:key]}-#{project.jobs_cache_index}")
881        end
882      end
883
884      cache
885    end
886
887    def credentials
888      Gitlab::Ci::Build::Credentials::Factory.new(self).create!
889    end
890
891    def has_valid_build_dependencies?
892      dependencies.valid?
893    end
894
895    def invalid_dependencies
896      dependencies.invalid_local
897    end
898
899    def valid_dependency?
900      return false if artifacts_expired? && !pipeline.artifacts_locked?
901      return false if erased?
902
903      true
904    end
905
906    def runner_required_feature_names
907      strong_memoize(:runner_required_feature_names) do
908        RUNNER_FEATURES.select do |feature, method|
909          method.call(self)
910        end.keys
911      end
912    end
913
914    def supported_runner?(features)
915      runner_required_feature_names.all? do |feature_name|
916        features&.dig(feature_name)
917      end
918    end
919
920    def publishes_artifacts_reports?
921      options&.dig(:artifacts, :reports)&.any?
922    end
923
924    def supports_artifacts_exclude?
925      options&.dig(:artifacts, :exclude)&.any?
926    end
927
928    def multi_build_steps?
929      options.dig(:release)&.any?
930    end
931
932    def hide_secrets(data, metrics = ::Gitlab::Ci::Trace::Metrics.new)
933      return unless trace
934
935      data.dup.tap do |trace|
936        Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project
937        Gitlab::Ci::MaskSecret.mask!(trace, token) if token
938
939        if trace != data
940          metrics.increment_trace_operation(operation: :mutated)
941        end
942      end
943    end
944
945    def serializable_hash(options = {})
946      super(options).merge(when: read_attribute(:when))
947    end
948
949    def has_terminal?
950      running? && runner_session_url.present?
951    end
952
953    def collect_test_reports!(test_reports)
954      test_reports.get_suite(group_name).tap do |test_suite|
955        each_report(Ci::JobArtifact::TEST_REPORT_FILE_TYPES) do |file_type, blob|
956          Gitlab::Ci::Parsers.fabricate!(file_type).parse!(
957            blob,
958            test_suite,
959            job: self
960          )
961        end
962      end
963    end
964
965    def collect_accessibility_reports!(accessibility_report)
966      each_report(Ci::JobArtifact::ACCESSIBILITY_REPORT_FILE_TYPES) do |file_type, blob|
967        Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, accessibility_report)
968      end
969
970      accessibility_report
971    end
972
973    def collect_coverage_reports!(coverage_report)
974      each_report(Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES) do |file_type, blob|
975        Gitlab::Ci::Parsers.fabricate!(file_type).parse!(
976          blob,
977          coverage_report,
978          project_path: project.full_path,
979          worktree_paths: pipeline.all_worktree_paths
980        )
981      end
982
983      coverage_report
984    end
985
986    def collect_codequality_reports!(codequality_report)
987      each_report(Ci::JobArtifact::CODEQUALITY_REPORT_FILE_TYPES) do |file_type, blob|
988        Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, codequality_report)
989      end
990
991      codequality_report
992    end
993
994    def collect_terraform_reports!(terraform_reports)
995      each_report(::Ci::JobArtifact::TERRAFORM_REPORT_FILE_TYPES) do |file_type, blob, report_artifact|
996        ::Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, terraform_reports, artifact: report_artifact)
997      end
998
999      terraform_reports
1000    end
1001
1002    def report_artifacts
1003      job_artifacts.with_reports
1004    end
1005
1006    # Virtual deployment status depending on the environment status.
1007    def deployment_status
1008      return unless starts_environment?
1009
1010      if success?
1011        return successful_deployment_status
1012      elsif failed?
1013        return :failed
1014      end
1015
1016      :creating
1017    end
1018
1019    # Consider this object to have a structural integrity problems
1020    def doom!
1021      transaction do
1022        update_columns(status: :failed, failure_reason: :data_integrity_failure)
1023        all_queuing_entries.delete_all
1024      end
1025    end
1026
1027    def degradation_threshold
1028      var = yaml_variables.find { |v| v[:key] == DEGRADATION_THRESHOLD_VARIABLE_NAME }
1029      var[:value]&.to_i if var
1030    end
1031
1032    def remove_pending_state!
1033      pending_state.try(:delete)
1034    end
1035
1036    def run_on_status_commit(&block)
1037      status_commit_hooks.push(block)
1038    end
1039
1040    def max_test_cases_per_report
1041      # NOTE: This is temporary and will be replaced later by a value
1042      # that would come from an actual application limit.
1043      ::Gitlab.com? ? 500_000 : 0
1044    end
1045
1046    def debug_mode?
1047      # TODO: Have `debug_mode?` check against data on sent back from runner
1048      # to capture all the ways that variables can be set.
1049      # See (https://gitlab.com/gitlab-org/gitlab/-/issues/290955)
1050      variables['CI_DEBUG_TRACE']&.value&.casecmp('true') == 0
1051    end
1052
1053    def drop_with_exit_code!(failure_reason, exit_code)
1054      transaction do
1055        conditionally_allow_failure!(exit_code)
1056        drop!(failure_reason)
1057      end
1058    end
1059
1060    def exit_codes_defined?
1061      options.dig(:allow_failure_criteria, :exit_codes).present?
1062    end
1063
1064    def create_queuing_entry!
1065      ::Ci::PendingBuild.upsert_from_build!(self)
1066    end
1067
1068    ##
1069    # We can have only one queuing entry or running build tracking entry,
1070    # because there is a unique index on `build_id` in each table, but we need
1071    # a relation to remove these entries more efficiently in a single statement
1072    # without actually loading data.
1073    #
1074    def all_queuing_entries
1075      ::Ci::PendingBuild.where(build_id: self.id)
1076    end
1077
1078    def all_runtime_metadata
1079      ::Ci::RunningBuild.where(build_id: self.id)
1080    end
1081
1082    def shared_runner_build?
1083      runner&.instance_type?
1084    end
1085
1086    def job_variables_attributes
1087      strong_memoize(:job_variables_attributes) do
1088        job_variables.internal_source.map do |variable|
1089          variable.attributes.except('id', 'job_id', 'encrypted_value', 'encrypted_value_iv').tap do |attrs|
1090            attrs[:value] = variable.value
1091          end
1092        end
1093      end
1094    end
1095
1096    protected
1097
1098    def run_status_commit_hooks!
1099      status_commit_hooks.reverse_each do |hook|
1100        instance_eval(&hook)
1101      end
1102    end
1103
1104    private
1105
1106    def stick_build_if_status_changed
1107      return unless saved_change_to_status?
1108      return unless running?
1109
1110      self.class.sticking.stick(:build, id)
1111    end
1112
1113    def status_commit_hooks
1114      @status_commit_hooks ||= []
1115    end
1116
1117    def auto_retry
1118      strong_memoize(:auto_retry) do
1119        Gitlab::Ci::Build::AutoRetry.new(self)
1120      end
1121    end
1122
1123    def build_data
1124      strong_memoize(:build_data) { Gitlab::DataBuilder::Build.build(self) }
1125    end
1126
1127    def successful_deployment_status
1128      if deployment&.last?
1129        :last
1130      else
1131        :out_of_date
1132      end
1133    end
1134
1135    def each_report(report_types)
1136      job_artifacts_for_types(report_types).each do |report_artifact|
1137        report_artifact.each_blob do |blob|
1138          yield report_artifact.file_type, blob, report_artifact
1139        end
1140      end
1141    end
1142
1143    def job_artifacts_for_types(report_types)
1144      # Use select to leverage cached associations and avoid N+1 queries
1145      job_artifacts.select { |artifact| artifact.file_type.in?(report_types) }
1146    end
1147
1148    def erase_trace!
1149      trace.erase!
1150    end
1151
1152    def update_erased!(user = nil)
1153      self.update(erased_by: user, erased_at: Time.current, artifacts_expire_at: nil)
1154    end
1155
1156    def environment_url
1157      options&.dig(:environment, :url) || persisted_environment&.external_url
1158    end
1159
1160    def environment_status
1161      strong_memoize(:environment_status) do
1162        if has_environment? && merge_request
1163          EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha)
1164        end
1165      end
1166    end
1167
1168    def has_expiring_artifacts?
1169      artifacts_expire_at.present? && artifacts_expire_at > Time.current
1170    end
1171
1172    def job_jwt_variables
1173      Gitlab::Ci::Variables::Collection.new.tap do |variables|
1174        break variables unless Feature.enabled?(:ci_job_jwt, project, default_enabled: true)
1175
1176        jwt = Gitlab::Ci::Jwt.for_build(self)
1177        variables.append(key: 'CI_JOB_JWT', value: jwt, public: false, masked: true)
1178      rescue OpenSSL::PKey::RSAError, Gitlab::Ci::Jwt::NoSigningKeyError => e
1179        Gitlab::ErrorTracking.track_exception(e)
1180      end
1181    end
1182
1183    def conditionally_allow_failure!(exit_code)
1184      return unless exit_code
1185
1186      if allowed_to_fail_with_code?(exit_code)
1187        update_columns(allow_failure: true)
1188      end
1189    end
1190
1191    def allowed_to_fail_with_code?(exit_code)
1192      options
1193        .dig(:allow_failure_criteria, :exit_codes)
1194        .to_a
1195        .include?(exit_code)
1196    end
1197
1198    def cache_for_online_runners(&block)
1199      Rails.cache.fetch(
1200        ['has-online-runners', id],
1201        expires_in: RUNNERS_STATUS_CACHE_EXPIRATION
1202      ) { yield }
1203    end
1204
1205    def cache_for_available_runners(&block)
1206      Rails.cache.fetch(
1207        ['has-available-runners', project.id],
1208        expires_in: RUNNERS_STATUS_CACHE_EXPIRATION
1209      ) { yield }
1210    end
1211  end
1212end
1213
1214Ci::Build.prepend_mod_with('Ci::Build')
1215