1# frozen_string_literal: true 2 3module Ci 4 class Bridge < Ci::Processable 5 include Ci::Contextable 6 include Ci::Metadatable 7 include Importable 8 include AfterCommitQueue 9 include Ci::HasRef 10 11 InvalidBridgeTypeError = Class.new(StandardError) 12 InvalidTransitionError = Class.new(StandardError) 13 14 belongs_to :project 15 belongs_to :trigger_request 16 has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline", 17 foreign_key: :source_job_id 18 19 has_one :sourced_pipeline, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_job_id 20 has_one :downstream_pipeline, through: :sourced_pipeline, source: :pipeline 21 22 validates :ref, presence: true 23 24 # rubocop:disable Cop/ActiveRecordSerialize 25 serialize :options 26 serialize :yaml_variables, ::Gitlab::Serializer::Ci::Variables 27 # rubocop:enable Cop/ActiveRecordSerialize 28 29 state_machine :status do 30 after_transition [:created, :manual, :waiting_for_resource] => :pending do |bridge| 31 next unless bridge.triggers_downstream_pipeline? 32 33 bridge.run_after_commit do 34 ::Ci::CreateDownstreamPipelineWorker.perform_async(bridge.id) 35 end 36 end 37 38 event :pending do 39 transition all => :pending 40 end 41 42 event :manual do 43 transition all => :manual 44 end 45 46 event :scheduled do 47 transition all => :scheduled 48 end 49 50 event :actionize do 51 transition created: :manual 52 end 53 end 54 55 def self.retry(bridge, current_user) 56 raise NotImplementedError 57 end 58 59 def self.with_preloads 60 preload( 61 :metadata, 62 downstream_pipeline: [project: [:route, { namespace: :route }]], 63 project: [:namespace] 64 ) 65 end 66 67 def inherit_status_from_downstream!(pipeline) 68 case pipeline.status 69 when 'success' 70 self.success! 71 when 'failed', 'canceled', 'skipped' 72 self.drop! 73 else 74 false 75 end 76 end 77 78 def has_downstream_pipeline? 79 sourced_pipelines.exists? 80 end 81 82 def downstream_pipeline_params 83 return child_params if triggers_child_pipeline? 84 return cross_project_params if downstream_project.present? 85 86 {} 87 end 88 89 def downstream_project 90 strong_memoize(:downstream_project) do 91 if downstream_project_path 92 ::Project.find_by_full_path(downstream_project_path) 93 elsif triggers_child_pipeline? 94 project 95 end 96 end 97 end 98 99 def downstream_project_path 100 strong_memoize(:downstream_project_path) do 101 options&.dig(:trigger, :project) 102 end 103 end 104 105 def parent_pipeline 106 pipeline if triggers_child_pipeline? 107 end 108 109 def triggers_downstream_pipeline? 110 triggers_child_pipeline? || triggers_cross_project_pipeline? 111 end 112 113 def triggers_child_pipeline? 114 yaml_for_downstream.present? 115 end 116 117 def triggers_cross_project_pipeline? 118 downstream_project_path.present? 119 end 120 121 def tags 122 [:bridge] 123 end 124 125 def detailed_status(current_user) 126 Gitlab::Ci::Status::Bridge::Factory 127 .new(self, current_user) 128 .fabricate! 129 end 130 131 def schedulable? 132 false 133 end 134 135 def playable? 136 action? && !archived? && manual? 137 end 138 139 def action? 140 %w[manual].include?(self.when) 141 end 142 143 # rubocop: disable CodeReuse/ServiceClass 144 # We don't need it but we are taking `job_variables_attributes` parameter 145 # to make it consistent with `Ci::Build#play` method. 146 def play(current_user, job_variables_attributes = nil) 147 Ci::PlayBridgeService 148 .new(project, current_user) 149 .execute(self) 150 end 151 # rubocop: enable CodeReuse/ServiceClass 152 153 def artifacts? 154 false 155 end 156 157 def runnable? 158 false 159 end 160 161 def any_unmet_prerequisites? 162 false 163 end 164 165 def expanded_environment_name 166 end 167 168 def persisted_environment 169 end 170 171 def execute_hooks 172 raise NotImplementedError 173 end 174 175 def to_partial_path 176 'projects/generic_commit_statuses/generic_commit_status' 177 end 178 179 def yaml_for_downstream 180 strong_memoize(:yaml_for_downstream) do 181 includes = options&.dig(:trigger, :include) 182 YAML.dump('include' => includes) if includes 183 end 184 end 185 186 def target_ref 187 branch = options&.dig(:trigger, :branch) 188 return unless branch 189 190 scoped_variables.to_runner_variables.yield_self do |all_variables| 191 ::ExpandVariables.expand(branch, all_variables) 192 end 193 end 194 195 def dependent? 196 strong_memoize(:dependent) do 197 options&.dig(:trigger, :strategy) == 'depend' 198 end 199 end 200 201 def downstream_variables 202 variables = scoped_variables.concat(pipeline.persisted_variables) 203 204 variables.to_runner_variables.yield_self do |all_variables| 205 yaml_variables.to_a.map do |hash| 206 { key: hash[:key], value: ::ExpandVariables.expand(hash[:value], all_variables) } 207 end 208 end 209 end 210 211 def target_revision_ref 212 downstream_pipeline_params.dig(:target_revision, :ref) 213 end 214 215 private 216 217 def cross_project_params 218 { 219 project: downstream_project, 220 source: :pipeline, 221 target_revision: { 222 ref: target_ref || downstream_project.default_branch, 223 variables_attributes: downstream_variables 224 }, 225 execute_params: { 226 ignore_skip_ci: true, 227 bridge: self 228 } 229 } 230 end 231 232 def child_params 233 parent_pipeline = pipeline 234 235 { 236 project: project, 237 source: :parent_pipeline, 238 target_revision: { 239 ref: parent_pipeline.ref, 240 checkout_sha: parent_pipeline.sha, 241 before: parent_pipeline.before_sha, 242 source_sha: parent_pipeline.source_sha, 243 target_sha: parent_pipeline.target_sha, 244 variables_attributes: downstream_variables 245 }, 246 execute_params: { 247 ignore_skip_ci: true, 248 bridge: self, 249 merge_request: parent_pipeline.merge_request 250 } 251 } 252 end 253 end 254end 255 256::Ci::Bridge.prepend_mod_with('Ci::Bridge') 257