Skip to content

Commands

Every task is a just recipe. just is a command runner: a justfile holds named recipes, and just <name> runs one.

The point is that continuous integration runs the same commands you run. "Works on my machine, fails in the pipeline" becomes rare when there is only one definition of what "lint" means.

Run just with no arguments to list the recipes.

The service

Run these from the project root.

Command What it does
just install Sync dependencies from uv.lock. Same as uv sync.
just dev Run with auto-reload on APP_HTTP_PORT (8000 by default).
just test Run the test suite with pytest.
just lint ruff check and ruff format --check. Reports; changes nothing.
just fmt ruff check --fix and ruff format. Fixes what it can.
just typecheck mypy. Strict on domain/ and services/, lenient elsewhere.
just imports import-linter: verify the dependency rule.
just precommit Run the pre-commit hooks over the project's tracked files.
just check Everything above, then git diff --exit-code. Run this before pushing.
just check-all Everything just check does, plus the site build, the container tier, all five schema gates and the configuration drift check, the SLO rule gates, and the contract gates. Needs Docker.
just up Build the image and start the container stack. Once the API is healthy, a one-shot seed container creates five fixed orders through it — see Getting started.
just down Stop the stack and remove its volumes, the seed's state included.
just seed Create the same five orders against a service on localhost:${APP_HTTP_PORT} (8000 by default) — for just dev, which the compose one-shot does not reach. Idempotent: the ids it issued are kept in .seed-state.json (ignored by git), and only an order that has gone missing is re-created. The compose one-shot and just seed keep separate state — a named volume versus .seed-state.json — so running just seed against the just up stack creates a second set of five orders; it is for just dev.
just build-images Build both container images (the service and the migrations runner) for this machine's architecture without starting them, exactly as CI's security job and the release workflow do before scanning.

Observability

Command What it does
just o11y Everything just up starts, plus Grafana, Prometheus, Tempo and Loki in one container, with the dashboards and SLO rules from ops/ mounted in. Grafana is on http://localhost:3000 with anonymous admin access — no login.
just o11y-down Stop that stack and remove its volumes, telemetry included.
just o11y-gates Validate the SLO rules with promtool: syntax first, then unit tests that feed synthetic series through the real rules and assert the numbers that come out. promtool runs from inside the pinned grafana/otel-lgtm image, so it needs no separate Prometheus install and can never drift from the version that actually evaluates the rules. The image name is read from compose.yaml through docker compose config, so the one pin there is the one the gate uses.

See Observability for what the dashboards show and how the objectives are defined.

The API contract

Command What it does
just openapi Regenerate the committed openapi.json from the running app. Read the diff before committing it — it is your API change, stated completely.
just test-contract The contract tier: generated conformance testing over ASGI (Schemathesis). Needs no Docker. The drift check runs in just test / just check instead.
just contract-gates just test-contract, then the breaking-change check (oasdiff against openapi.baseline.json, cross-checked for a breaking Conventional Commit in the range since the last release tag). Needs Docker, for the oasdiff image.
just contract-release Promote the current openapi.json to the baseline. Run this when cutting a release — never to make a red contract-gates pass.

See The API contract for what each of the three gates catches and the workflow that goes with them.

Configuration

Command What it does
just config-docs Regenerate .env.example and the table in Configuration from settings.py's Field(description=...). Read the diff before committing it — it is your configuration change, stated completely.
just config-docs-check Fail if either generated file has drifted from the settings model. Part of just gates.
just config-check Print the configuration the service would start with, as one JSON object, with every SecretStr and every URL password masked — or exit 78 with the same message the service prints when it refuses to start. Reads .env as the service does. The same module runs inside the image: docker compose run --rm app python -m reference_service.config_check. Not a gate; a tool for The service will not start.

settings.py is the single source of truth for every environment variable this service reads. Hand-editing .env.example or the table in Configuration instead of the model is what config-docs-check exists to catch.

Supply chain

Command What it does
just audit pip-audit over every pinned version in uv.lock, against the PyPI advisory database. Reads a uv export with --disable-pip --require-hashes, so nothing is resolved or installed. Needs no Docker.
just scan Trivy over both built images, for known vulnerabilities and embedded secrets. Fails on a HIGH or CRITICAL finding that has a fix; exemptions only through .trivyignore.yaml, and every one expires. Needs Docker, and the images from just build-images.
just sbom A CycloneDX software bill of materials per image, into sbom/ (ignored by git). Needs Docker, and the same images.
just security build-images, then audit, scan and sbom — everything CI's security job runs, in the order it runs it. Needs Docker. Not part of just check-all, because its result changes with the advisory databases rather than with the code.
just build-multiarch Build both images for linux/amd64 and linux/arm64 on a docker-container buildx builder, with no output — proof that both architectures still build, which is what CI's build job runs. Creates the builder (pyfr) on first use.
just publish-images VERSION Build both platforms of both images and push them to GHCR under VERSION only. Run by release.yml after scan has passed on the same commit's images; not something to run by hand against ghcr.io.
just scan-published VERSION The scan again, over the two images just pushed under VERSION, for both platforms. Release only.
just promote-latest VERSION Point latest at the pushed VERSION index without rebuilding. Release only, after scan-published.
just changelog Preview the changelog entry the next release would write from the Conventional Commits since the last tag. Read-only.
just next-version Preview the version the next release would choose. Read-only — the release itself runs in the project's release.yml.
just update [VERSION] Pull in a newer template version through a git merge, the newest by default. Stops with exit 1 on conflicts; run it again after resolving them. Needs the network. See Update from the template.
just update-check Exit 1 when a newer template version exists, 0 when this project is current. The weekly template-update.yml runs the same check, pyfr update-check through uvx, as its first step.

Supply chain says where each of these runs in CI and what to do when one is red.

Outbound HTTP and mutation testing

Command What it does
just test-record Re-record the outbound HTTP cassettes against the local payment stub. Needs Docker, to start the stub.
just mutants Mutation testing over domain/ and services/. Slow relative to just test; run it when you have changed business logic and read the survivors, not the percentage.
just mutants-changed The same, restricted to files changed against main — what a pull request actually needs.
just mutants-gate Fail if the mutation score falls below the recorded floor (pyproject.toml's mutation_threshold_percent).

See Outbound HTTP calls for the retry policy, the circuit breaker, and what the recorded cassettes do and do not prove.

The database

These need Docker. The schema is owned by golang-migrate as plain SQL in migrations/, applied by a container — nothing about applying it needs Python.

Command What it does
just migrate Apply every outstanding migration.
just migrate-new NAME Write a new .up.sql / .down.sql pair, numbered sequentially.
just migrate-down N Roll back N steps. Defaults to one.
just migrate-version The current version, and whether the database is dirty.
just migrate-force V Clear a dirty flag by declaring the true version. Read the warning below first.
just migrate-manifest Rebuild migrations/manifest.sha256 after adding a migration.
just schema-snapshot Regenerate the committed schema.sql after changing a migration.
just psql An interactive psql session against the running stack.

just migrate-force runs no SQL

It only overwrites the version recorded in schema_migrations and clears the dirty flag. A migration that fails partway leaves the database dirty, and golang-migrate then refuses every further command — correctly, because it cannot know how much of the failed file actually applied.

Recovering means a human looking at the real schema, finishing or reversing the partial change by hand, and only then declaring the version that is genuinely applied. Running it first, to make the error go away, tells the tool a lie it will believe for the rest of the database's life.

The cache

just up starts Redis alongside the rest of the stack — it is not behind a profile, so the containerised stack always exercises the same cached path production runs.

Command What it does
just redis-cli An interactive redis-cli session against the running compose cache.

The object store

just up starts MinIO alongside the rest of the stack — it is not behind a profile, so the containerised stack always exercises the same receipt-storing path production runs.

Command What it does
just minio-console Print, and try to open, the MinIO web console at http://localhost:9001 — log in with the local-only minioadmin / minioadmin credentials from compose.yaml. Use it to look at what the receipt store actually holds.

The bucket itself is created by a one-shot minio-bootstrap container that runs mc mb once MinIO reports healthy, because MinIO does not create a bucket on demand and the application deliberately does not create its own — that would need CreateBucket permission in production, on top of the GetObject/PutObject the receipt store actually needs. just up waits for minio-bootstrap to exit successfully before starting the API, the same arrangement it already has with the migration container.

Tests and schema gates

Command What it does
just test Unit and API tests. Milliseconds, and needs no Docker.
just test-integration The container-backed tier: real PostgreSQL, migrated by the real migrate image.
just test-all Both tiers.
just gates All five schema governance gates, plus config-docs-check.

The five schema gates are the snapshot (schema.sql still matches the migrations), reversibility (every down.sql truly reverses its up.sql), version collisions, model drift (the SQLAlchemy models still match the real schema), and the manifest (nobody edited a migration that has already been applied somewhere). config-docs-check — the configuration reference's own drift check, see Configuration above — runs alongside them in the same recipe, which is why editing .env.example by hand fails just gates even though it touches no migration. They run from just check-all, and CI's own gates job calls just gates directly.

Why just check ends with a diff check

Several pre-commit hooks rewrite files: ruff-format, uv-lock, end-of-file-fixer, trailing-whitespace. A hook that reformats your code and then exits 0 has "passed" while leaving the tree different from what you committed. Continuous integration would fail on the first run and pass on the second, which teaches people to just press the button again.

git diff --exit-code afterwards turns any such rewrite into an explicit failure. If just check fails there, run just fmt, commit the result, and push again.

Why just precommit checks for an enclosing repository first

The recipe runs git rev-parse --show-prefix before anything else. A non-empty result means this project's root is not the top of its git repository — for example, if it were vendored or nested inside another one — and the recipe prints a message and exits clean instead of running at all, rather than walking up to the enclosing repository's root with pre-commit run --all-files and reformatting files outside the project.

In the normal case — this project as its own repository, with nothing enclosing it — git rev-parse --show-prefix is empty from the project root, the guard does not fire, and the recipe runs pre-commit run --files $(git ls-files), scoped to the project's own tracked files rather than --all-files, which would be equivalent here but is not what the recipe calls.

The documented examples

Run this from the project root.

Command What it does
just docs-examples Start the full compose stack, run every curl example marked in docs/ against it, then tear the stack down — pass or fail. Needs Docker.

This is an executable check on the documentation's own prose, not on the code: breaking one of the documented examples, or the endpoint it calls, fails the recipe. It is not part of just check-all — it runs as its own docs-examples job in CI, against a stack that job starts itself.

The documentation

Run these from the project root. The site is built from docs/ and mkdocs.yml; Contributing says what a change to the code owes it.

Command What it does
just docs-install Install the documentation toolchain (uv sync --group docs).
just docs Serve a live preview on http://127.0.0.1:8001, rebuilding on save.
just docs-build Build the site into site/ with --strict, exactly as CI does. --strict turns a warning into a failure: a link to a page that no longer exists, a renamed heading anchor, or an unresolvable include each fail the build rather than printing a warning nobody reads.
just links Dead external links, checked with lychee. Internal ones are already mkdocs build --strict's job. Needs the lychee binary (brew install lychee), or run it in CI, where the action provides it.
just docs-freshness [BASE] [HEAD] Documentation hygiene warnings for a pull request range: a stale last_reviewed date, or a covers: path that changed while its page did not. Never fails — see Contributing for what has to be true before these become hard failures.