Skip to main content

Three-Plane CI Setup

Repository CI is split across three execution planes to stay inside free-tier limits on each:

  1. GitHub Actions: light jobs only — the secret scan, the Python control plane, the docs build, and the Cloudflare Pages deploy.
  2. Blacksmith: runner-only placement for the backend test job. No useblacksmith/* 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.
  3. 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​

PlaneConfigurationWorkloadsRunner and scheduleGate behavior
GitHub control.github/workflows/control-plane.ymlPython lint, format check, and typecheckubuntu-24.04; pushes to main and pull requests targeting main, excluding docs/frontend-only paths and the listed workflow pathsFast Python pull-request gate
GitHub security.github/workflows/security.ymlGitleaks secret scanubuntu-24.04; pushes and pull requests to mainAdvisory (job and scan step continue on error)
GitHub docs.github/workflows/docs.ymldocs-site npm ci + npm run buildubuntu-24.04; pull requests touching docs pathsDocs pull-request gate
GitHub deploy.github/workflows/cloudflare-pages.ymldocs-site build and Cloudflare Pages deployubuntu-latest; pushes to main touching docs pathsDeployment
Blacksmith.github/workflows/ci.ymlBackend test job: version check, lockfile check, frozen install, pytest with coverageblacksmith-2vcpu-ubuntu-2404 runner label only; pushes and pull requests to mainBackend pull-request gate
CircleCI batch.circleci/config.ymlFrontend 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-gateFrontend 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-acceptance runs on the ubuntu-2404:current machine executor (resource_class: medium) because the test drives a real isolated-Compose four-store lifecycle. It installs uv and just, syncs the dev extra, and runs just nightly-issue-203, storing issue-203-acceptance.xml as an artifact. It is the sole coverage for the publication/recoverability surface but does not gate pull requests: it runs nightly on main and on every v* release tag.
  • frontend-checks runs on the cimg/node:20.19 Docker executor (resource_class: medium): npm ci, typecheck, lint, tests, the sentinel-key build, and the API-key bundle check — the same steps the former frontend.yml job ran. Its first step diffs the PR against origin/main and halts the job early with success when nothing under frontend/ (and no package-lock.json) changed, so backend-only PRs skip the ~2-minute suite while a required check still reports green. On main it always runs fully.
  • license-audit runs on the cimg/python:3.11 Docker 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 former license-audit.yml job 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