Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCI · Pipeline PerformanceSkip rules

Skip when nothing changed — path filters and conditional execution

Intermediate⏱ ~22 mingit

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

Not yet marked complete on this device.

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-bench is 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

  1. Use trigger-level filtering for hard skip rules. A workflow that lints Terraform should not start on a Markdown change.
  2. Use job-level if: for context-dependent skips. “Only on pull request”, “only when label X is set”, “only on default branch”.
  3. Use step-level if: for output-dependent skips. “Skip deploy if test failed”.
  4. Test the path glob. A paths-ignore that 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

  1. Q1. A workflow has `paths: 'app/**'` and `paths-ignore: 'app/README.md'`. A commit changes only `app/README.md`. What happens?

  2. Q2. A job with `if: false` does not allocate a runner because the condition short-circuits the job.

  3. Q3. Explain why a too-broad `paths-ignore` pattern is a silent production bug, and how a CI lint would catch it.

  4. 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.