Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
36 changes: 14 additions & 22 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,28 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# --- Build stage -------------------------------------------------------------
# The full python:3.13 image carries the compilers/headers that a dependency
# without a prebuilt cp313 wheel would need. Install everything into a
# relocatable prefix (/install) that the slim runtime can drop in as-is, so the
# build can never fail for lack of a compiler on the slim base.
FROM python:3.13@sha256:e72bfff2ccf413e3c329074d643fac616d7e1dfe85ac57e527f1d13cd8e0ee6c AS builder

ENV PYTHONUNBUFFERED=1

COPY requirements.txt .
RUN pip install --no-cache-dir --require-hashes --prefix=/install -r requirements.txt

# --- Runtime stage -----------------------------------------------------------
# python:3.13-slim is ~850 MB smaller than the full image: faster to push to the
# registry and faster to cold-start. It carries only ffmpeg, the dependencies
# built above, and the app — no compilers or build cruft.
# --- Single-stage slim image -------------------------------------------------
# python:3.13-slim is ~850 MB smaller than the full python:3.13 image: faster to
# pull, build, push to Artifact Registry, and cold-start on Cloud Run. Every
# package in requirements.txt publishes a prebuilt cp313 manylinux wheel
# (--only-binary=:all:), so no C compiler or multi-stage /install copy is needed.
FROM python:3.13-slim@sha256:c33f0bc4364a6881bed1ec0cc2665e6c53c87a43e774aaeab88e6f17af105e4f

ENV PYTHONUNBUFFERED=1

# ffmpeg is required by the worker's video actions (combine/convert). One layer,
# no recommended extras, apt lists dropped to keep the image small.
# ffmpeg + ffprobe are required by the worker's video actions (combine/convert).
# Installed from official Debian packages with --no-install-recommends and apt
# lists dropped in the same layer.
RUN apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg \
&& rm -rf /var/lib/apt/lists/*

# Install locked Python dependencies from prebuilt binary wheels directly into
# /usr/local so the entire dependency layer is cached in the final pushed image.
COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir --only-binary=:all: --require-hashes -r /tmp/requirements.txt \
&& rm -f /tmp/requirements.txt

# Run as a non-root user with a real home, and give it a writable app dir it
# owns. The worker's video actions write temp files using bare relative names
# into the process CWD (== WORKDIR), so WORKDIR must be owned by this user.
Expand All @@ -47,10 +43,6 @@ RUN useradd --create-home --uid 10001 --shell /usr/sbin/nologin appuser \

WORKDIR /app

# Drop in the dependencies built in the full image (same python 3.13, so the
# installed packages and gunicorn entry point land on /usr/local and PATH).
# Left root-owned and world-readable — import/exec only need read access.
COPY --from=builder /install /usr/local

# Runtime files only (not the whole repo): explicit copies keep docs, examples,
# tests, deploy scripts, and .git out of the image. Root-owned but world-readable
Expand Down
256 changes: 145 additions & 111 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 @@ -650,6 +678,9 @@ echo "Granting the runtime SA self-impersonation (signBlob + Cloud Tasks OIDC)..
add_sa_iam_binding "${RUNTIME_SA}" "serviceAccount:${RUNTIME_SA}" "roles/iam.serviceAccountTokenCreator" "$PROJECT"
add_sa_iam_binding "${RUNTIME_SA}" "serviceAccount:${RUNTIME_SA}" "roles/iam.serviceAccountUser" "$PROJECT"

# --- Non-IAM runtime infrastructure + Firestore seed (overlapped with Cloud Build) ---
INFRA_SETUP_LOG=$(mktemp)
(
# --- Cloud Tasks queues -------------------------------------------------------
phase "Setting up Cloud Tasks queues..."
QUEUES=("Other" "Gemini" "Veo")
Expand Down Expand Up @@ -764,6 +795,74 @@ else
gcloud firestore databases describe --database="$FIRESTORE_DB_UI" --project=$PROJECT --format="value(locationId)"
fi

# --- SceneMachineUser custom role -----------------------------------------------
phase "Ensuring SceneMachineUser custom role matches user-role.yaml..."
if ! gcloud iam roles describe SceneMachineUser --project=$PROJECT &> /dev/null; then
echo "SceneMachineUser role doesn't exist. Creating it..."
gcloud iam roles create SceneMachineUser --project=$PROJECT --file=./user-role.yaml
else
echo "SceneMachineUser role exists. Syncing it to user-role.yaml..."
gcloud iam roles update SceneMachineUser --project=$PROJECT --file=./user-role.yaml --quiet || true
fi

# --- Seed Firestore config (front-door topology) --------------------------------
phase "Adding default Scene Machine configurations to Firestore..."
ADC_TOKEN=$(gcloud auth application-default print-access-token)
CONFIG_SEED_STATUS=$(curl -s -X PATCH \
"https://firestore.googleapis.com/v1/projects/${PROJECT}/databases/${FIRESTORE_DB_UI}/documents/config/global" \
-H "Authorization: Bearer ${ADC_TOKEN}" \
-H "x-goog-user-project: ${PROJECT}" \
-H "Content-Type: application/json" \
-o /dev/null -w '%{http_code}' \
-d @<(envsubst < ./firestore_config_frontdoor.template.json))
if [ "$CONFIG_SEED_STATUS" != "200" ]; then
echo "ERROR: seeding the UI config (config/global) failed (HTTP ${CONFIG_SEED_STATUS:-no response})." >&2
echo " The app's backend wiring was not written; aborting." >&2
exit 1
fi

MODELS_SEED_STATUS=$(python3 scripts/seed_config_models.py convert < ui/definitions/models.json | curl -s -X PATCH \
"https://firestore.googleapis.com/v1/projects/${PROJECT}/databases/${FIRESTORE_DB_UI}/documents/config/models" \
-H "Authorization: Bearer ${ADC_TOKEN}" \
-H "x-goog-user-project: ${PROJECT}" \
-H "Content-Type: application/json" \
-o /dev/null -w '%{http_code}' \
-d @-)
if [ "$MODELS_SEED_STATUS" != "200" ]; then
echo "ERROR: seeding the model catalog (config/models) failed (HTTP ${MODELS_SEED_STATUS:-no response})." >&2
echo " The runtime model catalog was not written; aborting." >&2
exit 1
fi

if ! ANNOUNCEMENT_SEED_STATUS=$(GOOGLE_CLOUD_PROJECT="$PROJECT" \
GOOGLE_OAUTH_ACCESS_TOKEN="${ADC_TOKEN}" \
python3 scripts/seed_announcement.py seed \
"https://firestore.googleapis.com/v1/projects/${PROJECT}/databases/${FIRESTORE_DB_UI}/documents/config?documentId=announcement" \
"$ANNOUNCEMENT_MARKDOWN_FILE" "$ANNOUNCEMENT_ENABLED"); then
echo "ERROR: seeding homepage announcement failed." >&2
echo " Existing operator content was not overwritten; aborting." >&2
exit 1
fi

for template in creative_templates/*.json; do
[ -e "$template" ] || continue
template_name=$(basename "$template" .json)

TEMPLATE_SEED_STATUS=$(curl -s -X PATCH \
"https://firestore.googleapis.com/v1/projects/${PROJECT}/databases/${FIRESTORE_DB_UI}/documents/creativeTemplates/${template_name}" \
-H "Authorization: Bearer ${ADC_TOKEN}" \
-H "x-goog-user-project: ${PROJECT}" \
-H "Content-Type: application/json" \
-o /dev/null -w '%{http_code}' \
-d @"$template")
if [ "$TEMPLATE_SEED_STATUS" != "200" ]; then
echo "ERROR: seeding creative template '${template_name}' failed (HTTP ${TEMPLATE_SEED_STATUS:-no response})." >&2
exit 1
fi
done
) >"$INFRA_SETUP_LOG" 2>&1 &
INFRA_SETUP_PID=$!

# --- Render UI env + config (must precede the single image build) -------------
# Order matters: these artifacts are baked into the image (Dockerfile
# `COPY . .`), so they must exist before `gcloud builds submit`.
Expand Down Expand Up @@ -817,12 +916,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 All @@ -833,9 +933,11 @@ COMMIT_DATE=$(git log -1 --format=%cI 2>/dev/null || echo "unknown")
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
echo "${GIT_BRANCH}/${COMMIT_DATE}" > deployed_version.txt
sync
BUILD_MACHINE_ARGS=()
if ! gcloud artifacts repositories describe "${ARTIFACT_REPO}" --project=$PROJECT --location="$REGION" &> /dev/null; then
echo "Creating artifact repository: $ARTIFACT_REPO"
gcloud artifacts repositories create "${ARTIFACT_REPO}" --repository-format=docker --project=$PROJECT --location="$REGION"
BUILD_MACHINE_ARGS=(--machine-type=e2-highcpu-8)
fi
# NOTE: the repo's .gcloudignore excludes ui/* but re-includes ui/dist/ and
# ui/remix-engine-status-viewer/ — both are LOAD-BEARING for this build: the
Expand All @@ -858,15 +960,27 @@ echo " (this step is quiet — the build runs remotely; a heartbeat prints belo
BUILD_SUBS="_IMAGE=${IMAGE}"
if [ "$NO_BUILD_CACHE" = "1" ]; then
BUILD_SUBS="${BUILD_SUBS},_USE_CACHE=0"
BUILD_MACHINE_ARGS=(--machine-type=e2-highcpu-8)
echo " Docker layer cache: OFF (--no-build-cache; forcing a cold rebuild)."
else
echo " Docker layer cache: ON (reuses unchanged layers from the previous image)."
fi
run_with_heartbeat "Cloud Build" \
gcloud builds submit . --config=cloudbuild.yaml --substitutions="$BUILD_SUBS" \
--project=$PROJECT --region=$REGION
"${BUILD_MACHINE_ARGS[@]}" --project=$PROJECT --region=$REGION

phase "Completing overlapped infrastructure & Firestore setup..."
if ! wait "$INFRA_SETUP_PID"; then
cat "$INFRA_SETUP_LOG" >&2
rm -f "$INFRA_SETUP_LOG"
exit 1
fi
cat "$INFRA_SETUP_LOG"
rm -f "$INFRA_SETUP_LOG"

# --- 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 +993,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 +1049,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 All @@ -957,92 +1077,6 @@ export UI_CORS_ORIGINS
envsubst < ./gcs-cors-config.template.json > ./gcs-cors-config.json
gcloud storage buckets update gs://$GCS_BUCKET --cors-file=./gcs-cors-config.json --project=$PROJECT

# --- SceneMachineUser custom role -----------------------------------------------
phase "Ensuring SceneMachineUser custom role matches user-role.yaml..."
if ! gcloud iam roles describe SceneMachineUser --project=$PROJECT &> /dev/null; then
echo "SceneMachineUser role doesn't exist. Creating it..."
gcloud iam roles create SceneMachineUser --project=$PROJECT --file=./user-role.yaml
else
# Update (not skip) so an edited user-role.yaml — e.g. the slimmed
# IAP-access-only permission set — actually takes effect on a project where the
# role already exists, instead of being silently ignored. '|| true' tolerates
# the benign "no changes to apply" case on a re-deploy; the role keeps its
# IAP-access permission regardless, so user admission is never at risk here.
echo "SceneMachineUser role exists. Syncing it to user-role.yaml..."
gcloud iam roles update SceneMachineUser --project=$PROJECT --file=./user-role.yaml --quiet || true
fi

# --- Seed Firestore config (front-door topology) --------------------------------
# Uses firestore_config_frontdoor.template.json: the UI Firestore config doc with
# the backend base fixed to the same-origin '/api' and no API key. Owner-
# credential REST writes bypass the (deliberately read-only) config rules — by
# design.
phase "Adding default Scene Machine configurations to Firestore..."
# Capture the HTTP status (as the other REST calls in this script do): a non-200
# here means the UI's same-origin '/api' wiring was NOT written, so fail loudly
# instead of reporting a successful deploy with a broken app config.
CONFIG_SEED_STATUS=$(curl -s -X PATCH \
"https://firestore.googleapis.com/v1/projects/${PROJECT}/databases/${FIRESTORE_DB_UI}/documents/config/global" \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "x-goog-user-project: ${PROJECT}" \
-H "Content-Type: application/json" \
-o /dev/null -w '%{http_code}' \
-d @<(envsubst < ./firestore_config_frontdoor.template.json))
if [ "$CONFIG_SEED_STATUS" != "200" ]; then
echo "ERROR: seeding the UI config (config/global) failed (HTTP ${CONFIG_SEED_STATUS:-no response})." >&2
echo " The app's backend wiring was not written; aborting." >&2
exit 1
fi

# The model catalog: config/models is overwritten from the repo file on every
# deploy. Operators may edit the live doc between deploys; the pre-flight
# preview above showed what this write replaces. Same fail-loudly contract as
# the config/global seed.
MODELS_SEED_STATUS=$(python3 scripts/seed_config_models.py convert < ui/definitions/models.json | curl -s -X PATCH \
"https://firestore.googleapis.com/v1/projects/${PROJECT}/databases/${FIRESTORE_DB_UI}/documents/config/models" \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "x-goog-user-project: ${PROJECT}" \
-H "Content-Type: application/json" \
-o /dev/null -w '%{http_code}' \
-d @-)
if [ "$MODELS_SEED_STATUS" != "200" ]; then
echo "ERROR: seeding the model catalog (config/models) failed (HTTP ${MODELS_SEED_STATUS:-no response})." >&2
echo " The runtime model catalog was not written; aborting." >&2
exit 1
fi

# The announcement is operator-authored after the first deploy. Firestore's
# create operation makes the initial seed race-safe and returns 409 when an
# operator document already exists; either result is a successful deploy.
if ! ANNOUNCEMENT_SEED_STATUS=$(GOOGLE_CLOUD_PROJECT="$PROJECT" \
GOOGLE_OAUTH_ACCESS_TOKEN="$(gcloud auth application-default print-access-token)" \
python3 scripts/seed_announcement.py seed \
"https://firestore.googleapis.com/v1/projects/${PROJECT}/databases/${FIRESTORE_DB_UI}/documents/config?documentId=announcement" \
"$ANNOUNCEMENT_MARKDOWN_FILE" "$ANNOUNCEMENT_ENABLED"); then
echo "ERROR: seeding homepage announcement failed." >&2
echo " Existing operator content was not overwritten; aborting." >&2
exit 1
fi

for template in creative_templates/*.json; do
# Skip cleanly if the directory is empty/absent: without 'nullglob' the glob
# would otherwise stay literal and run the body once on a non-existent file.
[ -e "$template" ] || continue
template_name=$(basename "$template" .json)

TEMPLATE_SEED_STATUS=$(curl -s -X PATCH \
"https://firestore.googleapis.com/v1/projects/${PROJECT}/databases/${FIRESTORE_DB_UI}/documents/creativeTemplates/${template_name}" \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H "x-goog-user-project: ${PROJECT}" \
-H "Content-Type: application/json" \
-o /dev/null -w '%{http_code}' \
-d @"$template")
if [ "$TEMPLATE_SEED_STATUS" != "200" ]; then
echo "ERROR: seeding creative template '${template_name}' failed (HTTP ${TEMPLATE_SEED_STATUS:-no response})." >&2
exit 1
fi
done

# --- Automated provisioning complete: timing checkpoint ----------------------------
close_phase
PROVISIONING_END=$(date +%s)
Expand Down
Loading
Loading