Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXLVII · Pipeline DependenciesFailure modes

Pipeline failure propagation — what happens when a dependency fails

Intermediate⏱ ~22 mingit

What you'll learn

  • Predict the default behaviour when a `needs:` predecessor fails
  • Apply `if: always()`, `if: failure()`, `if: success()`, and `if: cancelled()` to override
  • Distinguish `continue-on-error` from `if:` clauses
  • Design failure propagation deliberately rather than relying on defaults

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

When a job listed in needs: fails, the dependent is skipped by default. The default is right for most cases: a deploy that depends on a failed test should not run. But some jobs must run on failure (notifications) or cancellation (cleanup). Designing failure propagation is part of pipeline design.

The default: skip on failure

flowchart LR
    T["test"] -- fails --> D["deploy"]
    D -.skipped.-> X["(no run)"]

When test fails, deploy is skipped; the run is marked failed. Default propagates failure downstream; a job with no needs: is unaffected.

if: always()

notify:
  needs: [test]
  if: always()
  steps:
    - run: ./slack-notify.sh

Runs on every outcome. For notifications, audit.

if: failure()

diagnose:
  needs: [test]
  if: failure()
  steps:
    - run: ./collect-diagnostics.sh

Runs only on failure. For diagnostics, rollback.

if: success() and if: cancelled()

if: success() is the implicit default. if: cancelled() runs only when a predecessor was cancelled. For jobs that release resources on cancellation.

continue-on-error

continue-on-error marks a failure as success:

- name: Lint
  run: ./lint.sh
  continue-on-error: true

Step exits non-zero but job is success; dependents see success.

continue-on-error is not if: failure(). The first makes a failure look like success; the second makes a dependent run on failure.

flowchart LR
    subgraph N["Normal"]
        T1["test"] -->|fails| D1["deploy: skipped"]
    end
    subgraph F["continue-on-error"]
        T2["test"] -->|fails-as-success| D2["deploy: runs"]
    end
    subgraph I["if: failure()"]
        T3["test"] -->|fails| N3["notify: runs"]
    end

Combining the overrides

cleanup:
  needs: [build, test]
  if: failure() || cancelled()
  steps:
    - run: ./cleanup.sh
      continue-on-error: true

Runs only on failure or cancellation; cleanup step allowed to fail.

Production discipline

  1. Design failure propagation deliberately.
  2. if: always() for notifications/audit.
  3. if: failure() for diagnostics/rollback.
  4. continue-on-error sparingly; hides bugs.

Cross-course references

  • Ansible for Production Sysadmins - Part XXXVII uses the same override in AWX.
  • Linux for Production Sysadmins - Part XXX uses OnFailure= systemd.

Quiz

Knowledge check · 4 questions

  1. Q1. Job B has `needs: [A]`. A fails. What happens to B by default?

  2. Q2. `if: always()` makes a job run regardless of whether its predecessors succeeded, failed, or were cancelled.

  3. Q3. Name the four job-level `if:` clauses and identify which one is appropriate for a diagnostic collection job that should fire only on test failure.

  4. Q4. Diagnose why a notification never fires when tests fail, and recommend the fix.

    Team R: `test` and `notify`. notify has `needs: [test]`. When test fails, notify is skipped. Team expected Slack notification on every outcome.

Passing score: 75%. Answers are checked in this browser.