Skip to main content

ifchange

CI codecov Vulnerabilities
Sigstore SLSA 3 crates.io npm PyPI

Lint for cross-file dependencies. Rename an env var in your deploy config, forget the code that reads it? ifchange catches it in the diff. 128 file extensions, 50+ languages. Robust and fast.

How it works:

  • Mark related code sections with LINT.IfChange / LINT.ThenChange comments.
  • When a guarded section changes in a PR or commit, every referenced file must change too, or the build fails.

Rust implementation of Google's IfThisThenThat (IFTTT) linting pattern. TypeScript implementation: ebrevdo/ifttt-lint.

Install · Usage · Directive Syntax · CI / Automation · Performance · Supported Languages

Install

curl -fsSL https://raw.githubusercontent.com/slnc/ifchange/main/install.sh | sh
cargo install ifchange        # Rust / crates.io
npm install -g @slnc/ifchange # Node.js / npm
pip install ifchange          # Python / PyPI

Pre-built binaries for Linux, macOS, and Windows available on GitHub Releases.

Build from source:

cargo install --path .

Usage

1. Fence related sections with directives:

# deploy/app.yml
# LINT.IfChange
env:
  DATABASE_URL: postgres://prod:5432/myapp
  REDIS_URL: redis://prod:6379
# LINT.ThenChange(src/config.py#env)
# src/config.py
# LINT.Label(env)
DATABASE_URL = os.environ["DATABASE_URL"]
REDIS_URL = os.environ["REDIS_URL"]
# LINT.EndLabel

2. Rename an env var in the YAML, forget to update config.py, run ifchange:

git diff HEAD~1 | ifchange
error: deploy/app.yml:2 -> src/config.py#env: target section has no matching changes in diff

found 1 error (1 lint)

You can wire this into a pre-commit hook or CI action to run automatically.

3. More options:

ifchange changes.diff                              # pass a file
ifchange --no-lint                                 # scan only: validate directive syntax
ifchange --no-lint -s ./src                        # scan a specific directory
git diff HEAD~1 | ifchange --no-scan               # lint only: skip syntax scan
ifchange -i '**/*.sql' -i 'config.toml#db' f.diff  # ignore files or labeled sections

--ignore uses glob patterns (*, ?, **) and matches both full relative paths and basenames.

Flag Description
-w, --warn Warn instead of failing (exit 0)
-v, --verbose Show processing details and validation summary
-j, --jobs <N> Thread count (0 = auto)
-i, --ignore <pattern> Ignore path glob or path-glob#label (repeatable)
-s, --scan <dir> Scan directory for directive errors (default: .)
--no-scan Skip directive syntax scan
--no-lint Skip diff-based lint

Exit codes: 0 ok, 1 lint errors, 2 fatal error.

Directive Syntax

Directives go at the start of a comment line. Full syntax reference: docs/DIRECTIVES.md.

LINT.IfChange / LINT.ThenChange

IfChange opens a guarded section. ThenChange closes it and lists the files that must co-change.

Simplest case, whole-file target:

# deploy/app.yml
# LINT.IfChange
env:
  DATABASE_URL: postgres://prod:5432/myapp
  REDIS_URL: redis://prod:6379
# LINT.ThenChange(src/config.py)

If env changes, src/config.py must also be modified somewhere in the diff.

With labels, narrow the requirement to a specific section:

# deploy/app.yml                             |  # src/config.py
# LINT.IfChange("env")                       |  # LINT.Label("env")
env:                                         |  DATABASE_URL = os.environ["DATABASE_URL"]
  DATABASE_URL: postgres://prod:5432/myapp   |  REDIS_URL = os.environ["REDIS_URL"]
  REDIS_URL: redis://prod:6379               |  # LINT.EndLabel
# LINT.ThenChange(src/config.py#env)         |

Multiple targets:

# deploy/app.yml
# LINT.IfChange("env")
env:
  DATABASE_URL: postgres://prod:5432/myapp
# LINT.ThenChange([
#   "src/config.py#env",
#   "docs/env-reference.md",
# ])

Absolute paths (repo-root-relative)

A leading / resolves from the repo root, not the filesystem root. This works regardless of where you run ifchange within the repo. The repo root is detected by walking up from CWD looking for .git, .hg, .jj, .svn, .pijul, .fslckout, or _FOSSIL_:

# deploy/app.yml (anywhere in the repo)
# LINT.IfChange
env:
  DATABASE_URL: postgres://prod:5432/myapp
# LINT.ThenChange(/src/config.py#env)

/src/config.py resolves to <repo-root>/src/config.py. Without the leading /, paths are relative to the source file's directory.

Directory targets

A trailing / marks a directory target (like .gitignore conventions). ThenChange(lib/) means "if this block changes, at least one file anywhere under lib/ must also change in the diff." Matching is recursive.

# src/api.py
# LINT.IfChange
SCHEMA_VERSION = 3
# LINT.ThenChange(generated/)

If SCHEMA_VERSION changes, at least one file under generated/ (e.g. generated/models.py, generated/sub/types.py) must also appear in the diff.

Rules:

  • ThenChange(lib/) (trailing slash) = directory target, recursive matching
  • ThenChange(lib) where lib is a directory = error with suggestion to add /
  • Labels are not supported for directory targets (ThenChange(lib/#label) is an error)
  • Directory must exist on disk during scan validation
  • If the directory was deleted (not on disk), lint reports an error (same as deleted file targets)

Directory targets can be mixed with file targets:

# LINT.ThenChange(generated/, docs/schema.md)

Self-references

Point to a label in the same file with #label (no filename):

# deploy/app.yml
env:
  DATABASE_URL: postgres://prod:5432/myapp
  # LINT.IfChange
  REDIS_URL: redis://prod:6379
  # LINT.ThenChange(#redis)

# ...

# LINT.Label("redis")
redis:
  host: prod
  port: 6379
# LINT.EndLabel

Cross-references

When two or more files reference each other, only changes within an IfChange section trigger validation, not changes elsewhere in the file.

Best practice

Source of truth points at derived files. Bidirectional fencing only when both sides are live code.

CI / Automation

Run it as a pre-commit hook, or as a GitHub Action. See examples/.

GitHub Action

- uses: slnc/ifchange@v1
Input Description Default
version Release tag to install (e.g. v1.0.0). Empty means latest. latest
args Extra arguments passed to ifchange
diff Path to a pre-built diff file. If empty, the action generates one.
token GitHub token for downloading release assets github.token

Pre-commit hook

repos:
  - repo: https://github.com/slnc/ifchange
    rev: v0.1.0
    hooks:
      - id: ifchange        # requires ifchange in PATH
      - id: ifchange-pypi   # OR: auto-downloads binary via PyPI

Performance

Wall-clock time to lint a 30k-line diff or scan all directives in a synthetic 5000-file repo (21 language types, 12-core x86_64) vs the original TypeScript implementation.

Mode Rust TypeScript Speedup
Lint ~17 ms 714 ms ~42x
Scan ~34 ms 387 ms ~12x

Versioning

Supported Languages

128 file extensions across 50+ languages
.ada .cr .gleam .kt .proto .swift
.adb .cs .go .kts .ps1 .tex
.ads .css .gradle .latex .psd1 .tf
.asm .cxx .groovy .less .psm1 .tfvars
.bas .dart .h .lisp .py .thrift
.bash .el .hcl .lsp .r .toml
.bat .env .hh .lua .rb .ts
.bzl .erb .hpp .m .rkt .tsx
.c .erl .hrl .md .rs .v
.c++ .ex .hs .mjs .s .vb
.cc .exs .htm .mk .sass .vba
.cjs .f .html .mm .scala .vhd
.cl .f03 .hxx .mojo .scm .vhdl
.clj .f08 .ini .mts .scss .vue
.cljc .f90 .java .nim .sh .xml
.cljs .f95 .jl .nix .sql .xsl
.cls .for .js .php .sty .xslt
.cmake .fs .jsonc .pl .styl .yaml
.cmd .fsi .jsp .pm .sv .yml
.conf .fsx .jsx .pro .svelte .zig
.cpp .gd .ksh .prolog .svg .zsh

Special files: Dockerfile{,.*}, .gitignore, go.mod, Makefile / makefile / GNUmakefile

Copy this snippet into your repository's AGENTS.md so coding agents use ifchange directives correctly.
  - When two code sections need to change in sync, use ifchange comment directives to enforce it.

  ### Example

  ```yaml
  # deploy/app.yml
  # LINT.IfChange
  env:
    DATABASE_URL: postgres://prod:5432/myapp
    REDIS_URL: redis://prod:6379
  # LINT.ThenChange(src/config.py#env)
# src/config.py
# LINT.Label(env)
DATABASE_URL = os.environ["DATABASE_URL"]
REDIS_URL = os.environ["REDIS_URL"]
# LINT.EndLabel

Run ifchange --help for full syntax and options.

</details>

## [Architecture](docs/ARCHITECTURE.md) · [Contributing](docs/CONTRIBUTING.md) · [Versioning](docs/VERSIONING.md) · [License (MIT)](LICENSE)

Metadata

Release files for ifchange 0.3.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ifchange 0.3.2
File Size Uploaded Provenance Actions
ifchange-0.3.2.tar.gz 6.7 kB With provenance Details for ifchange-0.3.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for ifchange 0.3.2
File Interpreter ABI Platform Actions
ifchange-0.3.2-py3-none-any.whl Python 3 none any Details for ifchange-0.3.2-py3-none-any.whl

Total release size: 14.4 kB

Release files / ifchange-0.3.2.tar.gz

Download URL ifchange-0.3.2.tar.gz
Size 6.7 kB
Tags Source
SHA-256 checksum
How to use checksums
715e5a1604d2b3e1acfb4607001648c0120486f2b845e0aa447bf50539a3165e
BLAKE2b-256 checksum
How to use checksums
5559ae4ef6849af919eac54ad4a7eba6e2e209f8234a5bd6eb38475fddd25aec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.

Transparency log

Release files / ifchange-0.3.2-py3-none-any.whl

Download URL ifchange-0.3.2-py3-none-any.whl
Size 7.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
adc0fe96d310d153a5412b61f785bd3d3c2161a9fc578016d2a7642483aea0ab
BLAKE2b-256 checksum
How to use checksums
10f1b41b1fe1294838849650284e98cf5a9f9967d50f6760f9b4df35423893b5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.

Transparency log

Security for ifchange 0.3.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for ifchange-0.3.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page