Git, CI/CD & GitOpsXLVII · Pipeline DependenciesFailure modes
Pipeline failure propagation — what happens when a dependency fails
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
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
- Design failure propagation deliberately.
if: always()for notifications/audit.if: failure()for diagnostics/rollback.continue-on-errorsparingly; 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
Q1. Job B has `needs: [A]`. A fails. What happens to B by default?
Q2. `if: always()` makes a job run regardless of whether its predecessors succeeded, failed, or were cancelled.
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.
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.