# frozen_string_literal: true module Ci class Bridge < Ci::Processable include Ci::Contextable include Ci::Metadatable include Importable include AfterCommitQueue include Ci::HasRef extend ::Gitlab::Utils::Override InvalidBridgeTypeError = Class.new(StandardError) InvalidTransitionError = Class.new(StandardError) belongs_to :project belongs_to :trigger_request 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 serialize :options serialize :yaml_variables, ::Gitlab::Serializer::Ci::Variables # rubocop:enable Cop/ActiveRecordSerialize state_machine :status do after_transition [:created, :manual] => :pending do |bridge| next unless bridge.downstream_project bridge.run_after_commit do bridge.schedule_downstream_pipeline! end end event :pending do transition all => :pending end event :manual do transition all => :manual end event :scheduled do transition all => :scheduled end event :actionize do transition created: :manual end end def self.retry(bridge, current_user) raise NotImplementedError end def self.with_preloads preload( :metadata, downstream_pipeline: [project: [:route, { namespace: :route }]], project: [:namespace] ) end def schedule_downstream_pipeline! raise InvalidBridgeTypeError unless downstream_project ::Ci::CreateCrossProjectPipelineWorker.perform_async(self.id) end def inherit_status_from_downstream!(pipeline) case pipeline.status when 'success' self.success! when 'failed', 'canceled', 'skipped' self.drop! else false end end def has_downstream_pipeline? sourced_pipelines.exists? end def downstream_pipeline_params return child_params if triggers_child_pipeline? return cross_project_params if downstream_project.present? {} end def downstream_project strong_memoize(:downstream_project) do if downstream_project_path ::Project.find_by_full_path(downstream_project_path) elsif triggers_child_pipeline? project end end end def downstream_project_path strong_memoize(:downstream_project_path) do options&.dig(:trigger, :project) end end def parent_pipeline pipeline if triggers_child_pipeline? end def triggers_child_pipeline? yaml_for_downstream.present? end def tags [:bridge] end def detailed_status(current_user) Gitlab::Ci::Status::Bridge::Factory .new(self, current_user) .fabricate! end def schedulable? false end def playable? return false unless ::Gitlab::Ci::Features.manual_bridges_enabled?(project) action? && !archived? && manual? end def action? return false unless ::Gitlab::Ci::Features.manual_bridges_enabled?(project) %w[manual].include?(self.when) end # rubocop: disable CodeReuse/ServiceClass # We don't need it but we are taking `job_variables_attributes` parameter # to make it consistent with `Ci::Build#play` method. def play(current_user, job_variables_attributes = nil) Ci::PlayBridgeService .new(project, current_user) .execute(self) end # rubocop: enable CodeReuse/ServiceClass def artifacts? false end def runnable? false end def expanded_environment_name end def execute_hooks raise NotImplementedError end def to_partial_path 'projects/generic_commit_statuses/generic_commit_status' end def yaml_for_downstream strong_memoize(:yaml_for_downstream) do includes = options&.dig(:trigger, :include) YAML.dump('include' => includes) if includes end end def target_ref branch = options&.dig(:trigger, :branch) return unless branch scoped_variables.to_runner_variables.yield_self do |all_variables| ::ExpandVariables.expand(branch, all_variables) end end def dependent? strong_memoize(:dependent) do options&.dig(:trigger, :strategy) == 'depend' end end def downstream_variables variables = scoped_variables.concat(pipeline.persisted_variables) variables.to_runner_variables.yield_self do |all_variables| yaml_variables.to_a.map do |hash| { key: hash[:key], value: ::ExpandVariables.expand(hash[:value], all_variables) } end end end override :dependency_variables def dependency_variables return [] unless ::Feature.enabled?(:ci_bridge_dependency_variables, project) super end def target_revision_ref downstream_pipeline_params.dig(:target_revision, :ref) end private def cross_project_params { project: downstream_project, source: :pipeline, target_revision: { ref: target_ref || downstream_project.default_branch }, execute_params: { ignore_skip_ci: true, bridge: self } } end def child_params parent_pipeline = pipeline { project: project, source: :parent_pipeline, target_revision: { ref: parent_pipeline.ref, checkout_sha: parent_pipeline.sha, before: parent_pipeline.before_sha, source_sha: parent_pipeline.source_sha, target_sha: parent_pipeline.target_sha }, execute_params: { ignore_skip_ci: true, bridge: self, merge_request: parent_pipeline.merge_request } } end end end ::Ci::Bridge.prepend_if_ee('::EE::Ci::Bridge')