Skip to main content

Build and deploy

Deployment is branch-driven and mostly automatic. The parts that are not automatic are the ones that cause incidents, so read the exceptions before the happy path.

The happy path​

Test​

  1. Push to any non-main branch of an app or API repository.
  2. CI builds an image and tags it :test (a moving tag), stamping an OCI revision label.
  3. CI resolves the VPS branch, runs git reset --hard on the gateway repository, then generate.sh, then reconcile.sh.
  4. Compose brings the stack up and installs the crons.
  5. The deploy holds flock, so two deploys cannot overlap.

vps-reverse-proxy/stacks/greyscreentv-test/docker-compose.yml is the source of truth for the test stack. Its README is stale about image tags and the Firestore API.

Production​

  1. Push or merge to main.
  2. CI builds a :prod image and pushes it.
  3. A deploy job runs behind the GitHub production environment. Whether it needs approval depends on that environment's protection rules, not on the workflow file.
  4. The same flock-guarded deploy applies it.

Repositories with a production pipeline: gstv-website, greyscreen-webview, gstv-admin-web, gstv-customer-api, gstv-admin-api.

Repositories without one:

RepositoryReality
gstv-vpsIts main workflow is manual and has no deploy job. The production image is a separate Artifact Registry tag that no repo builds.
gstv-multimediaTest auto-deploys with no gate.
gstv-tv, greyscreen-expo-webviewEAS + store distribution, not servers.
gstv-infra, gstv-secretsOperator-run scripts.

The rule that matters​

Worker before API

When a change touches both the worker and the API — a new queue, new job fields, a lane move — deploy the worker image first, then the API.

A producer running ahead of its consumer is not a stuck job; it is an infinite work signal. The worker VM scheduler counts a non-empty queue as work and will create a machine every reconcile tick that nothing can drain. This is exactly what produced 212 VM creations in 24 hours on 2026-10-21.

Building locally​

RepositoryCommand
Websitenpm run build:test / npm run build:prod (mode selects the env file)
Webviewnpm run build
Admin webnpm run build:test / npm run build:prod
Customer APInpm run build
Admin APInpm run build
Workernpm run build
Multimediacontainer build; needs gcsfuse at runtime

A frontend build without the matching env file fails on purpose. Do not create a .env to get past it; set the mode.

Deploying the worker by hand​

Only when you are deliberately moving a queue contract and have deployed the consumer:

  1. Build and push the image the instance template references, not the CI reference.
  2. Confirm the tag and digest.
  3. Drain or pause producers if the new image is incompatible with in-flight jobs.
  4. Let the scheduler replace the VM, or delete the current one so the next reconcile creates it from the new template.

There is no automatic path for this. Treat it as a production change with a rollback plan.

Gateway changes​

domains.json is the source of truth. After adding or changing a route:

  1. Run generate.sh to regenerate the per-domain nginx configs.
  2. Run reconcile.sh to apply them.
  3. Confirm the path publicly — a route that only exists in the repo is not live.

Push to a non-main branch of the gateway repo to reach the test VPS.

Native releases​

  • Phone shell: EAS profiles in eas.json. APP_VARIANT / EAS_BUILD_PROFILE select the Firebase key folder and app IDs. Android production builds come from the android branch, iOS from ios.
  • TV: EAS plus Play. The submit profile references google-play-key.json, which does not exist in the repository.

Native releases do not follow a server rollback. A bad client can outlive a backend fix.

Pre-deploy checklist​

  • Which environments does this change affect? Production is a separate approval.
  • Does it touch a queue contract or shared image? If yes, worker first.
  • Does it add a public path? domains.json + generate.sh.
  • Does it change artwork dimensions or WebP settings? Both the server and the client mirror.
  • Does it change a Firestore document ID or field name? That is an API contract.
  • Are the env files present for the mode you are building?

Next: Rollback — what is actually possible.