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