Skip to content

Cloud Build

Four triggers, one service account, two output repos. The triggers fire on activity in the tomoda-labs/tomoda GitHub repo and push Docker images to Artifact Registry.

Code: infrastructure/gcp/cloudbuild.tf, infrastructure/gcp/cloudbuild_iam.tf. Pipeline definitions (cloudbuild-backend.yaml, cloudbuild-frontend.yaml) live in the tomoda repo, not here — see docs/infrastructure/ in that repo.

Trigger matrix

Trigger Fires on Target repo Approval
backend-push-trigger Push to main, paths backend/** or cloudbuild-backend.yaml tomoda-dev-repo Auto
frontend-push-trigger Push to main, paths frontend/** or cloudbuild-frontend.yaml tomoda-dev-repo Auto
backend-release-trigger Tag matching ^v[0-9]+\.[0-9]+\.[0-9]+$ tomoda-prod-repo Auto (tag)
frontend-release-trigger Tag matching ^v[0-9]+\.[0-9]+\.[0-9]+$ tomoda-prod-repo Auto (tag)

Backend + web release together on one vX.Y.Z tag — both triggers fire on it, so a single version ships the whole server-delivered app. The native mobile app versions on its own cadence (its marketing version in app.config.js, plus OTA), not this tag; see the app repo's docs/frontend/native/release.md. A merge to main auto-deploys backend + web to dev.

Push to main
auto
tomoda-dev-repo
Push tag v*
auto
tomoda-prod-repo
Two trigger styles: push → dev (automatic), tag → prod (the tag is the gate). One v* tag ships backend + web together; native versions separately.

Tag-based prod = single source of truth

The only way to get an image into tomoda-prod-repo is to push a vX.Y.Z tag to main (backend + web build together). There is no manual docker push, no per-environment branch, and no release/* branch — the tag itself is the release artifact.

Substitutions

Triggers pass environment-specific values into the YAML via substitutions. Both YAML files live in the tomoda repo; this Terraform code only sets the variables.

Common to all four triggers

Substitution Source
_REGION var.region (always us-central1)
_REPO_NAME dev triggers → tomoda-dev-repo; release triggers → tomoda-prod-repo

Dev push triggers add

Substitution Value
TAG_NAME latest (push triggers re-tag latest on every successful build)

Frontend triggers add

Substitution Dev value Prod value
_WS_URL wss://api-dev.tomoda.life/ws wss://api.tomoda.life/ws
_FRONTEND_API_URL https://api-dev.tomoda.life/api/v1 https://api.tomoda.life/api/v1
_GOOGLE_WEB_CLIENT_ID dev web client prod web client
_GOOGLE_IOS_CLIENT_ID shared iOS client shared iOS client
_GOOGLE_ANDROID_CLIENT_ID shared Android client shared Android client

One mobile OAuth client per platform

iOS/Android reuse a single OAuth client across dev + prod — an iOS client is keyed on the bundle id (com.tomoda.app) and Android on package + signing SHA-1, so one client per platform serves both. Add the prod release signing SHA-1 to the Android client before shipping prod Android.

Service account & IAM

All four triggers use a single dedicated SA: cloudbuild-worker-sa@${project_id}.iam.gserviceaccount.com. The default Cloud Build SA (287267207777@cloudbuild.gserviceaccount.com) is granted roles/iam.serviceAccountUser on it so the platform can impersonate it when running the build.

Role What it enables
roles/logging.logWriter Builds can stream logs to Cloud Logging
roles/artifactregistry.writer Push image layers to both dev and prod repos
roles/container.developer Reserved for future post-build kubectl steps (not currently used)

The roles/container.developer binding is forward-looking — today, deployment is done via Argo CD pulling from Argo CD Image Updater watching the registry, not by Cloud Build kubectl-applying. Until we add a kubectl step to one of the YAMLs, that role is unused.

Gating

Dev triggers deploy automatically: a merge to main builds and rolls out to the dev namespace with no approval step, so dev always reflects main. The release gate lives on prod, where a vX.Y.Z tag is the human decision and ships backend + web together. There is no manual approval on any Cloud Build trigger.

Build outputs

Every successful build pushes a Docker image to the configured Artifact Registry repo. From there:

  • Dev: Argo CD Image Updater detects the new :latest digest and patches the dev Argo CD application's image tag.
  • Prod: same flow, but pinned to the semver tag the trigger built.

See Artifact Registry for the repo layout and Argo CD for the GitOps side.

Vulnerability scanning

Artifact Registry's automatic vulnerability scanning is deliberately off (containerscanning.googleapis.com not enabled). It bills $0.26 per image scanned, and at dev's build frequency that runs ~$40/mo — not worth it pre-launch, especially with no one watching the AR findings feed yet.

Scanning instead belongs in the build: add a trivy (or grype) step to cloudbuild-backend.yaml / cloudbuild-frontend.yaml in the tomoda monorepo that fails the build on HIGH/CRITICAL CVEs. It's free, it blocks a bad image before it ever reaches the registry, and it lives next to the code being scanned. This lands with the app-side CI work, not here. Revisit AR's built-in scan only if we later want continuous re-scanning of already-pushed images against newly-disclosed CVEs (which trivy-at-build-time doesn't cover).

When triggers don't fire

Common reasons a push to main doesn't queue a build:

  1. No matching files changed. backend-push-trigger has included_files = ["backend/**", "cloudbuild-backend.yaml"]. A PR that only touches docs/ or frontend/ will not start a backend build.
  2. GitHub App permissions. The Cloud Build GitHub App must remain installed on the tomoda-labs/tomoda repo. Removing it silently disables every trigger.
  3. Wrong tag shape for prod. A prod build only fires for vX.Y.Z; any other tag is ignored.