{"id":"69de0c6e9b660f835cf45a71","title":"2026 04 14","path":"how-to/committee/release-notes/2026-04-14","contentMarkdown":"# 14-Apr-2026 — distributed Serenity upload worker with S3 report storage [#114](https://github.com/nbarrett/ngx-ramblers/issues/114)\n\n## [build 602](https://github.com/nbarrett/ngx-ramblers/actions/runs/24391522765) — [commit a741945](https://github.com/nbarrett/ngx-ramblers/commit/a741945dcd160f68edf9029a6c672a9ef6eb43ed)\n\n_____\n\nOffloads the browser-driven Ramblers walks upload from the main website app\nto a dedicated Fly machine so the website no longer needs Serenity, Chrome\nor a 460MB Node heap reserved for it. The website enqueues jobs over an\nHTTP/HMAC-signed contract; the worker runs the playwright scenario, streams\nprogress back, uploads the generated Serenity HTML report to the website's\nown S3 bucket, and posts a final result callback with the report key.\n\n### Worker infrastructure\n- New Fly app `ngx-ramblers-upload-worker` sharing the same Docker image\n  as the website but running `npm run worker-server`.\n- Worker build + deploy gated on `dorny/paths-filter` so only\n  worker-relevant file changes trigger a rebuild; standalone\n  workflow_dispatch workflow kept as a manual escape hatch.\n- Single GitHub secret `RAMBLERS_UPLOAD_WORKER_SECRETS_JSON` carries the\n  HMAC shared secret and AES encryption key; surfaced in admin UI under\n  Global Settings and atomically synced alongside `CONFIGS_JSON`.\n- Per-environment routing done via `RAMBLERS_UPLOAD_WORKER_URL`,\n  `RAMBLERS_UPLOAD_WORKER_SHARED_SECRET` and\n  `RAMBLERS_UPLOAD_WORKER_ENCRYPTION_KEY` on each env's secrets.\n\n### Upload lifecycle\n- website dispatches to `/api/ramblers-upload-worker/jobs` with encrypted\n  walks-manager credentials and encrypted AWS credentials for the report\n  bucket, plus the target bucket/region/keyPrefix.\n- Worker decrypts, runs `npm run serenity`, streams stdout as\n  STANDARD_OUT progress, test-step events as TEST_STEP progress, and new\n  LIFECYCLE events (\"Scenario execution complete\", \"Serenity report\n  generation starting\", \"Serenity report generation complete, uploading\n  to S3\").\n- On subprocess exit, worker uploads the full `target/site/serenity`\n  directory to S3 under `ramblers-upload-reports/<fileName>` and posts\n  `/result` with `reportKeyPrefix` + `reportBucket`.\n- website records a final SUCCESS audit \"Serenity report stored at s3://...\"\n  carrying the reportKeyPrefix, broadcast over the existing upload\n  websocket so the audit appears live without a page reload.\n\n### Login path\n- `Login` screenplay task no longer wraps its full body in\n  `mongooseClient.execute`; when RAMBLERS_USERNAME / RAMBLERS_PASSWORD are\n  present in the env (as on the worker) it skips the Mongo lookup\n  entirely. website in-process runs still fall back to systemConfig via\n  Mongo as before.\n\n### Report path resolution\n- `serenityReportLocalPath()` now resolves from `process.cwd()` rather\n  than `__dirname + folderNavigationsUp`, so the Serenity report\n  directory is found on the worker regardless of NODE_ENV or compiled\n  output layout.\n\n### Walk export UI\n- Per-row \"View Report\" buttons removed; single \"View Report\" button now\n  sits next to \"Back To Walks Admin\", driven by the currently selected\n  upload session's report audit. The getter scans the full unfiltered\n  audits list so report rows beyond the 500-row cap are still found.\n- Button restyled to `btn-primary` (NGX yellow) to match the page.\n\n### Local developer ergonomics\n- `ngx-cli local dev` now re-applies `server/.env` overrides for\n  RAMBLERS_UPLOAD_WORKER_URL / _CALLBACK_BASE_URL / _SHARED_SECRET /\n  _ENCRYPTION_KEY after building the env-secret-derived env, so\n  developers can toggle between local worker (http://localhost:5002) and\n  the Fly worker purely via `.env` edits without touching code.\n- Worker websocket-server log decodes test-step-reporter payloads and\n  prints `Serenity step: \"<name>\" → SUCCESS` / outcome code, replacing\n  the old `Message received of size: N b for type: test-step-reporter`\n  noise.\n\n### Shared content-type mapping\n- `contentTypeForExtension` + `fileContentTypeMappings` consolidated in\n  `content-metadata.model.ts` as a single data-structure-driven lookup\n  used by both the server's `aws-utils.contentTypeFrom` and the S3\n  directory uploader. Covers images, web assets, fonts, archives and\n  documents; falls back to application/octet-stream. Backed by\n  `aws-utils.spec.ts` unit tests.","contentHtml":"<h1>14-Apr-2026 — distributed Serenity upload worker with S3 report storage <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/114\">#114</a></h1>\n<h2><a href=\"https://github.com/nbarrett/ngx-ramblers/actions/runs/24391522765\">build 602</a> — <a href=\"https://github.com/nbarrett/ngx-ramblers/commit/a741945dcd160f68edf9029a6c672a9ef6eb43ed\">commit a741945</a></h2>\n<hr>\n<p>Offloads the browser-driven Ramblers walks upload from the main website app\nto a dedicated Fly machine so the website no longer needs Serenity, Chrome\nor a 460MB Node heap reserved for it. The website enqueues jobs over an\nHTTP/HMAC-signed contract; the worker runs the playwright scenario, streams\nprogress back, uploads the generated Serenity HTML report to the website&#39;s\nown S3 bucket, and posts a final result callback with the report key.</p>\n<h3>Worker infrastructure</h3>\n<ul>\n<li>New Fly app <code>ngx-ramblers-upload-worker</code> sharing the same Docker image\nas the website but running <code>npm run worker-server</code>.</li>\n<li>Worker build + deploy gated on <code>dorny/paths-filter</code> so only\nworker-relevant file changes trigger a rebuild; standalone\nworkflow_dispatch workflow kept as a manual escape hatch.</li>\n<li>Single GitHub secret <code>RAMBLERS_UPLOAD_WORKER_SECRETS_JSON</code> carries the\nHMAC shared secret and AES encryption key; surfaced in admin UI under\nGlobal Settings and atomically synced alongside <code>CONFIGS_JSON</code>.</li>\n<li>Per-environment routing done via <code>RAMBLERS_UPLOAD_WORKER_URL</code>,\n<code>RAMBLERS_UPLOAD_WORKER_SHARED_SECRET</code> and\n<code>RAMBLERS_UPLOAD_WORKER_ENCRYPTION_KEY</code> on each env&#39;s secrets.</li>\n</ul>\n<h3>Upload lifecycle</h3>\n<ul>\n<li>website dispatches to <code>/api/ramblers-upload-worker/jobs</code> with encrypted\nwalks-manager credentials and encrypted AWS credentials for the report\nbucket, plus the target bucket/region/keyPrefix.</li>\n<li>Worker decrypts, runs <code>npm run serenity</code>, streams stdout as\nSTANDARD_OUT progress, test-step events as TEST_STEP progress, and new\nLIFECYCLE events (&quot;Scenario execution complete&quot;, &quot;Serenity report\ngeneration starting&quot;, &quot;Serenity report generation complete, uploading\nto S3&quot;).</li>\n<li>On subprocess exit, worker uploads the full <code>target/site/serenity</code>\ndirectory to S3 under <code>ramblers-upload-reports/&lt;fileName&gt;</code> and posts\n<code>/result</code> with <code>reportKeyPrefix</code> + <code>reportBucket</code>.</li>\n<li>website records a final SUCCESS audit &quot;Serenity report stored at s3://...&quot;\ncarrying the reportKeyPrefix, broadcast over the existing upload\nwebsocket so the audit appears live without a page reload.</li>\n</ul>\n<h3>Login path</h3>\n<ul>\n<li><code>Login</code> screenplay task no longer wraps its full body in\n<code>mongooseClient.execute</code>; when RAMBLERS_USERNAME / RAMBLERS_PASSWORD are\npresent in the env (as on the worker) it skips the Mongo lookup\nentirely. website in-process runs still fall back to systemConfig via\nMongo as before.</li>\n</ul>\n<h3>Report path resolution</h3>\n<ul>\n<li><code>serenityReportLocalPath()</code> now resolves from <code>process.cwd()</code> rather\nthan <code>__dirname + folderNavigationsUp</code>, so the Serenity report\ndirectory is found on the worker regardless of NODE_ENV or compiled\noutput layout.</li>\n</ul>\n<h3>Walk export UI</h3>\n<ul>\n<li>Per-row &quot;View Report&quot; buttons removed; single &quot;View Report&quot; button now\nsits next to &quot;Back To Walks Admin&quot;, driven by the currently selected\nupload session&#39;s report audit. The getter scans the full unfiltered\naudits list so report rows beyond the 500-row cap are still found.</li>\n<li>Button restyled to <code>btn-primary</code> (NGX yellow) to match the page.</li>\n</ul>\n<h3>Local developer ergonomics</h3>\n<ul>\n<li><code>ngx-cli local dev</code> now re-applies <code>server/.env</code> overrides for\nRAMBLERS_UPLOAD_WORKER_URL / _CALLBACK_BASE_URL / _SHARED_SECRET /\n_ENCRYPTION_KEY after building the env-secret-derived env, so\ndevelopers can toggle between local worker (<a href=\"http://localhost:5002\">http://localhost:5002</a>) and\nthe Fly worker purely via <code>.env</code> edits without touching code.</li>\n<li>Worker websocket-server log decodes test-step-reporter payloads and\nprints <code>Serenity step: &quot;&lt;name&gt;&quot; → SUCCESS</code> / outcome code, replacing\nthe old <code>Message received of size: N b for type: test-step-reporter</code>\nnoise.</li>\n</ul>\n<h3>Shared content-type mapping</h3>\n<ul>\n<li><code>contentTypeForExtension</code> + <code>fileContentTypeMappings</code> consolidated in\n<code>content-metadata.model.ts</code> as a single data-structure-driven lookup\nused by both the server&#39;s <code>aws-utils.contentTypeFrom</code> and the S3\ndirectory uploader. Covers images, web assets, fonts, archives and\ndocuments; falls back to application/octet-stream. Backed by\n<code>aws-utils.spec.ts</code> unit tests.</li>\n</ul>\n"}