fix(build): restore @swc/helpers in the standalone bundle, health-check the deploy - #243
Conversation
…ck the deploy next 16.3.1 traces only the cjs half of @swc/helpers into output: standalone, while its own require-hook loads the ESM entry point. The bundle starts and then dies on the first request needing a helper: Cannot find module .../@swc/helpers/esm/_interop_require_default.js raised as an unhandledRejection. The process stays alive without ever binding the port, so PM2 reports it online while every request 503s. That took forms.msk-scripts.de down after the 16.2.12 -> 16.3.1 bump in #240. copy-standalone-assets.mjs already exists for this exact class of problem, it puts sharp and the @img closure back after Next declines to trace them. Add @swc/helpers to it, copying the whole package per store entry rather than the one missing directory, so a change to which half gets traced cannot bring this back. Version-matched: grafting the esm of one version onto another happens to work but is not something to ship. The deploy also gets a health check. It ran green through this outage because it only asked PM2, which answers about the process, not about whether anything is listening. Curl the port instead, retry for a minute, and dump the error log before failing. A deploy that cannot serve should not report success.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Graphify review — findings
Adds a copySwcHelpers() step to copy-standalone-assets.mjs that restores the full @swc/helpers package (per version-matched pnpm store entry) into the standalone bundle, since Next 16.3.1 only traces the cjs/ half while its require-hook loads the ESM entry. Adds a port-level health check to the deploy workflow that polls http://127.0.0.1:3008/ after pm2 save, dumps error logs and fails the job if it never returns 200.
Worth a look
- Broken PM2 process is saved before the new health check can reject it —
.github/workflows/deploy.yml:60· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
- Deploy success now requires root URL to return exactly HTTP 200 —
.github/workflows/deploy.yml:70· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 9 functions depend on the 9 functions this change touches.
Health — grade A; no new coupling hotspots.
Verification — 9 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 9 function(s) in the blast radius were not formally verified this run
…t serve (#246) The health check added in #243 turned a silent outage into a loud one, but it still leaves the site down: by the time it runs, pm2 reload has already swapped the process. On 2026-08-17 that would have meant a red workflow and a 503 until someone read it. So keep the previous bundle instead of deleting it. The build moves apps/web/.next aside rather than removing it, and if the port never answers, the deploy puts the old release back: reset to the previous SHA, reinstall against that lockfile, regenerate the client, restore the bundle, reload. The job still exits non-zero, and the error says plainly which commit is live. What this deliberately does not undo is the migration. `prisma migrate deploy` has already run and migrations are forward-only. That is fine for additive changes, since older code ignores columns it does not know about, but a migration that drops or renames something still needs a human. The comment in the workflow says so. pm2 save moved behind the health check as well. Persisting the process list before knowing whether the release works only makes a bad state stickier. Verified by running the script against stubbed git/pnpm/pm2/curl in a sandbox, since the failure path is the one that must not be discovered in production: a healthy deploy keeps the new bundle and exits 0; an unhealthy one with a recoverable predecessor restores the old bundle, reports that the pushed commit is not live and exits 1; and when the rollback cannot come up either, it says that instead of claiming success.
Fixes the production outage that followed the
next16.2.12 -> 16.3.1 bump in #240.What broke
next 16.3.1 traces only the
cjs/half of@swc/helpersintooutput: standalone, but its ownrequire-hookloads the ESM entry point:It surfaces as an unhandledRejection during startup. The process stays alive and never binds the port, so PM2 reported
onlinewith a climbing restart counter whilecurl 127.0.0.1:3008got no connection at all and every public request returned 503.Reproduced locally on a clean build of the same commit, so this is the bundle, not the server:
node_modules@swc/helpers/cjs@swc/helpers/esmThe fix
copy-standalone-assets.mjsalready exists for exactly this, it putssharpand the@imgclosure back after Next declines to trace them.@swc/helpersjoins that list.It copies the whole package per pnpm store entry rather than just the missing directory, so a future change to which half gets traced cannot reintroduce this quietly. Version-matched per entry: during recovery we grafted the
esm/of 0.5.15 onto 0.5.23 and it worked, which is a good reason not to rely on that.Verified on a clean rebuild:
Why nothing noticed
The deploy ran green four times through the outage. It asked PM2, and PM2 answers about the process, not about whether anything is listening.
pm2 reloadreturning ✓ is compatible with a bundle that cannot serve a single request.So the workflow now curls
127.0.0.1:3008after the reload, retries for a minute, and on failure dumps the last 40 error-log lines before exiting non-zero. That check alone would have caught this at the first deploy instead of 40 minutes later.Not changed:
concurrencywithcancel-in-progress: falseand therm -rf apps/web/.nextbefore the build were already in place, deploys never overlapped and the tree was not stale.Follow-up worth considering
The health check reports the failure but does not undo it,
pm2 reloadhas already swapped the process by then. An automatic rollback to the previous build would turn this from "loud failure" into "no outage", at the cost of keeping a previous build around on the server. Happy to do that separately if you want it.