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.
mainv*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
:latestdigest 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:
- No matching files changed.
backend-push-triggerhasincluded_files = ["backend/**", "cloudbuild-backend.yaml"]. A PR that only touchesdocs/orfrontend/will not start a backend build. - GitHub App permissions. The Cloud Build GitHub App must remain installed on the
tomoda-labs/tomodarepo. Removing it silently disables every trigger. - Wrong tag shape for prod. A prod build only fires for
vX.Y.Z; any other tag is ignored.