-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdeploy.sh
More file actions
executable file
·261 lines (233 loc) · 10.1 KB
/
Copy pathdeploy.sh
File metadata and controls
executable file
·261 lines (233 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
#!/usr/bin/env bash
# deploy.sh — Automated Cloud Run deployment for Ohmlet
#
# Usage:
# ./deploy.sh Deploy all services
# ./deploy.sh live-bridge Deploy live-bridge only
# ./deploy.sh quiz-engine Deploy quiz-engine only
# ./deploy.sh vision-verifier Deploy vision-verifier only
# ./deploy.sh frontend Build and deploy frontend
#
# Prerequisites:
# - gcloud CLI installed and authenticated
# - Project set: gcloud config set project ohmlet-app
set -euo pipefail
# ── Configuration ──
PROJECT_ID="${GOOGLE_CLOUD_PROJECT:-ohmlet-app}"
REGION="${GOOGLE_CLOUD_REGION:-europe-west1}"
# Service definitions
LIVE_BRIDGE_SERVICE="ohmlet-live-bridge"
LIVE_BRIDGE_SOURCE="backend/live-bridge"
# Least-privilege runtime identity (#46): only datastore.user, aiplatform.user,
# logging.logWriter, secretmanager.secretAccessor — NOT the editor-privileged
# default compute SA. Override per-env if needed.
LIVE_BRIDGE_SA="${OHMLET_LIVE_BRIDGE_SA:-ohmlet-live-bridge@${PROJECT_ID}.iam.gserviceaccount.com}"
LIVE_BRIDGE_ENV="GOOGLE_GENAI_USE_VERTEXAI=TRUE,\
GOOGLE_CLOUD_PROJECT=${PROJECT_ID},\
GOOGLE_CLOUD_LOCATION=${REGION},\
OHMLET_LIVE_MODEL=gemini-live-2.5-flash-native-audio,\
OHMLET_FLASH_MODEL=gemini-2.5-flash,\
OHMLET_PRO_MODEL=gemini-2.5-pro,\
OHMLET_REASONING_MODEL=gemini-2.5-pro"
# Stripe secrets + the metrics token, mounted by reference from Secret Manager
# (same names across test/live; only the secret VERSION changes). Never a value
# in code. OHMLET_METRICS_TOKEN guards /internal/metrics (#35).
LIVE_BRIDGE_SECRETS="STRIPE_SECRET_KEY=ohmlet-stripe-secret:latest,STRIPE_WEBHOOK_SECRET=ohmlet-stripe-webhook:latest,OHMLET_METRICS_TOKEN=ohmlet-metrics-token:latest"
# Non-secret, mode-specific billing config (Stripe price IDs + app URL). Kept in
# a gitignored file because the IDs differ between test and live mode. Each line
# is KEY=VALUE; see backend/live-bridge/.deploy.env.example.
LIVE_BRIDGE_ENV_FILE="${LIVE_BRIDGE_ENV_FILE:-backend/live-bridge/.deploy.env}"
QUIZ_ENGINE_SERVICE="ohmlet-quiz-engine"
QUIZ_ENGINE_SOURCE="backend/quiz-engine"
# Gemini 3.5 Flash (the GA, non-deprecating model; 2.5 retires 2026-10-16) is
# served from the `global` Vertex location, so the genai client must target it.
QUIZ_ENGINE_ENV="GOOGLE_GENAI_USE_VERTEXAI=TRUE,\
GOOGLE_CLOUD_PROJECT=${PROJECT_ID},\
GOOGLE_CLOUD_LOCATION=global"
# Cold starts add seconds to the (latency-critical) drawing assessment. cpu-boost
# is always on; set this to 1 to keep one instance warm and remove cold starts
# entirely (small standing cost). Default 0 to avoid standing spend.
QUIZ_ENGINE_MIN_INSTANCES="${OHMLET_QUIZ_MIN_INSTANCES:-0}"
# Metrics token guarding /internal/metrics (#35), by reference from Secret Manager.
QUIZ_ENGINE_SECRETS="OHMLET_METRICS_TOKEN=ohmlet-metrics-token:latest"
VISION_VERIFIER_SERVICE="ohmlet-vision-verifier"
VISION_VERIFIER_SOURCE="backend/vision-verifier"
# Same Vertex config as quiz-engine: Gemini 3.5 Flash on the `global` location.
VISION_VERIFIER_ENV="GOOGLE_GENAI_USE_VERTEXAI=TRUE,\
GOOGLE_CLOUD_PROJECT=${PROJECT_ID},\
GOOGLE_CLOUD_LOCATION=global"
# Optional least-privilege SA (needs aiplatform.user + logging.logWriter); empty
# falls back to the default compute SA, matching quiz-engine.
VISION_VERIFIER_SA="${OHMLET_VISION_VERIFIER_SA:-}"
# The inventory check runs at the start of a live session, so cold starts hurt.
# cpu-boost is always on; set this to 1 to keep one warm (small standing cost).
VISION_VERIFIER_MIN_INSTANCES="${OHMLET_VISION_MIN_INSTANCES:-0}"
# Metrics token guarding /internal/metrics (#35), by reference from Secret Manager.
VISION_VERIFIER_SECRETS="OHMLET_METRICS_TOKEN=ohmlet-metrics-token:latest"
COMPILER_SERVICE="ohmlet-compiler"
COMPILER_SOURCE="backend/compiler"
COMPILER_ENV="GOOGLE_CLOUD_PROJECT=${PROJECT_ID}"
COMPILER_SA="${OHMLET_COMPILER_SA:-}"
# Kept warm (1): the toolchain image is large, so a cold start is slow; one
# standing instance makes compiles instant. Set OHMLET_COMPILER_MIN_INSTANCES=0 to save cost.
COMPILER_MIN_INSTANCES="${OHMLET_COMPILER_MIN_INSTANCES:-1}"
COMPILER_SECRETS="OHMLET_METRICS_TOKEN=ohmlet-metrics-token:latest"
# Compiling avr-gcc is CPU/RAM-heavy and the image (with the AVR core) is large,
# so give it more headroom and a longer request timeout than the vision services.
COMPILER_EXTRA="--memory=2Gi --cpu=2 --timeout=120 --concurrency=4"
REPORTER_SERVICE="ohmlet-reporter"
REPORTER_SOURCE="backend/reporter"
REPORTER_ENV="GOOGLE_CLOUD_PROJECT=${PROJECT_ID},OHMLET_TWIN_PROVIDER=stability"
REPORTER_SA="${OHMLET_REPORTER_SA:-}"
REPORTER_MIN_INSTANCES="${OHMLET_REPORTER_MIN_INSTANCES:-0}"
# The 3D-provider API key + the metrics token, by reference from Secret Manager
# (never a value in code). Create the secret once: see backend/reporter/README.md.
REPORTER_SECRETS="STABILITY_API_KEY=ohmlet-stability-key:latest,OHMLET_METRICS_TOKEN=ohmlet-metrics-token:latest"
# Image→mesh generation can take tens of seconds; give it a long request timeout
# and low concurrency (each call holds an upstream request open).
REPORTER_EXTRA="--memory=1Gi --cpu=1 --timeout=300 --concurrency=8"
# ── Helpers ──
info() { echo -e "\033[1;34m[deploy]\033[0m $1"; }
ok() { echo -e "\033[1;32m[deploy]\033[0m $1"; }
err() { echo -e "\033[1;31m[deploy]\033[0m $1" >&2; }
check_gcloud() {
if ! command -v gcloud &>/dev/null; then
err "gcloud CLI not found. Install: https://cloud.google.com/sdk/docs/install"
exit 1
fi
local current_project
current_project=$(gcloud config get-value project 2>/dev/null)
if [[ "$current_project" != "$PROJECT_ID" ]]; then
info "Setting project to ${PROJECT_ID}"
gcloud config set project "$PROJECT_ID"
fi
}
deploy_service() {
local name="$1" source="$2" env_vars="$3" service_account="${4:-}" min_instances="${5:-0}" secrets="${6:-}" extra="${7:-}"
info "Deploying ${name} from ${source} to ${REGION}..."
local sa_flag=()
if [[ -n "$service_account" ]]; then
sa_flag=(--service-account="$service_account")
info "Running as least-privilege SA: ${service_account}"
fi
# Secrets are mounted from Secret Manager by reference (never a value in code).
# Kept in sync here so a plain `--set-env-vars` deploy never drops them.
local secrets_flag=()
if [[ -n "$secrets" ]]; then
secrets_flag=(--set-secrets="$secrets")
fi
# --cpu-boost speeds the cold start (extra CPU only during container start, so
# negligible cost). --min-instances keeps N warm to remove cold starts entirely
# (standing cost); default 0, set per service for latency-critical paths.
gcloud run deploy "$name" \
--source="$source" \
--region="$REGION" \
--allow-unauthenticated \
--set-env-vars="$env_vars" \
--cpu-boost \
--min-instances="$min_instances" \
${sa_flag[@]+"${sa_flag[@]}"} \
${secrets_flag[@]+"${secrets_flag[@]}"} \
${extra:+$extra} \
--quiet
local url
url=$(gcloud run services describe "$name" --region="$REGION" --format="value(status.url)")
ok "${name} deployed: ${url}"
}
deploy_live_bridge() {
local env_vars="$LIVE_BRIDGE_ENV"
# Append the gitignored billing config (price IDs + app URL) if present, so a
# redeploy never drops it. Lines are KEY=VALUE; blanks/comments ignored.
if [[ -f "$LIVE_BRIDGE_ENV_FILE" ]]; then
local line
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}"; line="${line//[[:space:]]/}"
[[ -n "$line" && "$line" == *=* ]] && env_vars="${env_vars},${line}"
done < "$LIVE_BRIDGE_ENV_FILE"
info "Loaded billing config from ${LIVE_BRIDGE_ENV_FILE}"
else
info "No ${LIVE_BRIDGE_ENV_FILE} found; deploying without Stripe price IDs (billing inert)."
fi
deploy_service "$LIVE_BRIDGE_SERVICE" "$LIVE_BRIDGE_SOURCE" "$env_vars" "$LIVE_BRIDGE_SA" 0 "$LIVE_BRIDGE_SECRETS"
}
deploy_quiz_engine() {
deploy_service "$QUIZ_ENGINE_SERVICE" "$QUIZ_ENGINE_SOURCE" "$QUIZ_ENGINE_ENV" "" "$QUIZ_ENGINE_MIN_INSTANCES" "$QUIZ_ENGINE_SECRETS"
}
deploy_vision_verifier() {
deploy_service "$VISION_VERIFIER_SERVICE" "$VISION_VERIFIER_SOURCE" "$VISION_VERIFIER_ENV" "$VISION_VERIFIER_SA" "$VISION_VERIFIER_MIN_INSTANCES" "$VISION_VERIFIER_SECRETS"
}
deploy_compiler() {
deploy_service "$COMPILER_SERVICE" "$COMPILER_SOURCE" "$COMPILER_ENV" "$COMPILER_SA" "$COMPILER_MIN_INSTANCES" "$COMPILER_SECRETS" "$COMPILER_EXTRA"
}
deploy_reporter() {
deploy_service "$REPORTER_SERVICE" "$REPORTER_SOURCE" "$REPORTER_ENV" "$REPORTER_SA" "$REPORTER_MIN_INSTANCES" "$REPORTER_SECRETS" "$REPORTER_EXTRA"
}
deploy_frontend() {
info "Building frontend..."
( cd frontend && npm run build )
info "Frontend built successfully in frontend/dist/"
ok "Deploy frontend/dist/ to your hosting provider (e.g. Firebase Hosting, Cloud Storage, Vercel)"
}
verify_services() {
info "Verifying deployed services..."
for service in "$LIVE_BRIDGE_SERVICE" "$QUIZ_ENGINE_SERVICE" "$VISION_VERIFIER_SERVICE" "$COMPILER_SERVICE" "$REPORTER_SERVICE"; do
local url
url=$(gcloud run services describe "$service" --region="$REGION" --format="value(status.url)" 2>/dev/null)
if [[ -n "$url" ]]; then
local status
status=$(curl -s -o /dev/null -w "%{http_code}" "${url}/health" 2>/dev/null || echo "000")
if [[ "$status" == "200" ]]; then
ok "${service}: healthy (${url})"
else
err "${service}: returned HTTP ${status} (${url})"
fi
else
err "${service}: not found in ${REGION}"
fi
done
}
# ── Main ──
main() {
check_gcloud
case "${1:-all}" in
live-bridge)
deploy_live_bridge
;;
quiz-engine)
deploy_quiz_engine
;;
vision-verifier)
deploy_vision_verifier
;;
compiler)
deploy_compiler
;;
reporter)
deploy_reporter
;;
frontend)
deploy_frontend
;;
verify)
verify_services
;;
all)
deploy_live_bridge
deploy_quiz_engine
deploy_vision_verifier
deploy_compiler
deploy_reporter
deploy_frontend
echo ""
verify_services
echo ""
ok "All services deployed!"
;;
*)
err "Unknown target: $1"
echo "Usage: ./deploy.sh [live-bridge|quiz-engine|vision-verifier|compiler|reporter|frontend|verify|all]"
exit 1
;;
esac
}
main "$@"