Cloud Build (CI)¶
Tomoda uses Google Cloud Build to turn a git tag into two container images — one for the backend, one for the frontend — and push them to Artifact Registry. Cloud Build does not deploy. Deployment is a separate ArgoCD-driven step covered in Deployment.
There are two pipeline configs at the repository root:
Each runs independently, triggered by the same git tag.
Pipeline diagram¶
flowchart LR
Tag[git tag v1.2.3<br/>scripts/release.sh] --> Triggers{Cloud Build<br/>triggers}
Triggers --> BE[cloudbuild-backend.yaml]
Triggers --> FE[cloudbuild-frontend.yaml]
BE -->|docker build| BEImg[tomoda-backend:SHA<br/>+ :TAG + :latest]
FE -->|docker build| FEImg[tomoda-frontend:SHA<br/>+ :TAG + :latest]
BEImg --> AR[(Artifact Registry<br/>asia-east1)]
FEImg --> AR
AR -.watched by.-> ArgoCD[ArgoCD<br/>devops/ repo]
ArgoCD --> GKE[GKE]
Backend pipeline (cloudbuild-backend.yaml)¶
Two steps, both gcr.io/cloud-builders/docker:
- Build — runs
docker buildagainstbackend/Dockerfilewith BuildKit enabled. The build uses--cache-fromagainst the:latesttag in Artifact Registry to reuse layers across builds. Two build args are passed through to the binary so it can self-report its version:COMMIT_SHA=${SHORT_SHA}VERSION=${TAG_NAME}
- Push — pushes all three resulting tags (
:${SHORT_SHA},:${TAG_NAME},:latest) to Artifact Registry.
The backend Dockerfile is a multi-stage build: golang:1.25-alpine builder → static binary (CGO_ENABLED=1 for the bundled H3 C sources, -extldflags '-static' against musl) → alpine:3.21 final stage that copies in ca-certificates, tzdata, the binary, and the config*.yaml files. The image runs as the non-root user tomoda (UID 10001) and exposes :8080.
Image path: ${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO_NAME}/tomoda-backend:{SHORT_SHA,TAG_NAME,latest} — in practice asia-east1-docker.pkg.dev/development-485000/tomoda-dev-repo/tomoda-backend:....
Frontend pipeline (cloudbuild-frontend.yaml)¶
Same two-step shape, but with more build args. The frontend Dockerfile is node:20-alpine builder → Nginx nginx:alpine final stage that serves the Expo web export from /usr/share/nginx/html on port 8081 (config in frontend/nginx.conf).
The crucial detail: EXPO_PUBLIC_* env vars are baked into the JS bundle at build time. Cloud Build passes them in as --build-arg:
| Build arg | What it sets |
|---|---|
EXPO_PUBLIC_API_URL |
Production API base URL (e.g. https://api.tomoda.life/api/v1) |
EXPO_PUBLIC_WS_URL |
Production WebSocket URL |
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID |
Google OAuth web client ID |
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID |
Google OAuth iOS client ID |
EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID |
Google OAuth Android client ID |
COMMIT_SHA / VERSION |
Surfaced as EXPO_PUBLIC_COMMIT_SHA / EXPO_PUBLIC_APP_VERSION |
These values come from Cloud Build substitution variables (${_FRONTEND_API_URL} etc.), which are set on the trigger. See Secrets for the security implications — anything prefixed EXPO_PUBLIC_ is shipped to clients.
Caching¶
Both pipelines opt into BuildKit (DOCKER_BUILDKIT=1) and use inline layer caching (BUILDKIT_INLINE_CACHE=1 build arg, --cache-from ...:latest). Logs go to Cloud Logging (logging: CLOUD_LOGGING_ONLY). dynamic_substitutions: true is set so substitution variables can reference each other.
Triggers¶
The release flow is driven by git tags. The convenience script scripts/release.sh:
- Verifies you're on
mainwith a clean working tree. - Prompts for a
vMAJOR.MINOR.PATCHversion. - Bumps
backend/VERSIONandfrontend/VERSION(both, so both Cloud Build triggers fire even if only one side changed). - Commits, tags, and pushes both the commit and the tag.
- Optionally creates a GitHub Release with the commit log as release notes.
The Cloud Build triggers themselves are configured in the GCP Console (or via Terraform in devops/infrastructure/gcp/) to fire on tags matching v*. They're not committed to this repo.
What Cloud Build does NOT do¶
It does not update any Kubernetes manifests, run kubectl apply, or touch the cluster. Image promotion to GKE is handled by ArgoCD watching the devops/ repo. See Deployment for the next step in the chain.