Skip to content

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:

  1. Build — runs docker build against backend/Dockerfile with BuildKit enabled. The build uses --cache-from against the :latest tag 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}
  2. 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:

  1. Verifies you're on main with a clean working tree.
  2. Prompts for a vMAJOR.MINOR.PATCH version.
  3. Bumps backend/VERSION and frontend/VERSION (both, so both Cloud Build triggers fire even if only one side changed).
  4. Commits, tags, and pushes both the commit and the tag.
  5. 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.