A pre-deploy audit of the GAE deploy pipeline found several CI/build-gate gaps. PR #810 only pinned engines.pnpm in the staged manifest and did not address any of these. They share a root cause: the CI gate that is supposed to validate what actually deploys does not faithfully reproduce the production runtime or the staged-deploy path, so whole classes of deploy-time breakage are invisible until gcloud app deploy.
These are distinct from the existing deploy tech-debt items (#39 install bloat / staged-deploy implementation, #40 mutating tracked public/) and from the Rust feature-matrix gap (tech-debt.md item 60). This issue is specifically about the CI gate fidelity for the web deploy.
Gap 1 -- Node major skew between the deploy gate and prod
The frontend job that runs the deploy-assembly verification installs Node 22 (.github/workflows/ci.yaml:178, node-version: '22'), but production runs nodejs24 (app.yaml:1 runtime: nodejs24, mirrored by .app.prod.yaml). The artifacts that actually deploy are never built or tested on Node 24. The serve-smoke job in the same workflow already uses Node 24 (ci.yaml:362), so the deploy gate is the odd one out.
Fix: pin the deploy-assembly job to node-version: '24'.
Gap 2 -- No Node version is pinned anywhere (engine-strict is a no-op)
.npmrc sets engine-strict=true, but no package.json declares engines.node -- not the root and not any src/* package. The root only declares packageManager: pnpm@10.6.0 (package.json:12). With no engines.node field, engine-strict has nothing to enforce, so a contributor on the wrong Node major gets no signal.
Fix: add engines.node (e.g. ">=24 <25") to the root package.json and/or add a .nvmrc, so engine-strict actually gates and local/CI Node majors converge on prod's.
Gap 3 -- deploy-assembly CI job installs with a non-frozen lockfile
The frontend job runs pnpm install (ci.yaml:205) WITHOUT --frozen-lockfile, so lockfile drift is silently auto-corrected in the very gate meant to validate the deploy. By contrast serve-smoke uses pnpm install --frozen-lockfile (ci.yaml:378). Lockfile drift that would fail a frozen install at deploy time (or under the GAE buildpack) passes this gate.
Fix: add --frozen-lockfile to the deploy-assembly install.
Gap 4 -- The staged-bundle deploy path is never exercised in CI
The new staged path (scripts/build-deploy-staging.mjs: file copy, pnpm-lock.yaml seed+generate, drift guard, self-verify) is the intended replacement for the old deploy:assemble path, but CI only runs the OLD path: pnpm --filter @simlin/app run deploy:assemble + scripts/verify-deploy-build.sh (ci.yaml:227-231). Only the PURE manifest transforms are unit-tested (scripts/tests/deploy-staging-manifests.test.mjs); the orchestration in build-deploy-staging.mjs (copy, lockfile generation, drift guard, self-verify) has no CI coverage. A regression there surfaces only at deploy time.
Fix: add a CI step that runs build-deploy-staging.mjs and asserts the staged dir is well-formed (expected package.json prod closure, generated pnpm-lock.yaml, vendored @simlin/{core,engine} rewritten to file: deps, app.yaml present).
Why it matters
Production deploys are a manual local command with no gcloud in CI (see docs/dev/deploy.md); the assembly smoke test is the only gate. If that gate runs a different Node major, an auto-healed lockfile, and the wrong (old) assembly path, it provides false confidence -- exactly the failure mode this gate exists to prevent.
Components affected
.github/workflows/ci.yaml (deploy-assembly / frontend job ~178, ~205, ~227)
.npmrc, root package.json (engines.node)
app.yaml / .app.prod.yaml (nodejs24, the source of truth for the target major)
scripts/build-deploy-staging.mjs, scripts/deploy-staging-manifests.mjs
Discovery
Identified during a pre-deploy audit of the GAE deploy pipeline, while reviewing the deploy/staged-server-bundle branch (PR #810 scope did not cover these gate gaps).
A pre-deploy audit of the GAE deploy pipeline found several CI/build-gate gaps. PR #810 only pinned
engines.pnpmin the staged manifest and did not address any of these. They share a root cause: the CI gate that is supposed to validate what actually deploys does not faithfully reproduce the production runtime or the staged-deploy path, so whole classes of deploy-time breakage are invisible untilgcloud app deploy.These are distinct from the existing deploy tech-debt items (#39 install bloat / staged-deploy implementation, #40 mutating tracked
public/) and from the Rust feature-matrix gap (tech-debt.md item 60). This issue is specifically about the CI gate fidelity for the web deploy.Gap 1 -- Node major skew between the deploy gate and prod
The
frontendjob that runs the deploy-assembly verification installs Node 22 (.github/workflows/ci.yaml:178,node-version: '22'), but production runsnodejs24(app.yaml:1runtime: nodejs24, mirrored by.app.prod.yaml). The artifacts that actually deploy are never built or tested on Node 24. Theserve-smokejob in the same workflow already uses Node 24 (ci.yaml:362), so the deploy gate is the odd one out.Fix: pin the deploy-assembly job to
node-version: '24'.Gap 2 -- No Node version is pinned anywhere (engine-strict is a no-op)
.npmrcsetsengine-strict=true, but nopackage.jsondeclaresengines.node-- not the root and not anysrc/*package. The root only declarespackageManager: pnpm@10.6.0(package.json:12). With noengines.nodefield,engine-stricthas nothing to enforce, so a contributor on the wrong Node major gets no signal.Fix: add
engines.node(e.g.">=24 <25") to the rootpackage.jsonand/or add a.nvmrc, soengine-strictactually gates and local/CI Node majors converge on prod's.Gap 3 -- deploy-assembly CI job installs with a non-frozen lockfile
The
frontendjob runspnpm install(ci.yaml:205) WITHOUT--frozen-lockfile, so lockfile drift is silently auto-corrected in the very gate meant to validate the deploy. By contrastserve-smokeusespnpm install --frozen-lockfile(ci.yaml:378). Lockfile drift that would fail a frozen install at deploy time (or under the GAE buildpack) passes this gate.Fix: add
--frozen-lockfileto the deploy-assembly install.Gap 4 -- The staged-bundle deploy path is never exercised in CI
The new staged path (
scripts/build-deploy-staging.mjs: file copy,pnpm-lock.yamlseed+generate, drift guard, self-verify) is the intended replacement for the olddeploy:assemblepath, but CI only runs the OLD path:pnpm --filter @simlin/app run deploy:assemble+scripts/verify-deploy-build.sh(ci.yaml:227-231). Only the PURE manifest transforms are unit-tested (scripts/tests/deploy-staging-manifests.test.mjs); the orchestration inbuild-deploy-staging.mjs(copy, lockfile generation, drift guard, self-verify) has no CI coverage. A regression there surfaces only at deploy time.Fix: add a CI step that runs
build-deploy-staging.mjsand asserts the staged dir is well-formed (expectedpackage.jsonprod closure, generatedpnpm-lock.yaml, vendored@simlin/{core,engine}rewritten tofile:deps,app.yamlpresent).Why it matters
Production deploys are a manual local command with no
gcloudin CI (seedocs/dev/deploy.md); the assembly smoke test is the only gate. If that gate runs a different Node major, an auto-healed lockfile, and the wrong (old) assembly path, it provides false confidence -- exactly the failure mode this gate exists to prevent.Components affected
.github/workflows/ci.yaml(deploy-assembly /frontendjob ~178, ~205, ~227).npmrc, rootpackage.json(engines.node)app.yaml/.app.prod.yaml(nodejs24, the source of truth for the target major)scripts/build-deploy-staging.mjs,scripts/deploy-staging-manifests.mjsDiscovery
Identified during a pre-deploy audit of the GAE deploy pipeline, while reviewing the deploy/staged-server-bundle branch (PR #810 scope did not cover these gate gaps).