Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
85ba3d1
Speed up deploy.sh via lockfile-hash npm ci guard, IAM policy cache, …
christophervoelpel Sep 23, 2026
d087d17
Drop IAM cache, use cmp -s lockfile guard, overlap UI build, and surf…
christophervoelpel Sep 23, 2026
0745f18
perf(deploy): use E2_HIGHCPU_8 machineType in cloudbuild.yaml
christophervoelpel Sep 23, 2026
dab530f
perf(deploy): keep cloudbuild.yaml on default pre-warmed worker pool
christophervoelpel Sep 23, 2026
8c4b265
perf(deploy): use e2-highcpu-8 only on cold Cloud Builds and merge main
christophervoelpel Sep 23, 2026
aa64949
perf(deploy): single-stage slim Dockerfile, single-read IAM snapshot,…
christophervoelpel Sep 23, 2026
69279f8
perf(docker): parallel BuildKit stages with mirror.gcr.io, force-unsa…
christophervoelpel Sep 23, 2026
cf4fcb9
perf(deploy): use E2_HIGHCPU_8 on cold builds and pre-warmed worker p…
christophervoelpel Sep 23, 2026
803fa6f
sec(docker): digest-pin uv:0.6.6 and enforce --only-binary :all: with…
christophervoelpel Sep 23, 2026
afc1a58
style(test): remove trailing blank line at EOF in test_deploy_safety.py
christophervoelpel Sep 23, 2026
5907cff
fix(deploy): remove duplicate env.ts/config.json write during backgro…
christophervoelpel Sep 23, 2026
ed61ed6
Address PR #206 review: retry Cloud Build on IAM propagation, add EXI…
christophervoelpel Sep 23, 2026
a9aeb77
Tighten Cloud Build retry regex, use deterministic WORKER_URL across …
christophervoelpel Sep 23, 2026
4cd63be
Fix PR206 rollout ordering and cached deployment safety
christophervoelpel Sep 23, 2026
033808b
Address final PR206 deployment review comments
christophervoelpel Sep 23, 2026
3675f2d
Validate --skip-ui-build early, guard IAM cache on error, and harden …
christophervoelpel Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cloudbuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ images:
- '${_IMAGE}'
options:
logging: CLOUD_LOGGING_ONLY
machineType: 'E2_HIGHCPU_8'
85 changes: 61 additions & 24 deletions deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,34 @@ echo "════════════════════════
# human think-time at the prompt doesn't pollute the timing deliverable.
SCRIPT_START=$(date +%s)

# Render UI env + config and kick off the local Angular UI build in the
# background right away so local CPU work (npm ci / ng build) overlaps with
# cloud API, service-account, IAM, Cloud Tasks, bucket, and Firestore setup.
export UI_CONTROL_PLANE_MODE="iap"
envsubst < ./ui/src/env.template.txt > ./ui/src/env.ts
generate_config
if grep -q "controlPlaneMode: 'none'" ./ui/src/env.ts; then
echo "ERROR: ui/src/env.ts rendered with controlPlaneMode 'none' (sign-in disabled)." >&2
exit 1
fi
UI_BUILD_PID=""
UI_BUILD_LOG=""
if [ "$SKIP_UI_BUILD" != "1" ]; then
UI_BUILD_LOG=$(mktemp)
Comment thread
christophervoelpel marked this conversation as resolved.
(
export NG_CLI_ANALYTICS=ci
if [ -f ui/node_modules/.package-lock.stamp ] \
&& cmp -s ui/package-lock.json ui/node_modules/.package-lock.stamp; then
echo " ✓ ui/package-lock.json unchanged — skipping npm ci."
else
( cd ui && npm ci )
cp ui/package-lock.json ui/node_modules/.package-lock.stamp
fi
( cd ui && npx ng build --configuration production )
) >"$UI_BUILD_LOG" 2>&1 &
Comment thread
christophervoelpel marked this conversation as resolved.
Outdated
UI_BUILD_PID=$!
fi
Comment on lines +493 to +513

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving if [ "$SKIP_UI_BUILD" != "1" ]; then up to SCRIPT_START (lines 493–504) while leaving the SKIP_UI_BUILD = 1 validation ([ ! -d ui/dist ] and grep -rqs 'controlPlaneMode:"none"' ui/dist) at lines 888–902 means that if --skip-ui-build is passed with a missing or local-dev ui/dist, the script runs API enablement and IAM setup, launches INFRA_SETUP_PID in the background at line 879, and then immediately exits at line 892/901—causing cleanup() to send SIGTERM/SIGKILL to INFRA_SETUP_PID mid-flight while it is creating or updating Cloud Tasks queues, GCS buckets, or Firestore databases. Validating ui/dist right here at SCRIPT_START fails fast before any cloud mutations or background jobs begin.

Suggested change
if [ "$SKIP_UI_BUILD" != "1" ]; then
UI_BUILD_LOG=$(mktemp)
# A separate job-control process group lets EXIT cleanup stop npm/ng children.
set -m
(
export NG_CLI_ANALYTICS=ci
( cd ui && npm ci )
( cd ui && npx ng build --configuration production )
) </dev/null >"$UI_BUILD_LOG" 2>&1 &
UI_BUILD_PID=$!
set +m
fi
if [ "$SKIP_UI_BUILD" != "1" ]; then
UI_BUILD_LOG=$(mktemp)
# A separate job-control process group lets EXIT cleanup stop npm/ng children.
set -m
(
export NG_CLI_ANALYTICS=ci
( cd ui && npm ci )
( cd ui && npx ng build --configuration production )
) </dev/null >"$UI_BUILD_LOG" 2>&1 &
UI_BUILD_PID=$!
set +m
elif [ ! -d ui/dist ]; then
echo "ERROR: --skip-ui-build given but ui/dist does not exist." >&2
echo " Run a normal deploy once (or 'cd ui && npx ng build') first." >&2
exit 1
elif grep -rqs 'controlPlaneMode:"none"' ui/dist || grep -rqs "controlPlaneMode:'none'" ui/dist; then
echo "ERROR: the existing ui/dist was built for local dev (controlPlaneMode 'none'," >&2
echo " sign-in disabled). Refusing to deploy it. Drop --skip-ui-build and run a" >&2
echo " normal deploy to rebuild the UI first." >&2
exit 1
fi

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and applied. When --skip-ui-build is passed, deploy.sh now validates ui/dist existence and checks for controlPlaneMode:"none" / controlPlaneMode:'none' right at SCRIPT_START (deploy.sh:504-512), failing fast before any cloud API enablement, IAM changes, or background INFRA_SETUP_PID jobs begin.

Added test_skip_ui_build_fails_fast_and_build_id_failure_clears_log in test/test_deploy_safety.py, which executes the SCRIPT_START block under bash -c (set -euo pipefail) and verifies missing ui/dist and local-dev ui/dist both exit 1 immediately. Mutation-tested against pre-fix deploy.sh (fails 1/1, passes with fix).


# --- Enable services ---------------------------------------------------------
# Note: compute.googleapis.com is enabled here so the default Compute Engine
# service account (used for role bindings below) is guaranteed to exist.
Expand Down Expand Up @@ -817,12 +845,13 @@ if [ "$SKIP_UI_BUILD" = "1" ]; then
echo "[skip] Building the Angular UI — skipped (--skip-ui-build); reusing ui/dist."
else
phase "Building the Angular UI (npm ci + ng build)..."
export NG_CLI_ANALYTICS=ci
(
cd ui \
&& npm ci \
&& npx ng build --configuration production
)
if ! wait "$UI_BUILD_PID"; then
cat "$UI_BUILD_LOG" >&2
rm -f "$UI_BUILD_LOG"
exit 1
fi
cat "$UI_BUILD_LOG"
rm -f "$UI_BUILD_LOG"
fi

# --- Version stamp + Artifact Registry + ONE image build -----------------------
Expand Down Expand Up @@ -867,6 +896,8 @@ run_with_heartbeat "Cloud Build" \
--project=$PROJECT --region=$REGION

# --- Cloud Run: worker (private, Cloud-Tasks-invoked) --------------------------
WORKER_DEPLOY_PID=""
WORKER_ERR_FILE=""
if [ "$APP_ONLY" = "1" ]; then
phase "Reusing existing 'worker' Cloud Run service (--app-only)..."
echo "[skip] Deploying 'worker' — skipped (--app-only); reusing the live service."
Expand All @@ -879,27 +910,23 @@ if [ "$APP_ONLY" = "1" ]; then
fi
echo " Reusing worker: ${WORKER_URL}"
else
phase "Deploying 'worker' Cloud Run service (private)..."
# GUNICORN_TIMEOUT just above the worker's 1800s Cloud Run request timeout so
# gunicorn reaps a thread only AFTER Cloud Run has already returned, never
# killing a legitimate long render mid-flight. (D7)
gcloud run deploy worker --image "$IMAGE" --region $REGION --project $PROJECT \
--cpu=8 --memory=16G --timeout=1800 --no-allow-unauthenticated \
--service-account="$RUNTIME_SA" \
--set-env-vars=ROLE=worker,GUNICORN_TIMEOUT=1830
WORKER_URL=$(gcloud run services describe worker --region=$REGION --project=$PROJECT --format='value(status.url)')
echo "✓ Worker deployed: ${WORKER_URL}"

# The only run.invoker grant the runtime SA gets: service-scoped to the
# worker, exactly what the Cloud-Tasks-minted OIDC tokens need to invoke it.
# (There is no project-wide run.invoker, so the app cannot invoke other
# Cloud Run services.)
echo "Granting service-scoped run.invoker on 'worker' to ${RUNTIME_SA}..."
add_run_invoker_binding worker "$REGION" "$PROJECT" "serviceAccount:${RUNTIME_SA}"
EXISTING_WORKER_URL=$(gcloud run services describe worker --region=$REGION --project=$PROJECT --format='value(status.url)' 2>/dev/null || true)
WORKER_URL="${EXISTING_WORKER_URL:-https://worker-${PROJECT_NUMBER}.${REGION}.run.app}"
Comment thread
christophervoelpel marked this conversation as resolved.
Outdated
WORKER_ERR_FILE=$(mktemp)
(
if ! gcloud run deploy worker --image "$IMAGE" --region "$REGION" --project "$PROJECT" \
--cpu=8 --memory=16G --timeout=1800 --no-allow-unauthenticated \
--service-account="$RUNTIME_SA" \
--set-env-vars=ROLE=worker,GUNICORN_TIMEOUT=1830 >/dev/null 2>"$WORKER_ERR_FILE"; then
exit 1
fi
add_run_invoker_binding worker "$REGION" "$PROJECT" "serviceAccount:${RUNTIME_SA}" >/dev/null 2>>"$WORKER_ERR_FILE"
) &
WORKER_DEPLOY_PID=$!
fi

# --- Cloud Run: app (UI + same-origin /api control plane) -----------------------
phase "Deploying 'app' Cloud Run service (AUTH_MODE=${AUTH_MODE})..."
phase "Deploying 'worker' and 'app' Cloud Run services in parallel (AUTH_MODE=${AUTH_MODE})..."
IAP_FLAG_AVAILABLE=true
# IAP front door. The --iap flag (built-in IAP for Cloud Run, GA March 2026) may
# not exist on older gcloud installs — gate it behind a CLI capability check and
Expand Down Expand Up @@ -939,6 +966,16 @@ fi
IAP_SA="service-${PROJECT_NUMBER}@gcp-sa-iap.iam.gserviceaccount.com"
echo "Granting service-scoped run.invoker on 'app' to the IAP service agent..."
add_run_invoker_binding app "$REGION" "$PROJECT" "serviceAccount:${IAP_SA}"
if [ -n "$WORKER_DEPLOY_PID" ]; then
if ! wait "$WORKER_DEPLOY_PID"; then
cat "$WORKER_ERR_FILE" >&2
rm -f "$WORKER_ERR_FILE"
exit 1
fi
rm -f "$WORKER_ERR_FILE"
WORKER_URL=$(gcloud run services describe worker --region=$REGION --project=$PROJECT --format='value(status.url)')
echo "✓ Worker deployed: ${WORKER_URL}"
fi
APP_URL=$(gcloud run services describe app --region=$REGION --project=$PROJECT --format='value(status.url)')
echo "✓ App deployed: ${APP_URL}"
# Nothing was predicted: the image carries only same-origin URLs, so the app and
Expand Down
Loading