@stackframe/stack-cli 2.8.83 → 2.8.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,615 @@
1
+ #cloud-config
2
+
3
+ hostname: stack-emulator
4
+ manage_etc_hosts: true
5
+
6
+ users:
7
+ - name: stack
8
+ shell: /bin/bash
9
+ sudo: ALL=(ALL) NOPASSWD:ALL
10
+ lock_passwd: false
11
+
12
+ chpasswd:
13
+ list: |
14
+ root:stack-emulator
15
+ stack:stack-emulator
16
+ expire: false
17
+
18
+ ssh_pwauth: false
19
+
20
+ package_update: true
21
+ package_upgrade: false
22
+
23
+ packages:
24
+ - docker.io
25
+ - ca-certificates
26
+ - curl
27
+ - netcat-openbsd
28
+ - qemu-guest-agent
29
+
30
+ write_files:
31
+ - path: /usr/local/bin/install-emulator-containers
32
+ permissions: '0755'
33
+ content: |
34
+ #!/bin/bash
35
+ set -euo pipefail
36
+
37
+ mkdir -p /mnt/stack-bundle
38
+ bundle_device="$(readlink -f /dev/disk/by-label/STACKBUNDLE)"
39
+ mount -o ro "$bundle_device" /mnt/stack-bundle
40
+
41
+ systemctl enable --now docker
42
+ until docker info >/dev/null 2>&1; do sleep 1; done
43
+
44
+ gzip -dc /mnt/stack-bundle/img.tgz | docker load
45
+
46
+ if [ -f /mnt/stack-bundle/build.env ]; then
47
+ cp /mnt/stack-bundle/build.env /etc/stack-build.env
48
+ fi
49
+
50
+ # build-arch.env lets the guest skip the smoke test on cross-arch TCG.
51
+ if [ -f /mnt/stack-bundle/build-arch.env ]; then
52
+ cp /mnt/stack-bundle/build-arch.env /etc/stack-build-arch.env
53
+ fi
54
+
55
+ - path: /usr/local/bin/render-stack-env
56
+ permissions: '0755'
57
+ content: |
58
+ #!/bin/bash
59
+ set -euo pipefail
60
+
61
+ mkdir -p /mnt/stack-runtime /run/stack-auth /var/lib/stack-auth
62
+ runtime_device="$(readlink -f /dev/disk/by-label/STACKCFG)"
63
+ mountpoint -q /mnt/stack-runtime || mount -o ro "$runtime_device" /mnt/stack-runtime
64
+
65
+ set -a
66
+ source /mnt/stack-runtime/runtime.env
67
+ source /mnt/stack-runtime/base.env
68
+ set +a
69
+
70
+ # Generate and persist the internal-project keys on first boot; reuse
71
+ # across container restarts so the dashboard keeps its internal-project
72
+ # session. Reset via `stack emulator reset`.
73
+ #
74
+ # pck: used by stack-cli to auth against /api/v1/internal/local-emulator/project
75
+ # ssk/sak: required by the emulator's own dashboard (StackServerApp
76
+ # construction throws without them). Not used by user-app flows; the
77
+ # /local-emulator/project route mints separate per-project credentials.
78
+ umask 077
79
+ for key in internal-pck internal-ssk internal-sak; do
80
+ if [ ! -s "/var/lib/stack-auth/$key" ]; then
81
+ openssl rand -hex 32 > "/var/lib/stack-auth/$key"
82
+ fi
83
+ done
84
+ INTERNAL_PCK="$(cat /var/lib/stack-auth/internal-pck)"
85
+ INTERNAL_SSK="$(cat /var/lib/stack-auth/internal-ssk)"
86
+ INTERNAL_SAK="$(cat /var/lib/stack-auth/internal-sak)"
87
+
88
+ # Container-local dependencies run on localhost. Host-only development
89
+ # services (such as the OAuth mock server) are reachable via the QEMU
90
+ # user-network host alias.
91
+ DEPS_HOST=127.0.0.1
92
+ HOST_SERVICES_HOST=10.0.2.2
93
+ P="$STACK_EMULATOR_PORT_PREFIX"
94
+
95
+ {
96
+ # Static vars from base config and runtime (e.g. API keys, feature flags)
97
+ cat /mnt/stack-runtime/base.env
98
+ cat /mnt/stack-runtime/runtime.env
99
+ printf 'STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=%s\n' "$INTERNAL_PCK"
100
+ printf 'STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=%s\n' "$INTERNAL_SSK"
101
+ printf 'STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=%s\n' "$INTERNAL_SAK"
102
+
103
+ # Computed vars — depend on port prefix or deps host
104
+ # Host-side ports (for browser URLs — browser runs on host, not in VM)
105
+ HP_BACKEND="$STACK_EMULATOR_BACKEND_HOST_PORT"
106
+ HP_DASHBOARD="$STACK_EMULATOR_DASHBOARD_HOST_PORT"
107
+ HP_MINIO="$STACK_EMULATOR_MINIO_HOST_PORT"
108
+ HP_INBUCKET="$STACK_EMULATOR_INBUCKET_HOST_PORT"
109
+
110
+ cat <<COMPUTED
111
+ STACK_SKIP_MIGRATIONS=true
112
+ STACK_SKIP_SEED_SCRIPT=true
113
+ NEXT_PUBLIC_STACK_PORT_PREFIX=${P}
114
+ STACK_RUNTIME_WORK_DIR=/app
115
+ STACK_LOCAL_EMULATOR_HOST_MOUNT_ROOT=/host
116
+ NEXT_PUBLIC_STACK_API_URL=http://localhost:${HP_BACKEND}
117
+ NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:${HP_DASHBOARD}
118
+ NEXT_PUBLIC_BROWSER_STACK_API_URL=http://localhost:${HP_BACKEND}
119
+ NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL=http://localhost:${HP_DASHBOARD}
120
+ NEXT_PUBLIC_SERVER_STACK_API_URL=http://127.0.0.1:${P}02
121
+ NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL=http://127.0.0.1:${P}01
122
+ NEXT_PUBLIC_STACK_SVIX_SERVER_URL=http://localhost:${HP_BACKEND}
123
+ STACK_DATABASE_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@${DEPS_HOST}:5432/stackframe
124
+ STACK_EMAIL_HOST=${DEPS_HOST}
125
+ STACK_SVIX_SERVER_URL=http://${DEPS_HOST}:8071
126
+ STACK_S3_ENDPOINT=http://${DEPS_HOST}:9090
127
+ STACK_S3_PUBLIC_ENDPOINT=http://localhost:${HP_MINIO}/stack-storage
128
+ STACK_QSTASH_URL=http://${DEPS_HOST}:8080
129
+ STACK_CLICKHOUSE_URL=http://${DEPS_HOST}:8123
130
+ STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=http://localhost:${HP_DASHBOARD}/handler/email-verification
131
+ STACK_EMAIL_MONITOR_INBUCKET_API_URL=http://${DEPS_HOST}:9001
132
+ STACK_OAUTH_MOCK_URL=http://localhost:${P}14
133
+ STACK_FREESTYLE_API_ENDPOINT=http://${DEPS_HOST}:8180
134
+ STACK_STRIPE_MOCK_PORT=12111
135
+ NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY=pk_test_mock_publishable_key_for_local_emulator
136
+ BACKEND_PORT=${P}02
137
+ DASHBOARD_PORT=${P}01
138
+ COMPUTED
139
+ } > /run/stack-auth/local-emulator.env
140
+
141
+ - path: /usr/local/bin/mount-host-fs
142
+ permissions: '0755'
143
+ content: |
144
+ #!/bin/bash
145
+ set -euo pipefail
146
+ mkdir -p /host
147
+ if ! mountpoint -q /host; then
148
+ if ! mount -t 9p -o trans=virtio,version=9p2000.L hostfs /host; then
149
+ echo "Failed to mount host filesystem at /host" >&2
150
+ exit 1
151
+ fi
152
+ fi
153
+
154
+ - path: /usr/local/bin/run-stack-container
155
+ permissions: '0755'
156
+ content: |
157
+ #!/bin/bash
158
+ set -euo pipefail
159
+
160
+ /usr/local/bin/mount-host-fs
161
+ /usr/local/bin/render-stack-env
162
+
163
+ # Publish the internal publishable client key to the host via 9p so the
164
+ # stack-cli can authenticate its bootstrap call to
165
+ # /api/v1/internal/local-emulator/project.
166
+ set -a
167
+ source /mnt/stack-runtime/runtime.env
168
+ set +a
169
+ if [ -n "${STACK_EMULATOR_VM_DIR_HOST:-}" ] && [ -s /var/lib/stack-auth/internal-pck ]; then
170
+ install -m 0600 /var/lib/stack-auth/internal-pck \
171
+ "/host${STACK_EMULATOR_VM_DIR_HOST}/internal-pck"
172
+ fi
173
+
174
+ docker rm -f stack >/dev/null 2>&1 || true
175
+
176
+ # Mirror container stdout/stderr to a host-visible log for debugging.
177
+ # The container already bind-mounts /host:/host, so we reuse that path.
178
+ # Falls back to stdout (captured by systemd-journald) when no host log is set.
179
+ if [ -n "${STACK_EMULATOR_VM_DIR_HOST:-}" ]; then
180
+ host_log="/host${STACK_EMULATOR_VM_DIR_HOST}/stack.log"
181
+ : > "$host_log" 2>/dev/null || true
182
+ exec docker run \
183
+ --rm \
184
+ --name stack \
185
+ --network host \
186
+ --add-host host.docker.internal:host-gateway \
187
+ --env-file /run/stack-auth/local-emulator.env \
188
+ -v stack-postgres-data:/data/postgres \
189
+ -v stack-redis-data:/data/redis \
190
+ -v stack-clickhouse-data:/data/clickhouse \
191
+ -v stack-minio-data:/data/minio \
192
+ -v stack-inbucket-data:/data/inbucket \
193
+ -v /host:/host \
194
+ stack-local-emulator 2>&1 | tee -a "$host_log"
195
+ else
196
+ exec docker run \
197
+ --rm \
198
+ --name stack \
199
+ --network host \
200
+ --add-host host.docker.internal:host-gateway \
201
+ --env-file /run/stack-auth/local-emulator.env \
202
+ -v stack-postgres-data:/data/postgres \
203
+ -v stack-redis-data:/data/redis \
204
+ -v stack-clickhouse-data:/data/clickhouse \
205
+ -v stack-minio-data:/data/minio \
206
+ -v stack-inbucket-data:/data/inbucket \
207
+ -v /host:/host \
208
+ stack-local-emulator
209
+ fi
210
+
211
+ - path: /usr/local/bin/wait-for-deps
212
+ permissions: '0755'
213
+ content: |
214
+ #!/bin/bash
215
+ set -uo pipefail
216
+
217
+ # Hard upper bound across the whole dep wait. Under TCG every service
218
+ # init is 5-20x slower than native, so we allow a generous budget, but
219
+ # if we cross it something is genuinely stuck and we need to surface it.
220
+ DEPS_TIMEOUT="${STACK_DEPS_TIMEOUT:-1500}"
221
+ DEPS_CONTAINER="${STACK_DEPS_CONTAINER:-stack-build-init}"
222
+ start=$SECONDS
223
+ log() { /usr/local/bin/log-provision "wait-for-deps: $*"; }
224
+
225
+ # name|probe pairs — probe runs through `eval` and must exit 0 when ready.
226
+ # No --max-time on these: under slow TCG a service may take >3s to
227
+ # respond; let curl wait, outer DEPS_TIMEOUT bounds the whole dep wait.
228
+ SERVICES=(
229
+ 'postgres|nc -z 127.0.0.1 5432'
230
+ 'clickhouse|curl -sf http://127.0.0.1:8123/ping'
231
+ 'svix|curl -sf http://127.0.0.1:8071/api/v1/health/'
232
+ 'minio|curl -sf http://127.0.0.1:9090/minio/health/live'
233
+ 'qstash|[ "$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/ 2>/dev/null || true)" = "401" ]'
234
+ )
235
+
236
+ dump_diagnostics() {
237
+ log "dumping diagnostics for stuck dep wait..."
238
+ log "--- docker ps -a ---"
239
+ docker ps -a 2>&1 | /usr/local/bin/log-provision-stream "wait-for-deps: ps" || true
240
+ log "--- docker logs ${DEPS_CONTAINER} (last 300 lines) ---"
241
+ docker logs --tail 300 "$DEPS_CONTAINER" 2>&1 | /usr/local/bin/log-provision-stream "wait-for-deps: deps" || true
242
+ log "--- per-service probes (3s timeout) ---"
243
+ nc -z -w 3 127.0.0.1 5432 >/dev/null 2>&1 && log "postgres:5432 reachable" || log "postgres:5432 NOT reachable"
244
+ curl -sf --max-time 3 http://127.0.0.1:8123/ping >/dev/null 2>&1 && log "clickhouse:8123 reachable" || log "clickhouse:8123 NOT reachable"
245
+ curl -sf --max-time 3 http://127.0.0.1:8071/api/v1/health/ >/dev/null 2>&1 && log "svix:8071 reachable" || log "svix:8071 NOT reachable"
246
+ curl -sf --max-time 3 http://127.0.0.1:9090/minio/health/live >/dev/null 2>&1 && log "minio:9090 reachable" || log "minio:9090 NOT reachable"
247
+ code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 http://127.0.0.1:8080/ 2>/dev/null || true)
248
+ [ "$code" = "401" ] && log "qstash:8080 reachable (401)" || log "qstash:8080 NOT reachable (code=${code:-none})"
249
+ }
250
+
251
+ wait_for() {
252
+ local name="$1" probe="$2" elapsed
253
+ local svc_start=$SECONDS
254
+ local next_heartbeat=$((svc_start + 30))
255
+ while true; do
256
+ if eval "$probe" >/dev/null 2>&1; then
257
+ elapsed=$((SECONDS - svc_start))
258
+ log "${name} ready (${elapsed}s)"
259
+ return 0
260
+ fi
261
+ if [ "$SECONDS" -ge "$next_heartbeat" ]; then
262
+ log "still waiting for ${name} ($((SECONDS - svc_start))s elapsed)"
263
+ next_heartbeat=$((SECONDS + 30))
264
+ fi
265
+ if [ "$((SECONDS - start))" -ge "$DEPS_TIMEOUT" ]; then
266
+ elapsed=$((SECONDS - start))
267
+ log "TIMEOUT waiting for ${name} after ${elapsed}s (hard cap ${DEPS_TIMEOUT}s)"
268
+ dump_diagnostics
269
+ exit 1
270
+ fi
271
+ sleep 2
272
+ done
273
+ }
274
+
275
+ log "starting dep wait (timeout=${DEPS_TIMEOUT}s)"
276
+ for entry in "${SERVICES[@]}"; do
277
+ wait_for "${entry%%|*}" "${entry#*|}"
278
+ done
279
+ log "all deps ready ($((SECONDS - start))s total)"
280
+
281
+ - path: /etc/stack-build-computed.env
282
+ content: |
283
+ USE_INLINE_ENV_VARS=true
284
+ NEXT_PUBLIC_STACK_API_URL=http://localhost:8102
285
+ NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:8101
286
+ NEXT_PUBLIC_BROWSER_STACK_API_URL=http://localhost:8102
287
+ NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL=http://localhost:8101
288
+ NEXT_PUBLIC_SERVER_STACK_API_URL=http://127.0.0.1:8102
289
+ NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL=http://127.0.0.1:8101
290
+ NEXT_PUBLIC_STACK_SVIX_SERVER_URL=http://localhost:8071
291
+ NEXT_PUBLIC_STACK_PORT_PREFIX=81
292
+ STACK_CLICKHOUSE_DATABASE=default
293
+ BACKEND_PORT=8102
294
+ DASHBOARD_PORT=8101
295
+
296
+ - path: /usr/local/bin/log-provision
297
+ permissions: '0755'
298
+ content: |
299
+ #!/bin/bash
300
+ set -euo pipefail
301
+
302
+ msg="$*"
303
+ echo "STACK_PROVISION: $msg"
304
+ if [ -n "${STACK_PROVISION_LOG_FILE:-}" ]; then
305
+ printf '%s\n' "$msg" >> "$STACK_PROVISION_LOG_FILE"
306
+ fi
307
+
308
+ - path: /usr/local/bin/log-provision-stream
309
+ permissions: '0755'
310
+ content: |
311
+ #!/bin/bash
312
+ set -uo pipefail
313
+
314
+ prefix="${1:-}"
315
+ while IFS= read -r line; do
316
+ /usr/local/bin/log-provision "${prefix}: ${line}"
317
+ done
318
+
319
+ - path: /usr/local/bin/run-build-migrations
320
+ permissions: '0755'
321
+ content: |
322
+ #!/bin/bash
323
+ set -euo pipefail
324
+
325
+ log() { /usr/local/bin/log-provision "$*"; }
326
+
327
+ log "Starting deps container..."
328
+ docker run --rm --name stack-build-init \
329
+ --network host \
330
+ -e STACK_DEPS_ONLY=true \
331
+ -v stack-postgres-data:/data/postgres \
332
+ -v stack-redis-data:/data/redis \
333
+ -v stack-clickhouse-data:/data/clickhouse \
334
+ -v stack-minio-data:/data/minio \
335
+ -v stack-inbucket-data:/data/inbucket \
336
+ -d stack-local-emulator
337
+
338
+ log "Waiting for deps (postgres, redis, clickhouse, minio, qstash)..."
339
+ /usr/local/bin/wait-for-deps
340
+ log "Deps ready."
341
+
342
+ # Wait for init-services.sh (MinIO buckets, ClickHouse DB creation)
343
+ log "Waiting for init-services.sh..."
344
+ timeout=120
345
+ elapsed=0
346
+ while [ "$elapsed" -lt "$timeout" ]; do
347
+ if docker exec stack-build-init test -f /var/run/stack-local-init-services.done 2>/dev/null; then
348
+ break
349
+ fi
350
+ sleep 1
351
+ elapsed=$((elapsed + 1))
352
+ done
353
+ if [ "$elapsed" -ge "$timeout" ]; then
354
+ log "ERROR: init-services.sh did not finish within ${timeout}s"
355
+ exit 1
356
+ fi
357
+ log "init-services done (${elapsed}s)."
358
+
359
+ log "Running migrations..."
360
+ # Cross-arch TCG mistranslates V8's JIT-emitted arm64, and V8's wasm
361
+ # tier-up path trips an InnerPointerToCodeCache check deep in the heap
362
+ # (Runtime_WasmTriggerTierUp → StackFrameIterator::Advance crashes
363
+ # when Wasm code has been freed while a frame still references it).
364
+ # --no-opt keeps JS off TurboFan/Maglev
365
+ # --no-wasm-tier-up keeps Wasm on Liftoff (no TurboFan)
366
+ # --no-wasm-dynamic-tiering suppresses the tier-up decision runtime call
367
+ # --no-wasm-code-gc keeps Wasm code alive across stack walks
368
+ # All four are no-ops under KVM, and must be passed on node's CLI
369
+ # (NODE_OPTIONS rejects them).
370
+ migrate_log="$(mktemp)"
371
+ set +e
372
+ docker exec \
373
+ --env-file /etc/stack-build.env \
374
+ --env-file /etc/stack-build-computed.env \
375
+ stack-build-init \
376
+ sh -c 'cd /app/apps/backend && node --no-opt --no-wasm-tier-up --no-wasm-dynamic-tiering --no-wasm-code-gc dist/db-migrations.mjs migrate && node --no-opt --no-wasm-tier-up --no-wasm-dynamic-tiering --no-wasm-code-gc dist/db-migrations.mjs seed' \
377
+ > "$migrate_log" 2>&1
378
+ migrate_status=$?
379
+ set -e
380
+ if [ "$migrate_status" -ne 0 ]; then
381
+ log "MIGRATIONS FAILED (exit ${migrate_status}) — last 200 lines of migration output:"
382
+ tail -200 "$migrate_log" | /usr/local/bin/log-provision-stream "migrate" || true
383
+ rm -f "$migrate_log"
384
+ exit "$migrate_status"
385
+ fi
386
+ rm -f "$migrate_log"
387
+ log "Migrations + seed complete."
388
+
389
+ log "Stopping deps container..."
390
+ docker stop stack-build-init || true
391
+ log "run-build-migrations done."
392
+
393
+ - path: /usr/local/bin/slim-docker-image
394
+ permissions: '0755'
395
+ content: |
396
+ #!/bin/bash
397
+ set -euo pipefail
398
+
399
+ log() { /usr/local/bin/log-provision "$*"; }
400
+
401
+ log "Building slim Docker image..."
402
+ docker build -t stack-local-emulator-slim - <<'DOCKERFILE'
403
+ FROM stack-local-emulator
404
+ RUN rm -rf /app/node_modules /app/apps/backend/dist && \
405
+ mv /app/node_modules.standalone /app/node_modules && \
406
+ for entry in /app/node_modules/.pnpm/node_modules/*; do \
407
+ name="$(basename "$entry")"; \
408
+ [ "$name" = ".bin" ] && continue; \
409
+ ln -sf ".pnpm/node_modules/$name" "/app/node_modules/$name" 2>/dev/null || true; \
410
+ done
411
+ DOCKERFILE
412
+ log "Slim image built."
413
+
414
+ # Determine build arch to decide whether to run the smoke test. Cross-arch
415
+ # (TCG) builds can't reliably run the Next.js backend inside the smoke
416
+ # test container: V8 JIT ↔ QEMU TCG mistranslations crash the process,
417
+ # and even with --jitless the backend is too slow to respond within any
418
+ # sane timeout. amd64 builds run under KVM and are unaffected.
419
+ BUILD_ARCH=""
420
+ if [ -f /etc/stack-build-arch.env ]; then
421
+ # shellcheck disable=SC1091
422
+ . /etc/stack-build-arch.env
423
+ BUILD_ARCH="${STACK_EMULATOR_BUILD_ARCH:-}"
424
+ fi
425
+
426
+ if [ "$BUILD_ARCH" = "arm64" ]; then
427
+ log "Skipping smoke test: build arch is arm64 and cross-arch TCG can't reliably run the backend."
428
+ else
429
+ log "Running smoke test on slim image..."
430
+ # build.env sets NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true, which makes
431
+ # docker/server/entrypoint.sh require the three internal SEED keys.
432
+ # At real-VM boot those come from render-stack-env via
433
+ # /run/stack-auth/local-emulator.env, but that path doesn't run during
434
+ # the build-time smoke test. Mint throwaway hex keys for this container
435
+ # only; they must be hex because entrypoint.sh also validates that
436
+ # before the internal ApiKeySet bootstrap SQL.
437
+ SMOKE_PCK="$(openssl rand -hex 32)"
438
+ SMOKE_SSK="$(openssl rand -hex 32)"
439
+ SMOKE_SAK="$(openssl rand -hex 32)"
440
+ docker run --rm --name smoke-test \
441
+ --network host \
442
+ --env-file /etc/stack-build.env \
443
+ --env-file /etc/stack-build-computed.env \
444
+ -e STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY="$SMOKE_PCK" \
445
+ -e STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY="$SMOKE_SSK" \
446
+ -e STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY="$SMOKE_SAK" \
447
+ -e STACK_SKIP_MIGRATIONS=true \
448
+ -e STACK_SKIP_SEED_SCRIPT=true \
449
+ -e STACK_RUNTIME_WORK_DIR=/app \
450
+ -v stack-postgres-data:/data/postgres \
451
+ -v stack-redis-data:/data/redis \
452
+ -v stack-clickhouse-data:/data/clickhouse \
453
+ -v stack-minio-data:/data/minio \
454
+ -v stack-inbucket-data:/data/inbucket \
455
+ -d stack-local-emulator-slim
456
+
457
+ smoke_timeout=300
458
+ smoke_elapsed=0
459
+ smoke_passed=false
460
+ while [ "$smoke_elapsed" -lt "$smoke_timeout" ]; do
461
+ code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 http://127.0.0.1:8102/health?db=1 2>/dev/null || true)
462
+ if [ "$code" = "200" ]; then
463
+ smoke_passed=true
464
+ break
465
+ fi
466
+ sleep 2
467
+ smoke_elapsed=$((smoke_elapsed + 2))
468
+ done
469
+
470
+ if [ "$smoke_passed" = "false" ]; then
471
+ log "SMOKE TEST FAILED: backend /health?db=1 did not return 200 within ${smoke_timeout}s"
472
+ log "--- docker ps -a ---"
473
+ docker ps -a 2>&1 | /usr/local/bin/log-provision-stream "ps" || true
474
+ log "--- smoke-test container logs (last 200 lines) ---"
475
+ docker logs --tail 200 smoke-test 2>&1 | /usr/local/bin/log-provision-stream "smoke-test" || true
476
+ log "--- free -m ---"
477
+ free -m 2>&1 | /usr/local/bin/log-provision-stream "mem" || true
478
+ log "--- curl -v /health?db=1 ---"
479
+ curl -v --max-time 5 http://127.0.0.1:8102/health?db=1 2>&1 | /usr/local/bin/log-provision-stream "curl" || true
480
+ docker stop smoke-test 2>/dev/null || true
481
+ exit 1
482
+ fi
483
+
484
+ docker stop smoke-test 2>/dev/null || true
485
+ sleep 2
486
+ log "Smoke test passed (${smoke_elapsed}s)."
487
+ fi
488
+
489
+ log "Flattening image (docker export/import)..."
490
+ docker create --name flatten stack-local-emulator-slim /bin/true
491
+ docker export flatten | docker import \
492
+ --change 'WORKDIR /app' \
493
+ --change 'ENTRYPOINT ["/entrypoint.sh"]' \
494
+ --change 'EXPOSE 5432 6379 2500 9001 1100 8071 8123 9009 9090 8080 8101 8102' \
495
+ --change 'ENV DEBIAN_FRONTEND=noninteractive' \
496
+ - stack-local-emulator:final
497
+ log "Flatten done."
498
+
499
+ log "Saving final image to /var/tmp..."
500
+ docker rm flatten
501
+ docker save stack-local-emulator:final -o /var/tmp/final-image.tar
502
+ mv /var/lib/docker/volumes /var/tmp/volumes-backup
503
+ log "Nuking Docker storage and reloading..."
504
+ systemctl stop docker containerd
505
+ rm -rf /var/lib/docker /var/lib/containerd
506
+ systemctl start docker containerd
507
+ until docker info >/dev/null 2>&1; do sleep 1; done
508
+ docker load -i /var/tmp/final-image.tar
509
+ docker tag stack-local-emulator:final stack-local-emulator
510
+ docker rmi stack-local-emulator:final || true
511
+ rm -f /var/tmp/final-image.tar
512
+ systemctl stop docker
513
+ rm -rf /var/lib/docker/volumes
514
+ mv /var/tmp/volumes-backup /var/lib/docker/volumes
515
+ systemctl start docker
516
+ log "Docker storage rebuilt."
517
+
518
+ log "Zeroing free space for qcow2 compression..."
519
+ dd if=/dev/zero of=/zero.fill bs=1M 2>/dev/null || true
520
+ rm -f /zero.fill
521
+ sync
522
+ fstrim -av 2>/dev/null || true
523
+ log "slim-docker-image done."
524
+
525
+ - path: /etc/systemd/system/stack.service
526
+ content: |
527
+ [Unit]
528
+ Description=Stack Auth local emulator
529
+ Wants=network-online.target docker.service
530
+ After=network-online.target docker.service
531
+
532
+ [Service]
533
+ Restart=always
534
+ RestartSec=5
535
+ TimeoutStartSec=0
536
+ ExecStart=/usr/local/bin/run-stack-container
537
+ ExecStop=/usr/bin/docker stop stack
538
+
539
+ [Install]
540
+ WantedBy=multi-user.target
541
+
542
+ - path: /usr/local/bin/provision-build
543
+ permissions: '0755'
544
+ content: |
545
+ #!/bin/bash
546
+ set -euo pipefail
547
+
548
+ if bash /usr/local/bin/mount-host-fs 2>/dev/null; then
549
+ export STACK_PROVISION_LOG_FILE=/host/provision.log
550
+ : > "$STACK_PROVISION_LOG_FILE"
551
+ else
552
+ export STACK_PROVISION_LOG_FILE=""
553
+ fi
554
+
555
+ write_marker_to_consoles() {
556
+ local marker="$1"
557
+ for dev in /dev/console /dev/ttyAMA0 /dev/ttyS0; do
558
+ echo "$marker" > "$dev" 2>/dev/null || true
559
+ done
560
+ }
561
+
562
+ cleanup() {
563
+ local status=$?
564
+ if [ "$status" -ne 0 ]; then
565
+ if [ -n "${STACK_PROVISION_LOG_FILE:-}" ]; then
566
+ printf 'ERROR: provision-build exited with code %s\n' "$status" >> "$STACK_PROVISION_LOG_FILE"
567
+ printf '%s\n' "STACK_CLOUD_INIT_FAILED" >> "$STACK_PROVISION_LOG_FILE"
568
+ fi
569
+ write_marker_to_consoles "STACK_CLOUD_INIT_FAILED"
570
+ sync || true
571
+ (sleep 2 && shutdown -P now) &
572
+ (sleep 15 && poweroff -f) &
573
+ fi
574
+ }
575
+ trap cleanup EXIT
576
+
577
+ SERIAL=""
578
+ for d in /dev/ttyAMA0 /dev/ttyS0; do
579
+ [ -c "$d" ] && SERIAL="$d" && break
580
+ done
581
+ if [ -n "$SERIAL" ]; then
582
+ exec > >(tee -a "$SERIAL") 2>&1
583
+ fi
584
+
585
+ log_provision() {
586
+ /usr/local/bin/log-provision "$*"
587
+ }
588
+
589
+ log_provision "runcmd starting"
590
+
591
+ systemctl disable --now ssh || true
592
+ systemctl mask ssh || true
593
+
594
+ log_provision "installing emulator containers"
595
+ bash /usr/local/bin/install-emulator-containers
596
+
597
+ systemctl daemon-reload
598
+ systemctl enable stack.service
599
+
600
+ log_provision "starting build migrations"
601
+ bash /usr/local/bin/run-build-migrations
602
+
603
+ log_provision "starting slim-docker-image"
604
+ bash /usr/local/bin/slim-docker-image
605
+
606
+ log_provision "build pipeline complete"
607
+ if [ -n "${STACK_PROVISION_LOG_FILE:-}" ]; then
608
+ printf '%s\n' "STACK_CLOUD_INIT_DONE" >> "$STACK_PROVISION_LOG_FILE"
609
+ fi
610
+ write_marker_to_consoles "STACK_CLOUD_INIT_DONE"
611
+
612
+ shutdown -P now
613
+
614
+ runcmd:
615
+ - [bash, /usr/local/bin/provision-build]
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # Shared helpers for QEMU emulator scripts.
3
+ # Source this file; do not execute it directly.
4
+
5
+ AARCH64_FIRMWARE_PATHS=(
6
+ /opt/homebrew/share/qemu/edk2-aarch64-code.fd
7
+ /usr/share/qemu/edk2-aarch64-code.fd
8
+ /usr/share/AAVMF/AAVMF_CODE.fd
9
+ /usr/share/qemu-efi-aarch64/QEMU_EFI.fd
10
+ )
11
+
12
+ detect_host() {
13
+ case "$(uname -m)" in
14
+ arm64|aarch64) HOST_ARCH="arm64" ;;
15
+ x86_64|amd64) HOST_ARCH="amd64" ;;
16
+ *) echo "Unsupported host architecture: $(uname -m)" >&2; exit 1 ;;
17
+ esac
18
+
19
+ case "$(uname -s)" in
20
+ Darwin) HOST_OS="darwin" ;;
21
+ Linux) HOST_OS="linux" ;;
22
+ MINGW*|MSYS*|CYGWIN*) HOST_OS="windows" ;;
23
+ *) HOST_OS="unknown" ;;
24
+ esac
25
+ }
26
+
27
+ qemu_binary_for_arch() {
28
+ case "$1" in
29
+ arm64) echo "qemu-system-aarch64" ;;
30
+ amd64) echo "qemu-system-x86_64" ;;
31
+ *) return 1 ;;
32
+ esac
33
+ }
34
+
35
+ find_aarch64_firmware() {
36
+ local p
37
+ for p in "${AARCH64_FIRMWARE_PATHS[@]}"; do
38
+ if [ -f "$p" ]; then
39
+ echo "$p"
40
+ return 0
41
+ fi
42
+ done
43
+ echo "No aarch64 UEFI firmware found." >&2
44
+ return 1
45
+ }
46
+
47
+ make_iso_from_dir() {
48
+ local iso_path="$1"
49
+ local volume_name="$2"
50
+ local source_dir="$3"
51
+
52
+ rm -f "$iso_path" "${iso_path}.iso"
53
+ if command -v hdiutil >/dev/null 2>&1; then
54
+ local tmp_dir
55
+ tmp_dir="$(mktemp -d /tmp/stack-emulator-iso-XXXXXX)"
56
+ cp -R "$source_dir/." "$tmp_dir/"
57
+ hdiutil makehybrid -o "$iso_path" "$tmp_dir" -joliet -iso -default-volume-name "$volume_name" 2>/dev/null
58
+ if [ -f "${iso_path}.iso" ]; then
59
+ mv "${iso_path}.iso" "$iso_path"
60
+ fi
61
+ rm -rf "$tmp_dir"
62
+ elif command -v mkisofs >/dev/null 2>&1; then
63
+ mkisofs -output "$iso_path" -volid "$volume_name" -joliet -rock "$source_dir" >/dev/null 2>&1
64
+ elif command -v genisoimage >/dev/null 2>&1; then
65
+ genisoimage -output "$iso_path" -volid "$volume_name" -joliet -rock "$source_dir" >/dev/null 2>&1
66
+ else
67
+ echo "Missing ISO creation tool (need hdiutil, mkisofs, or genisoimage)" >&2
68
+ exit 1
69
+ fi
70
+ }