CI workflows
Read this when you need to know what CI checks on a pull request, why a
workflow ran, how backend CI caches its work or how a Webv3 image reaches
prod. The commands each workflow runs
are explained in Toolchain and repository.
All workflows live in .github/workflows/.
Backend CI runs two jobs on the repository-scoped tadoku-ci Kubernetes runner
pool. The build job uses bazel test --nobuild_tests_only to build the full
target set and run its tests together against disposable services; Bazel runs
up to eight actions, tests included, at once. Development-only images listed in
tools/ci/dev_image_targets are excluded there and built by the parallel
dev-images job, so tests do not wait on their base image pulls. The build job
turns off PostgreSQL fsync, full-page writes and synchronous commit on its
disposable service before the tests. Test results are cached until their inputs or the job's service
definitions change; see Running tests. Release image commands follow the tests so their
distinct configuration does not invalidate normal analysis between the build and
tests. Main publication runs only after the tests and image scanning succeed.
Image scanning uses the Trivy that the runner image puts on PATH;
GitHub-hosted runs install the same version before scanning. The scan step
asserts the version in both environments.
Race tests run separately and never gate publication; see Race tests.
ARC admits at most five Tadoku build runners, and each backend run takes two;
resource reservations may keep excess jobs queued.
The runners use the private Bazel HTTP action cache and Lab CA. Pull requests
read existing results without uploading; trusted non-PR runs populate the cache.
The normal job also uses Bazel's remote repository contents cache for repository
rules that declare reproducible outputs. Bazel validates their declared and
recorded inputs before reuse. Changed inputs and cache lookup misses or errors
fall back to repository evaluation. Missing or corrupt content discovered later
during lazy downloads is not guaranteed to recover within that command. The
upload policy applies to all Bazel commands, including dependency resolution
and queries. The first trusted
run pays the repository upload cost; later jobs can reuse those results without
repeating repository generation. This is separate from compiled action caching.
Each runner Pod prefetches trusted public main dependencies while becoming
idle: frozen module metadata, Gazelle and the OpenAPI generator, plus the
workflow's digest-pinned service images. Native repository archives and extracted
source trees use the Pod's private Bazelisk emptyDir under
/home/runner/bazelisk/repositories; output roots and Bazel servers remain
separate and disposable. Source-cache hits differ from HTTP action-cache hits.
Prewarm failure falls back to ordinary cold setup. The bazel-prewarm.json
receipt in bazel-performance records the seed revision, duration and size, or
reports cold when no successful source seed receipt was available. A ready
receipt means repository fetches completed; service-image pulls are best effort.
Local action outputs and service data are disposable per runner. Fork pull
requests retain GitHub-hosted isolation and cannot use the private runner pool.
Manual dispatch can select the hosted control for an equivalent-workload
comparison. Both scheduled and manual builds execute the wrapper, publication
authentication and OpenAPI checks even when their successful receipts exist. bazel-performance artifacts retain build profiles and build-event
JSON; include queue/setup time and publication when comparing complete CI runs.
See CI setup verification for exact-input receipt keys, runtime invalidation, cache scope and the fingerprint check command.
When the test step fails, build-bazel.yaml uploads the available HTTP
E2E target's test.log and test.xml as a workflow artifact for seven days.
Artifact names include the job and run attempt, for example http-e2e-build-1;
race-bazel.yaml uploads the same files as http-e2e-race-<attempt>.
Download these files from the workflow run when Bazel suppresses oversized
console output; they include assertion output and fixture cleanup errors.
| Workflow | Runs on | Checks |
|---|---|---|
build-bazel.yaml | PRs and main pushes touching Bazel or Go inputs | Frozen lockfile, changed forward migration risk check on PRs (the Migration safety check, which an approval of the migration-safety-override environment can override for one run), Gazelle diff, visibility, provider-dependency, repository, Go comment and .depolicy.yaml import checks, OpenAPI bundle and generation diff, build, image target coverage, tests and, in a parallel job, development images; scans and publishes backend images from main |
race-bazel.yaml | Daily, manual dispatch and PRs changing it | Race-instrumented Tadoku API tests; alerts through Notify when a scheduled or manual run does not succeed. See Race tests |
verify-sqlc.yaml | Every PR | Reruns ./scripts/generate-sqlc.sh and fails on any change |
verify-frontend-api-client.yaml | PRs and main pushes touching the contract, recorded HTTP golden responses, the client package or the frontend lockfile | Regenerates the frontend API client, fails on any change, then typechecks it and replays the recorded responses through it |
verify-standalone-migrations.yaml | Every PR | Fails when migration SQL files change together with other files, unless the PR has the migration-move label |
verify-docs.yaml | Every PR | Runs docs/scripts/docs-check.test.mjs, so removing code that the docs still name fails |
verify-dev-base.yaml | PRs and main pushes touching development manifests, migration inputs or its workflow | Runs k8s/dev/base/e2e.cjs against real published images with disposable Docker fixtures, checks profiles and owner/runtime separation, and uploads its report and logs |
verify-config-contract.yaml | Every PR | Runs the production config contract self-check and checker |
verify-file-sizes.yaml | Every PR | Runs ./tools/ci/check_file_sizes.sh, which fails when a handwritten text file is over 20,000 bytes; see Splitting a long file |
verify-http-error-mapping.yaml | Every PR | Rejects literal 5xx responses in services/tadoku-api/transport/http/; services/tadoku-api/transport/http/errors.go maps errors to statuses |
build-frontend-webv2.yaml | Pushes touching webv2, ui or the lockfile | Builds webv2; publishes its image from main |
build-frontend-admin.yaml | Pushes touching admin, ui or the lockfile | Builds admin; publishes its image from main |
build-frontend-styleguide.yaml | Pushes touching styleguide, ui or the lockfile | Builds the ui styleguide; publishes its image from main |
build-frontend-paper-styleguide.yaml | PRs and main pushes touching Paper | Paper boundaries, paper-ui and paper-styleguide lint, typecheck, test and build, package checks, image smoke test; publishes from main |
build-frontend-authv2.yaml | PRs and main pushes touching authv2 or Paper | Node 22 typecheck, lint, boundaries and container build; main publishes commit, latest and prod; Playwright remains manual |
build-frontend-webv3.yaml | PRs and main pushes touching an input of the Webv3 image or its release gate | Node 22 lint, typecheck, boundaries, standalone container and synthetic read-only browser journeys; from main, publishes the commit tag and latest, then tags prod only after the release gate passes on that digest |
webv3-production-canary.yaml | Hourly and manual dispatch from main, once WEBV3_CANARY_ENABLED is true | Signs in on next.tadoku.app, walks a read-only journey and publishes green or red; see Webv3 production canary |
check-paper-playground.yaml | PRs and main pushes touching the playground or paper-ui | Paper boundaries and paper-playground lint, typecheck, test and build; never published |
pr-branch.yaml | pull_request_target label, push, reopen and close events | Authorizes the deploy label, deploys the pinned commit beside production with dev-cli and tears the branch down when the label is removed or the pull request closes. Its deploy and teardown jobs are the called workflows pr-branch-called-deploy.yaml and pr-branch-called-teardown.yaml. See Pull-request deploys |
pr-branch-reconcile.yaml | Manual dispatch, started every 15 minutes by a homelab CronJob | Tears down pull-request branches whose pull request is closed or unlabelled, or whose branch expired; never runs dev cleanup. See Pull-request deploys |
clean-branch-images.yaml | Daily, and manual dispatch as a dry run by default | Deletes old production-branch image versions; see Production branches |
deploy-docs.yaml | PRs and main pushes touching docs/ or the API spec | pnpm api:check, typecheck, build and the public API boundary (pnpm api:check-build and no internal paths); deploys GitHub Pages from main |
Frontend application images from dev/ branches publish only their immutable commit tag.
They never update latest or prod. Development manifests can pin those images
without a production release. Main publication updates the commit tag, latest
and prod, except that Webv3's prod waits for its release gate. Development
follows latest and production follows prod, so merging frontend source to
main releases each affected frontend to production, as it does for the backend
images.
Webv3 release gate
build-frontend-webv3.yaml tags prod only on an image digest that passed the credentialed browser suite on a development branch tenant. See Webv3 release gate.
Production config contract
Production manifests depend on names and paths that this repository defines:
runtime environment variables, ingress hosts, probe paths, the auth cookie
domain and feature flag keys.
tools/ci/configcontract/contract.json lists each item with the files that
consume it and the text that must appear there.
tools/ci/configcontract/check.py fails and names the item and file when that
text or file is missing. The text must not be part of a longer name: a key does
not match a longer key, HOST= does not match APP_HOST= and tadoku.app does
not match tadoku.app.example. When an entry's text ends in punctuation, such
as a template literal, include its closing delimiter so a suffix cannot extend
it. Matching searches the raw file text, comments and strings included. It
catches a missing file and an ordinary rename but does not prove the consumer is
still active, so when you rename a consumer, also update comments that quote its
old name. An equivalent rewrite of the consuming code, such as
process.env['X'] for process.env.X, fails until its entry is updated.
verify-config-contract.yaml runs it on every pull request, so it can be a
required check.
python3 tools/ci/configcontract/check.py
python3 tools/ci/configcontract/self_check.py
When you add, rename or remove an item that production depends on, change the production configuration and the contract in step: add one entry line per file that consumes the item, pointing at the code that reads or serves it rather than at a copy of the value. Renaming an item breaks the deployed manifests until they are updated to match.
The production manifests live outside this repository and are not checked here:
the Webv3 and Webv2 Deployments' environment values and probes, the HTTPRoute
hostnames, the Tadoku API's API_IDENTITY_COOKIE_DOMAIN (production relies on
its default) and the production Flipt flag and segment state. Webv3's optional
runtime NODE_EXTRA_CA_CERTS is read by Node itself and is not in the contract.