From 2eba48fe0cb645bee010dad498015cd82d90f5c0 Mon Sep 17 00:00:00 2001 From: Allison Browne Date: Mon, 21 Jul 2025 17:12:59 -0400 Subject: [PATCH] Handle invalid transitions in Commit Status POST API This commit implements idempotent behavior for the commit status API by handling invalid state transitions gracefully. Key changes: 1. Skip state transitions when job is already in target state 2. Handle invalid transitions by allowing external systems to override GitLab's state machine rules when appropriate 3. Add proper error handling and logging for transition failures This addresses the issue where posting the same status twice would cause a bad request error instead of being idempotent. Fixes: https://gitlab.com/gitlab-org/gitlab/-/issues/556757 --- .../ci/create_commit_status_service.rb | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/app/services/ci/create_commit_status_service.rb b/app/services/ci/create_commit_status_service.rb index 945dfa42f5e4ad..9303f7d6dfb397 100644 --- a/app/services/ci/create_commit_status_service.rb +++ b/app/services/ci/create_commit_status_service.rb @@ -160,11 +160,45 @@ def add_or_update_external_job end def apply_job_state!(job) - case params[:state] + target_state = params[:state] + current_state = job.status.to_s + + # Skip if already in target state (idempotency) + return if current_state == target_state + + begin + # Try normal transitions first + perform_state_transition(job, target_state) + rescue StateMachines::InvalidTransition => e + # External system override - force the state for external systems + # that may have different state machine rules than GitLab + Rails.logger.warn( + message: "External system transition override", + error: e.message, + job_id: job.id, + current_state: current_state, + target_state: target_state, + project_id: project.id, + user_id: current_user.id + ) + + # For external commit statuses, we allow more permissive state transitions + # since external systems are the source of truth + if job.is_a?(GenericCommitStatus) + job.update_column(:status, target_state) + else + # Re-raise for internal jobs to maintain strict state machine rules + raise e + end + end + end + + def perform_state_transition(job, target_state) + case target_state when 'pending' job.enqueue! when 'running' - job.enqueue + job.enqueue unless job.pending? || job.running? job.run! when 'success' job.success! @@ -175,7 +209,7 @@ def apply_job_state!(job) when 'skipped' job.skip! else - raise('invalid state') + raise ArgumentError, "Invalid state: #{target_state}" end end -- GitLab