{"id":"69fb87632d3be50e28f7c738","title":"2026 05 04","path":"how-to/committee/release-notes/2026-05-04","contentMarkdown":"# 04-May-2026 — make Create Environment wizard work end-to-end [closes #258](https://github.com/nbarrett/ngx-ramblers/issues/258)\n\n## [build 637](https://github.com/nbarrett/ngx-ramblers/actions/runs/25450246053) — [commit 8fb1097](https://github.com/nbarrett/ngx-ramblers/commit/8fb1097b0777db28a6903d0592796a9f70bd357b)\n\n_____\n\nThe Environment Setup wizard at `/admin/environment-management/setup`\ndid not work on any deployed machine. Five failure modes stacked on\ntop of each other depending on how far a run got. This change addresses\nall of them so that clicking Create Environment, on a deployed machine,\nends up with a fully working Fly app, and clicking Resume on a\nhalf-completed environment finishes cleanly with no manual `flyctl`\nintervention.\n\n## Failure mode 1: persistent `non-vcs/fly-io/configs.json` read\n\nThe wizard ran inside the production Fly machine. `non-vcs/` is\ngitignored and never makes it into the Docker image, so the deploy\ndied on `ENOENT` and the residual code path then called\n`process.exit(1)`, taking the whole production server with it. Both\nwizard entry points (`handleEnvironmentCreate` and\n`handleEnvironmentSetup`) hit the same read at `deployToFlyio:141`.\n\n### What changed\n\n- Deleted `server/lib/shared/configs-json.ts` outright.\n- Deleted `server/deploy/manage-configs.ts` (the only remaining writer\nof `configs.json` outside the wizard) and its npm script.\n- `server/lib/cli/commands/fly.ts` now reads `dockerImage` from the\n`ConfigKey.ENVIRONMENTS` document via a new\n`dockerImageFromDatabase()` helper. After a successful deploy it\npersists the env entry through `upsertEnvironmentInDatabase` instead\nof writing a JSON file.\n- Added optional `dockerImage` and `region` fields to\n`EnvironmentsConfig` plus a `DEPLOYMENT_DEFAULTS` constant\n(`nbarrett36/ngx-ramblers:latest` / `lhr`) matching what the\npublished image actually is and the region the user has been\ndeploying to. `region` is wired through `buildDeploymentConfig` so\nthe legacy CLI deploy path also reads it from the DB.\n- `server/lib/cli/commands/destroy.ts` lost its `--skip-configs` flag\nand now removes the env entry through\n`removeEnvironmentFromDatabase`.\n- `server/lib/environments/environments-config.ts` rewritten:\n`loadFromFiles()` and the interactive \"fall back to file-based\nconfigs.json?\" prompt removed; `configuredEnvironments()` throws on\na missing DB document instead of exiting; added\n`upsertEnvironmentInDatabase` and `removeEnvironmentFromDatabase`.\n- `server/lib/backup/config-initializer.ts` seeds from the DB.\n- `server/deploy/config-loader.ts` and `server/deploy/mongo-cli.ts`\nread configs through the DB-backed `loadConfigs()` helper.\n- `server/deploy/deploy-to-environments.ts` and\n`server/deploy/deploy-integration-worker.ts` had duplicate\nhard-coded image strings. Both now read from\n`DEPLOYMENT_DEFAULTS.DOCKER_IMAGE`.\n\n## Failure mode 2: errors never reached the browser\n\n`readConfigFile` and several other helpers called `process.exit(1)`\non failure. From inside an HTTP/WebSocket request handler that killed\nthe production process before the wizard's `sendError(ws, ...)` could\nfire. Fly then restarted the machine.\n\n### What changed\n\n- `readConfigFile` removed entirely. `runCommand` lost its\n`throwOnError` boolean and now always throws. Fly volume helpers\nthrow rather than exit.\n- `configuredEnvironments()` and `findEnvironmentFromDatabase()`\nthrow on missing DB config instead of exiting.\n- Audited every `process.exit(...)` call in `server/lib` and\n`server/deploy`. The remaining exits are all in CLI-only entry\npoints; none reachable from request handlers.\n\n## Failure mode 3: AUTH_SECRET (and other generated secrets) were not persisted\n\nThe wizard generated `AUTH_SECRET` and a full secrets bundle, wrote\nthem to a local file under `non-vcs/secrets/` (useless on a deployed\nmachine), and passed them in-memory to `deployToFlyio`. The first\ndeploy worked because `flyctl secrets import` ran with that in-memory\nbundle, but the resume flow loaded secrets from the DB - and the DB\nrecord never had `AUTH_SECRET` or the rest of the bundle written\ninto it.\n\n### What changed\n\n- `updateEnvironmentsConfig` in\n`server/lib/environment-setup/environment-setup-service.ts` now\npersists the full `secrets` bundle to `envConfig.secrets`. The\nlocal `writeSecretsFile` call is gone, and the `writeSecretsFile`\nand `secretsPath` imports with it.\n- `deployToFlyio` performs a `REQUIRED_SECRETS` pre-flight check\nbefore running `flyctl deploy`. If any required secret is missing\nit throws with a clear list of missing keys, which the wizard\nrelays through `sendError(...)`.\n- The Fly secrets-import file is now an `os.tmpdir()` temp file\nunlinked in a `finally` block; no per-app secrets file lingers on\ndisk.\n- The CLI `ngx-cli fly deploy <name>` action uses\n`loadSecretsWithFallback` so it works on a deployed machine where\nthe local file no longer exists.\n\n## Failure mode 4: NODE_ENV-dependent path resolver caused silent SPA 404s\n\nDiscovered while testing on a freshly resumed environment. After the\nAUTH_SECRET pre-flight passed, the deployed Fly app booted but every\nrequest to `/` returned `Cannot GET /`. Root cause:\n`server/lib/shared/path-utils.ts` had a `resolveClientPath` whose\noutput depended on a runtime check of `NODE_ENV === \"production\"`.\nThe wizard's `buildSecretsConfig` always sets `NODE_ENV: \"production\"`\nfor new environments, but environments created during the original bug\nwindow had an incomplete persisted secrets bundle and the resume flow\nimported only the keys it could reconstruct, which did not include\n`NODE_ENV`. The Fly app booted with `NODE_ENV` empty, the resolver took\nthe dev branch, the resolved `distFolder` did not exist, the\nstatic-asset middleware was skipped, and Express fell through to\ndefault 404.\n\n### What changed\n\n- Rewrote `server/lib/shared/path-utils.ts` to anchor on the project\nroot by walking up from `__dirname` looking for `fly.toml` - a\nstable marker present in dev (at the repo root) and in the deployed\nimage (at `/usr/src/app`). The `navigateUpFromCurrentExecutionDirectory`\nhelper and the `isProduction()` branch are gone. `resolveClientPath`\nreturns paths relative to the project root, `resolveServerPath`\nreturns paths relative to `<projectRoot>/server`. Same input\nproduces the same output regardless of `NODE_ENV`.\n- Added `NODE_ENV` to `REQUIRED_SECRETS` in\n`server/lib/shared/secrets.ts`. The path resolver no longer cares\nabout `NODE_ENV`, but the surrounding app code does (env-config,\nisProduction guards elsewhere) and a missing `NODE_ENV` is a real\nmisconfiguration worth catching at deploy time rather than as a\nsilent runtime regression.\n\n## Failure mode 5: brevo template migration ENOENT shared the same root cause\n\nThe migration\n`server/lib/mongo/migrations/database/20260120000000-update-brevo-transactional-template.ts`\nreads brevo HTML templates via `localTemplatePath` in\n`server/lib/brevo/templates/local-template-reader.ts`, which calls\n`resolveClientPath`. On the deployed container during the wizard's\ninitial bootstrap, `NODE_ENV` was unset, so `resolveClientPath`\nreturned `/usr/src/app/server/ts-gen/projects/ngx-ramblers/src/brevo/templates/fully-automated-text-body.html` -\na path that does not exist in the image. ENOENT, migration FAILED,\nwizard's \"Migration Failed\" page rendered. A retry succeeded only\nbecause by then the user was running locally, where the resolver\nlanded somewhere that happened to have the files.\n\n### What changed\n\nThe failure mode 4 fix is the entire fix here - `localTemplatePath`\nnow lands at `<projectRoot>/projects/ngx-ramblers/src/brevo/templates/<name>.html`\nconsistently, regardless of `NODE_ENV` or which machine the migration\nruns on. No changes to the migration or the brevo resolver were\nneeded.\n\n## Cleanup\n\n- Removed the dead duplicate `generateAuthSecret` function in\n`server/lib/cli/commands/fly.ts`. The active one is in\n`environment-setup-service.ts`.\n\n## How they slipped through\n\nAll five failure modes share the same shape: deployment-environment-only\nbugs that dev laptops never exercise, because dev laptops happen to\nsatisfy the fragile assumptions by accident - they have the local\n`non-vcs/` files, they always run with `NODE_ENV` set in the shell,\nand they have the source tree sitting at the resolved paths regardless\nof how the resolver computes them. Wizard / migration changes need at\nleast one CI run that exercises a clean container - no host\nfilesystem, no inherited shell env - before being declared green.\n\n## Verification\n\n- `npm run lint` clean\n- `npm run lint:server` clean\n- `npx tsc --noEmit` for `server/tsconfig.json` and the frontend\ntsconfig clean\n- `npm run test:server` passes 619 mocha tests\n- `grep -rn \"configs\\.json\" server/` (excluding tooling JSONs) and\n`grep -rn \"non-vcs/fly-io\" server/` both empty\n- `grep -rn \"writeSecretsFile\" server/lib/environment-setup/` empty\n- `grep -rn \"nbarrett.*/ngx-ramblers\" server/ projects/` returns\nexactly one match: the single `DEPLOYMENT_DEFAULTS.DOCKER_IMAGE`\ndeclaration\n- `grep -rn \"navigateUpFromCurrentExecutionDirectory\\|isProduction\" server/lib/shared/`\nempty - the path resolver no longer reads `NODE_ENV`\n\n## End-to-end behaviour\n\nClicking Create Environment in the wizard, on a deployed machine,\nends up with a fully working Fly app: the env record and full secrets\nbundle (including `NODE_ENV`) are written to the DB, `dockerImage` is\nread from the DB, the deploy runs with secrets passed in-memory and\nimported to Fly, the env entry is upserted with the deploy token at\nthe end, the SPA serves `/`, and migrations that read bundled assets\nresolve them from the project root regardless of `NODE_ENV` state.\nClicking Resume on a half-completed env reads the full secrets bundle\nback from the DB and produces a working app without any manual\n`flyctl secrets set`. A simulated failure inside `deployToFlyio` (bad\nFly API token, missing required secret) produces a red error in the\nwizard UI with the failure details, and the production server stays\nup.\n\n## Out of scope\n\n- The vestigial `EnvironmentSetupResult.configsJsonUpdated` boolean\nwas left as-is. Renaming it touches the wizard component and\nwebsocket payload shape, which would broaden the patch.","contentHtml":"<h1>04-May-2026 — make Create Environment wizard work end-to-end <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/258\">closes #258</a></h1>\n<h2><a href=\"https://github.com/nbarrett/ngx-ramblers/actions/runs/25450246053\">build 637</a> — <a href=\"https://github.com/nbarrett/ngx-ramblers/commit/8fb1097b0777db28a6903d0592796a9f70bd357b\">commit 8fb1097</a></h2>\n<hr>\n<p>The Environment Setup wizard at <code>/admin/environment-management/setup</code>\ndid not work on any deployed machine. Five failure modes stacked on\ntop of each other depending on how far a run got. This change addresses\nall of them so that clicking Create Environment, on a deployed machine,\nends up with a fully working Fly app, and clicking Resume on a\nhalf-completed environment finishes cleanly with no manual <code>flyctl</code>\nintervention.</p>\n<h2>Failure mode 1: persistent <code>non-vcs/fly-io/configs.json</code> read</h2>\n<p>The wizard ran inside the production Fly machine. <code>non-vcs/</code> is\ngitignored and never makes it into the Docker image, so the deploy\ndied on <code>ENOENT</code> and the residual code path then called\n<code>process.exit(1)</code>, taking the whole production server with it. Both\nwizard entry points (<code>handleEnvironmentCreate</code> and\n<code>handleEnvironmentSetup</code>) hit the same read at <code>deployToFlyio:141</code>.</p>\n<h3>What changed</h3>\n<ul>\n<li>Deleted <code>server/lib/shared/configs-json.ts</code> outright.</li>\n<li>Deleted <code>server/deploy/manage-configs.ts</code> (the only remaining writer\nof <code>configs.json</code> outside the wizard) and its npm script.</li>\n<li><code>server/lib/cli/commands/fly.ts</code> now reads <code>dockerImage</code> from the\n<code>ConfigKey.ENVIRONMENTS</code> document via a new\n<code>dockerImageFromDatabase()</code> helper. After a successful deploy it\npersists the env entry through <code>upsertEnvironmentInDatabase</code> instead\nof writing a JSON file.</li>\n<li>Added optional <code>dockerImage</code> and <code>region</code> fields to\n<code>EnvironmentsConfig</code> plus a <code>DEPLOYMENT_DEFAULTS</code> constant\n(<code>nbarrett36/ngx-ramblers:latest</code> / <code>lhr</code>) matching what the\npublished image actually is and the region the user has been\ndeploying to. <code>region</code> is wired through <code>buildDeploymentConfig</code> so\nthe legacy CLI deploy path also reads it from the DB.</li>\n<li><code>server/lib/cli/commands/destroy.ts</code> lost its <code>--skip-configs</code> flag\nand now removes the env entry through\n<code>removeEnvironmentFromDatabase</code>.</li>\n<li><code>server/lib/environments/environments-config.ts</code> rewritten:\n<code>loadFromFiles()</code> and the interactive &quot;fall back to file-based\nconfigs.json?&quot; prompt removed; <code>configuredEnvironments()</code> throws on\na missing DB document instead of exiting; added\n<code>upsertEnvironmentInDatabase</code> and <code>removeEnvironmentFromDatabase</code>.</li>\n<li><code>server/lib/backup/config-initializer.ts</code> seeds from the DB.</li>\n<li><code>server/deploy/config-loader.ts</code> and <code>server/deploy/mongo-cli.ts</code>\nread configs through the DB-backed <code>loadConfigs()</code> helper.</li>\n<li><code>server/deploy/deploy-to-environments.ts</code> and\n<code>server/deploy/deploy-integration-worker.ts</code> had duplicate\nhard-coded image strings. Both now read from\n<code>DEPLOYMENT_DEFAULTS.DOCKER_IMAGE</code>.</li>\n</ul>\n<h2>Failure mode 2: errors never reached the browser</h2>\n<p><code>readConfigFile</code> and several other helpers called <code>process.exit(1)</code>\non failure. From inside an HTTP/WebSocket request handler that killed\nthe production process before the wizard&#39;s <code>sendError(ws, ...)</code> could\nfire. Fly then restarted the machine.</p>\n<h3>What changed</h3>\n<ul>\n<li><code>readConfigFile</code> removed entirely. <code>runCommand</code> lost its\n<code>throwOnError</code> boolean and now always throws. Fly volume helpers\nthrow rather than exit.</li>\n<li><code>configuredEnvironments()</code> and <code>findEnvironmentFromDatabase()</code>\nthrow on missing DB config instead of exiting.</li>\n<li>Audited every <code>process.exit(...)</code> call in <code>server/lib</code> and\n<code>server/deploy</code>. The remaining exits are all in CLI-only entry\npoints; none reachable from request handlers.</li>\n</ul>\n<h2>Failure mode 3: AUTH_SECRET (and other generated secrets) were not persisted</h2>\n<p>The wizard generated <code>AUTH_SECRET</code> and a full secrets bundle, wrote\nthem to a local file under <code>non-vcs/secrets/</code> (useless on a deployed\nmachine), and passed them in-memory to <code>deployToFlyio</code>. The first\ndeploy worked because <code>flyctl secrets import</code> ran with that in-memory\nbundle, but the resume flow loaded secrets from the DB - and the DB\nrecord never had <code>AUTH_SECRET</code> or the rest of the bundle written\ninto it.</p>\n<h3>What changed</h3>\n<ul>\n<li><code>updateEnvironmentsConfig</code> in\n<code>server/lib/environment-setup/environment-setup-service.ts</code> now\npersists the full <code>secrets</code> bundle to <code>envConfig.secrets</code>. The\nlocal <code>writeSecretsFile</code> call is gone, and the <code>writeSecretsFile</code>\nand <code>secretsPath</code> imports with it.</li>\n<li><code>deployToFlyio</code> performs a <code>REQUIRED_SECRETS</code> pre-flight check\nbefore running <code>flyctl deploy</code>. If any required secret is missing\nit throws with a clear list of missing keys, which the wizard\nrelays through <code>sendError(...)</code>.</li>\n<li>The Fly secrets-import file is now an <code>os.tmpdir()</code> temp file\nunlinked in a <code>finally</code> block; no per-app secrets file lingers on\ndisk.</li>\n<li>The CLI <code>ngx-cli fly deploy &lt;name&gt;</code> action uses\n<code>loadSecretsWithFallback</code> so it works on a deployed machine where\nthe local file no longer exists.</li>\n</ul>\n<h2>Failure mode 4: NODE_ENV-dependent path resolver caused silent SPA 404s</h2>\n<p>Discovered while testing on a freshly resumed environment. After the\nAUTH_SECRET pre-flight passed, the deployed Fly app booted but every\nrequest to <code>/</code> returned <code>Cannot GET /</code>. Root cause:\n<code>server/lib/shared/path-utils.ts</code> had a <code>resolveClientPath</code> whose\noutput depended on a runtime check of <code>NODE_ENV === &quot;production&quot;</code>.\nThe wizard&#39;s <code>buildSecretsConfig</code> always sets <code>NODE_ENV: &quot;production&quot;</code>\nfor new environments, but environments created during the original bug\nwindow had an incomplete persisted secrets bundle and the resume flow\nimported only the keys it could reconstruct, which did not include\n<code>NODE_ENV</code>. The Fly app booted with <code>NODE_ENV</code> empty, the resolver took\nthe dev branch, the resolved <code>distFolder</code> did not exist, the\nstatic-asset middleware was skipped, and Express fell through to\ndefault 404.</p>\n<h3>What changed</h3>\n<ul>\n<li>Rewrote <code>server/lib/shared/path-utils.ts</code> to anchor on the project\nroot by walking up from <code>__dirname</code> looking for <code>fly.toml</code> - a\nstable marker present in dev (at the repo root) and in the deployed\nimage (at <code>/usr/src/app</code>). The <code>navigateUpFromCurrentExecutionDirectory</code>\nhelper and the <code>isProduction()</code> branch are gone. <code>resolveClientPath</code>\nreturns paths relative to the project root, <code>resolveServerPath</code>\nreturns paths relative to <code>&lt;projectRoot&gt;/server</code>. Same input\nproduces the same output regardless of <code>NODE_ENV</code>.</li>\n<li>Added <code>NODE_ENV</code> to <code>REQUIRED_SECRETS</code> in\n<code>server/lib/shared/secrets.ts</code>. The path resolver no longer cares\nabout <code>NODE_ENV</code>, but the surrounding app code does (env-config,\nisProduction guards elsewhere) and a missing <code>NODE_ENV</code> is a real\nmisconfiguration worth catching at deploy time rather than as a\nsilent runtime regression.</li>\n</ul>\n<h2>Failure mode 5: brevo template migration ENOENT shared the same root cause</h2>\n<p>The migration\n<code>server/lib/mongo/migrations/database/20260120000000-update-brevo-transactional-template.ts</code>\nreads brevo HTML templates via <code>localTemplatePath</code> in\n<code>server/lib/brevo/templates/local-template-reader.ts</code>, which calls\n<code>resolveClientPath</code>. On the deployed container during the wizard&#39;s\ninitial bootstrap, <code>NODE_ENV</code> was unset, so <code>resolveClientPath</code>\nreturned <code>/usr/src/app/server/ts-gen/projects/ngx-ramblers/src/brevo/templates/fully-automated-text-body.html</code> -\na path that does not exist in the image. ENOENT, migration FAILED,\nwizard&#39;s &quot;Migration Failed&quot; page rendered. A retry succeeded only\nbecause by then the user was running locally, where the resolver\nlanded somewhere that happened to have the files.</p>\n<h3>What changed</h3>\n<p>The failure mode 4 fix is the entire fix here - <code>localTemplatePath</code>\nnow lands at <code>&lt;projectRoot&gt;/projects/ngx-ramblers/src/brevo/templates/&lt;name&gt;.html</code>\nconsistently, regardless of <code>NODE_ENV</code> or which machine the migration\nruns on. No changes to the migration or the brevo resolver were\nneeded.</p>\n<h2>Cleanup</h2>\n<ul>\n<li>Removed the dead duplicate <code>generateAuthSecret</code> function in\n<code>server/lib/cli/commands/fly.ts</code>. The active one is in\n<code>environment-setup-service.ts</code>.</li>\n</ul>\n<h2>How they slipped through</h2>\n<p>All five failure modes share the same shape: deployment-environment-only\nbugs that dev laptops never exercise, because dev laptops happen to\nsatisfy the fragile assumptions by accident - they have the local\n<code>non-vcs/</code> files, they always run with <code>NODE_ENV</code> set in the shell,\nand they have the source tree sitting at the resolved paths regardless\nof how the resolver computes them. Wizard / migration changes need at\nleast one CI run that exercises a clean container - no host\nfilesystem, no inherited shell env - before being declared green.</p>\n<h2>Verification</h2>\n<ul>\n<li><code>npm run lint</code> clean</li>\n<li><code>npm run lint:server</code> clean</li>\n<li><code>npx tsc --noEmit</code> for <code>server/tsconfig.json</code> and the frontend\ntsconfig clean</li>\n<li><code>npm run test:server</code> passes 619 mocha tests</li>\n<li><code>grep -rn &quot;configs\\.json&quot; server/</code> (excluding tooling JSONs) and\n<code>grep -rn &quot;non-vcs/fly-io&quot; server/</code> both empty</li>\n<li><code>grep -rn &quot;writeSecretsFile&quot; server/lib/environment-setup/</code> empty</li>\n<li><code>grep -rn &quot;nbarrett.*/ngx-ramblers&quot; server/ projects/</code> returns\nexactly one match: the single <code>DEPLOYMENT_DEFAULTS.DOCKER_IMAGE</code>\ndeclaration</li>\n<li><code>grep -rn &quot;navigateUpFromCurrentExecutionDirectory\\|isProduction&quot; server/lib/shared/</code>\nempty - the path resolver no longer reads <code>NODE_ENV</code></li>\n</ul>\n<h2>End-to-end behaviour</h2>\n<p>Clicking Create Environment in the wizard, on a deployed machine,\nends up with a fully working Fly app: the env record and full secrets\nbundle (including <code>NODE_ENV</code>) are written to the DB, <code>dockerImage</code> is\nread from the DB, the deploy runs with secrets passed in-memory and\nimported to Fly, the env entry is upserted with the deploy token at\nthe end, the SPA serves <code>/</code>, and migrations that read bundled assets\nresolve them from the project root regardless of <code>NODE_ENV</code> state.\nClicking Resume on a half-completed env reads the full secrets bundle\nback from the DB and produces a working app without any manual\n<code>flyctl secrets set</code>. A simulated failure inside <code>deployToFlyio</code> (bad\nFly API token, missing required secret) produces a red error in the\nwizard UI with the failure details, and the production server stays\nup.</p>\n<h2>Out of scope</h2>\n<ul>\n<li>The vestigial <code>EnvironmentSetupResult.configsJsonUpdated</code> boolean\nwas left as-is. Renaming it touches the wizard component and\nwebsocket payload shape, which would broaden the patch.</li>\n</ul>\n"}