Skip to main content

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.

WorkflowRuns onChecks
build-bazel.yamlPRs and main pushes touching Bazel or Go inputsFrozen 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.yamlDaily, manual dispatch and PRs changing itRace-instrumented Tadoku API tests; alerts through Notify when a scheduled or manual run does not succeed. See Race tests
verify-sqlc.yamlEvery PRReruns ./scripts/generate-sqlc.sh and fails on any change
verify-frontend-api-client.yamlPRs and main pushes touching the contract, recorded HTTP golden responses, the client package or the frontend lockfileRegenerates the frontend API client, fails on any change, then typechecks it and replays the recorded responses through it
verify-standalone-migrations.yamlEvery PRFails when migration SQL files change together with other files, unless the PR has the migration-move label
verify-docs.yamlEvery PRRuns docs/scripts/docs-check.test.mjs, so removing code that the docs still name fails
verify-dev-base.yamlPRs and main pushes touching development manifests, migration inputs or its workflowRuns 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.yamlEvery PRRuns the production config contract self-check and checker
verify-file-sizes.yamlEvery PRRuns ./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.yamlEvery PRRejects literal 5xx responses in services/tadoku-api/transport/http/; services/tadoku-api/transport/http/errors.go maps errors to statuses
build-frontend-webv2.yamlPushes touching webv2, ui or the lockfileBuilds webv2; publishes its image from main
build-frontend-admin.yamlPushes touching admin, ui or the lockfileBuilds admin; publishes its image from main
build-frontend-styleguide.yamlPushes touching styleguide, ui or the lockfileBuilds the ui styleguide; publishes its image from main
build-frontend-paper-styleguide.yamlPRs and main pushes touching PaperPaper boundaries, paper-ui and paper-styleguide lint, typecheck, test and build, package checks, image smoke test; publishes from main
build-frontend-authv2.yamlPRs and main pushes touching authv2 or PaperNode 22 typecheck, lint, boundaries and container build; main publishes commit, latest and prod; Playwright remains manual
build-frontend-webv3.yamlPRs and main pushes touching an input of the Webv3 image or its release gateNode 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.yamlHourly and manual dispatch from main, once WEBV3_CANARY_ENABLED is trueSigns in on next.tadoku.app, walks a read-only journey and publishes green or red; see Webv3 production canary
check-paper-playground.yamlPRs and main pushes touching the playground or paper-uiPaper boundaries and paper-playground lint, typecheck, test and build; never published
pr-branch.yamlpull_request_target label, push, reopen and close eventsAuthorizes 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.yamlManual dispatch, started every 15 minutes by a homelab CronJobTears 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.yamlDaily, and manual dispatch as a dry run by defaultDeletes old production-branch image versions; see Production branches
deploy-docs.yamlPRs and main pushes touching docs/ or the API specpnpm 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.