Skip to content

Build Pipeline Runbook

git push β†’ trigger filter β†’ cloudbuild-node.yaml | cloudbuild-workers.yaml β†’ node-builder (Node only) β†’ webapp/internal build β†’ push β†’ Cloud Run deploy
File path patternTrigger fileCloud Build config
apps/webapp/**erp-webapp-stagingcloudbuild-node.yaml
apps/internal/**erp-internal-stagingcloudbuild-node.yaml
apps/temporal-worker/**erp-workers-stagingcloudbuild-workers.yaml
apps/dagster/**erp-dagster-stagingcloudbuild-workers.yaml
images/node-builder/**erp-webapp-stagingcloudbuild-node.yaml
cloudbuild-*.yamlerp-all-staging-manualcloudbuild-all.yaml
  • pnpm-lock.yaml, pnpm-workspace.yaml, package.json, any workspace package.json β†’ node-builder install layer invalidated
  • apps/webapp/** (excluding package.json) β†’ webapp build layer invalidated, but install layer cached
  • apps/internal/** β†’ internal build invalidated, install cached
  • pyproject.toml, uv.lock, apps/temporal-worker/pyproject.toml, apps/dagster/pyproject.toml β†’ respective Python uv sync invalidated
  • Python source changes β†’ only final source-copy layer invalidates, uv sync cached
ServiceCold buildWarm buildNotes
Node Builder8-12 min2-4 minpnpm install + Docker build
Webapp (cold)12-18 min4-8 minIncludes node-builder if cache miss
Webapp (warm)4-6 min2-3 minUses cached node-builder
Internal (cold)10-15 min3-6 minIncludes node-builder if cache miss
Internal (warm)3-5 min2-3 minUses cached node-builder
Temporal Worker6-10 min2-4 minuv sync + Docker build
Dagster8-12 min3-6 minuv sync + Docker build
  • Look at ::cache target: echo lines for which cache tag was tried
  • Look for CACHED markers in BuildKit output (--progress=plain makes them visible)
  • Check Artifact Registry for whether the expected cache tag exists
  • Common causes: lockfile changed (cold install), Artifact Registry GC removed the tag, content hash differs from expected
  • CLI: gcloud builds submit --config=cloudbuild-all.yaml --region=us-east1 --substitutions=BRANCH_NAME=staging,COMMIT_SHA=$(git rev-parse HEAD) .
  • Console: Cloud Build β†’ Triggers β†’ erp-all-staging-manual β†’ Run

The cloudbuild*.yaml configs pin serviceAccount: cloud-build-runner@.... A build that does NOT name that SA (e.g. a bare --tag build) runs as the default compute SA, which holds no project bindings and 403s reading its own source tarball. See infrastructure/chatwoot/GCP-DEPLOY.md for the --tag invocation.

Step-by-step:

  1. Create apps/<new>/Dockerfile following the multi-stage pattern (consume node-builder if Node, follow temporal-worker pattern if Python)
  2. Add the package.json to pnpm-workspace.yaml (Node) or to root [tool.uv.workspace] (Python)
  3. Run pnpm generate:dockerfile-copies to update images/node-builder/Dockerfile if Node
  4. Decide which cloudbuild-*.yaml it joins (cloudbuild-node.yaml if Node + Cloud Run deploy; cloudbuild-workers.yaml if Python + VM deploy; new file if neither)
  5. Add a new module instance in infrastructure-gcp/terraform/environments/{staging,production}/main.tf with included_files and substitutions
  6. terraform plan && terraform apply
  7. Push to test
SymptomCauseFix
image not found: node-builder:${COMMIT_SHA} in webapp buildnode-builder push failedcheck push-node-builder step logs
cp: cannot stat 'apps/internal/otel-init.cjs'HAS_OTEL not set to false for internal triggercheck trigger substitutions
uv sync re-runs despite no Python deps changeWrong Dockerfile layer order; source COPYed before uv syncfollow apps/temporal-worker/Dockerfile pattern
pnpm install re-runs despite no lockfile changeWorkspace package.json was edited; the COPY block invalidatedexpected; check images/node-builder/Dockerfile
Build fails with β€œAuthentication required”TRIGGER_* secret missing in GCP Secret Managercreate via gcloud secrets create erp-prod-trigger-*

When adding a new build-time env that’s NOT a PUBLIC_* Astro var:

  1. Create the secret in GCP Secret Manager: gcloud secrets create erp-prod-<name>
  2. Add to cloudbuild-node.yaml availableSecrets β†’ secretEnv
  3. In the build step, write to a file in /workspace/_secret_<name>, then pass to docker via --secret id=<name>,src=...
  4. In Dockerfile, consume via RUN --mount=type=secret,id=<name> ...
  5. NEVER use --build-arg for non-PUBLIC values β€” they leak to image history
  • Test a Dockerfile change locally: ./scripts/test-cloudbuild-local.sh <target>
  • Regenerate node-builder COPY block: pnpm generate:dockerfile-copies
  • Verify drift: pnpm check:dockerfile-copies