The CI/CD pipeline
Each repository ships a GitHub Actions workflow so pushes build and deploy themselves. This page explains the model so you can reproduce it for your own org.
The branch model
Section titled “The branch model”| Branch | Environment | Trigger | Deploys to |
|---|---|---|---|
dev | Staging | push (code changes; markdown-only pushes skipped) | the dev stack |
main | Production | tagged release | the production stack |
So you test on dev (dev.example.com), and cut a release on main when it’s ready.
The backend pipeline (build-image.yml)
Section titled “The backend pipeline (build-image.yml)”On push, the backend workflow:
- Lints with
ruff(pyflakes correctness rules) — blocking. - Type-checks with
mypy— informational, non-blocking. - Runs
pytest— blocking. The suite includes a contract test that imports the app and generates the OpenAPI schema, so “it builds in CI” really means “it boots.” - Builds + pushes a private image to GitHub Container Registry (GHCR).
- Deploys to the swarm: a
devpush rolls an immutable:dev-<sha>tag onto the dev stack; a release deploys to production.
The backend has no dependency on your sources repo, so its pipeline is entirely self-contained.
The client pipeline — the cross-repo submodule dance
Section titled “The client pipeline — the cross-repo submodule dance”This is the one part that needs care, because the client bundles a private submodule (your sources). The workflow:
- Checks out the repo with submodules, using a Personal Access Token so it can
read a different private repo:
- uses: actions/checkout@v4with:submodules: recursivetoken: ${{ secrets.SUBMODULES_TOKEN || github.token }}
- Advances each submodule to its freshest branch tip per channel (e.g. sources
@devon a dev push,@mainon a release), so a build always bundles the latest engine without a manual submodule bump. - Builds the static bundle with
VITE_API_BASE_URLandVITE_SITE_URLbaked in per environment (the dev push uses the dev backend +dev.origin; a release uses prod) — the latter fixes social-embed (og:image) URLs to the right host — and ships the Nginx image. - Deploys to the matching stack.
The build also bakes two optional display strings from repository Actions
variables (not secrets): HOSTED_IN and DMCA_MAIL, which become the client’s
VITE_HOSTED_IN / VITE_DMCA_MAIL. Set them under Settings → Secrets and variables →
Actions → Variables to brand a fork; leave them unset to keep the built-in defaults.
See The client.
The one secret you must add
Section titled “The one secret you must add”Create a Personal Access Token with read access to your private sources repository,
and add it as a repository or organisation Actions secret named
SUBMODULES_TOKEN. Without it, CI can’t clone the private submodule and the build
fails. (The default github.token can only read the repo it’s running in.)
The companion extension is not part of this dance any more — it ships on the Chrome Web Store, and its own repo has a separate workflow that publishes to the store on a tagged release.
The proxy pipeline
Section titled “The proxy pipeline”The proxy deploys to edge hosting on push to main:
- Cloudflare via
wrangler-action(uploadsNITRO_PROXY_SECRETas a Worker secret each deploy). - Netlify via the Netlify CLI (
netlify deploy --prod), since git integration won’t connect a private org repo.
Both self-skip when their tokens are absent, so you can run one, the other, or both.
Order of operations when you push everything
Section titled “Order of operations when you push everything”Because the client pins its submodule, push the submodule target before the client that bundles it:
- Push your sources repo.
- Push the backend.
- Push the client (which bundles 1 and talks to 2).
CI for the client resolves the freshest submodule tip, so as long as 1 is on its remote before the client build runs, everything lines up. (The companion extension publishes to the Chrome Web Store from its own repo, independently of this order.)
Reproducing it for your org
Section titled “Reproducing it for your org”- Fork/clone all repos into one GitHub organisation.
- Add the
SUBMODULES_TOKENsecret (read on your private repos). - Add the deploy secrets you use: GHCR is automatic; for the proxy add the
Cloudflare/Netlify secrets +
NITRO_PROXY_SECRET. - Point the deploy steps at your own server/registry (the reference uses a
self-hosted runner on a jump host with durable
read:packageslogin and adeploy.shon the managers).