Skip to main content

Catch contract drift across documentation, configuration, specifications, and tests.

Project description

RepoInvariant

CI PyPI Python License GitHub Marketplace

Alpha: catch contract drift across repository artifacts before merge.

RepoInvariant is a deterministic CLI and GitHub Action that checks whether the contracts spread across your repository still agree. It focuses on two expensive, repeatable failure modes:

  • environment variables drifting between .env.example, Docker Compose, Kubernetes, GitHub Actions, and Spring configuration;
  • requirement IDs disappearing between Markdown, OpenAPI x-feature-id, and tests.

It reports exact evidence instead of guessing intent. No API key, LLM, or source upload is required.

Status

RepoInvariant v0.3.0 is a public alpha. The configuration and finding codes may change before v1.0.0. Pin an exact commit SHA when using the GitHub Action.

GitHub Action

The repository must be checked out before RepoInvariant runs. For supply-chain safety, pin an exact commit SHA:

name: RepoInvariant

on:
  pull_request:

permissions:
  contents: read

jobs:
  contracts:
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    steps:
      - name: Check out repository
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - name: Check repository contracts
        uses: xixvivji/RepoInvariant@6eb532eeba44c652f1cf8d94c8831c6807091387 # v0.3.0

The action installs only the source bundled with the pinned action revision. It does not transmit repository contents or require secrets.

Input Default Description
path . Repository-relative directory to scan.
config empty Optional repository-relative configuration file.
format text text, json, markdown, or sarif.
output empty Optional repository-relative report path.
baseline empty Optional repository-relative adoption baseline.
fail-on error Blocking threshold: error or warning.
no-env false Skip environment-contract checks.
no-features false Skip feature-traceability checks.

Prefer rule-level severity configuration when only one check needs a temporary downgrade.

Quick start

Install the CLI from PyPI:

# uv (recommended)
uv tool install repoinvariant

# pipx
pipx install repoinvariant

# pip (inside an activated virtual environment)
python -m pip install repoinvariant

Then initialize and scan a repository:

cd /path/to/your/repository
repoinvariant init
repoinvariant check .

See it catch drift

The repository includes a passing ticket-service example and an intentionally broken copy. Clone the repository and run both to see the merge gate turn red without configuring a real project first:

$ repoinvariant check examples/ticket-service
PASS: 6 files, 0 errors, 0 warnings

$ repoinvariant check examples/ticket-service-drift
compose.yml:7:25: error ENV001: Environment variable 'HOLD_TTL_SECONDS' is consumed but missing from the contract.
  hint: Declare the variable in an environment contract or explicitly ignore it.
openapi.yml:8:21: error TRACE003: Specification feature 'REQ-HOLD-CREATE' has no matching test.
  hint: Reference REQ-HOLD-CREATE in a configured test file.
FAIL: 6 files, 2 errors, 0 warnings

The drift example breaks two contracts on purpose: Compose consumes HOLD_TTL_SECONDS without declaring it in .env.example, and the OpenAPI operation has no matching requirement ID in its test. Add HOLD_TTL_SECONDS=300 to the environment contract and REQ-HOLD-CREATE to the test to make it pass.

Configuration

Run repoinvariant init to create .repoinvariant.yml:

version: 1

env:
  contracts: [.env.example]
  compose: [compose*.yml, docker-compose*.yml]
  kubernetes: [k8s/**/*.yml]
  workflows: [.github/workflows/*.yml]
  spring:
    - src/main/resources/application*.yml
    - src/main/resources/application*.properties
  ignore: [CI, HOME, PATH, GITHUB_*, RUNNER_*]

features:
  requirements: [docs/**/*.md]
  specifications: [openapi*.yml, docs/openapi*.yml]
  tests: [tests/**/*, src/test/**/*]
  id_pattern: '\bREQ-[A-Z0-9][A-Z0-9-]*\b'
  openapi_extension: x-feature-id
  requirements_mode: definitions
  ignore: []

rules:
  ENV001: error
  ENV002: warning
  ENV003: warning
  TRACE001: error
  TRACE002: error
  TRACE003: error
  TRACE004: warning

requirements_mode: definitions counts IDs only when they look like canonical Markdown definitions (headings, definition lists, and the first column of tables). Set it to mentions for legacy repositories where any prose reference is authoritative.

The built-in REQ-* pattern is printed verbatim because it is a constrained public identifier format. Matches from a custom id_pattern are reported as deterministic custom-id-N labels; source locations remain exact, but arbitrary matched repository text never reaches logs or report artifacts.

Each finding code can be set to error, warning, or off. This makes staged adoption explicit: start a noisy rule as a warning, reduce the accepted backlog, then promote it to an error. Quote "off" when editing YAML to avoid YAML 1.1 boolean parsing surprises.

Adopt an existing repository

repoinvariant baseline [path] [--config FILE] [--output FILE] [--force]
                         [--no-env] [--no-features]

If an established repository already has findings that cannot all be fixed at once, snapshot the current set and gate only newly introduced drift:

repoinvariant baseline .
git add .repoinvariant-baseline.json
repoinvariant check . --baseline .repoinvariant-baseline.json

repoinvariant baseline returns exit code 0 after a successful scan and write even when it records blocking findings. A check using that baseline suppresses matching rule/entity/severity identities from reports, GitHub annotations, and the exit decision; new identities still behave normally. Messages and source locations are evidence, not identity, so moving the same violation does not make it new. Resolved entries become non-blocking stale entries. Remove them promptly: if an identical violation returns while its stale entry remains, it is still accepted.

The baseline is bound to the effective configuration and enabled scanner set. Use the same --config, --no-env, and --no-features options for generation and checking. RepoInvariant returns exit code 2 on a scope mismatch instead of silently applying an incompatible baseline. For the GitHub Action, add the optional input (default: empty):

with:
  baseline: .repoinvariant-baseline.json

A baseline is a reviewed allowlist, so generate it from a trusted default branch, commit it, and protect changes with CODEOWNERS or separate approval. Never regenerate it blindly from an untrusted pull-request branch. The file stores versioned hashes plus finding codes and severities; it does not store variable names, requirement identifiers, messages, or source paths.

Before using baseline --force, run a baseline-free check, review every current finding, and inspect the baseline diff. Regeneration accepts all findings visible in that scan, including newly introduced ones.

Then run one of the stable report formats:

repoinvariant check . --format text
repoinvariant check . --format json --output repoinvariant-report.json
repoinvariant check . --format markdown --output repoinvariant-report.md
repoinvariant check . --format sarif --output repoinvariant-report.sarif

Exit code 0 means no blocking drift, 1 means a configured contract failed, and 2 means the command or configuration was invalid. Add --fail-on warning for a stricter merge gate.

Finding codes

Code Meaning Default severity
ENV001 A consumer uses an environment variable absent from the contract error
ENV002 A contract variable has no discovered consumer warning
ENV003 Explicit defaults disagree across artifacts warning
TRACE001 A requirement ID is absent from the specification error
TRACE002 A specification ID has no requirement error
TRACE003 A specification ID has no test reference error
TRACE004 A requirement appears to be defined more than once warning

Design boundaries

RepoInvariant deliberately does not:

  • decide whether two differently worded requirements mean the same thing;
  • modify repository files automatically;
  • compare live infrastructure or databases;
  • print secret values found in configuration;
  • claim full OpenAPI, Compose, Kubernetes, or Spring validation.

Use their native validators alongside RepoInvariant. RepoInvariant owns the gap between artifacts.

Configured files, baselines, and report destinations must stay inside the repository. RepoInvariant rejects their symlinks, limits configuration files to 256 KiB and scanned or baseline files to 2 MiB, and fails closed on malformed configured YAML or baseline JSON. Custom requirement patterns run with a timeout and one shared matching-time budget. File reads and atomic writes use no-follow directory descriptors so a concurrent symlink swap cannot redirect them outside the repository.

Roadmap

  • Environment contract checks
  • Requirement → OpenAPI → test traceability
  • Text, JSON, Markdown, and SARIF reports
  • Composite GitHub Action
  • Version-baseline contracts across Gradle, Docker, CI, and documentation
  • Reusable parser plugin API
  • PyPI trusted publishing and provenance-attested release automation
  • Real-world compatibility fixtures from external projects

See CONTRIBUTING.md to help shape future releases. Participation is governed by the Code of Conduct. For usage help, open a question with a minimal synthetic example.

License

Apache License 2.0. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

repoinvariant-0.3.0.tar.gz (120.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

repoinvariant-0.3.0-py3-none-any.whl (43.0 kB view details)

Uploaded Python 3

File details

Details for the file repoinvariant-0.3.0.tar.gz.

File metadata

  • Download URL: repoinvariant-0.3.0.tar.gz
  • Upload date:
  • Size: 120.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for repoinvariant-0.3.0.tar.gz
Algorithm Hash digest
SHA256 0a28419369bcf0ea10123c9248bb7eb4efcb77a5249a979d5fe2dfc815bfe899
MD5 c3bd1e7ca39cabdfa73ca905d0e105bd
BLAKE2b-256 f8f7402ff3bbfd8934e3b8256481c06bf1d17ff1703e8b8a964749aed9d2d390

See more details on using hashes here.

Provenance

The following attestation bundles were made for repoinvariant-0.3.0.tar.gz:

Publisher: release.yml on xixvivji/RepoInvariant

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file repoinvariant-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: repoinvariant-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 43.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for repoinvariant-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 01d2dc7e33c7173c25ba6d1eed5d73214d662198e3043774db81c909cd066cea
MD5 2bd7bc88116d79ccb6d2c8e48795c2a4
BLAKE2b-256 b6a083c7d169ef62868aedd7812e2a037530d0b13e8e3c895748f5ead84a8c39

See more details on using hashes here.

Provenance

The following attestation bundles were made for repoinvariant-0.3.0-py3-none-any.whl:

Publisher: release.yml on xixvivji/RepoInvariant

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page