From f79302ccd95b4e599a1e760f0a636c69bf5f8f2c Mon Sep 17 00:00:00 2001 From: "Andrew B." <37745667+AndrianBalanescu@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:05:41 -0500 Subject: [PATCH] fix(bootstrap): filter empty process.env values to prevent Docker env crash loop (#6828) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first); keeps only the author's bootstrap change. Co-authored-by: Andrian B. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- .../fixes/6828-bootstrap-filter-empty-env.md | 1 + scripts/build/bootstrap-env.mjs | 9 ++++++- tests/unit/bootstrap-env.test.ts | 24 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/6828-bootstrap-filter-empty-env.md diff --git a/changelog.d/fixes/6828-bootstrap-filter-empty-env.md b/changelog.d/fixes/6828-bootstrap-filter-empty-env.md new file mode 100644 index 0000000000..d4a6ca34ad --- /dev/null +++ b/changelog.d/fixes/6828-bootstrap-filter-empty-env.md @@ -0,0 +1 @@ +- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). diff --git a/scripts/build/bootstrap-env.mjs b/scripts/build/bootstrap-env.mjs index 90ee46ddd6..e3bdd673ec 100644 --- a/scripts/build/bootstrap-env.mjs +++ b/scripts/build/bootstrap-env.mjs @@ -173,7 +173,14 @@ export function bootstrapEnv({ dataDirOverride, quiet = false } = {}) { const preferredEnvFiltered = Object.fromEntries( Object.entries(preferredEnv).filter(([, v]) => typeof v === "string" && v.length > 0) ); - const merged = { ...persisted, ...preferredEnvFiltered, ...process.env }; + // Filter empty strings from process.env so that Docker `-e KEY=` (which sets an + // empty string) does not override real values persisted in server.env or set + // in .env. Only shell/Docker vars that the operator actually set should win. + // Mirrors the filtering already applied to preferredEnv above. (fixes #6824) + const processEnvFiltered = Object.fromEntries( + Object.entries(process.env).filter(([, v]) => typeof v === "string" && v.length > 0) + ); + const merged = { ...persisted, ...preferredEnvFiltered, ...processEnvFiltered }; // ── Auto-generate required secrets ──────────────────────────────────────── let needsPersist = false; diff --git a/tests/unit/bootstrap-env.test.ts b/tests/unit/bootstrap-env.test.ts index 45d2911776..204ceea672 100644 --- a/tests/unit/bootstrap-env.test.ts +++ b/tests/unit/bootstrap-env.test.ts @@ -120,6 +120,30 @@ test("bootstrapEnv fails closed when existing database cannot be inspected", () }); }); +test("bootstrapEnv ignores blank process.env values that would override persisted secrets (#6824)", () => { + withTempEnv(({ dataDir }) => { + process.env.DATA_DIR = dataDir; + fs.mkdirSync(dataDir, { recursive: true }); + + // Persisted secrets in server.env + fs.writeFileSync( + path.join(dataDir, "server.env"), + "STORAGE_ENCRYPTION_KEY=persisted-key\nJWT_SECRET=persisted-jwt\n", + "utf8" + ); + + // Simulate Docker `-e STORAGE_ENCRYPTION_KEY=` — sets an empty string + process.env.STORAGE_ENCRYPTION_KEY = ""; + process.env.JWT_SECRET = ""; + + const env = bootstrapEnv({ quiet: true }); + + // Empty process.env values must NOT override persisted secrets + assert.equal(env.STORAGE_ENCRYPTION_KEY, "persisted-key"); + assert.equal(env.JWT_SECRET, "persisted-jwt"); + }); +}); + test("bootstrapEnv ignores blank dataDirOverride values", () => { withTempEnv(({ dataDir }) => { process.env.DATA_DIR = dataDir;