Three-Plane CI Setup
Repository CI is split across three execution planes to stay inside free-tier limits on each:
- GitHub Actions: light jobs only — the secret scan, the Python control plane, the docs build, and the Cloudflare Pages deploy.
- Blacksmith: runner-only placement for the backend
testjob. Nouseblacksmith/*actions are used anywhere; sticky-disk-backed features (setup-docker-builder, build-push caching, stickydisk, cached checkout) are banned because they caused a prior bill. - CircleCI batch plane: the frontend checks (per-PR, skipped when no frontend file changed), the issue #203 recoverability acceptance (nightly plus version-tag release gate), and the scheduled commercial-license audit.
Plane map
| Plane | Configuration | Workloads | Runner and schedule | Gate behavior |
|---|---|---|---|---|
| GitHub control | .github/workflows/control-plane.yml | Python lint, format check, and typecheck | ubuntu-24.04; pushes to main and pull requests targeting main, excluding docs/frontend-only paths and the listed workflow paths | Fast Python pull-request gate |
| GitHub security | .github/workflows/security.yml | Gitleaks secret scan | ubuntu-24.04; pushes and pull requests to main | Advisory (job and scan step continue on error) |
| GitHub docs | .github/workflows/docs.yml | docs-site npm ci + npm run build | ubuntu-24.04; pull requests touching docs paths | Docs pull-request gate |
| GitHub deploy | .github/workflows/cloudflare-pages.yml | docs-site build and Cloudflare Pages deploy | ubuntu-latest; pushes to main touching docs paths | Deployment |
| Blacksmith | .github/workflows/ci.yml | Backend test job: version check, lockfile check, frozen install, pytest with coverage | blacksmith-2vcpu-ubuntu-2404 runner label only; pushes and pull requests to main | Backend pull-request gate |
| CircleCI batch | .circleci/config.yml | Frontend checks (cimg/node:20.19, medium, per-PR with a frontend-path early exit), issue #203 recoverability acceptance (machine executor, medium, scheduled nightly at 02:00 UTC on main plus every v* tag), commercial-license audit (cimg/python:3.11, small, scheduled daily at 03:17 UTC on main) | Per-commit ci workflow plus scheduled license-audit/nightly workflows and a tag-triggered release-gate | Frontend pull-request gate; acceptance and audit batch, not additional GitHub minutes |
No paid CI feature is used on any plane: no Blacksmith sticky-disk actions, no
CircleCI Docker Layer Caching, and the smallest fitting resource classes
(medium machine for the Compose lifecycle, medium Docker executor for the
frontend suite, small for the license audit).
tests/scripts/test_ci_security_workflow.py guards this: it fails
if any useblacksmith/ reference or stray blacksmith mention appears in a
workflow file, or if runs-on: blacksmith-* appears outside the test job.
Control plane
.github/workflows/control-plane.yml has one python-quality job. It installs
Python 3.11 with uv, installs the locked development environment with
uv sync --frozen --all-extras --dev, and runs:
just lint
uv run ruff format --check app tests scripts/audit_commercial_licenses.py scripts/download_models.py
just typecheck
The workflow runs for pushes to main and pull requests targeting main. Its
paths-ignore entries skip docs-site and frontend changes, plus the Cloudflare
Pages and docs workflow files. Those paths are covered by their own workflows.
Blacksmith test job
.github/workflows/ci.yml defines a single test job on
runs-on: blacksmith-2vcpu-ubuntu-2404. It runs the version check, lockfile
check, frozen development install, and pytest coverage while excluding the
issue #203 integration test, then uploads coverage.xml. Only the runner label
changed; every step is identical to the GitHub-hosted version, so no
Blacksmith-specific action or paid feature is required.
CircleCI batch plane
.circleci/config.yml defines three jobs:
issue-203-acceptanceruns on theubuntu-2404:currentmachine executor (resource_class: medium) because the test drives a real isolated-Compose four-store lifecycle. It installsuvandjust, syncs the dev extra, and runsjust nightly-issue-203, storingissue-203-acceptance.xmlas an artifact. It is the sole coverage for the publication/recoverability surface but does not gate pull requests: it runs nightly onmainand on everyv*release tag.frontend-checksruns on thecimg/node:20.19Docker executor (resource_class: medium):npm ci, typecheck, lint, tests, the sentinel-key build, and the API-key bundle check — the same steps the formerfrontend.ymljob ran. Its first step diffs the PR againstorigin/mainand halts the job early with success when nothing underfrontend/(and nopackage-lock.json) changed, so backend-only PRs skip the ~2-minute suite while a required check still reports green. Onmainit always runs fully.license-auditruns on thecimg/python:3.11Docker executor (resource_class: small): the Python/model audit stays advisory (issue #163), and the docs and frontend lockfile audits are blocking — the same steps the formerlicense-audit.ymljob ran.
The ci workflow runs frontend-checks on every commit. The nightly
workflow runs issue-203-acceptance on a config-file schedule daily at
02:00 UTC on main, and the release-gate workflow runs it again on
every tag matching v*. The license-audit workflow runs on a config-file
schedule daily at 03:17 UTC on main, matching the former GitHub
schedule. No CircleCI context or secret is referenced by the checked-in
config, and Docker Layer Caching is not enabled.
Validate workflow changes
Run these commands from the repository root when changing the workflow or CircleCI configuration:
go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 \
.github/workflows/*.yml
uvx --from yamllint yamllint \
-d '{extends: relaxed, rules: {line-length: disable}}' \
.github/workflows/*.yml .circleci/config.yml
circleci config validate .circleci/config.yml
If the CircleCI CLI is not installed, use its official container:
docker run --rm -v "$PWD:/repo" -w /repo \
circleci/circleci-cli:latest circleci config validate .circleci/config.yml