From 6c51e34850b60b3ce11e9520e1a233014afc3ac4 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 24 Aug 2026 17:21:49 +0200 Subject: [PATCH 1/3] fix(cli): exec remaining start wrappers so docker stop is not 10s Postgres exec and postgres-meta v0.98.0 already landed with the shadow cache. Keep Kong, Vector, Edge Runtime, and a Logflare TERM-then-KILL supervisor so sh is no longer PID 1 on supabase stop. Vector's wait-loop now exits on TERM instead of burning the grace period while Logflare is still down. --- .../functions/serve/serve.integration.test.ts | 4 ++-- .../commands/start/services/kong.service.ts | 7 ++++--- .../start/services/kong.service.unit.test.ts | 2 +- .../commands/start/services/logflare.service.ts | 17 ++++++++++++++++- .../services/logflare.service.unit.test.ts | 11 ++++++++++- .../commands/start/services/vector.service.ts | 12 ++++++------ .../start/services/vector.service.unit.test.ts | 4 ++-- .../legacy-edge-runtime-script.service.ts | 11 +++++------ .../legacy-edge-runtime-script.unit.test.ts | 14 ++++++++------ apps/cli/src/shared/functions/serve.ts | 3 ++- .../cli/src/shared/functions/serve.unit.test.ts | 4 ++-- 11 files changed, 58 insertions(+), 31 deletions(-) diff --git a/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts b/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts index 9f3c3baba7..22c465e83a 100644 --- a/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts +++ b/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts @@ -758,7 +758,7 @@ describe("legacy functions serve integration", () => { toDockerPath(tempRoot.current), ]); expect(dockerRun.args[dockerRun.args.length - 1]).toBe( - "edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n", + "exec edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n", ); const envs = yield* Effect.promise(() => extractDockerEnvEntries(dockerRun)); @@ -1879,7 +1879,7 @@ describe("legacy functions serve integration", () => { const commandScript = dockerRun.args[dockerRun.args.length - 1] ?? ""; expect(commandScript).toBe( - "edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n", + "exec edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n", ); const cp = deployMockState.runCalls.find( diff --git a/apps/cli/src/legacy/commands/start/services/kong.service.ts b/apps/cli/src/legacy/commands/start/services/kong.service.ts index ec72cd667c..0ed7cde9c3 100644 --- a/apps/cli/src/legacy/commands/start/services/kong.service.ts +++ b/apps/cli/src/legacy/commands/start/services/kong.service.ts @@ -37,8 +37,8 @@ * disk when TLS is enabled AND both `cert_path`/`key_path` are configured — * see {@link LegacyKongContainerSpecInput.tlsCertContent}'s doc comment. * {@link legacyBuildKongEntrypointScript} reproduces the remaining - * `custom_nginx.template` heredoc + exec line byte-for-byte; see its doc - * comment for the exact shell mechanics. + * `custom_nginx.template` heredoc; the final command is `exec`'d so Kong + * is PID 1 (stop timing is outside Go parity, ADR 0016). * * Kong mints no JWTs of its own: `BearerToken`/`QueryToken` are Kong * `request-transformer`/lua expression STRINGS built from the four @@ -172,9 +172,10 @@ export function legacyBuildKongEmailTemplateBind( return `${hostPath}:${dockerPath}:rw`; } +// `exec` so Kong is PID 1; stop timing is outside Go parity (ADR 0016). const LEGACY_KONG_ENTRYPOINT_HEAD = "cat <<'EOF' > /home/kong/custom_nginx.template && \\\n" + - "./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template\n"; + "exec ./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template\n"; /** * Builds the surviving (non-secret) half of the Kong entrypoint: only the diff --git a/apps/cli/src/legacy/commands/start/services/kong.service.unit.test.ts b/apps/cli/src/legacy/commands/start/services/kong.service.unit.test.ts index 61f219e2b0..1e80c2cee5 100644 --- a/apps/cli/src/legacy/commands/start/services/kong.service.unit.test.ts +++ b/apps/cli/src/legacy/commands/start/services/kong.service.unit.test.ts @@ -117,7 +117,7 @@ describe("legacyBuildKongEntrypointScript", () => { const script = legacyBuildKongEntrypointScript("NGINX_TEMPLATE"); expect(script).toBe( "cat <<'EOF' > /home/kong/custom_nginx.template && \\\n" + - "./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template\n" + + "exec ./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template\n" + "NGINX_TEMPLATE\nEOF\n", ); }); diff --git a/apps/cli/src/legacy/commands/start/services/logflare.service.ts b/apps/cli/src/legacy/commands/start/services/logflare.service.ts index 229ccef273..6167d57252 100644 --- a/apps/cli/src/legacy/commands/start/services/logflare.service.ts +++ b/apps/cli/src/legacy/commands/start/services/logflare.service.ts @@ -50,9 +50,24 @@ const LEGACY_LOGFLARE_API_KEY = "api-key"; * the `unless-stopped` restart policy retries until the db is ready — * running Logflare against an unmigrated database lets Oban die on the * missing `public.oban_jobs` table instead. + * + * `run.sh` stays PID 1 on purpose: a plain `exec` of `beam.smp` still burned + * Docker's 10s SIGTERM grace (upstream hang). Forward TERM, wait 3s, then + * KILL. Interrupted `wait` is >128; a second `wait` recovers the BEAM's + * status unless it was already reaped (127). Stop timing is outside Go + * parity (ADR 0016). */ const LEGACY_LOGFLARE_ENTRYPOINT_SCRIPT = - "cat <<'EOF' > run.sh && sh run.sh\n./logflare eval Logflare.Release.migrate &&\n./logflare start --sname logflare\nEOF\n"; + "cat <<'EOF' > run.sh && exec sh run.sh\n" + + "./logflare eval Logflare.Release.migrate || exit $?\n" + + "./logflare start --sname logflare &\n" + + "BEAM_PID=$!\n" + + 'trap \'kill -TERM "$BEAM_PID" 2>/dev/null; n=0; while [ "$n" -lt 3 ] && kill -0 "$BEAM_PID" 2>/dev/null; do n=$((n+1)); sleep 1; done; kill -KILL "$BEAM_PID" 2>/dev/null\' TERM\n' + + 'wait "$BEAM_PID"\n' + + "code=$?\n" + + 'if [ "$code" -gt 128 ]; then wait "$BEAM_PID" 2>/dev/null; code2=$?; [ "$code2" -ne 127 ] && code=$code2; fi\n' + + 'exit "$code"\n' + + "EOF\n"; export interface LegacyLogflareContainerSpecInput { /** diff --git a/apps/cli/src/legacy/commands/start/services/logflare.service.unit.test.ts b/apps/cli/src/legacy/commands/start/services/logflare.service.unit.test.ts index 7a9cf7aa93..33b5db08cb 100644 --- a/apps/cli/src/legacy/commands/start/services/logflare.service.unit.test.ts +++ b/apps/cli/src/legacy/commands/start/services/logflare.service.unit.test.ts @@ -32,7 +32,16 @@ describe("legacyBuildLogflareContainerSpec", () => { expect(spec.entrypoint).toBe("sh"); expect(spec.cmd).toEqual([ "-c", - "cat <<'EOF' > run.sh && sh run.sh\n./logflare eval Logflare.Release.migrate &&\n./logflare start --sname logflare\nEOF\n", + "cat <<'EOF' > run.sh && exec sh run.sh\n" + + "./logflare eval Logflare.Release.migrate || exit $?\n" + + "./logflare start --sname logflare &\n" + + "BEAM_PID=$!\n" + + 'trap \'kill -TERM "$BEAM_PID" 2>/dev/null; n=0; while [ "$n" -lt 3 ] && kill -0 "$BEAM_PID" 2>/dev/null; do n=$((n+1)); sleep 1; done; kill -KILL "$BEAM_PID" 2>/dev/null\' TERM\n' + + 'wait "$BEAM_PID"\n' + + "code=$?\n" + + 'if [ "$code" -gt 128 ]; then wait "$BEAM_PID" 2>/dev/null; code2=$?; [ "$code2" -ne 127 ] && code=$code2; fi\n' + + 'exit "$code"\n' + + "EOF\n", ]); expect(spec.exposedPorts).toEqual([{ containerPort: "4000" }]); expect(spec.ports).toEqual([{ hostPort: "54327", containerPort: "4000" }]); diff --git a/apps/cli/src/legacy/commands/start/services/vector.service.ts b/apps/cli/src/legacy/commands/start/services/vector.service.ts index 496f740566..8cf114a0e9 100644 --- a/apps/cli/src/legacy/commands/start/services/vector.service.ts +++ b/apps/cli/src/legacy/commands/start/services/vector.service.ts @@ -279,18 +279,18 @@ const LEGACY_VECTOR_HEALTHCHECK = { /** * Builds the Vector entrypoint script: writes the rendered `vector.yaml` via - * a `cat <<'EOF'` heredoc, then blocks on a `wget`-poll loop against - * Logflare's own `/health` endpoint (Vector's `docker_logs`/HTTP sinks would - * otherwise start before Logflare is ready to receive them) before finally - * exec'ing `vector`. + * a `cat <<'EOF'` heredoc, waits on Logflare `/health` (sinks would otherwise + * start too early), then `exec`s Vector so it is PID 1. A TERM trap covers + * the wait so `docker stop` does not burn 10s if Logflare is still down; + * cleared before `exec`. Stop timing is outside Go parity (ADR 0016). */ export function legacyBuildVectorEntrypointScript(vectorYaml: string, logflareId: string): string { return ( "cat <<'EOF' > /etc/vector/vector.yaml\n" + vectorYaml + - "\nEOF\nuntil wget --no-verbose --tries=1 --spider http://" + + "\nEOF\ntrap 'exit 143' TERM\nuntil wget --no-verbose --tries=1 --spider http://" + logflareId + - ":4000/health 2>/dev/null; do sleep 2; done\nvector --config /etc/vector/vector.yaml\n" + ":4000/health 2>/dev/null; do sleep 2; done\ntrap - TERM\nexec vector --config /etc/vector/vector.yaml\n" ); } diff --git a/apps/cli/src/legacy/commands/start/services/vector.service.unit.test.ts b/apps/cli/src/legacy/commands/start/services/vector.service.unit.test.ts index 69429ea9dd..c6c0d6e673 100644 --- a/apps/cli/src/legacy/commands/start/services/vector.service.unit.test.ts +++ b/apps/cli/src/legacy/commands/start/services/vector.service.unit.test.ts @@ -225,9 +225,9 @@ describe("legacyBuildVectorEntrypointScript", () => { expect(legacyBuildVectorEntrypointScript("VECTOR_YAML", "supabase_analytics_proj")).toBe( "cat <<'EOF' > /etc/vector/vector.yaml\n" + "VECTOR_YAML" + - "\nEOF\nuntil wget --no-verbose --tries=1 --spider http://" + + "\nEOF\ntrap 'exit 143' TERM\nuntil wget --no-verbose --tries=1 --spider http://" + "supabase_analytics_proj" + - ":4000/health 2>/dev/null; do sleep 2; done\nvector --config /etc/vector/vector.yaml\n", + ":4000/health 2>/dev/null; do sleep 2; done\ntrap - TERM\nexec vector --config /etc/vector/vector.yaml\n", ); }); }); diff --git a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts index 1c6531d6af..979b1c7cee 100644 --- a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts +++ b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts @@ -95,16 +95,15 @@ export function legacyBuildEdgeRuntimeStartCmd(opts: { /** * Builds the `sh -c` entrypoint body that writes each file via a here-document - * (so contents may contain `EOF`) and then runs `cmd`. Byte-for-byte port of - * `buildEdgeRuntimeEntrypoint` (`apps/cli-go/internal/utils/edgeruntime.go`): - * all heredoc openers are joined with `&&` before the bodies so the shell stacks - * them in declaration order; each body ends with a unique sentinel. + * (so contents may contain `EOF`) and then `exec`s `cmd` so edge-runtime is + * PID 1. Heredoc stacking matches Go's `buildEdgeRuntimeEntrypoint`; `exec` + * is a deliberate timing divergence (ADR 0016). */ export function legacyBuildEdgeRuntimeEntrypoint( files: ReadonlyArray, cmd: string, ): string { - if (files.length === 0) return `${cmd}\n`; + if (files.length === 0) return `exec ${cmd}\n`; let head = ""; let bodies = ""; files.forEach((file, index) => { @@ -112,5 +111,5 @@ export function legacyBuildEdgeRuntimeEntrypoint( head += `cat <<'${sentinel}' > ${file.name} && `; bodies += `${file.content}\n${sentinel}\n`; }); - return `${head}${cmd}\n${bodies}`; + return `${head}exec ${cmd}\n${bodies}`; } diff --git a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.unit.test.ts b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.unit.test.ts index e8d668a0ad..9a6cc505eb 100644 --- a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.unit.test.ts @@ -37,7 +37,9 @@ describe("legacyBuildEdgeRuntimeStartCmd", () => { describe("legacyBuildEdgeRuntimeEntrypoint", () => { it("returns just the command (newline-terminated) when there are no files", () => { - expect(legacyBuildEdgeRuntimeEntrypoint([], "edge-runtime start")).toBe("edge-runtime start\n"); + expect(legacyBuildEdgeRuntimeEntrypoint([], "edge-runtime start")).toBe( + "exec edge-runtime start\n", + ); }); it("writes a single file via a sentinel here-document then runs the command", () => { @@ -45,10 +47,8 @@ describe("legacyBuildEdgeRuntimeEntrypoint", () => { [{ name: "index.ts", content: "console.log(1);" }], "edge-runtime start --main-service=. --port=5", ); - // Byte-for-byte port of Go's buildEdgeRuntimeEntrypoint: openers (joined with - // ` && `) precede the command, then the bodies with their sentinels. expect(out).toBe( - "cat <<'__EDGE_RT_FILE_0__' > index.ts && edge-runtime start --main-service=. --port=5\n" + + "cat <<'__EDGE_RT_FILE_0__' > index.ts && exec edge-runtime start --main-service=. --port=5\n" + "console.log(1);\n__EDGE_RT_FILE_0__\n", ); }); @@ -62,13 +62,15 @@ describe("legacyBuildEdgeRuntimeEntrypoint", () => { "CMD", ); expect(out).toBe( - "cat <<'__EDGE_RT_FILE_0__' > index.ts && cat <<'__EDGE_RT_FILE_1__' > .npmrc && CMD\n" + + "cat <<'__EDGE_RT_FILE_0__' > index.ts && cat <<'__EDGE_RT_FILE_1__' > .npmrc && exec CMD\n" + "A\n__EDGE_RT_FILE_0__\nB\n__EDGE_RT_FILE_1__\n", ); }); it("preserves file contents that themselves contain EOF-like text", () => { const out = legacyBuildEdgeRuntimeEntrypoint([{ name: "index.ts", content: "EOF\nmore" }], "C"); - expect(out).toBe("cat <<'__EDGE_RT_FILE_0__' > index.ts && C\nEOF\nmore\n__EDGE_RT_FILE_0__\n"); + expect(out).toBe( + "cat <<'__EDGE_RT_FILE_0__' > index.ts && exec C\nEOF\nmore\n__EDGE_RT_FILE_0__\n", + ); }); }); diff --git a/apps/cli/src/shared/functions/serve.ts b/apps/cli/src/shared/functions/serve.ts index 04d4227723..d95edd1f39 100644 --- a/apps/cli/src/shared/functions/serve.ts +++ b/apps/cli/src/shared/functions/serve.ts @@ -1482,7 +1482,8 @@ export function buildServeEntrypointCommand( command: ReadonlyArray, multilineEnvScriptPath?: string, ) { - return `${multilineEnvScriptPath === undefined ? "" : `. ${multilineEnvScriptPath}\n`}${command.join(" ")} + // `exec` so edge-runtime is PID 1; sourced env survives. Stop timing is outside Go parity (ADR 0016). + return `${multilineEnvScriptPath === undefined ? "" : `. ${multilineEnvScriptPath}\n`}exec ${command.join(" ")} `; } diff --git a/apps/cli/src/shared/functions/serve.unit.test.ts b/apps/cli/src/shared/functions/serve.unit.test.ts index a12ac1b9db..21eb3dfea8 100644 --- a/apps/cli/src/shared/functions/serve.unit.test.ts +++ b/apps/cli/src/shared/functions/serve.unit.test.ts @@ -6,13 +6,13 @@ import { buildServeEntrypointCommand } from "./serve.ts"; describe("buildServeEntrypointCommand", () => { it("returns the runtime command without embedding the template body", () => { const script = buildServeEntrypointCommand(["edge-runtime", "start"]); - expect(script).toBe("edge-runtime start\n"); + expect(script).toBe("exec edge-runtime start\n"); expect(script).not.toContain("Deno.serve"); }); it("sources the multiline env script before the runtime command when provided", () => { const script = buildServeEntrypointCommand(["edge-runtime", "start"], "/root/env.sh"); - expect(script).toContain(". /root/env.sh\nedge-runtime start"); + expect(script).toContain(". /root/env.sh\nexec edge-runtime start"); }); it("keeps the spawned command short even with the real bundled template", async () => { From 5c3ebb59478dff272349a54003af61f47f30ac69 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 24 Aug 2026 17:30:52 +0200 Subject: [PATCH 2/3] fix(cli): bound Vector health wget so stop is not deferred BusyBox ash defers TERM until the foreground probe finishes. Cap each wget at 2s so a hung Logflare health endpoint cannot eat Docker's grace period. Drop leftover Go-parity comment framing. --- apps/cli/src/legacy/commands/start/services/kong.service.ts | 3 +-- .../src/legacy/commands/start/services/logflare.service.ts | 3 +-- apps/cli/src/legacy/commands/start/services/vector.service.ts | 4 ++-- .../commands/start/services/vector.service.unit.test.ts | 2 +- .../src/legacy/shared/legacy-edge-runtime-script.service.ts | 3 +-- apps/cli/src/shared/functions/serve.ts | 2 +- 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/apps/cli/src/legacy/commands/start/services/kong.service.ts b/apps/cli/src/legacy/commands/start/services/kong.service.ts index 0ed7cde9c3..1dbd11c7af 100644 --- a/apps/cli/src/legacy/commands/start/services/kong.service.ts +++ b/apps/cli/src/legacy/commands/start/services/kong.service.ts @@ -38,7 +38,7 @@ * see {@link LegacyKongContainerSpecInput.tlsCertContent}'s doc comment. * {@link legacyBuildKongEntrypointScript} reproduces the remaining * `custom_nginx.template` heredoc; the final command is `exec`'d so Kong - * is PID 1 (stop timing is outside Go parity, ADR 0016). + * is PID 1 and `docker stop` reaches it directly. * * Kong mints no JWTs of its own: `BearerToken`/`QueryToken` are Kong * `request-transformer`/lua expression STRINGS built from the four @@ -172,7 +172,6 @@ export function legacyBuildKongEmailTemplateBind( return `${hostPath}:${dockerPath}:rw`; } -// `exec` so Kong is PID 1; stop timing is outside Go parity (ADR 0016). const LEGACY_KONG_ENTRYPOINT_HEAD = "cat <<'EOF' > /home/kong/custom_nginx.template && \\\n" + "exec ./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template\n"; diff --git a/apps/cli/src/legacy/commands/start/services/logflare.service.ts b/apps/cli/src/legacy/commands/start/services/logflare.service.ts index 6167d57252..d51ad00fb1 100644 --- a/apps/cli/src/legacy/commands/start/services/logflare.service.ts +++ b/apps/cli/src/legacy/commands/start/services/logflare.service.ts @@ -54,8 +54,7 @@ const LEGACY_LOGFLARE_API_KEY = "api-key"; * `run.sh` stays PID 1 on purpose: a plain `exec` of `beam.smp` still burned * Docker's 10s SIGTERM grace (upstream hang). Forward TERM, wait 3s, then * KILL. Interrupted `wait` is >128; a second `wait` recovers the BEAM's - * status unless it was already reaped (127). Stop timing is outside Go - * parity (ADR 0016). + * status unless it was already reaped (127). */ const LEGACY_LOGFLARE_ENTRYPOINT_SCRIPT = "cat <<'EOF' > run.sh && exec sh run.sh\n" + diff --git a/apps/cli/src/legacy/commands/start/services/vector.service.ts b/apps/cli/src/legacy/commands/start/services/vector.service.ts index 8cf114a0e9..d1813c2604 100644 --- a/apps/cli/src/legacy/commands/start/services/vector.service.ts +++ b/apps/cli/src/legacy/commands/start/services/vector.service.ts @@ -282,13 +282,13 @@ const LEGACY_VECTOR_HEALTHCHECK = { * a `cat <<'EOF'` heredoc, waits on Logflare `/health` (sinks would otherwise * start too early), then `exec`s Vector so it is PID 1. A TERM trap covers * the wait so `docker stop` does not burn 10s if Logflare is still down; - * cleared before `exec`. Stop timing is outside Go parity (ADR 0016). + * `-T 2` bounds each probe so a hung health endpoint cannot defer the trap. */ export function legacyBuildVectorEntrypointScript(vectorYaml: string, logflareId: string): string { return ( "cat <<'EOF' > /etc/vector/vector.yaml\n" + vectorYaml + - "\nEOF\ntrap 'exit 143' TERM\nuntil wget --no-verbose --tries=1 --spider http://" + + "\nEOF\ntrap 'exit 143' TERM\nuntil wget --no-verbose --tries=1 -T 2 --spider http://" + logflareId + ":4000/health 2>/dev/null; do sleep 2; done\ntrap - TERM\nexec vector --config /etc/vector/vector.yaml\n" ); diff --git a/apps/cli/src/legacy/commands/start/services/vector.service.unit.test.ts b/apps/cli/src/legacy/commands/start/services/vector.service.unit.test.ts index c6c0d6e673..24762e5f5b 100644 --- a/apps/cli/src/legacy/commands/start/services/vector.service.unit.test.ts +++ b/apps/cli/src/legacy/commands/start/services/vector.service.unit.test.ts @@ -225,7 +225,7 @@ describe("legacyBuildVectorEntrypointScript", () => { expect(legacyBuildVectorEntrypointScript("VECTOR_YAML", "supabase_analytics_proj")).toBe( "cat <<'EOF' > /etc/vector/vector.yaml\n" + "VECTOR_YAML" + - "\nEOF\ntrap 'exit 143' TERM\nuntil wget --no-verbose --tries=1 --spider http://" + + "\nEOF\ntrap 'exit 143' TERM\nuntil wget --no-verbose --tries=1 -T 2 --spider http://" + "supabase_analytics_proj" + ":4000/health 2>/dev/null; do sleep 2; done\ntrap - TERM\nexec vector --config /etc/vector/vector.yaml\n", ); diff --git a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts index 979b1c7cee..d3dca2ce53 100644 --- a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts +++ b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts @@ -96,8 +96,7 @@ export function legacyBuildEdgeRuntimeStartCmd(opts: { /** * Builds the `sh -c` entrypoint body that writes each file via a here-document * (so contents may contain `EOF`) and then `exec`s `cmd` so edge-runtime is - * PID 1. Heredoc stacking matches Go's `buildEdgeRuntimeEntrypoint`; `exec` - * is a deliberate timing divergence (ADR 0016). + * PID 1 and an early `docker stop` reaches it directly. */ export function legacyBuildEdgeRuntimeEntrypoint( files: ReadonlyArray, diff --git a/apps/cli/src/shared/functions/serve.ts b/apps/cli/src/shared/functions/serve.ts index d95edd1f39..0af53f35cf 100644 --- a/apps/cli/src/shared/functions/serve.ts +++ b/apps/cli/src/shared/functions/serve.ts @@ -1482,7 +1482,7 @@ export function buildServeEntrypointCommand( command: ReadonlyArray, multilineEnvScriptPath?: string, ) { - // `exec` so edge-runtime is PID 1; sourced env survives. Stop timing is outside Go parity (ADR 0016). + // `exec` so edge-runtime is PID 1; sourced env survives into the replacement process. return `${multilineEnvScriptPath === undefined ? "" : `. ${multilineEnvScriptPath}\n`}exec ${command.join(" ")} `; } From aba42e87ae03d6887a4180a19fec965c1d10f3d0 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 24 Aug 2026 18:13:57 +0200 Subject: [PATCH 3/3] fix(cli): exec vector in the stack runtime wrapper too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same fast-stop fix as the legacy wrappers: exec vector so it is PID 1 and docker stop reaches it directly instead of burning the 10s grace on the sh wrapper. No TERM trap needed here — the stack wrapper has no health wait-loop; readiness is handled via service dependencies. Co-Authored-By: Claude Fable 5 --- packages/stack/src/services/vector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stack/src/services/vector.ts b/packages/stack/src/services/vector.ts index de4fc9bf93..7dc003a8cb 100644 --- a/packages/stack/src/services/vector.ts +++ b/packages/stack/src/services/vector.ts @@ -106,7 +106,7 @@ export const makeVectorServiceDocker = (opts: DockerVectorOptions) => { entrypoint: "sh", cmd: [ "-c", - `cat <<'EOF' > /etc/vector/vector.yaml && vector --config /etc/vector/vector.yaml + `cat <<'EOF' > /etc/vector/vector.yaml && exec vector --config /etc/vector/vector.yaml ${vectorConfig( opts.serviceHost, opts.analyticsPort,