Skip to content

Continuous Integration ​

Every pull request, and every push to master, runs .github/workflows/ci.yml on GitHub Actions. It checks what the change affects and nothing more. Results show on the PR and in the Actions tab.

Status
PR checks: lint, types, tests, database tests, migration safety, Docker buildsCI/CD Phase 1
Required before merge (branch protection)Not on GitHub Free
Images pushed and deployedCI/CD Phase 4

The repo is private on GitHub Free, which can't enforce required checks. Don't merge a PR with a red check. Nothing stops you except this rule.

What runs ​

JobWhat it doesRuns when
changesPicks the base commit (the PR's base, or the previous master commit) and lists affected packages and services with turbo ls --affectedalways
migrationsscripts/ci/check-migrations.mjs: fails if an applied migration was edited or deleted, or if a new one is destructive without the contract-migration labelalways
checksturbo run lint check-types test --affected, the docs build (catches dead links), and the API's database tests against Postgres 15always; database tests only when api or one of its packages changed
helmscripts/ci/check-helm.sh: lints both charts with every values file, renders every release, and validates the result against Kubernetes 1.34's schemas (kubeconform)always
dockerBuilds the image of each affected service (linux/amd64, not pushed), so a broken Dockerfile fails here instead of at deploywhen a deployable service changed

Affected follows the dependency graph:

  • A change in packages/database checks every app that uses it.
  • A change to a root file (turbo.json, pnpm-lock.yaml, the workflow itself) checks everything.
  • A change to one app's docs page checks only docs.

A new push to a PR cancels that PR's previous run. Runs on master always finish.

Limits. GitHub's free runners for private repos have 2 vCPUs and 7 GB of memory. Turbo runs two tasks at a time (--concurrency=2); more than that runs slower, not faster, on a box that size. Every job has a timeout (checks 45 minutes, docker 30), so a stuck task fails instead of running for GitHub's 6-hour default and using up the month's minutes.

Reading the log. Each task's output prints as a block when that task finishes, with a pass/fail summary at the end. If the step seems quiet, the task that hasn't printed yet is the one still running.

Migration safety ​

Code rollback depends on migrations being additive (see Developing & Committing → Migrations). The check flags any new migration.sql that:

  • drops a table, column or type
  • renames something
  • changes a column's type
  • adds SET NOT NULL
  • truncates or deletes rows

If it fails on a migration you meant to ship:

  1. Make sure it really is the contract step: the code that stopped using the old shape is already deployed.
  2. Add the contract-migration label to the PR (create it once under Issues → Labels).
  3. Re-run the job: Actions → the run → Re-run failed jobs. Labels are read when the job runs.
  4. Take a backup before deploying it.

Editing or deleting an applied migration always fails. Production has its checksum, so write a new migration instead.

When a check fails ​

Open the failed job and expand the red step. Reproduce it locally with the same command:

bash
pnpm --filter @playpals/db db:generate                     # once, after pulling schema changes
TURBO_SCM_BASE=master pnpm turbo run lint check-types test --affected   # turbo's default base is main
pnpm --filter @playpals/api test:db                         # needs local Postgres, see Testing
node scripts/ci/check-migrations.mjs origin/master
docker build --platform linux/amd64 -f apps/<service>/Dockerfile .
scripts/ci/check-helm.sh                                    # needs helm and docker

CI starts from a clean checkout, so it catches what your machine hides: generated files that are gitignored (like Expo's expo-env.d.ts), a missing package build, or a dependency that's only installed locally. When CI fails and your machine passes, suspect one of those first.

A known flaky database test

Now and then (about one run in four locally), one API database test fails and then passes on a re-run. Suites share one test database, and a few assertions depend on ordering or timing (vipPasses, adminCoins, tips). Re-run failed jobs once. If the same test fails twice, it's real.

Cost ​

GitHub Free includes 2,000 Actions minutes a month for private repositories. A typical PR uses:

  • 5–10 minutes for the checks
  • plus about 3–6 minutes per Docker image it affects

A change to a shared package or a root file affects all eight images. Check usage under Settings → Billing. If it gets close to the limit, run the Docker builds only on master, not on every PR push.

Changing the workflow ​

  • Edit .github/workflows/ci.yml in a PR like any other change. It runs on that PR, so you see the result before merging.
  • Lint it first: docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:1.7.7 -no-color.
  • Keep the jobs runnable locally with plain pnpm, node and docker commands. The workflow should only wire them together.

Internal documentation — PlayPalz platform