# frozen_string_literal: true module Ci class CreateCommitStatusService < BaseService include ::Gitlab::ExclusiveLeaseHelpers include ::Gitlab::Utils::StrongMemoize include ::Services::ReturnServiceResponses delegate :sha, to: :commit def execute(optional_commit_status_params:) in_lock(pipeline_lock_key, **pipeline_lock_params) do @optional_commit_status_params = optional_commit_status_params unsafe_execute end end private attr_reader :pipeline, :stage, :commit_status, :optional_commit_status_params def unsafe_execute return not_found('Commit') if commit.blank? return bad_request('State is required') if params[:state].blank? return not_found('References for commit') if ref.blank? @pipeline = first_matching_pipeline || create_pipeline return forbidden unless ::Ability.allowed?(current_user, :update_pipeline, pipeline) @stage = find_or_create_external_stage @commit_status = find_or_build_external_commit_status return bad_request(commit_status.errors.messages) if commit_status.invalid? response = add_or_update_external_job return bad_request(response.message) if response.error? update_merge_request_head_pipeline response end def ref params[:ref] || first_matching_pipeline&.ref || repository.branch_names_contains(sha).first end strong_memoize_attr :ref def commit project.commit(params[:sha]) end strong_memoize_attr :commit def first_matching_pipeline pipelines = project.ci_pipelines.newest_first(sha: sha) pipelines = pipelines.for_ref(params[:ref]) if params[:ref] pipelines = pipelines.id_in(params[:pipeline_id]) if params[:pipeline_id] pipelines.first end strong_memoize_attr :first_matching_pipeline def name params[:name] || params[:context] || 'default' end def create_pipeline project.ci_pipelines.build( source: :external, sha: sha, ref: ref, user: current_user, protected: project.protected_for?(ref) ).tap do |new_pipeline| new_pipeline.ensure_project_iid! new_pipeline.save! end end def find_or_create_external_stage pipeline.stages.safe_find_or_create_by!(name: 'external') do |stage| # rubocop:disable Performance/ActiveRecordSubtransactionMethods stage.position = ::GenericCommitStatus::EXTERNAL_STAGE_IDX stage.project = project end end def find_or_build_external_commit_status ::GenericCommitStatus.running_or_pending.find_or_initialize_by( # rubocop:disable CodeReuse/ActiveRecord project: project, pipeline: pipeline, name: name, ref: ref, user: current_user, protected: project.protected_for?(ref), ci_stage: stage, stage_idx: stage.position, stage: 'external', partition_id: pipeline.partition_id ).tap do |new_commit_status| new_commit_status.assign_attributes(optional_commit_status_params) end end def add_or_update_external_job ::Ci::Pipelines::AddJobService.new(pipeline).execute!(commit_status) do |job| apply_job_state!(job) end end def update_merge_request_head_pipeline return unless pipeline.latest? ::MergeRequest .from_project(project).from_source_branches(ref) .update_all(head_pipeline_id: pipeline.id) end def apply_job_state!(job) case params[:state] when 'pending' job.enqueue! when 'running' job.enqueue job.run! when 'success' job.success! when 'failed' job.drop!(:api_failure) when 'canceled' job.cancel! else raise('invalid state') end end def pipeline_lock_key "api:commit_statuses:project:#{project.id}:sha:#{params[:sha]}" end def pipeline_lock_params { ttl: 5.seconds, sleep_sec: 0.1.seconds, retries: 20 } end def not_found(message) error("404 #{message} Not Found", :not_found) end def bad_request(message) error(message, :bad_request) end def forbidden error("403 Forbidden", :forbidden) end end end