Git, CI/CD & GitOpsCI · Pipeline PerformanceSkip rules
Skip when nothing changed — path filters and conditional execution
What you'll learn
- Apply paths and paths-ignore to restrict a workflow trigger to relevant file changes
- Use if: conditional expressions to skip individual steps and jobs
- Distinguish trigger-level filtering from step-level filtering and choose the right level
- Recognise the failure mode of a too-broad paths-ignore that swallows real changes
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 workflow that runs on every push regardless of which files changed wastes CI minutes. A documentation change triggers a container-image build. A README typo triggers a Terraform plan. A dependency bump in a service unrelated to the test suite triggers the entire test matrix. Path filters and conditional execution are the two levers for skipping work that does not need to happen. They work at different levels and have different costs.
Path filters on the trigger
Path filters restrict the workflow trigger to commits that touch relevant files. A workflow whose job is to lint Terraform code should not start when the change is to a Markdown file.
on:
push:
branches: [main]
paths:
- 'terraform/**'
- '**.tf'
- '**.tfvars'
paths-ignore:
- 'terraform/README.md'
- 'docs/**'
The paths list is positive: the trigger fires when any
listed pattern matches a changed file. The paths-ignore list is
negative: the trigger is suppressed when any listed pattern
matches. The two are evaluated together: a change to
terraform/README.md matches terraform/** (positive) but also
matches paths-ignore (negative), and the negative wins.
The path filter is evaluated by GitHub before the runner pool is even notified. A push that does not match the filter does not start a workflow run at all; no runner is consumed, no queue time is added, and the CI minutes are zero. This is the cheapest possible lever.
Conditional execution on jobs and steps
Conditional execution restricts individual jobs or steps. The workflow still starts; the runner is still allocated; only the job or step is skipped.
jobs:
terraform-plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: terraform plan
The if: expression evaluates against the GitHub Actions context.
github.event_name == 'pull_request' is true only on pull-request
events; on a push to main, the job is skipped without running
any step. The runner is allocated but the job is a no-op.
flowchart TB
A["Commit pushed"] --> B{"paths filter?"}
B -->|no match| C["No workflow run"]
B -->|match| D["Workflow run starts"]
D --> E{"if: condition?"}
E -->|false| F["Job skipped"]
E -->|true| G["Job runs"]
G --> H{"Step-level if?"}
H -->|false| I["Step skipped"]
H -->|true| J["Step runs"]
F --> K["Workflow completes"]
I --> K
J --> K
The diagram shows the three layers of skipping. The trigger-level filter is the cheapest (no runner); the job-level condition is next (runner allocated, job no-op); the step-level condition is the most expensive (runner allocated, job runs, individual step no-op).
Choosing the right level
The right level depends on how certain the skip rule is:
- Trigger-level is right when the skip rule is a hard invariant: “this workflow never needs to run for changes outside this directory”. The CI minutes saved are total; the cost is a precise path glob.
- Job-level is right when the skip rule depends on the
context that the trigger does not capture: “this job should
run only on pull requests”, “this job should run only when the
label
run-benchis set”, “this job should run only on the default branch”. The runner is still allocated but the job is skipped. - Step-level is right when the skip rule depends on the output of a previous step: “skip the deploy step if the test step failed”, “skip the cache-save step if the install step errored”. The runner, the job, and all preceding steps run.
A too-broad paths-ignore swallows real changes. The negative
pattern matches the file path glob, not the file content; a
documentation file in a directory that contains code will silence
the trigger when only the documentation changed, but a
documentation file whose directory is also the code’s directory
will silence the trigger for code changes too. The right
paths-ignore is one that excludes only files that are
guaranteed not to affect the workflow’s job.
Path filters and conditional execution compose
The two levers compose. A workflow that lints Terraform code can
have a path filter that restricts the trigger to terraform/**
and a conditional inside the lint job that further restricts to
main branch. A push to a feature branch that touches
terraform/** starts the workflow but skips the lint job. A push
to main that touches terraform/** starts the workflow and runs
the lint job.
on:
push:
branches: [main, 'releases/**']
paths:
- 'terraform/**'
- '**.tf'
jobs:
lint:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: tflint
The path filter restricts the trigger to Terraform changes; the
branch filter restricts the trigger to main and release
branches; the job-level condition restricts the lint job to
main. A push to a release branch that touches Terraform starts
the workflow but skips the lint job; the conditional is doing the
extra filtering.
Production discipline
- Use trigger-level filtering for hard skip rules. A workflow that lints Terraform should not start on a Markdown change.
- Use job-level
if:for context-dependent skips. “Only on pull request”, “only when label X is set”, “only on default branch”. - Use step-level
if:for output-dependent skips. “Skip deploy if test failed”. - Test the path glob. A
paths-ignorethat swallows real changes is a silent production bug. Add a CI lint that verifies the path filter is non-trivial.
Cross-course references
- Linux for Production Sysadmins - Part XXXIV (ConfigMgmt) applies the path-filter pattern to per-package CI lanes.
- Ansible for Production Sysadmins - Part XXXVII (RepoArch) applies the conditional-execution pattern to per-inventory molecule runs.
- Terraform for Production Sysadmins - Parts IX-XII (State) apply the path-filter pattern to per-workspace plans.
Quiz
Knowledge check · 4 questions
Q1. A workflow has `paths: 'app/**'` and `paths-ignore: 'app/README.md'`. A commit changes only `app/README.md`. What happens?
Q2. A job with `if: false` does not allocate a runner because the condition short-circuits the job.
Q3. Explain why a too-broad `paths-ignore` pattern is a silent production bug, and how a CI lint would catch it.
Q4. Diagnose why a Terraform CI workflow has not run for three weeks despite active development, and propose the fix.
Team I's Terraform CI workflow used to run on every push to `main`. After adding `paths-ignore: 'docs/**'` to silence documentation-only changes, the workflow stopped running entirely. The team has been pushing Terraform changes for three weeks; the CI dashboard shows no workflow runs.
Passing score: 75%. Answers are checked in this browser.