diff options
Diffstat (limited to 'app/models/ci')
-rw-r--r-- | app/models/ci/bridge.rb | 3 | ||||
-rw-r--r-- | app/models/ci/build.rb | 49 | ||||
-rw-r--r-- | app/models/ci/build_dependencies.rb | 2 | ||||
-rw-r--r-- | app/models/ci/build_report_result.rb | 45 | ||||
-rw-r--r-- | app/models/ci/build_runner_session.rb | 15 | ||||
-rw-r--r-- | app/models/ci/daily_build_group_report_result.rb | 2 | ||||
-rw-r--r-- | app/models/ci/freeze_period.rb | 2 | ||||
-rw-r--r-- | app/models/ci/group.rb | 2 | ||||
-rw-r--r-- | app/models/ci/instance_variable.rb | 53 | ||||
-rw-r--r-- | app/models/ci/job_artifact.rb | 29 | ||||
-rw-r--r-- | app/models/ci/pipeline.rb | 50 | ||||
-rw-r--r-- | app/models/ci/pipeline_enums.rb | 5 | ||||
-rw-r--r-- | app/models/ci/processable.rb | 4 | ||||
-rw-r--r-- | app/models/ci/ref.rb | 71 | ||||
-rw-r--r-- | app/models/ci/runner.rb | 32 |
15 files changed, 251 insertions, 113 deletions
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 1e92a47ab49..58c26e8c806 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -16,6 +16,9 @@ module Ci has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_job_id + has_one :sourced_pipeline, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_job_id + has_one :downstream_pipeline, through: :sourced_pipeline, source: :pipeline + validates :ref, presence: true # rubocop:disable Cop/ActiveRecordSerialize diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 7f64ea7dd97..b5e68b55f72 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -26,7 +26,8 @@ module Ci RUNNER_FEATURES = { upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? }, refspecs: -> (build) { build.merge_request_ref? }, - artifacts_exclude: -> (build) { build.supports_artifacts_exclude? } + artifacts_exclude: -> (build) { build.supports_artifacts_exclude? }, + release_steps: -> (build) { build.release_steps? } }.freeze DEFAULT_RETRIES = { @@ -39,6 +40,7 @@ module Ci has_one :resource, class_name: 'Ci::Resource', inverse_of: :build has_many :trace_sections, class_name: 'Ci::BuildTraceSection' has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id + has_many :report_results, class_name: 'Ci::BuildReportResult', inverse_of: :build has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id @@ -55,6 +57,7 @@ module Ci delegate :url, to: :runner_session, prefix: true, allow_nil: true delegate :terminal_specification, to: :runner_session, allow_nil: true + delegate :service_specification, to: :runner_session, allow_nil: true delegate :gitlab_deploy_token, to: :project delegate :trigger_short_token, to: :trigger_request, allow_nil: true @@ -137,8 +140,8 @@ module Ci .includes(:metadata, :job_artifacts_metadata) end - scope :with_artifacts_not_expired, ->() { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } - scope :with_expired_artifacts, ->() { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.now) } + scope :with_artifacts_not_expired, ->() { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.current) } + scope :with_expired_artifacts, ->() { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.current) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) } scope :scheduled_actions, ->() { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) } @@ -259,7 +262,7 @@ module Ci end before_transition any => :waiting_for_resource do |build| - build.waiting_for_resource_at = Time.now + build.waiting_for_resource_at = Time.current end before_transition on: :enqueue_waiting_for_resource do |build| @@ -352,7 +355,7 @@ module Ci begin Ci::Build.retry(build, build.user) rescue Gitlab::Access::AccessDeniedError => ex - Rails.logger.error "Unable to auto-retry job #{build.id}: #{ex}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error "Unable to auto-retry job #{build.id}: #{ex}" end end end @@ -576,7 +579,7 @@ module Ci def environment_changed_page_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| - break variables unless environment_status + break variables unless environment_status && Feature.enabled?(:modifed_path_ci_variables, project) variables.append(key: 'CI_MERGE_REQUEST_CHANGED_PAGE_PATHS', value: environment_status.changed_paths.join(',')) variables.append(key: 'CI_MERGE_REQUEST_CHANGED_PAGE_URLS', value: environment_status.changed_urls.join(',')) @@ -686,6 +689,10 @@ module Ci job_artifacts.any? end + def has_test_reports? + job_artifacts.test_reports.exists? + end + def has_old_trace? old_trace.present? end @@ -713,7 +720,7 @@ module Ci end def needs_touch? - Time.now - updated_at > 15.minutes.to_i + Time.current - updated_at > 15.minutes.to_i end def valid_token?(token) @@ -756,13 +763,13 @@ module Ci # and use that for `ExpireBuildInstanceArtifactsWorker`? def erase_erasable_artifacts! - job_artifacts.erasable.destroy_all # rubocop: disable DestroyAll + job_artifacts.erasable.destroy_all # rubocop: disable Cop/DestroyAll end def erase(opts = {}) return false unless erasable? - job_artifacts.destroy_all # rubocop: disable DestroyAll + job_artifacts.destroy_all # rubocop: disable Cop/DestroyAll erase_trace! update_erased!(opts[:erased_by]) end @@ -776,11 +783,11 @@ module Ci end def artifacts_expired? - artifacts_expire_at && artifacts_expire_at < Time.now + artifacts_expire_at && artifacts_expire_at < Time.current end def artifacts_expire_in - artifacts_expire_at - Time.now if artifacts_expire_at + artifacts_expire_at - Time.current if artifacts_expire_at end def artifacts_expire_in=(value) @@ -809,6 +816,7 @@ module Ci def steps [Gitlab::Ci::Build::Step.from_commands(self), + Gitlab::Ci::Build::Step.from_release(self), Gitlab::Ci::Build::Step.from_after_script(self)].compact end @@ -872,6 +880,16 @@ module Ci options&.dig(:artifacts, :reports)&.any? end + def supports_artifacts_exclude? + options&.dig(:artifacts, :exclude)&.any? && + Gitlab::Ci::Features.artifacts_exclude_enabled? + end + + def release_steps? + options.dig(:release)&.any? && + Gitlab::Ci::Features.release_generation_enabled? + end + def hide_secrets(trace) return unless trace @@ -945,11 +963,6 @@ module Ci failure_reason: :data_integrity_failure) end - def supports_artifacts_exclude? - options&.dig(:artifacts, :exclude)&.any? && - Gitlab::Ci::Features.artifacts_exclude_enabled? - end - def degradation_threshold var = yaml_variables.find { |v| v[:key] == DEGRADATION_THRESHOLD_VARIABLE_NAME } var[:value]&.to_i if var @@ -993,7 +1006,7 @@ module Ci end def update_erased!(user = nil) - self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil) + self.update(erased_by: user, erased_at: Time.current, artifacts_expire_at: nil) end def unscoped_project @@ -1026,7 +1039,7 @@ module Ci end def has_expiring_artifacts? - artifacts_expire_at.present? && artifacts_expire_at > Time.now + artifacts_expire_at.present? && artifacts_expire_at > Time.current end def job_jwt_variables diff --git a/app/models/ci/build_dependencies.rb b/app/models/ci/build_dependencies.rb index d3ff870e36a..2fcd1708cf4 100644 --- a/app/models/ci/build_dependencies.rb +++ b/app/models/ci/build_dependencies.rb @@ -45,7 +45,7 @@ module Ci end def valid_local? - return true if Feature.enabled?('ci_disable_validates_dependencies') + return true if Feature.enabled?(:ci_disable_validates_dependencies) local.all?(&:valid_dependency?) end diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb new file mode 100644 index 00000000000..530233ad5c0 --- /dev/null +++ b/app/models/ci/build_report_result.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Ci + class BuildReportResult < ApplicationRecord + extend Gitlab::Ci::Model + + self.primary_key = :build_id + + belongs_to :build, class_name: "Ci::Build", inverse_of: :report_results + belongs_to :project, class_name: "Project", inverse_of: :build_report_results + + validates :build, :project, presence: true + validates :data, json_schema: { filename: "build_report_result_data" } + + store_accessor :data, :tests + + def tests_name + tests.dig("name") + end + + def tests_duration + tests.dig("duration") + end + + def tests_success + tests.dig("success").to_i + end + + def tests_failed + tests.dig("failed").to_i + end + + def tests_errored + tests.dig("errored").to_i + end + + def tests_skipped + tests.dig("skipped").to_i + end + + def tests_total + [tests_success, tests_failed, tests_errored, tests_skipped].sum + end + end +end diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb index b46bbe69c7c..bc7f17f046c 100644 --- a/app/models/ci/build_runner_session.rb +++ b/app/models/ci/build_runner_session.rb @@ -7,6 +7,8 @@ module Ci extend Gitlab::Ci::Model TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com' + DEFAULT_SERVICE_NAME = 'build'.freeze + DEFAULT_PORT_NAME = 'default_port'.freeze self.table_name = 'ci_builds_runner_session' @@ -23,6 +25,17 @@ module Ci channel_specification(wss_url, TERMINAL_SUBPROTOCOL) end + def service_specification(service: nil, path: nil, port: nil, subprotocols: nil) + return {} unless url.present? + + port = port.presence || DEFAULT_PORT_NAME + service = service.presence || DEFAULT_SERVICE_NAME + url = "#{self.url}/proxy/#{service}/#{port}/#{path}" + subprotocols = subprotocols.presence || ::Ci::BuildRunnerSession::TERMINAL_SUBPROTOCOL + + channel_specification(url, subprotocols) + end + private def channel_specification(url, subprotocol) @@ -37,5 +50,3 @@ module Ci end end end - -Ci::BuildRunnerSession.prepend_if_ee('EE::Ci::BuildRunnerSession') diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb index 3506b27e974..d6617b8c2eb 100644 --- a/app/models/ci/daily_build_group_report_result.rb +++ b/app/models/ci/daily_build_group_report_result.rb @@ -9,6 +9,8 @@ module Ci belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id belongs_to :project + validates :data, json_schema: { filename: "daily_build_group_report_result_data" } + def self.upsert_reports(data) upsert_all(data, unique_by: :index_daily_build_group_report_results_unique_columns) if data.any? end diff --git a/app/models/ci/freeze_period.rb b/app/models/ci/freeze_period.rb index bf03b92259a..d215372bb45 100644 --- a/app/models/ci/freeze_period.rb +++ b/app/models/ci/freeze_period.rb @@ -5,7 +5,7 @@ module Ci include StripAttribute self.table_name = 'ci_freeze_periods' - default_scope { order(created_at: :asc) } + default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope belongs_to :project, inverse_of: :freeze_periods diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb index 4b2081f2977..779c6c0396f 100644 --- a/app/models/ci/group.rb +++ b/app/models/ci/group.rb @@ -24,7 +24,7 @@ module Ci def status strong_memoize(:status) do - if Feature.enabled?(:ci_composite_status, project, default_enabled: false) + if ::Gitlab::Ci::Features.composite_status?(project) Gitlab::Ci::Status::Composite .new(@jobs) .status diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb index c674f76d229..8245729a884 100644 --- a/app/models/ci/instance_variable.rb +++ b/app/models/ci/instance_variable.rb @@ -3,8 +3,13 @@ module Ci class InstanceVariable < ApplicationRecord extend Gitlab::Ci::Model + extend Gitlab::ProcessMemoryCache::Helper include Ci::NewHasVariable include Ci::Maskable + include Limitable + + self.limit_name = 'ci_instance_level_variables' + self.limit_scope = Limitable::GLOBAL_SCOPE alias_attribute :secret_value, :value @@ -12,8 +17,14 @@ module Ci message: "(%{value}) has already been taken" } + validates :encrypted_value, length: { + maximum: 1024, + too_long: 'The encrypted value of the provided variable exceeds %{count} bytes. Variables over 700 characters risk exceeding the limit.' + } + scope :unprotected, -> { where(protected: false) } - after_commit { self.class.touch_redis_cache_timestamp } + + after_commit { self.class.invalidate_memory_cache(:ci_instance_variable_data) } class << self def all_cached @@ -24,10 +35,6 @@ module Ci cached_data[:unprotected] end - def touch_redis_cache_timestamp(time = Time.current.to_f) - shared_backend.write(:ci_instance_variable_changed_at, time) - end - private def cached_data @@ -37,39 +44,13 @@ module Ci { all: all_records, unprotected: all_records.reject(&:protected?) } end end + end - def fetch_memory_cache(key, &payload) - cache = process_backend.read(key) - - if cache && !stale_cache?(cache) - cache[:data] - else - store_cache(key, &payload) - end - end - - def stale_cache?(cache_info) - shared_timestamp = shared_backend.read(:ci_instance_variable_changed_at) - return true unless shared_timestamp - - shared_timestamp.to_f > cache_info[:cached_at].to_f - end - - def store_cache(key) - data = yield - time = Time.current.to_f - - process_backend.write(key, data: data, cached_at: time) - touch_redis_cache_timestamp(time) - data - end - - def shared_backend - Rails.cache - end + private - def process_backend - Gitlab::ProcessMemoryCache.cache_backend + def validate_plan_limit_not_exceeded + if Gitlab::Ci::Features.instance_level_variables_limit_enabled? + super end end end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index d931428dccd..8aba9356949 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -5,6 +5,7 @@ module Ci include AfterCommitQueue include ObjectStorage::BackgroundMove include UpdateProjectStatistics + include UsageStatistics include Sortable extend Gitlab::Ci::Model @@ -26,6 +27,7 @@ module Ci accessibility: 'gl-accessibility.json', codequality: 'gl-code-quality-report.json', sast: 'gl-sast-report.json', + secret_detection: 'gl-secret-detection-report.json', dependency_scanning: 'gl-dependency-scanning-report.json', container_scanning: 'gl-container-scanning-report.json', dast: 'gl-dast-report.json', @@ -37,7 +39,8 @@ module Ci dotenv: '.env', cobertura: 'cobertura-coverage.xml', terraform: 'tfplan.json', - cluster_applications: 'gl-cluster-applications.json' + cluster_applications: 'gl-cluster-applications.json', + requirements: 'requirements.json' }.freeze INTERNAL_TYPES = { @@ -62,13 +65,15 @@ module Ci accessibility: :raw, codequality: :raw, sast: :raw, + secret_detection: :raw, dependency_scanning: :raw, container_scanning: :raw, dast: :raw, license_management: :raw, license_scanning: :raw, performance: :raw, - terraform: :raw + terraform: :raw, + requirements: :raw }.freeze DOWNLOADABLE_TYPES = %w[ @@ -87,6 +92,8 @@ module Ci metrics performance sast + secret_detection + requirements ].freeze TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze @@ -109,6 +116,7 @@ module Ci after_save :update_file_store, if: :saved_change_to_file? + scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) } scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) } scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) } scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) } @@ -147,7 +155,8 @@ module Ci where(file_type: types) end - scope :expired, -> (limit) { where('expire_at < ?', Time.now).limit(limit) } + scope :expired, -> (limit) { where('expire_at < ?', Time.current).limit(limit) } + scope :downloadable, -> { where(file_type: DOWNLOADABLE_TYPES) } scope :locked, -> { where(locked: true) } scope :unlocked, -> { where(locked: [false, nil]) } @@ -176,7 +185,9 @@ module Ci cobertura: 17, terraform: 18, # Transformed json accessibility: 19, - cluster_applications: 20 + cluster_applications: 20, + secret_detection: 21, ## EE-specific + requirements: 22 ## EE-specific } enum file_format: { @@ -242,8 +253,16 @@ module Ci super || self.file_location.nil? end + def expired? + expire_at.present? && expire_at < Time.current + end + + def expiring? + expire_at.present? && expire_at > Time.current + end + def expire_in - expire_at - Time.now if expire_at + expire_at - Time.current if expire_at end def expire_in=(value) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 5db1635f64d..497e1a4d74a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -31,6 +31,7 @@ module Ci belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule' belongs_to :merge_request, class_name: 'MergeRequest' belongs_to :external_pull_request + belongs_to :ci_ref, class_name: 'Ci::Ref', foreign_key: :ci_ref_id, inverse_of: :pipelines has_internal_id :iid, scope: :project, presence: false, track_if: -> { !importing? }, ensure_if: -> { !importing? }, init: ->(s) do s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count @@ -40,11 +41,15 @@ module Ci has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline has_many :latest_statuses_ordered_by_stage, -> { latest.order(:stage_idx, :stage) }, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline has_many :processables, class_name: 'Ci::Processable', foreign_key: :commit_id, inverse_of: :pipeline + has_many :bridges, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline + has_many :job_artifacts, through: :builds has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent has_many :variables, class_name: 'Ci::PipelineVariable' has_many :deployments, through: :builds has_many :environments, -> { distinct }, through: :deployments + has_many :latest_builds, -> { latest }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build' + has_many :downloadable_artifacts, -> { not_expired.downloadable }, through: :latest_builds, source: :job_artifacts # Merge requests for which the current pipeline is running against # the merge request's latest commit. @@ -56,7 +61,6 @@ module Ci has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus' has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :scheduled_actions, -> { latest.scheduled_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' - has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id' has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id' @@ -64,13 +68,6 @@ module Ci has_one :source_pipeline, class_name: 'Ci::Sources::Pipeline', inverse_of: :pipeline - has_one :ref_status, ->(pipeline) { - # We use .read_attribute to save 1 extra unneeded query to load the :project. - unscope(:where) - .where(project_id: pipeline.read_attribute(:project_id), ref: pipeline.ref, tag: pipeline.tag) - # Sadly :inverse_of is not supported (yet) by Rails for composite PKs. - }, class_name: 'Ci::Ref', inverse_of: :pipelines - has_one :chat_data, class_name: 'Ci::PipelineChatData' has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline @@ -163,11 +160,11 @@ module Ci # Create a separate worker for each new operation before_transition [:created, :waiting_for_resource, :preparing, :pending] => :running do |pipeline| - pipeline.started_at = Time.now + pipeline.started_at = Time.current end before_transition any => [:success, :failed, :canceled] do |pipeline| - pipeline.finished_at = Time.now + pipeline.finished_at = Time.current pipeline.update_duration end @@ -235,12 +232,10 @@ module Ci end after_transition any => [:success, :failed] do |pipeline| + ref_status = pipeline.ci_ref&.update_status_by!(pipeline) + pipeline.run_after_commit do - if Feature.enabled?(:ci_pipeline_fixed_notifications) - PipelineUpdateCiRefStatusWorker.perform_async(pipeline.id) - else - PipelineNotificationWorker.perform_async(pipeline.id) - end + PipelineNotificationWorker.perform_async(pipeline.id, ref_status: ref_status) end end @@ -260,6 +255,7 @@ module Ci scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) } scope :for_ref, -> (ref) { where(ref: ref) } scope :for_id, -> (id) { where(id: id) } + scope :for_iid, -> (iid) { where(iid: iid) } scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) } scope :with_reports, -> (reports_scope) do @@ -397,11 +393,11 @@ module Ci end def ordered_stages - if Feature.enabled?(:ci_atomic_processing, project, default_enabled: false) + if ::Gitlab::Ci::Features.atomic_processing?(project) # The `Ci::Stage` contains all up-to date data # as atomic processing updates all data in-bulk stages - elsif Feature.enabled?(:ci_pipeline_persisted_stages, default_enabled: true) && complete? + elsif complete? # The `Ci::Stage` contains up-to date data only for `completed` pipelines # this is due to asynchronous processing of pipeline, and stages possibly # not updated inline with processing of pipeline @@ -445,7 +441,7 @@ module Ci end def legacy_stages - if Feature.enabled?(:ci_composite_status, project, default_enabled: false) + if ::Gitlab::Ci::Features.composite_status?(project) legacy_stages_using_composite_status else legacy_stages_using_sql @@ -798,13 +794,17 @@ module Ci @latest_builds_with_artifacts ||= builds.latest.with_artifacts_not_expired.to_a end + def latest_report_builds(reports_scope = ::Ci::JobArtifact.with_reports) + builds.latest.with_reports(reports_scope) + end + def has_reports?(reports_scope) - complete? && builds.latest.with_reports(reports_scope).exists? + complete? && latest_report_builds(reports_scope).exists? end def test_reports Gitlab::Ci::Reports::TestReports.new.tap do |test_reports| - builds.latest.with_reports(Ci::JobArtifact.test_reports).preload(:project).find_each do |build| + latest_report_builds(Ci::JobArtifact.test_reports).preload(:project).find_each do |build| build.collect_test_reports!(test_reports) end end @@ -826,7 +826,7 @@ module Ci def coverage_reports Gitlab::Ci::Reports::CoverageReports.new.tap do |coverage_reports| - builds.latest.with_reports(Ci::JobArtifact.coverage_reports).each do |build| + latest_report_builds(Ci::JobArtifact.coverage_reports).each do |build| build.collect_coverage_reports!(coverage_reports) end end @@ -834,7 +834,7 @@ module Ci def terraform_reports ::Gitlab::Ci::Reports::TerraformReports.new.tap do |terraform_reports| - builds.latest.with_reports(::Ci::JobArtifact.terraform_reports).each do |build| + latest_report_builds(::Ci::JobArtifact.terraform_reports).each do |build| build.collect_terraform_reports!(terraform_reports) end end @@ -969,6 +969,12 @@ module Ci processables.populate_scheduling_type! end + def ensure_ci_ref! + return unless Gitlab::Ci::Features.pipeline_fixed_notifications? + + self.ci_ref = Ci::Ref.ensure_for(self) + end + private def pipeline_data diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb index 7e203cb67c4..2ccd8445aa8 100644 --- a/app/models/ci/pipeline_enums.rb +++ b/app/models/ci/pipeline_enums.rb @@ -27,9 +27,11 @@ module Ci # https://gitlab.com/gitlab-org/gitlab/issues/195991 pipeline: 7, chat: 8, + webide: 9, merge_request_event: 10, external_pull_request_event: 11, - parent_pipeline: 12 + parent_pipeline: 12, + ondemand_scan: 13 } end @@ -40,6 +42,7 @@ module Ci unknown_source: nil, repository_source: 1, auto_devops_source: 2, + webide_source: 3, remote_source: 4, external_project_source: 5, bridge_source: 6 diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb index cc00500662d..ac5785d9c91 100644 --- a/app/models/ci/processable.rb +++ b/app/models/ci/processable.rb @@ -4,12 +4,8 @@ module Ci class Processable < ::CommitStatus include Gitlab::Utils::StrongMemoize - has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build - accepts_nested_attributes_for :needs - enum scheduling_type: { stage: 0, dag: 1 }, _prefix: true - scope :preload_needs, -> { preload(:needs) } scope :with_needs, -> (names = nil) do diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb index a0782bc0444..be6062b6e6e 100644 --- a/app/models/ci/ref.rb +++ b/app/models/ci/ref.rb @@ -3,21 +3,62 @@ module Ci class Ref < ApplicationRecord extend Gitlab::Ci::Model + include Gitlab::OptimisticLocking - STATUSES = %w[success failed fixed].freeze - - belongs_to :project - belongs_to :last_updated_by_pipeline, foreign_key: :last_updated_by_pipeline_id, class_name: 'Ci::Pipeline' - # ActiveRecord doesn't support composite FKs for this reason we have to do the 'unscope(:where)' - # hack. - has_many :pipelines, ->(ref) { - # We use .read_attribute to save 1 extra unneeded query to load the :project. - unscope(:where) - .where(ref: ref.ref, project_id: ref.read_attribute(:project_id), tag: ref.tag) - # Sadly :inverse_of is not supported (yet) by Rails for composite PKs. - }, inverse_of: :ref_status - - validates :status, inclusion: { in: STATUSES } - validates :last_updated_by_pipeline, presence: true + FAILING_STATUSES = %w[failed broken still_failing].freeze + + belongs_to :project, inverse_of: :ci_refs + has_many :pipelines, class_name: 'Ci::Pipeline', foreign_key: :ci_ref_id, inverse_of: :ci_ref + + state_machine :status, initial: :unknown do + event :succeed do + transition unknown: :success + transition fixed: :success + transition %i[failed broken still_failing] => :fixed + end + + event :do_fail do + transition unknown: :failed + transition %i[failed broken] => :still_failing + transition %i[success fixed] => :broken + end + + state :unknown, value: 0 + state :success, value: 1 + state :failed, value: 2 + state :fixed, value: 3 + state :broken, value: 4 + state :still_failing, value: 5 + end + + class << self + def ensure_for(pipeline) + safe_find_or_create_by(project_id: pipeline.project_id, + ref_path: pipeline.source_ref_path) + end + + def failing_state?(status_name) + FAILING_STATUSES.include?(status_name) + end + end + + def last_finished_pipeline_id + Ci::Pipeline.where(ci_ref_id: self.id).finished.order(id: :desc).select(:id).take&.id + end + + def update_status_by!(pipeline) + return unless Gitlab::Ci::Features.pipeline_fixed_notifications? + + retry_lock(self) do + next unless last_finished_pipeline_id == pipeline.id + + case pipeline.status + when 'success' then self.succeed + when 'failed' then self.do_fail + end + + self.status_name + end + end end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index d4e9217ff9f..8fc273556f0 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -23,10 +23,17 @@ module Ci project_type: 3 } - ONLINE_CONTACT_TIMEOUT = 1.hour + # This `ONLINE_CONTACT_TIMEOUT` needs to be larger than + # `RUNNER_QUEUE_EXPIRY_TIME+UPDATE_CONTACT_COLUMN_EVERY` + # + ONLINE_CONTACT_TIMEOUT = 2.hours + + # The `RUNNER_QUEUE_EXPIRY_TIME` indicates the longest interval that + # Runner request needs to be refreshed by Rails instead of being handled + # by Workhorse RUNNER_QUEUE_EXPIRY_TIME = 1.hour - # This needs to be less than `ONLINE_CONTACT_TIMEOUT` + # The `UPDATE_CONTACT_COLUMN_EVERY` defines how often the Runner DB entry can be updated UPDATE_CONTACT_COLUMN_EVERY = (40.minutes..55.minutes).freeze AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze @@ -81,6 +88,17 @@ module Ci joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups }) } + scope :belonging_to_group_or_project, -> (group_id, project_id) { + groups = ::Group.where(id: group_id) + + group_runners = joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups }) + project_runners = joins(:runner_projects).where(ci_runner_projects: { project_id: project_id }) + + union_sql = ::Gitlab::SQL::Union.new([group_runners, project_runners]).to_sql + + from("(#{union_sql}) #{table_name}") + } + scope :belonging_to_parent_group_of_project, -> (project_id) { project_groups = ::Group.joins(:projects).where(projects: { id: project_id }) hierarchy_groups = Gitlab::ObjectHierarchy.new(project_groups).base_and_ancestors @@ -145,14 +163,14 @@ module Ci # Searches for runners matching the given query. # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # This method uses ILIKE on PostgreSQL. # # This method performs a *partial* match on tokens, thus a query for "a" # will match any runner where the token contains the letter "a". As a result # you should *not* use this method for non-admin purposes as otherwise users # might be able to query a list of all runners. # - # query - The search query as a String + # query - The search query as a String. # # Returns an ActiveRecord::Relation. def self.search(query) @@ -271,9 +289,9 @@ module Ci ensure_runner_queue_value == value if value.present? end - def update_cached_info(values) + def heartbeat(values) values = values&.slice(:version, :revision, :platform, :architecture, :ip_address) || {} - values[:contacted_at] = Time.now + values[:contacted_at] = Time.current cache_attributes(values) @@ -309,7 +327,7 @@ module Ci real_contacted_at = read_attribute(:contacted_at) real_contacted_at.nil? || - (Time.now - real_contacted_at) >= contacted_at_max_age + (Time.current - real_contacted_at) >= contacted_at_max_age end def tag_constraints |