if-changed
Gate a step or job on file changes with one shared paths-filter setup job.
A step- or job-level macro that runs a node only when files changed in the pull
request or push match a glob pattern. Actio synthesizes one shared
dorny/paths-filter setup job, computes
every flag once, and folds the guard into each node's if:.
Compiles to a shared setup job plus an if: guard and needs: on the consuming
job:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build docs
run: ./build-docs.sh
if-changed:
- "docs/**"
- "*.md"
- name: Always run
run: ./test.shjobs:
actio_changes:
runs-on: ubuntu-latest
outputs:
filter_1: ${{ steps.actio_filter.outputs.filter_1 }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: actio_filter
with:
filters: |-
filter_1:
- 'docs/**'
- '*.md'
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build docs
run: ./build-docs.sh
if: needs.actio_changes.outputs.filter_1 == 'true'
- name: Always run
run: ./test.sh
needs:
- actio_changesjobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build docs
run: ./build-docs.sh
if-changed:
- "docs/**"
- "*.md"
- name: Always run
run: ./test.shjobs:
actio_changes:
runs-on: ubuntu-latest
outputs:
filter_1: ${{ steps.actio_filter.outputs.filter_1 }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: actio_filter
with:
filters: |-
filter_1:
- 'docs/**'
- '*.md'
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build docs
run: ./build-docs.sh
if: needs.actio_changes.outputs.filter_1 == 'true'
- name: Always run
run: ./test.sh
needs:
- actio_changesValue
if-changed takes a single glob string or an array of globs:
if-changed: "services/api/**" # one pattern
if-changed: ["docs/**", "*.md"] # multiple (matches if ANY hit)Globs use the standard paths-filter pattern
syntax (*, **, negation
with !).
Job-level gating
Put if-changed directly on a job to skip the whole job when nothing relevant
changed:
jobs:
deploy:
runs-on: ubuntu-latest
if-changed: "services/api/**"
steps:
- uses: actions/checkout@v4
- run: ./deploy.shShared setup, deduped flags
Every if-changed in the workflow feeds the same actio_changes setup job.
Identical glob groups collapse to a single filter flag, so two nodes watching
services/api/** share one output instead of running paths-filter twice:
jobs:
deploy:
runs-on: ubuntu-latest
if-changed: "services/api/**"
steps:
- run: ./deploy.sh
smoke:
runs-on: ubuntu-latest
if-changed: "services/api/**"
steps:
- run: ./smoke.shBoth jobs reference needs.actio_changes.outputs.filter_1; the setup job runs
paths-filter once.
Combining with if:
An existing if: is preserved and AND-combined with the change guard, so both
conditions must hold:
jobs:
smoke:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
if-changed: "services/api/**"
steps:
- run: ./smoke.sh smoke:
runs-on: ubuntu-latest
if: needs.actio_changes.outputs.filter_1 == 'true' && github.ref == 'refs/heads/main'jobs:
smoke:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
if-changed: "services/api/**"
steps:
- run: ./smoke.sh smoke:
runs-on: ubuntu-latest
if: needs.actio_changes.outputs.filter_1 == 'true' && github.ref == 'refs/heads/main'Diff base
dorny/paths-filter auto-detects the comparison base from the event, so the same
source works for both triggers with no extra config:
pull_request— diffs against the PR base branch (github.event.pull_request.base.sha).push— diffs against the previous commit (github.event.before), with a first-push / force-push fallback to the default branch.
Notes
- The setup job is named
actio_changesand the filter stepactio_filter. If a job namedactio_changesalready exists, Actio emits an error and leaves the workflow unchanged. - The
if-changedkey is stripped from the generated workflow. - A raw
git diffmode (no marketplace action) is a possible future option; today the macro standardizes ondorny/paths-filterfor robust base detection.