Deployment and releases
On this page
A change reaches people in three steps: a PR preview when you ask for one, staging when it is merged to main, and production with the next release tag. One workflow does all of it: .github/workflows/platform.yml. Its header comment is the source of truth, together with dembrane/infra/README.md.
The three environments#
| Environment | How a change gets there |
|---|---|
| PR preview | Add the label preview to a pull request. |
| Staging | Deploys main: by hand, and on every push to main once the repository variable STAGING_DEPLOY_ON_MAIN is true. |
| Production | A release tag vX.Y.Z on main, after approval in the prod GitHub environment. |
So merging to main makes a change available on staging. Production gets it with the next release.
PR previews#
- Adding the
previewlabel deploys a preview. Each push to a labelled pull request redeploys it. - Removing the label, closing or merging the pull request removes it.
- At most three previews exist. A fourth removes the oldest, so take the label off when you are done.
- Each preview has its own database and is seeded with fictional sample data.
- The workflow posts one comment on the pull request with the links.
- There is no manual path. Previews come only from the label.
Production#
- The tag must be on
mainand must have the formvX.Y.Z. The workflow refuses anything else. - The reviewers of the
prodenvironment approve each deploy before the job runs. - After the deploy the workflow publishes the GitHub Release.
- The documentation site is published with the release. See maintaining the docs.
Checks on a pull request#
The workflow runs on pull requests that touch dembrane/platform, dembrane/frontend, dembrane/infra, the workflow itself or .github/scripts. A run checks only the side that changed.
| Job | What it checks |
|---|---|
10-check-server |
Biome lint and format, the type check, bun run config check, bun run packages check, that the schema matches the migrations, that the frontend’s generated API types are in sync, and bun test --parallel. |
20-check-frontend |
Path case, pnpm lint, tsc, the translations, and the generated API types. |
21-test-frontend |
vitest run. |
40-build-images |
Builds the five images once, smoke tests them, and checks the portal’s first-load bundle size. |
A separate workflow, .github/workflows/check-cla.yml, checks that the author is listed in contributors.yml.
Run the main checks yourself before you push:
cd dembrane/platform && bun run check
cd dembrane/frontend && pnpm lint && pnpm exec tsc && pnpm test
Three checks catch people out:
- Schema drift. Change a table in the Drizzle schema and CI fails until you commit the migration that
bun run db:generatewrites. - Translations. CI fails when
pnpm messages:extractorpnpm messages:compilechanges a catalog, or when a locale lacks a message that English has.TRANSLATING.mdexplains the catalogs. - Config.
bun run config checkfails when a key is declared but never read, and when an environment file is invalid.
How a deploy runs#
- The five images are built once per commit from
dembrane/platform/apps/*/Dockerfile. Each image is tagged with a hash of its inputs. An image whose inputs did not change is tagged for the new commit instead of built again. - The frontend is not deployed separately. It ships inside the
webimage, and one build runs in every environment. - A deploy only migrates, rolls out and verifies.
- In every environment the
migrateapp runs before the new version takes traffic. A failed migration stops the deploy. - The running build reports its release at
/health.
PR previews and staging run on Google Cloud Run in europe-west4. Each environment is its own cloud project with its own identities, so a preview cannot reach staging or production. Production runs on Kubernetes through a GitOps repository: the production job pushes the images and sets the image tag there, and Argo CD migrates, rolls out and smoke tests.
.github/scripts/build-images.sh --load builds the images into your local Docker daemon when you need to test one.
Migrations#
- Add or change a table in the Drizzle schema, run
bun run db:generate, and commit the SQL file. - A migration whose name contains
_contract_drops something. Outside the local, test and preview environments it runs only after an archive of the dropped tables is recorded. - An advisory lock keeps two migrate jobs from racing.
- Changing the order or number of steps in a durable workflow needs a bump of
WORKFLOW_VERSION. See background jobs and schedules.
Config per environment#
Non-secret values for each environment are checked in: packages/config/environments/{local,test,preview,staging,prod}.ts. A secret may only come from the process environment. Setting one in an environment file fails the boot.
Two tests guard these files. A residency test fails when a model, embedding or email location leaves the EU. A capacity test fails when an environment’s database connections would not fit its server.
Feature switches are config keys too. bun run config show <env> prints the effective values for an environment, and bun run config diff <env> <env> compares two.
Comments