Detect breaking changes in reusable GitHub Actions workflows.
A reusable workflow's on.workflow_call block is a public API: every repository that calls it depends on those inputs, secrets, outputs, and permissions. Nothing in GitHub Actions tells you when you break it, so the failure surfaces in someone else's pipeline. workflow-contract treats that block as a versioned contract, compares it against a Git base, and reports breaking changes before the pull request merges. It also validates the callers of local reusable workflows.
The analysis commands are static and read-only: they do not execute workflows, send repository content to a service, require a token, or collect telemetry. snapshot --output writes only to the destination explicitly selected by the user.
.github/workflows/deploy.yml
BREAKING [input.removed] Input "environment" was removed.
BREAKING [secret.required-added] Required secret "subscription_id" was added.
WARNING [input.default-changed] Input "retries" changed its default value.
Local caller validation
ERROR .github/workflows/release.yml:jobs.deploy.with.region
[caller.missing-required-input] Job "deploy" does not provide required input "region".
| Change | Default result |
|---|---|
| Remove an input, secret, output, or reusable workflow | Breaking |
| Add a required input or secret | Breaking |
| Make an optional input or secret required | Breaking |
| Change an input type | Breaking |
| Increase an explicit permission request | Breaking |
| Change a default or output value expression | Warning |
| Add an optional input, optional secret, or output | Informational |
| Invalid local caller input, secret, literal type, or permission cap | Error |
Description-only edits do not produce compatibility findings.
- Node.js 24 or newer
- Git history containing the base revision when using
check
After the first npm release:
npm install --save-dev workflow-contractFrom a source checkout:
npm ci
npm run build
node dist/cli.js --helpCompare every reusable workflow at a base revision with the working tree and validate local callers:
npx workflow-contract check --base origin/mainLimit comparison to selected workflow files:
npx workflow-contract check \
--base origin/main \
--path .github/workflows/deploy.yml \
--path .github/workflows/test.ymlCompare two files without a Git repository:
npx workflow-contract diff before.yml after.ymlExtract a deterministic contract snapshot:
npx workflow-contract snapshot .github/workflows/deploy.yml \
--output deploy.contract.jsonValidate current local callers only:
npx workflow-contract validatenpx workflow-contract check --base origin/main --format json
npx workflow-contract check --base origin/main --format github
npx workflow-contract check --base origin/main --fail-on warning
npx workflow-contract check --base origin/main --fail-on neverFormats are pretty, json, and github. --fail-on breaking is the default. Exit codes are:
0: selected policy passed1: compatibility findings failed the selected policy2: invocation, YAML, Git, or runtime failure
Use --no-validate-callers when only base/head contract comparison is wanted.
The action is bundled and does not install packages at runtime. Full history is required because it reads the base contract through local Git rather than the GitHub API.
name: Reusable workflow compatibility
on:
pull_request:
paths:
- '.github/workflows/*.yml'
- '.github/workflows/*.yaml'
permissions:
contents: read
jobs:
contract:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: helgafinn/workflow-contract@v1
with:
fail-on: breaking
validate-callers: trueThe action automatically uses pull_request.base.sha or the push event's before SHA. Override it when necessary:
- uses: helgafinn/workflow-contract@v1
id: contract
with:
base: origin/main
paths: |
.github/workflows/deploy.yml
.github/workflows/test.yml
- run: echo "Breaking changes: ${{ steps.contract.outputs.breaking-changes }}"Outputs are breaking-changes, caller-errors, warnings, and passed. Findings appear as file annotations and in the job summary.
For jobs with a local reference such as:
jobs:
deploy:
uses: ./.github/workflows/deploy.ymlThe validator reports:
- undeclared or missing required inputs
- literal input values whose YAML type does not match the declaration
- undeclared or missing required secrets
- explicit caller permissions lower than explicit callee requests
- missing or non-reusable local workflow files
Expression values containing ${{ ... }} are accepted without static type evaluation. secrets: inherit satisfies required-secret presence because the available names are only known at runtime.
Version 0.1 intentionally does not:
- fetch or authenticate to workflows in other repositories
- execute or emulate GitHub Actions
- resolve expressions, matrices, or runtime outputs
- infer behavior from implementation-only step changes
- replace
actionlintor a security scanner
Permission comparisons are conservative. Explicit increases are breaking because a caller may cap the token, while transitions involving GitHub's implicit defaults are warnings because repository defaults are not available to static analysis.
See docs/design.md for the complete compatibility policy.
The package exports the same primitives used by the CLI and action:
import {
diffContracts,
extractWorkflowContract,
runCheck,
} from 'workflow-contract';npm ci
npm run typecheck
npm test
npm run build
npm run checkThe generated action/ bundle must be committed with source changes that affect the action. See CONTRIBUTING.md.
Analysis is local, deterministic, and read-only. Workflow files are parsed as data and are never executed. Git commands use argument arrays rather than a shell. Do not add telemetry or remote uploads without an explicit design decision and an opt-in privacy review.