# --- Backend Foundation (Cloud Run + Workers) --- BACKEND_REGION ?= us-central1 BACKEND_ARTIFACT_REPO ?= krow-backend BACKEND_CORE_SERVICE_NAME ?= krow-core-api BACKEND_COMMAND_SERVICE_NAME ?= krow-command-api BACKEND_RUNTIME_SA_NAME ?= krow-backend-runtime BACKEND_RUNTIME_SA_EMAIL := $(BACKEND_RUNTIME_SA_NAME)@$(GCP_PROJECT_ID).iam.gserviceaccount.com BACKEND_CORE_DIR ?= backend/core-api BACKEND_COMMAND_DIR ?= backend/command-api BACKEND_WORKERS_DIR ?= backend/cloud-functions BACKEND_DEV_PUBLIC_BUCKET ?= krow-workforce-dev-public BACKEND_DEV_PRIVATE_BUCKET ?= krow-workforce-dev-private BACKEND_STAGING_PUBLIC_BUCKET ?= krow-workforce-staging-public BACKEND_STAGING_PRIVATE_BUCKET ?= krow-workforce-staging-private ifeq ($(ENV),staging) BACKEND_PUBLIC_BUCKET := $(BACKEND_STAGING_PUBLIC_BUCKET) BACKEND_PRIVATE_BUCKET := $(BACKEND_STAGING_PRIVATE_BUCKET) BACKEND_RUN_AUTH_FLAG := --no-allow-unauthenticated else BACKEND_PUBLIC_BUCKET := $(BACKEND_DEV_PUBLIC_BUCKET) BACKEND_PRIVATE_BUCKET := $(BACKEND_DEV_PRIVATE_BUCKET) BACKEND_RUN_AUTH_FLAG := --allow-unauthenticated endif BACKEND_CORE_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_ARTIFACT_REPO)/core-api:latest BACKEND_COMMAND_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_ARTIFACT_REPO)/command-api:latest BACKEND_LOG_LIMIT ?= 100 BACKEND_LLM_MODEL ?= gemini-2.0-flash-001 BACKEND_VERIFICATION_ATTIRE_MODEL ?= gemini-2.0-flash-lite-001 BACKEND_VERIFICATION_PROVIDER_TIMEOUT_MS ?= 8000 BACKEND_MAX_SIGNED_URL_SECONDS ?= 900 BACKEND_LLM_RATE_LIMIT_PER_MINUTE ?= 20 BACKEND_V2_ARTIFACT_REPO ?= krow-backend-v2 BACKEND_V2_CORE_SERVICE_NAME ?= krow-core-api-v2 BACKEND_V2_COMMAND_SERVICE_NAME ?= krow-command-api-v2 BACKEND_V2_QUERY_SERVICE_NAME ?= krow-query-api-v2 BACKEND_V2_UNIFIED_SERVICE_NAME ?= krow-api-v2 BACKEND_V2_NOTIFICATION_JOB_NAME ?= krow-notification-dispatcher-v2 BACKEND_V2_NOTIFICATION_WORKER_SERVICE_NAME ?= krow-notification-worker-v2 BACKEND_V2_NOTIFICATION_SCHEDULER_JOB_NAME ?= krow-notification-dispatch-v2 BACKEND_V2_RUNTIME_SA_NAME ?= krow-backend-v2-runtime BACKEND_V2_RUNTIME_SA_EMAIL := $(BACKEND_V2_RUNTIME_SA_NAME)@$(GCP_PROJECT_ID).iam.gserviceaccount.com BACKEND_V2_SCHEDULER_SA_NAME ?= krow-backend-v2-scheduler BACKEND_V2_SCHEDULER_SA_EMAIL := $(BACKEND_V2_SCHEDULER_SA_NAME)@$(GCP_PROJECT_ID).iam.gserviceaccount.com BACKEND_V2_CORE_DIR ?= backend/core-api BACKEND_V2_COMMAND_DIR ?= backend/command-api BACKEND_V2_QUERY_DIR ?= backend/query-api BACKEND_V2_UNIFIED_DIR ?= backend/unified-api BACKEND_V2_SQL_INSTANCE ?= krow-sql-v2 BACKEND_V2_SQL_DATABASE ?= krow_v2_db BACKEND_V2_SQL_APP_USER ?= krow_v2_app BACKEND_V2_SQL_PASSWORD_SECRET ?= krow-v2-sql-app-password BACKEND_V2_SQL_CONNECTION_NAME ?= $(GCP_PROJECT_ID):$(BACKEND_REGION):$(BACKEND_V2_SQL_INSTANCE) BACKEND_V2_SQL_TIER ?= $(SQL_TIER) BACKEND_V2_DEV_PUBLIC_BUCKET ?= krow-workforce-dev-v2-public BACKEND_V2_DEV_PRIVATE_BUCKET ?= krow-workforce-dev-v2-private BACKEND_V2_STAGING_PUBLIC_BUCKET ?= krow-workforce-staging-v2-public BACKEND_V2_STAGING_PRIVATE_BUCKET ?= krow-workforce-staging-v2-private ifeq ($(ENV),staging) BACKEND_V2_PUBLIC_BUCKET := $(BACKEND_V2_STAGING_PUBLIC_BUCKET) BACKEND_V2_PRIVATE_BUCKET := $(BACKEND_V2_STAGING_PRIVATE_BUCKET) BACKEND_V2_RUN_AUTH_FLAG := --no-allow-unauthenticated else BACKEND_V2_PUBLIC_BUCKET := $(BACKEND_V2_DEV_PUBLIC_BUCKET) BACKEND_V2_PRIVATE_BUCKET := $(BACKEND_V2_DEV_PRIVATE_BUCKET) BACKEND_V2_RUN_AUTH_FLAG := --allow-unauthenticated endif BACKEND_V2_CORE_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_V2_ARTIFACT_REPO)/core-api-v2:latest BACKEND_V2_COMMAND_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_V2_ARTIFACT_REPO)/command-api-v2:latest BACKEND_V2_QUERY_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_V2_ARTIFACT_REPO)/query-api-v2:latest BACKEND_V2_UNIFIED_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_V2_ARTIFACT_REPO)/unified-api-v2:latest BACKEND_V2_FIREBASE_WEB_API_KEY_SECRET ?= firebase-web-api-key BACKEND_V2_NOTIFICATION_BATCH_LIMIT ?= 50 BACKEND_V2_PUSH_DELIVERY_MODE ?= live BACKEND_V2_SHIFT_REMINDERS_ENABLED ?= true BACKEND_V2_SHIFT_REMINDER_LEAD_MINUTES ?= 60,15 BACKEND_V2_SHIFT_REMINDER_WINDOW_MINUTES ?= 5 BACKEND_V2_NOTIFICATION_SCHEDULE ?= * * * * * BACKEND_V2_NOTIFICATION_SCHEDULER_TIME_ZONE ?= UTC BACKEND_V2_NFC_ENFORCE_PROOF_NONCE ?= false BACKEND_V2_NFC_ENFORCE_DEVICE_ID ?= false BACKEND_V2_NFC_ENFORCE_ATTESTATION ?= false BACKEND_V2_NFC_PROOF_MAX_AGE_SECONDS ?= 120 .PHONY: backend-help backend-enable-apis backend-bootstrap-dev backend-migrate-idempotency backend-deploy-core backend-deploy-commands backend-deploy-workers backend-smoke-core backend-smoke-commands backend-logs-core backend-bootstrap-v2-dev backend-deploy-core-v2 backend-deploy-commands-v2 backend-deploy-query-v2 backend-deploy-unified-v2 backend-deploy-notification-worker-v2 backend-configure-notification-scheduler-v2 backend-run-notification-worker-v2 backend-smoke-notification-worker-v2 backend-deploy-notification-job-v2 backend-run-notification-job-v2 backend-smoke-core-v2 backend-smoke-commands-v2 backend-smoke-query-v2 backend-smoke-unified-v2 backend-logs-core-v2 backend-v2-migrate-idempotency backend-v2-migrate-schema backend-help: @echo "--> Backend Foundation Commands" @echo " make backend-enable-apis [ENV=dev] Enable Cloud Run/Functions/Build/Secret APIs" @echo " make backend-bootstrap-dev Bootstrap artifact repo, runtime SA, and buckets" @echo " make backend-migrate-idempotency Create/upgrade idempotency table in Cloud SQL" @echo " make backend-deploy-core [ENV=dev] Build + deploy core API service" @echo " make backend-deploy-commands [ENV=dev] Build + deploy command API service" @echo " make backend-deploy-workers [ENV=dev] Deploy worker scaffold" @echo " make backend-smoke-core [ENV=dev] Smoke test core /health" @echo " make backend-smoke-commands [ENV=dev] Smoke test commands /health" @echo " make backend-logs-core [ENV=dev] Read core service logs" @echo "" @echo "--> Backend Foundation Commands (isolated v2 stack)" @echo " make backend-bootstrap-v2-dev [ENV=dev] Bootstrap isolated v2 resources and SQL instance" @echo " make backend-deploy-core-v2 [ENV=dev] Build + deploy core API v2 service" @echo " make backend-deploy-commands-v2 [ENV=dev] Build + deploy command API v2 service" @echo " make backend-deploy-query-v2 [ENV=dev] Build + deploy query API v2 service" @echo " make backend-deploy-unified-v2 [ENV=dev] Build + deploy unified API v2 gateway" @echo " make backend-deploy-notification-worker-v2 Deploy private notification worker v2 service" @echo " make backend-configure-notification-scheduler-v2 Configure Cloud Scheduler for notification worker" @echo " make backend-run-notification-worker-v2 Invoke notification worker v2 once" @echo " make backend-smoke-notification-worker-v2 Smoke test private notification worker v2" @echo " make backend-v2-migrate-schema Apply v2 domain schema against krow-sql-v2" @echo " make backend-v2-migrate-idempotency Apply command idempotency migration against v2 DB" @echo " make backend-smoke-core-v2 [ENV=dev] Smoke test core API v2 /health" @echo " make backend-smoke-commands-v2 [ENV=dev] Smoke test command API v2 /health" @echo " make backend-smoke-query-v2 [ENV=dev] Smoke test query API v2 /health" @echo " make backend-smoke-unified-v2 [ENV=dev] Smoke test unified API v2 /health" @echo " make backend-logs-core-v2 [ENV=dev] Read core API v2 logs" backend-enable-apis: @echo "--> Enabling backend APIs on project [$(GCP_PROJECT_ID)]..." @for api in \ run.googleapis.com \ cloudbuild.googleapis.com \ artifactregistry.googleapis.com \ secretmanager.googleapis.com \ cloudfunctions.googleapis.com \ eventarc.googleapis.com \ aiplatform.googleapis.com \ storage.googleapis.com \ iam.googleapis.com \ iamcredentials.googleapis.com \ serviceusage.googleapis.com \ firebase.googleapis.com \ cloudscheduler.googleapis.com; do \ echo " - $$api"; \ gcloud services enable $$api --project=$(GCP_PROJECT_ID); \ done @echo "✅ Backend APIs enabled." backend-bootstrap-dev: backend-enable-apis @echo "--> Bootstrapping backend foundation for [$(ENV)] on project [$(GCP_PROJECT_ID)]..." @echo "--> Ensuring Artifact Registry repo [$(BACKEND_ARTIFACT_REPO)] exists..." @if ! gcloud artifacts repositories describe $(BACKEND_ARTIFACT_REPO) --location=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ gcloud artifacts repositories create $(BACKEND_ARTIFACT_REPO) \ --repository-format=docker \ --location=$(BACKEND_REGION) \ --description="KROW backend services" \ --project=$(GCP_PROJECT_ID); \ else \ echo " - Artifact Registry repo already exists."; \ fi @echo "--> Ensuring runtime service account [$(BACKEND_RUNTIME_SA_NAME)] exists..." @if ! gcloud iam service-accounts describe $(BACKEND_RUNTIME_SA_EMAIL) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ gcloud iam service-accounts create $(BACKEND_RUNTIME_SA_NAME) \ --display-name="KROW Backend Runtime" \ --project=$(GCP_PROJECT_ID); \ else \ echo " - Runtime service account already exists."; \ fi @echo "--> Ensuring runtime service account IAM roles..." @gcloud projects add-iam-policy-binding $(GCP_PROJECT_ID) \ --member="serviceAccount:$(BACKEND_RUNTIME_SA_EMAIL)" \ --role="roles/storage.objectAdmin" \ --quiet >/dev/null @gcloud projects add-iam-policy-binding $(GCP_PROJECT_ID) \ --member="serviceAccount:$(BACKEND_RUNTIME_SA_EMAIL)" \ --role="roles/aiplatform.user" \ --quiet >/dev/null @gcloud iam service-accounts add-iam-policy-binding $(BACKEND_RUNTIME_SA_EMAIL) \ --member="serviceAccount:$(BACKEND_RUNTIME_SA_EMAIL)" \ --role="roles/iam.serviceAccountTokenCreator" \ --project=$(GCP_PROJECT_ID) \ --quiet >/dev/null @echo "--> Ensuring storage buckets exist..." @if ! gcloud storage buckets describe gs://$(BACKEND_PUBLIC_BUCKET) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ gcloud storage buckets create gs://$(BACKEND_PUBLIC_BUCKET) --location=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID); \ else \ echo " - Public bucket already exists: $(BACKEND_PUBLIC_BUCKET)"; \ fi @if ! gcloud storage buckets describe gs://$(BACKEND_PRIVATE_BUCKET) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ gcloud storage buckets create gs://$(BACKEND_PRIVATE_BUCKET) --location=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID); \ else \ echo " - Private bucket already exists: $(BACKEND_PRIVATE_BUCKET)"; \ fi @echo "✅ Backend foundation bootstrap complete for [$(ENV)]." backend-migrate-idempotency: @echo "--> Applying idempotency table migration..." @test -n "$(IDEMPOTENCY_DATABASE_URL)" || (echo "❌ IDEMPOTENCY_DATABASE_URL is required" && exit 1) @cd $(BACKEND_COMMAND_DIR) && IDEMPOTENCY_DATABASE_URL="$(IDEMPOTENCY_DATABASE_URL)" npm run migrate:idempotency @echo "✅ Idempotency migration applied." backend-deploy-core: @echo "--> Deploying core backend service [$(BACKEND_CORE_SERVICE_NAME)] to [$(ENV)]..." @test -d $(BACKEND_CORE_DIR) || (echo "❌ Missing directory: $(BACKEND_CORE_DIR)" && exit 1) @test -f $(BACKEND_CORE_DIR)/Dockerfile || (echo "❌ Missing Dockerfile: $(BACKEND_CORE_DIR)/Dockerfile" && exit 1) @gcloud builds submit $(BACKEND_CORE_DIR) --tag $(BACKEND_CORE_IMAGE) --project=$(GCP_PROJECT_ID) @gcloud run deploy $(BACKEND_CORE_SERVICE_NAME) \ --image=$(BACKEND_CORE_IMAGE) \ --region=$(BACKEND_REGION) \ --project=$(GCP_PROJECT_ID) \ --service-account=$(BACKEND_RUNTIME_SA_EMAIL) \ --set-env-vars=APP_ENV=$(ENV),GCP_PROJECT_ID=$(GCP_PROJECT_ID),PUBLIC_BUCKET=$(BACKEND_PUBLIC_BUCKET),PRIVATE_BUCKET=$(BACKEND_PRIVATE_BUCKET),UPLOAD_MOCK=false,SIGNED_URL_MOCK=false,LLM_MOCK=false,LLM_LOCATION=$(BACKEND_REGION),LLM_MODEL=$(BACKEND_LLM_MODEL),LLM_TIMEOUT_MS=20000,MAX_SIGNED_URL_SECONDS=$(BACKEND_MAX_SIGNED_URL_SECONDS),LLM_RATE_LIMIT_PER_MINUTE=$(BACKEND_LLM_RATE_LIMIT_PER_MINUTE),VERIFICATION_ACCESS_MODE=authenticated,VERIFICATION_REQUIRE_FILE_EXISTS=true,VERIFICATION_ATTIRE_PROVIDER=vertex,VERIFICATION_ATTIRE_MODEL=$(BACKEND_VERIFICATION_ATTIRE_MODEL),VERIFICATION_PROVIDER_TIMEOUT_MS=$(BACKEND_VERIFICATION_PROVIDER_TIMEOUT_MS) \ $(BACKEND_RUN_AUTH_FLAG) @echo "✅ Core backend service deployed." backend-deploy-commands: @echo "--> Deploying command backend service [$(BACKEND_COMMAND_SERVICE_NAME)] to [$(ENV)]..." @test -d $(BACKEND_COMMAND_DIR) || (echo "❌ Missing directory: $(BACKEND_COMMAND_DIR)" && exit 1) @test -f $(BACKEND_COMMAND_DIR)/Dockerfile || (echo "❌ Missing Dockerfile: $(BACKEND_COMMAND_DIR)/Dockerfile" && exit 1) @gcloud builds submit $(BACKEND_COMMAND_DIR) --tag $(BACKEND_COMMAND_IMAGE) --project=$(GCP_PROJECT_ID) @gcloud run deploy $(BACKEND_COMMAND_SERVICE_NAME) \ --image=$(BACKEND_COMMAND_IMAGE) \ --region=$(BACKEND_REGION) \ --project=$(GCP_PROJECT_ID) \ --service-account=$(BACKEND_RUNTIME_SA_EMAIL) \ --set-env-vars=APP_ENV=$(ENV),GCP_PROJECT_ID=$(GCP_PROJECT_ID),PUBLIC_BUCKET=$(BACKEND_PUBLIC_BUCKET),PRIVATE_BUCKET=$(BACKEND_PRIVATE_BUCKET) \ $(BACKEND_RUN_AUTH_FLAG) @echo "✅ Command backend service deployed." backend-deploy-workers: @echo "--> Deploying worker scaffold for [$(ENV)]..." @if [ ! -d "$(BACKEND_WORKERS_DIR)" ]; then \ echo "❌ Missing directory: $(BACKEND_WORKERS_DIR)"; \ exit 1; \ fi @if [ -z "$$(find $(BACKEND_WORKERS_DIR) -mindepth 1 ! -name '.keep' -print -quit)" ]; then \ echo "⚠️ No worker code found in $(BACKEND_WORKERS_DIR). Skipping deployment."; \ exit 0; \ fi @echo "⚠️ Worker deployment is scaffold-only for now." @echo " Add concrete worker deployment commands once worker code is introduced." backend-smoke-core: @echo "--> Running core smoke check..." @URL=$$(gcloud run services describe $(BACKEND_CORE_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ if [ -z "$$URL" ]; then \ echo "❌ Could not resolve URL for service $(BACKEND_CORE_SERVICE_NAME)"; \ exit 1; \ fi; \ TOKEN=$$(gcloud auth print-identity-token); \ curl -fsS -H "Authorization: Bearer $$TOKEN" "$$URL/health" >/dev/null && echo "✅ Core smoke check passed: $$URL/health" backend-smoke-commands: @echo "--> Running commands smoke check..." @URL=$$(gcloud run services describe $(BACKEND_COMMAND_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ if [ -z "$$URL" ]; then \ echo "❌ Could not resolve URL for service $(BACKEND_COMMAND_SERVICE_NAME)"; \ exit 1; \ fi; \ TOKEN=$$(gcloud auth print-identity-token); \ curl -fsS -H "Authorization: Bearer $$TOKEN" "$$URL/health" >/dev/null && echo "✅ Commands smoke check passed: $$URL/health" backend-logs-core: @echo "--> Reading logs for core backend service [$(BACKEND_CORE_SERVICE_NAME)]..." @gcloud run services logs read $(BACKEND_CORE_SERVICE_NAME) \ --region=$(BACKEND_REGION) \ --project=$(GCP_PROJECT_ID) \ --limit=$(BACKEND_LOG_LIMIT) backend-bootstrap-v2-dev: backend-enable-apis @echo "--> Bootstrapping isolated backend v2 foundation for [$(ENV)] on project [$(GCP_PROJECT_ID)]..." @echo "--> Ensuring Artifact Registry repo [$(BACKEND_V2_ARTIFACT_REPO)] exists..." @if ! gcloud artifacts repositories describe $(BACKEND_V2_ARTIFACT_REPO) --location=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ gcloud artifacts repositories create $(BACKEND_V2_ARTIFACT_REPO) \ --repository-format=docker \ --location=$(BACKEND_REGION) \ --description="KROW backend v2 services" \ --project=$(GCP_PROJECT_ID); \ else \ echo " - Artifact Registry repo already exists."; \ fi @echo "--> Ensuring v2 runtime service account [$(BACKEND_V2_RUNTIME_SA_NAME)] exists..." @if ! gcloud iam service-accounts describe $(BACKEND_V2_RUNTIME_SA_EMAIL) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ gcloud iam service-accounts create $(BACKEND_V2_RUNTIME_SA_NAME) \ --display-name="KROW Backend Runtime V2" \ --project=$(GCP_PROJECT_ID); \ else \ echo " - Runtime service account already exists."; \ fi @echo "--> Ensuring v2 scheduler service account [$(BACKEND_V2_SCHEDULER_SA_NAME)] exists..." @if ! gcloud iam service-accounts describe $(BACKEND_V2_SCHEDULER_SA_EMAIL) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ gcloud iam service-accounts create $(BACKEND_V2_SCHEDULER_SA_NAME) \ --display-name="KROW Backend Scheduler V2" \ --project=$(GCP_PROJECT_ID); \ else \ echo " - Scheduler service account already exists."; \ fi @echo "--> Ensuring v2 runtime service account IAM roles..." @gcloud projects add-iam-policy-binding $(GCP_PROJECT_ID) \ --member="serviceAccount:$(BACKEND_V2_RUNTIME_SA_EMAIL)" \ --role="roles/storage.objectAdmin" \ --quiet >/dev/null @gcloud projects add-iam-policy-binding $(GCP_PROJECT_ID) \ --member="serviceAccount:$(BACKEND_V2_RUNTIME_SA_EMAIL)" \ --role="roles/aiplatform.user" \ --quiet >/dev/null @gcloud projects add-iam-policy-binding $(GCP_PROJECT_ID) \ --member="serviceAccount:$(BACKEND_V2_RUNTIME_SA_EMAIL)" \ --role="roles/cloudsql.client" \ --quiet >/dev/null @gcloud projects add-iam-policy-binding $(GCP_PROJECT_ID) \ --member="serviceAccount:$(BACKEND_V2_RUNTIME_SA_EMAIL)" \ --role="roles/secretmanager.secretAccessor" \ --quiet >/dev/null @gcloud projects add-iam-policy-binding $(GCP_PROJECT_ID) \ --member="serviceAccount:$(BACKEND_V2_RUNTIME_SA_EMAIL)" \ --role="roles/firebasecloudmessaging.admin" \ --quiet >/dev/null @gcloud iam service-accounts add-iam-policy-binding $(BACKEND_V2_RUNTIME_SA_EMAIL) \ --member="serviceAccount:$(BACKEND_V2_RUNTIME_SA_EMAIL)" \ --role="roles/iam.serviceAccountTokenCreator" \ --project=$(GCP_PROJECT_ID) \ --quiet >/dev/null @echo "--> Ensuring v2 storage buckets exist..." @if ! gcloud storage buckets describe gs://$(BACKEND_V2_PUBLIC_BUCKET) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ gcloud storage buckets create gs://$(BACKEND_V2_PUBLIC_BUCKET) --location=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID); \ else \ echo " - Public bucket already exists: $(BACKEND_V2_PUBLIC_BUCKET)"; \ fi @if ! gcloud storage buckets describe gs://$(BACKEND_V2_PRIVATE_BUCKET) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ gcloud storage buckets create gs://$(BACKEND_V2_PRIVATE_BUCKET) --location=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID); \ else \ echo " - Private bucket already exists: $(BACKEND_V2_PRIVATE_BUCKET)"; \ fi @echo "--> Ensuring v2 Cloud SQL instance [$(BACKEND_V2_SQL_INSTANCE)] exists..." @if ! gcloud sql instances describe $(BACKEND_V2_SQL_INSTANCE) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ gcloud sql instances create $(BACKEND_V2_SQL_INSTANCE) \ --database-version=POSTGRES_15 \ --tier=$(BACKEND_V2_SQL_TIER) \ --region=$(BACKEND_REGION) \ --storage-size=10 \ --storage-auto-increase \ --availability-type=zonal \ --backup-start-time=03:00 \ --project=$(GCP_PROJECT_ID); \ else \ echo " - Cloud SQL instance already exists: $(BACKEND_V2_SQL_INSTANCE)"; \ fi @echo "--> Ensuring v2 Cloud SQL database [$(BACKEND_V2_SQL_DATABASE)] exists..." @if ! gcloud sql databases describe $(BACKEND_V2_SQL_DATABASE) --instance=$(BACKEND_V2_SQL_INSTANCE) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ gcloud sql databases create $(BACKEND_V2_SQL_DATABASE) --instance=$(BACKEND_V2_SQL_INSTANCE) --project=$(GCP_PROJECT_ID); \ else \ echo " - Cloud SQL database already exists: $(BACKEND_V2_SQL_DATABASE)"; \ fi @echo "--> Ensuring v2 SQL application password secret [$(BACKEND_V2_SQL_PASSWORD_SECRET)] exists..." @if ! gcloud secrets describe $(BACKEND_V2_SQL_PASSWORD_SECRET) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ PASSWORD=$$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 32); \ printf "%s" "$$PASSWORD" | gcloud secrets create $(BACKEND_V2_SQL_PASSWORD_SECRET) \ --replication-policy=automatic \ --data-file=- \ --project=$(GCP_PROJECT_ID); \ else \ echo " - Secret already exists: $(BACKEND_V2_SQL_PASSWORD_SECRET)"; \ fi @echo "--> Ensuring v2 SQL application user [$(BACKEND_V2_SQL_APP_USER)] exists and matches the current secret..." @DB_PASSWORD=$$(gcloud secrets versions access latest --secret=$(BACKEND_V2_SQL_PASSWORD_SECRET) --project=$(GCP_PROJECT_ID)); \ if gcloud sql users list --instance=$(BACKEND_V2_SQL_INSTANCE) --project=$(GCP_PROJECT_ID) --format='value(name)' | grep -qx "$(BACKEND_V2_SQL_APP_USER)"; then \ gcloud sql users set-password $(BACKEND_V2_SQL_APP_USER) \ --instance=$(BACKEND_V2_SQL_INSTANCE) \ --password="$$DB_PASSWORD" \ --project=$(GCP_PROJECT_ID) >/dev/null; \ else \ gcloud sql users create $(BACKEND_V2_SQL_APP_USER) \ --instance=$(BACKEND_V2_SQL_INSTANCE) \ --password="$$DB_PASSWORD" \ --project=$(GCP_PROJECT_ID) >/dev/null; \ fi @echo "✅ Backend v2 foundation bootstrap complete for [$(ENV)]." backend-deploy-core-v2: @echo "--> Deploying core backend v2 service [$(BACKEND_V2_CORE_SERVICE_NAME)] to [$(ENV)]..." @test -d $(BACKEND_V2_CORE_DIR) || (echo "❌ Missing directory: $(BACKEND_V2_CORE_DIR)" && exit 1) @test -f $(BACKEND_V2_CORE_DIR)/Dockerfile || (echo "❌ Missing Dockerfile: $(BACKEND_V2_CORE_DIR)/Dockerfile" && exit 1) @gcloud builds submit $(BACKEND_V2_CORE_DIR) --tag $(BACKEND_V2_CORE_IMAGE) --project=$(GCP_PROJECT_ID) @EXTRA_ENV="APP_ENV=$(ENV),APP_STACK=v2,GCP_PROJECT_ID=$(GCP_PROJECT_ID),PUBLIC_BUCKET=$(BACKEND_V2_PUBLIC_BUCKET),PRIVATE_BUCKET=$(BACKEND_V2_PRIVATE_BUCKET),UPLOAD_MOCK=false,SIGNED_URL_MOCK=false,LLM_MOCK=false,LLM_LOCATION=$(BACKEND_REGION),LLM_MODEL=$(BACKEND_LLM_MODEL),LLM_TIMEOUT_MS=20000,MAX_SIGNED_URL_SECONDS=$(BACKEND_MAX_SIGNED_URL_SECONDS),LLM_RATE_LIMIT_PER_MINUTE=$(BACKEND_LLM_RATE_LIMIT_PER_MINUTE),VERIFICATION_STORE=sql,VERIFICATION_ACCESS_MODE=authenticated,VERIFICATION_REQUIRE_FILE_EXISTS=true,VERIFICATION_ATTIRE_PROVIDER=vertex,VERIFICATION_ATTIRE_MODEL=$(BACKEND_VERIFICATION_ATTIRE_MODEL),VERIFICATION_PROVIDER_TIMEOUT_MS=$(BACKEND_VERIFICATION_PROVIDER_TIMEOUT_MS),INSTANCE_CONNECTION_NAME=$(BACKEND_V2_SQL_CONNECTION_NAME),DB_NAME=$(BACKEND_V2_SQL_DATABASE),DB_USER=$(BACKEND_V2_SQL_APP_USER)"; \ gcloud run deploy $(BACKEND_V2_CORE_SERVICE_NAME) \ --image=$(BACKEND_V2_CORE_IMAGE) \ --region=$(BACKEND_REGION) \ --project=$(GCP_PROJECT_ID) \ --service-account=$(BACKEND_V2_RUNTIME_SA_EMAIL) \ --set-env-vars=$$EXTRA_ENV \ --set-secrets=DB_PASSWORD=$(BACKEND_V2_SQL_PASSWORD_SECRET):latest \ --set-cloudsql-instances=$(BACKEND_V2_SQL_CONNECTION_NAME) \ $(BACKEND_V2_RUN_AUTH_FLAG) @echo "✅ Core backend v2 service deployed." backend-deploy-commands-v2: @echo "--> Deploying command backend v2 service [$(BACKEND_V2_COMMAND_SERVICE_NAME)] to [$(ENV)]..." @test -d $(BACKEND_V2_COMMAND_DIR) || (echo "❌ Missing directory: $(BACKEND_V2_COMMAND_DIR)" && exit 1) @test -f $(BACKEND_V2_COMMAND_DIR)/Dockerfile || (echo "❌ Missing Dockerfile: $(BACKEND_V2_COMMAND_DIR)/Dockerfile" && exit 1) @gcloud builds submit $(BACKEND_V2_COMMAND_DIR) --tag $(BACKEND_V2_COMMAND_IMAGE) --project=$(GCP_PROJECT_ID) @EXTRA_ENV="APP_ENV=$(ENV),APP_STACK=v2,GCP_PROJECT_ID=$(GCP_PROJECT_ID),PUBLIC_BUCKET=$(BACKEND_V2_PUBLIC_BUCKET),PRIVATE_BUCKET=$(BACKEND_V2_PRIVATE_BUCKET),IDEMPOTENCY_STORE=sql,INSTANCE_CONNECTION_NAME=$(BACKEND_V2_SQL_CONNECTION_NAME),DB_NAME=$(BACKEND_V2_SQL_DATABASE),DB_USER=$(BACKEND_V2_SQL_APP_USER),NOTIFICATION_BATCH_LIMIT=$(BACKEND_V2_NOTIFICATION_BATCH_LIMIT),PUSH_DELIVERY_MODE=$(BACKEND_V2_PUSH_DELIVERY_MODE),SHIFT_REMINDERS_ENABLED=$(BACKEND_V2_SHIFT_REMINDERS_ENABLED),SHIFT_REMINDER_WINDOW_MINUTES=$(BACKEND_V2_SHIFT_REMINDER_WINDOW_MINUTES),NFC_ENFORCE_PROOF_NONCE=$(BACKEND_V2_NFC_ENFORCE_PROOF_NONCE),NFC_ENFORCE_DEVICE_ID=$(BACKEND_V2_NFC_ENFORCE_DEVICE_ID),NFC_ENFORCE_ATTESTATION=$(BACKEND_V2_NFC_ENFORCE_ATTESTATION),NFC_PROOF_MAX_AGE_SECONDS=$(BACKEND_V2_NFC_PROOF_MAX_AGE_SECONDS)"; \ gcloud run deploy $(BACKEND_V2_COMMAND_SERVICE_NAME) \ --image=$(BACKEND_V2_COMMAND_IMAGE) \ --region=$(BACKEND_REGION) \ --project=$(GCP_PROJECT_ID) \ --service-account=$(BACKEND_V2_RUNTIME_SA_EMAIL) \ --set-env-vars=$$EXTRA_ENV \ --set-secrets=DB_PASSWORD=$(BACKEND_V2_SQL_PASSWORD_SECRET):latest \ --set-cloudsql-instances=$(BACKEND_V2_SQL_CONNECTION_NAME) \ $(BACKEND_V2_RUN_AUTH_FLAG) @echo "✅ Command backend v2 service deployed." backend-deploy-notification-worker-v2: @echo "--> Deploying private notification worker v2 service [$(BACKEND_V2_NOTIFICATION_WORKER_SERVICE_NAME)]..." @test -d $(BACKEND_V2_COMMAND_DIR) || (echo "❌ Missing directory: $(BACKEND_V2_COMMAND_DIR)" && exit 1) @test -f $(BACKEND_V2_COMMAND_DIR)/Dockerfile || (echo "❌ Missing Dockerfile: $(BACKEND_V2_COMMAND_DIR)/Dockerfile" && exit 1) @gcloud builds submit $(BACKEND_V2_COMMAND_DIR) --tag $(BACKEND_V2_COMMAND_IMAGE) --project=$(GCP_PROJECT_ID) @EXTRA_ENV="APP_ENV=$(ENV),APP_STACK=v2,GCP_PROJECT_ID=$(GCP_PROJECT_ID),PUBLIC_BUCKET=$(BACKEND_V2_PUBLIC_BUCKET),PRIVATE_BUCKET=$(BACKEND_V2_PRIVATE_BUCKET),INSTANCE_CONNECTION_NAME=$(BACKEND_V2_SQL_CONNECTION_NAME),DB_NAME=$(BACKEND_V2_SQL_DATABASE),DB_USER=$(BACKEND_V2_SQL_APP_USER),NOTIFICATION_BATCH_LIMIT=$(BACKEND_V2_NOTIFICATION_BATCH_LIMIT),PUSH_DELIVERY_MODE=$(BACKEND_V2_PUSH_DELIVERY_MODE),SHIFT_REMINDERS_ENABLED=$(BACKEND_V2_SHIFT_REMINDERS_ENABLED),SHIFT_REMINDER_WINDOW_MINUTES=$(BACKEND_V2_SHIFT_REMINDER_WINDOW_MINUTES),NFC_ENFORCE_PROOF_NONCE=$(BACKEND_V2_NFC_ENFORCE_PROOF_NONCE),NFC_ENFORCE_DEVICE_ID=$(BACKEND_V2_NFC_ENFORCE_DEVICE_ID),NFC_ENFORCE_ATTESTATION=$(BACKEND_V2_NFC_ENFORCE_ATTESTATION),NFC_PROOF_MAX_AGE_SECONDS=$(BACKEND_V2_NFC_PROOF_MAX_AGE_SECONDS)"; \ gcloud run deploy $(BACKEND_V2_NOTIFICATION_WORKER_SERVICE_NAME) \ --image=$(BACKEND_V2_COMMAND_IMAGE) \ --region=$(BACKEND_REGION) \ --project=$(GCP_PROJECT_ID) \ --service-account=$(BACKEND_V2_RUNTIME_SA_EMAIL) \ --command=node \ --args=src/worker-server.js \ --set-env-vars=$$EXTRA_ENV \ --set-secrets=DB_PASSWORD=$(BACKEND_V2_SQL_PASSWORD_SECRET):latest \ --set-cloudsql-instances=$(BACKEND_V2_SQL_CONNECTION_NAME) \ --concurrency=1 \ --max-instances=1 \ --no-allow-unauthenticated @if ! gcloud iam service-accounts describe $(BACKEND_V2_SCHEDULER_SA_EMAIL) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ gcloud iam service-accounts create $(BACKEND_V2_SCHEDULER_SA_NAME) \ --display-name="KROW Backend Scheduler V2" \ --project=$(GCP_PROJECT_ID); \ fi @gcloud run services add-iam-policy-binding $(BACKEND_V2_NOTIFICATION_WORKER_SERVICE_NAME) \ --region=$(BACKEND_REGION) \ --project=$(GCP_PROJECT_ID) \ --member="serviceAccount:$(BACKEND_V2_SCHEDULER_SA_EMAIL)" \ --role="roles/run.invoker" \ --quiet >/dev/null @echo "✅ Notification worker v2 service deployed." backend-configure-notification-scheduler-v2: @echo "--> Configuring notification scheduler [$(BACKEND_V2_NOTIFICATION_SCHEDULER_JOB_NAME)]..." @gcloud services enable cloudscheduler.googleapis.com --project=$(GCP_PROJECT_ID) >/dev/null @URL=$$(gcloud run services describe $(BACKEND_V2_NOTIFICATION_WORKER_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ if [ -z "$$URL" ]; then \ echo "❌ Could not resolve URL for service $(BACKEND_V2_NOTIFICATION_WORKER_SERVICE_NAME)"; \ exit 1; \ fi; \ if gcloud scheduler jobs describe $(BACKEND_V2_NOTIFICATION_SCHEDULER_JOB_NAME) --location=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ gcloud scheduler jobs update http $(BACKEND_V2_NOTIFICATION_SCHEDULER_JOB_NAME) \ --location=$(BACKEND_REGION) \ --project=$(GCP_PROJECT_ID) \ --schedule='$(BACKEND_V2_NOTIFICATION_SCHEDULE)' \ --time-zone='$(BACKEND_V2_NOTIFICATION_SCHEDULER_TIME_ZONE)' \ --uri="$$URL/tasks/dispatch-notifications" \ --http-method=POST \ --update-headers=Content-Type=application/json \ --message-body='{}' \ --oidc-service-account-email=$(BACKEND_V2_SCHEDULER_SA_EMAIL) \ --oidc-token-audience="$$URL"; \ else \ gcloud scheduler jobs create http $(BACKEND_V2_NOTIFICATION_SCHEDULER_JOB_NAME) \ --location=$(BACKEND_REGION) \ --project=$(GCP_PROJECT_ID) \ --schedule='$(BACKEND_V2_NOTIFICATION_SCHEDULE)' \ --time-zone='$(BACKEND_V2_NOTIFICATION_SCHEDULER_TIME_ZONE)' \ --uri="$$URL/tasks/dispatch-notifications" \ --http-method=POST \ --headers=Content-Type=application/json \ --message-body='{}' \ --oidc-service-account-email=$(BACKEND_V2_SCHEDULER_SA_EMAIL) \ --oidc-token-audience="$$URL"; \ fi @echo "✅ Notification scheduler configured." backend-smoke-notification-worker-v2: @echo "--> Running notification worker smoke check..." @URL=$$(gcloud run services describe $(BACKEND_V2_NOTIFICATION_WORKER_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ if [ -z "$$URL" ]; then \ echo "❌ Could not resolve URL for service $(BACKEND_V2_NOTIFICATION_WORKER_SERVICE_NAME)"; \ exit 1; \ fi; \ gcloud scheduler jobs describe $(BACKEND_V2_NOTIFICATION_SCHEDULER_JOB_NAME) --location=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) >/dev/null && \ echo "✅ Notification worker smoke check passed: $$URL (scheduler wired)" backend-run-notification-worker-v2: @echo "--> Triggering notification worker via scheduler job [$(BACKEND_V2_NOTIFICATION_SCHEDULER_JOB_NAME)]..." @gcloud scheduler jobs run $(BACKEND_V2_NOTIFICATION_SCHEDULER_JOB_NAME) \ --location=$(BACKEND_REGION) \ --project=$(GCP_PROJECT_ID) >/dev/null @echo "✅ Notification worker v2 invocation requested through Cloud Scheduler." backend-deploy-notification-job-v2: backend-deploy-notification-worker-v2 @echo "⚠️ Cloud Run Job dispatcher is deprecated. Using private worker service instead." backend-run-notification-job-v2: backend-run-notification-worker-v2 @echo "⚠️ Cloud Run Job dispatcher is deprecated. Using private worker service instead." backend-deploy-query-v2: @echo "--> Deploying query backend v2 service [$(BACKEND_V2_QUERY_SERVICE_NAME)] to [$(ENV)]..." @test -d $(BACKEND_V2_QUERY_DIR) || (echo "❌ Missing directory: $(BACKEND_V2_QUERY_DIR)" && exit 1) @test -f $(BACKEND_V2_QUERY_DIR)/Dockerfile || (echo "❌ Missing Dockerfile: $(BACKEND_V2_QUERY_DIR)/Dockerfile" && exit 1) @gcloud builds submit $(BACKEND_V2_QUERY_DIR) --tag $(BACKEND_V2_QUERY_IMAGE) --project=$(GCP_PROJECT_ID) @gcloud run deploy $(BACKEND_V2_QUERY_SERVICE_NAME) \ --image=$(BACKEND_V2_QUERY_IMAGE) \ --region=$(BACKEND_REGION) \ --project=$(GCP_PROJECT_ID) \ --service-account=$(BACKEND_V2_RUNTIME_SA_EMAIL) \ --set-env-vars=APP_ENV=$(ENV),APP_STACK=v2,GCP_PROJECT_ID=$(GCP_PROJECT_ID),INSTANCE_CONNECTION_NAME=$(BACKEND_V2_SQL_CONNECTION_NAME),DB_NAME=$(BACKEND_V2_SQL_DATABASE),DB_USER=$(BACKEND_V2_SQL_APP_USER) \ --set-secrets=DB_PASSWORD=$(BACKEND_V2_SQL_PASSWORD_SECRET):latest \ --add-cloudsql-instances=$(BACKEND_V2_SQL_CONNECTION_NAME) \ $(BACKEND_V2_RUN_AUTH_FLAG) @echo "✅ Query backend v2 service deployed." backend-deploy-unified-v2: @echo "--> Deploying unified backend v2 gateway [$(BACKEND_V2_UNIFIED_SERVICE_NAME)] to [$(ENV)]..." @test -d $(BACKEND_V2_UNIFIED_DIR) || (echo "❌ Missing directory: $(BACKEND_V2_UNIFIED_DIR)" && exit 1) @test -f $(BACKEND_V2_UNIFIED_DIR)/Dockerfile || (echo "❌ Missing Dockerfile: $(BACKEND_V2_UNIFIED_DIR)/Dockerfile" && exit 1) @if ! gcloud secrets describe $(BACKEND_V2_FIREBASE_WEB_API_KEY_SECRET) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ echo "❌ Missing secret: $(BACKEND_V2_FIREBASE_WEB_API_KEY_SECRET)"; \ exit 1; \ fi @CORE_URL=$$(gcloud run services describe $(BACKEND_V2_CORE_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ COMMAND_URL=$$(gcloud run services describe $(BACKEND_V2_COMMAND_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ QUERY_URL=$$(gcloud run services describe $(BACKEND_V2_QUERY_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ if [ -z "$$CORE_URL" ] || [ -z "$$COMMAND_URL" ] || [ -z "$$QUERY_URL" ]; then \ echo "❌ Core, command, and query v2 services must be deployed before unified gateway"; \ exit 1; \ fi; \ gcloud builds submit $(BACKEND_V2_UNIFIED_DIR) --tag $(BACKEND_V2_UNIFIED_IMAGE) --project=$(GCP_PROJECT_ID); \ gcloud run deploy $(BACKEND_V2_UNIFIED_SERVICE_NAME) \ --image=$(BACKEND_V2_UNIFIED_IMAGE) \ --region=$(BACKEND_REGION) \ --project=$(GCP_PROJECT_ID) \ --service-account=$(BACKEND_V2_RUNTIME_SA_EMAIL) \ --set-env-vars=APP_ENV=$(ENV),APP_STACK=v2,GCP_PROJECT_ID=$(GCP_PROJECT_ID),INSTANCE_CONNECTION_NAME=$(BACKEND_V2_SQL_CONNECTION_NAME),DB_NAME=$(BACKEND_V2_SQL_DATABASE),DB_USER=$(BACKEND_V2_SQL_APP_USER),CORE_API_BASE_URL=$$CORE_URL,COMMAND_API_BASE_URL=$$COMMAND_URL,QUERY_API_BASE_URL=$$QUERY_URL \ --set-secrets=DB_PASSWORD=$(BACKEND_V2_SQL_PASSWORD_SECRET):latest,FIREBASE_WEB_API_KEY=$(BACKEND_V2_FIREBASE_WEB_API_KEY_SECRET):latest \ --add-cloudsql-instances=$(BACKEND_V2_SQL_CONNECTION_NAME) \ $(BACKEND_V2_RUN_AUTH_FLAG) @echo "✅ Unified backend v2 gateway deployed." backend-v2-migrate-idempotency: @echo "--> Applying idempotency table migration for command API v2..." @test -n "$(IDEMPOTENCY_DATABASE_URL)$(DATABASE_URL)" || (echo "❌ IDEMPOTENCY_DATABASE_URL or DATABASE_URL is required" && exit 1) @cd $(BACKEND_V2_COMMAND_DIR) && IDEMPOTENCY_DATABASE_URL="$(IDEMPOTENCY_DATABASE_URL)" DATABASE_URL="$(DATABASE_URL)" npm run migrate:idempotency @echo "✅ Idempotency migration applied for command API v2." backend-v2-migrate-schema: @echo "--> Applying v2 domain schema migration..." @test -n "$(DATABASE_URL)" || (echo "❌ DATABASE_URL is required" && exit 1) @cd $(BACKEND_V2_COMMAND_DIR) && DATABASE_URL="$(DATABASE_URL)" npm run migrate:v2-schema @echo "✅ V2 domain schema migration applied." backend-smoke-core-v2: @echo "--> Running core v2 smoke check..." @URL=$$(gcloud run services describe $(BACKEND_V2_CORE_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ if [ -z "$$URL" ]; then \ echo "❌ Could not resolve URL for service $(BACKEND_V2_CORE_SERVICE_NAME)"; \ exit 1; \ fi; \ TOKEN=$$(gcloud auth print-identity-token); \ curl -fsS -H "Authorization: Bearer $$TOKEN" "$$URL/readyz" >/dev/null && echo "✅ Core v2 smoke check passed: $$URL/readyz" backend-smoke-commands-v2: @echo "--> Running command v2 smoke check..." @URL=$$(gcloud run services describe $(BACKEND_V2_COMMAND_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ if [ -z "$$URL" ]; then \ echo "❌ Could not resolve URL for service $(BACKEND_V2_COMMAND_SERVICE_NAME)"; \ exit 1; \ fi; \ TOKEN=$$(gcloud auth print-identity-token); \ curl -fsS -H "Authorization: Bearer $$TOKEN" "$$URL/readyz" >/dev/null && echo "✅ Command v2 smoke check passed: $$URL/readyz" backend-smoke-query-v2: @echo "--> Running query v2 smoke check..." @URL=$$(gcloud run services describe $(BACKEND_V2_QUERY_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ if [ -z "$$URL" ]; then \ echo "❌ Could not resolve URL for service $(BACKEND_V2_QUERY_SERVICE_NAME)"; \ exit 1; \ fi; \ TOKEN=$$(gcloud auth print-identity-token); \ curl -fsS -H "Authorization: Bearer $$TOKEN" "$$URL/readyz" >/dev/null && echo "✅ Query v2 smoke check passed: $$URL/readyz" backend-smoke-unified-v2: @echo "--> Running unified v2 smoke check..." @URL=$$(gcloud run services describe $(BACKEND_V2_UNIFIED_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ if [ -z "$$URL" ]; then \ echo "❌ Could not resolve URL for service $(BACKEND_V2_UNIFIED_SERVICE_NAME)"; \ exit 1; \ fi; \ TOKEN=$$(gcloud auth print-identity-token); \ curl -fsS -H "Authorization: Bearer $$TOKEN" "$$URL/readyz" >/dev/null && echo "✅ Unified v2 smoke check passed: $$URL/readyz" backend-logs-core-v2: @echo "--> Reading logs for core backend v2 service [$(BACKEND_V2_CORE_SERVICE_NAME)]..." @gcloud run services logs read $(BACKEND_V2_CORE_SERVICE_NAME) \ --region=$(BACKEND_REGION) \ --project=$(GCP_PROJECT_ID) \ --limit=$(BACKEND_LOG_LIMIT)