Logic, Sequencing, and Conditional Execution with on_success in GitLab CI/CD

The orchestration of complex software delivery lifecycles requires a profound understanding of how individual tasks interact within a Directed Acyclic Graph (DAG) or a traditional staged pipeline. In the GitLab CI/CD ecosystem, the on_success directive serves as a foundational mechanism for controlling job execution based on the terminal state of preceding tasks. This directive is not merely a passive observer; it is an active participant in the pipeline's decision-making logic, determining whether a workflow continues its intended path or halts due to a failure in a prerequisite stage. Achieving mastery over this keyword requires navigating the nuances of job rules, stage dependencies, and the potential pitfalls of complex conditional logic.

Understanding on_success necessitates a mental model of how GitLab interprets the relationship between jobs and stages. A common misconception among practitioners is that the directive acts as a simple "if previous job worked" switch. While fundamentally true, the real-world application involves managing artifacts, runner availability, and the specific behavior of the rules keyword versus legacy configuration methods. When a job is configured with when: on_success, it effectively waits for all jobs in the preceding stage to complete with a successful exit code. If any job in that preceding stage fails, the on_success job is skipped, preserving the pipeline's failure state and preventing downstream corruption or invalid deployments.

The Mechanics of Job Dependencies and the when Keyword

The when keyword is the primary engine for defining the execution conditions of a job. It dictates the circumstances under which a job is added to the pipeline and how it reacts to the status of its predecessors.

The following table delineates the standard behaviors associated with the when directive:

Keyword Value Execution Condition Impact on Pipeline Flow
on_success Executes only if all jobs in previous stages passed. Ensures strict linear progression and safety.
on_failure Executes only if at least one job in previous stages failed. Ideal for cleanup, notifications, or error recovery.
always Executes regardless of the status of previous jobs. Useful for mandatory cleanup or persistent logging.
manual Requires human intervention via the GitLab UI to trigger. Provides a "gate" for production deployments.

The interaction between these keywords and the concept of "stages" is critical. In a standard pipeline, jobs within the same stage execute in parallel on available runners. There is no guaranteed sequencing among jobs that share a stage. Therefore, an on_success job in a subsequent stage cannot depend on a specific job within the current stage; it depends on the aggregate success of the entire stage. If a stage contains three jobs and one fails, the on_success job in the next stage will be skipped, even if the other two jobs succeeded.

This behavior creates specific challenges for developers attempting to perform conditional branching. For instance, a user might attempt to create a branch on a remote server (like GitHub) only if a specific local check passes. If the logic is split across jobs—one to check for a branch and one to create it—the user may find that on_success does not behave as expected if the intervening "check" job is not correctly integrated into the dependency chain or if the job logic itself returns an unexpected exit code.

Navigating Complex Rules and Pipeline Duplication

Modern GitLab CI/CD configuration relies heavily on the rules keyword rather than the legacy only and except keywords. While rules provides significantly more granular control, it introduces complexities regarding how jobs are evaluated and how they interact with the pipeline's lifecycle.

A major risk when implementing rules with when: on_success is the accidental creation of duplicate pipelines. This often occurs when a configuration includes both branch pipelines and merge request pipelines without proper coordination.

Preventing Duplicate Pipelines

To avoid the overhead and confusion of duplicate pipelines, developers must utilize workflow: rules. This top-level configuration ensures that the entire pipeline follows a unified set of logic, preventing a scenario where a single push triggers both a branch pipeline and a merge request pipeline simultaneously.

The following configuration pattern is recommended to prevent duplicate execution:

yaml job: script: echo "Hello, Rules!" rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" when: never - if: $CI_PIPELINE_SOURCE == "schedule" when: never - when: on_success

In this example, the job is explicitly told to ignore merge request events and scheduled pipelines under certain conditions, eventually defaulting to on_success. If this logic is not paired with a global workflow rule, GitLab may still issue warnings or execute redundant jobs, leading to wasted runner resources and inconsistent deployment states.

The Danger of Mixing Configuration Methods

One of the most difficult troubleshooting scenarios in GitLab CI/CD arises from mixing only/except and rules. Although this might not trigger a YAML syntax error, the different default behaviors of these two systems can lead to unpredictable job scheduling. Because rules are evaluated more dynamically during the pipeline creation phase, mixing them with the static nature of only/except can result in jobs being skipped or included in ways that contradict the developer's intent.

The Manual Deployment Dilemma and Variable Workarounds

A frequent requirement in DevOps workflows is the "Manual Gate" deployment model. In this model, a job is set to when: manual, meaning it will not run unless a user clicks a button in the GitLab UI. However, this presents a limitation: once a pipeline is initiated, the user cannot easily convert a manual job into an on_success (automatic) job through the standard UI.

The Variable-Based Toggle Strategy

Since the GitLab UI does not currently offer a direct toggle to change a job's when property after a pipeline has started, the most robust solution is to use CI/CD variables combined with conditional rules. This approach allows a user to "program" the pipeline's behavior at the moment of creation.

The implementation follows this logic:

  1. Define a custom variable, such as AUTO_DEPLOY.
  2. Use rules to check the value of this variable.
  3. If the variable is set to a specific value (e.g., true), set the job to on_success.
  4. Otherwise, default the job to manual.

Example configuration for an automated or manual deployment toggle:

yaml deploy: stage: deploy script: ./deploy.sh rules: - if: '$AUTO_DEPLOY == "true"' when: on_success - when: manual

When a user goes to the "Run pipeline" screen in GitLab, they can manually enter AUTO_DEPLOY with the value true. This transforms the deployment job from a manual interaction into an automated step that triggers immediately upon the success of the previous test stages. This workaround effectively bypasses the UI limitation by shifting the logic from a static state to a dynamic, variable-driven state.

Advanced Execution Control: CIJOBSTATUS and After_Script

Standard job execution follows a linear path: before_script -> script -> after_script. However, developers often need to perform different actions based on whether the script section succeeded or failed. While on_success and on_failure are primarily used to control the execution of entire jobs in the pipeline, there is a need for more granular control within a single job.

Leveraging CIJOBSTATUS

While a direct "per-job" when: on_success for the after_script section has been a subject of discussion and proposal in the GitLab community, the current high-level implementation involves utilizing the CI_JOB_STATUS predefined variable. This allows the after_script to execute logic that branches based on the outcome of the main script block.

This is particularly useful for tasks such as:
- Tagging a Docker image with :latest only if the build succeeds.
- Cleaning up intermediate container images or dangling volumes only if the job fails.
- Sending specific error telemetry to an observability stack (like the ELK stack or Grafana) upon failure.

The concept involves using the after_script as a catch-all that then uses shell logic to inspect the job's state. This ensures that the cleanup or notification logic is coupled tightly with the job that generated the event, rather than relying on a separate, subsequent job that might be subject to runner availability or network issues.

Deep Analysis of Pipeline Failure Modes

Understanding why an on_success job might be skipped is essential for debugging failed pipelines. Failure can occur at several layers of the orchestration.

The Runner and Artifact Layer

A common point of confusion involves the lifecycle of artifacts. Artifacts are only preserved between jobs if they are explicitly defined. If a job in an earlier stage produces a critical build artifact but fails to upload it (due to a script error or a runner crash), any subsequent on_success job that depends on that artifact will fail, even if the logic for the on_success job itself is perfectly configured.

Furthermore, jobs within a stage run simultaneously. If a pipeline is designed such that Job A and Job B run in parallel in the build stage, and Job C is set to on_success in the test stage, Job C will only execute if both Job A and Job B exit with code 0. This parallelism can hide dependencies; if Job C actually requires the output of Job A, but Job B fails, Job C will be skipped, making it appear as though the failure of Job B broke Job C, when in reality, it was a violation of the stage-level success requirement.

The "On_Failure" Feedback Loop

When using on_failure jobs for recovery (such as creating a new branch to fix a broken state), a secondary issue arises: the dependency of subsequent jobs. If an on_failure job is triggered to repair a state, the jobs following it must be carefully configured. If a job is set to on_success, it typically looks at the status of the immediate predecessor. If the "repair" job (the on_failure job) succeeds, the next job in the pipeline might see that success and proceed, but the developer must ensure that the repair job actually restores the environment to a state that the next job can use.

In some complex scenarios, users have observed that on_success jobs do not seem to "care" if a preceding on_failure job was executed. This is because the pipeline logic treats the successful execution of the on_failure job as a successful event in its own right. The pipeline continues to move forward, which can lead to "skipping" or "executing" behaviors that feel inconsistent if the developer has not accounted for the fact that the on_failure job has now become a successful link in the execution chain.

Conclusion

The on_success directive is a powerful tool for enforcing reliability and order within GitLab CI/CD pipelines, but its effectiveness is entirely dependent on a precise understanding of job rules, stage dependencies, and the broader pipeline lifecycle. Mastery requires moving beyond a simple understanding of "success vs. failure" and into the realms of variable-driven conditional logic, careful management of pipeline sources to avoid duplication, and the strategic use of after_script for granular, per-job error handling. By implementing robust workflow: rules and utilizing variables to create "manual-to-automatic" toggles, DevOps engineers can build pipelines that are both highly controlled and flexibly automated, capable of navigating the complexities of modern software delivery.

Sources

  1. GitLab Forum: Using onsuccess and onfailure or conditions in GitLab CI
  2. University of Toronto: GitLab CI Job Rules Documentation
  3. GitLab Forum: Deploy on success
  4. GitLab Forum: yaml when onfailure onsuccess
  5. GitLab Docs: Job Rules
  6. GitLab Issue: Define onfailure and onsuccess after_script

Related Posts