Git, CI/CD & GitOpsXLVIII · Conditional ExecutionGuards
Conditional fundamentals — when to skip, when to run, and the cost of always-running
What you'll learn
- Recognise that `if:` and `rules:` are guards, not policies
- Identify three situations where unconditional execution wastes resources
- Distinguish "skip the job" from "run the job and let it fail"
- Apply the `always()`, `success()`, and `failure()` result functions to step-level guards
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
A conditional is a guard - a Boolean expression that decides whether a job or step starts at all. It is the first line of triage: does this unit of work have any business running for this event?
When to skip, when to run
Three situations where skipping is correct: the event
is not relevant (a push to docs/ triggering a
Terraform plan); the trigger already implies the
answer (a pull_request from a fork on a public repo
carries untrusted code - a deploy step there is a
vulnerability, not a conditional); the job is
downstream of one that failed (a notification that
only matters on test failure should run on
if: failure(), not always).
Three situations where running is correct: the job
is the answer (a build job on push to main);
the result is itself the signal (a lint job that
fails on style violations); a downstream consumer
needs the artifact regardless of branch.
flowchart LR
A[Event arrives] --> B{Guard evaluates}
B -- "true" --> C[Run job or step]
B -- "false" --> D[Skip]
Reading a guard
jobs:
plan:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: terraform plan
test:
runs-on: ubuntu-latest
steps:
- run: make test
- run: upload-coverage
if: always()
- run: notify-failure
if: failure()
The if: value is an expression evaluated against the
run context; the job runs when the expression is
true. A skipped job is recorded with a neutral
status - it does not fail the pipeline, does not block
a merge, does not count towards required-checks. The
semantic difference between if: false and
run: exit 1 is the difference between not
applicable and broken. The result functions
success(), failure(), always(), cancelled()
look at the previous step in the same job, not at
the trigger.
Production discipline
- Default to running. If you skip a guard, write the comment that explains why.
- A guard is a contract. Treat changes to
if:clauses as pull-request review items. - Never guard with a side effect. A guard that reaches into an external system turns the pipeline into a dependency on that system.
Cross-course references
- Ansible for Production Sysadmins - Part XXVI covers the same guards at the playbook-task level.
- Terraform for Production Sysadmins - Part XXII
covers
terraform planagainst specific resources.
Quiz
Knowledge check · 4 questions
Q1. A pipeline runs five unconditional jobs on every push to every branch. The strongest production argument for adding guards is:
Q2. A skipped job in GitHub Actions is recorded as a failure and blocks required-status checks.
Q3. Name the three GitHub Actions result functions and the appropriate use case for each.
Q4. Diagnose why a `terraform plan` that should be the team's most-watched job has become unreadable.
The pipeline runs `terraform plan` (9 min warm cache) on every push to every branch in a 30-engineer monorepo. `docs/` commits trigger a full plan that produces no signal because the Terraform module does not depend on `docs/`.
Passing score: 75%. Answers are checked in this browser.