Continuous integration¶
api-contract check exits 0 when nothing fails and 1 when something does, so
it works in any CI system as a plain step:
Whatever the system, three things must be true:
api-contract.json(andapi-contract-allow.yml, if you use one) is committed.- Your project's own dependencies are installed, because the check imports your
app to generate the schema, followed by
pip install --pre "api-contract[<extra>]". - For Django, the settings module is known: set
DJANGO_SETTINGS_MODULE, or runpython manage.py api_contract check.
GitHub Actions¶
This is examples/github-actions.yml.
Copy it to .github/workflows/api-contract.yml. It runs the check on every pull
request, shows the Markdown report on the run's summary page, and fails the job
when the check fails.
# Copy to .github/workflows/api-contract.yml.
#
# Runs `api-contract check` on every pull request, shows the Markdown report on
# the run's summary page, and fails the job when the check fails.
#
# Prerequisite: the snapshot (api-contract.json) and, if you use one, the
# allowlist (api-contract-allow.yml) are committed to the repository.
name: API contract
on:
pull_request:
permissions:
contents: read
jobs:
api-contract:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
# Install your project's own dependencies first: the check imports your
# app to generate the schema, so it needs everything the app imports.
- name: Install
run: |
pip install -r requirements.txt
# Pick the extra for your framework: django, ninja, fastapi, flask,
# apiflask, smorest or flask-openapi3.
pip install --pre "api-contract[fastapi]"
- name: Check the API contract
# Django only: the loader needs to know your settings module.
# env:
# DJANGO_SETTINGS_MODULE: myproject.settings
run: |
set +e # keep going after a non-zero exit so the report is still published
api-contract check --format markdown --output api-contract-report.md
status=$?
# Exit 0 = passed, 1 = failing changes (or an expired allowlist entry),
# 2 = configuration or load error (no report is written; see the log).
if [ -f api-contract-report.md ]; then
cat api-contract-report.md >> "$GITHUB_STEP_SUMMARY"
fi
if [ "$status" -eq 1 ]; then
echo "::error::Breaking API changes found. Fix them, or acknowledge them with 'api-contract allow'."
elif [ "$status" -ne 0 ]; then
echo "::error::api-contract could not run (exit $status). See the log above."
fi
exit "$status"
The exit code is kept in status instead of letting the step die, so the
report is still published when the check fails. Exit 2 (a configuration or load
error) writes no report; read the job log.
GitLab CI¶
api-contract:
image: python:3.12
stage: test
script:
- pip install -r requirements.txt
- pip install --pre "api-contract[fastapi]"
- api-contract check --format markdown --output api-contract-report.md || status=$?
- '[ -f api-contract-report.md ] && cat api-contract-report.md'
- exit "${status:-0}"
artifacts:
when: always
paths:
- api-contract-report.md
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
For Django, add variables: DJANGO_SETTINGS_MODULE: myproject.settings.
pre-commit hook¶
api-contract ships no pre-commit hook definition. Use a local hook, which
runs in your own environment where the app can be imported:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: api-contract
name: API contract
entry: api-contract check
language: system
pass_filenames: false
always_run: true
always_run is needed because the hook depends on the whole app rather than on
the staged files. Importing the app on every commit can be slow; if so, run it
at the pre-push stage (stages: [pre-push]) instead.
Updating the snapshot¶
The snapshot is the contract you have promised. Change it on purpose, in the same pull request as the API change, so reviewers see the contract diff:
- Change the API.
api-contract checkfails if the change is breaking. - Fix it so it is compatible, if you can.
- Otherwise, accept it deliberately in one of two ways:
- run
api-contract snapshotand commit the newapi-contract.json. The new schema becomes the baseline, and the diff of that file is what reviewers read; or - if the break must wait (clients are still migrating), add an
allowlist entry with
api-contract allowand an expiry date. CI passes until then.
- run
- After a change has shipped, run
api-contract snapshotand remove the allowlist entry, which has gone stale.
api-contract changelog turns the same difference into release notes; see
Reports.
Tip
Do not regenerate the snapshot automatically in CI. That would make check
always pass and defeat its purpose.