Filed unassigned by the dev seat working the publish-smoke.sh port card (#9647), from the sweep that card asked for. Not fixed there — different file, different blast radius, and a workflow change wants its own review.
The shape
.github/workflows/scaffold-e2e.yml has three boot-and-probe blocks on fixed ports:
| step | port | shape |
|---|
Boot from the artifact and probe health (~line 154) | npx os start --port 8080 | wait loop on http://localhost:8080/api/v1/health, then asserts /api/v1/ready |
docker run -d --name e2e -p 18080:8080 (~line 371) | host 18080, container name e2e | wait loop on http://localhost:18080/api/v1/health |
Boot and probe health (blank only) in registry-canary (~line 424) | npx os start --port 8080 | same as the first |
Each wait loop asks one question — does the port answer 200? — and nothing more. SERVER_PID is captured but never checked for liveness inside the loop; it is only used for the closing kill.
Why that is the same defect as #9647, and why it is also NOT the same
Different from #9647: these use os start, not os dev. packages/cli/src/commands/serve.ts gates the port auto-shift on flags.dev, so the non-dev branch takes the loud refusal instead — run B's own server prints ✗ Port 8080 is already in use. and exits 1 into its own server.log.
Same as #9647: that refusal does not help, and this is exactly the half that surprised on #9578. Run B's server is gone, but run A keeps answering on 8080, so run B's loop still gets its 200, still sets ok=1, and still asserts /api/v1/ready against run A's app. Then kill "$SERVER_PID" kills a pid that already exited. The run reports green about an app it never booted.
The docker leg has a second shared name on top of the port: --name e2e is fixed too, so a second concurrent run's docker run fails on the name — loudly, which is better, but the host port 18080 is still shared with whatever the first container published.
Severity — deliberately not graded here
Unmeasured, and the mitigating fact is real: GitHub-hosted runners give every job its own VM, so two concurrent CI runs of this workflow never share a network namespace. The exposure is self-hosted runners and agent dispatch containers, where several jobs or several agents do share one namespace. Whether this repo runs scaffold-e2e anywhere like that is the question that grades it, and I did not establish it.
What is verified: the three literals above, the absence of any liveness check inside the three wait loops, and the non-dev refusal branch in serve.ts (read, and consistent with the measured dev-path behaviour recorded on #9647).
Sweep context
scripts/ was re-swept on current main while working #9647 and is clean: publish-smoke.sh was the only remaining fixed port, and every /tmp use under scripts/ is mktemp-qualified (gen-sdui-manifest.sh, publish-smoke.sh, downstream-smoke.sh, release-spec-changes.sh, scripts/pm/release-rehearsal-clone.mjs); the two other /tmp hits are documentation examples. .github/workflows/** had never been swept — this is what that extension found. The database service ports in ci.yml (5432, 3306) are GitHub service containers, which the runner allocates per job; they are not this shape.
Filed unassigned by the dev seat working the
publish-smoke.shport card (#9647), from the sweep that card asked for. Not fixed there — different file, different blast radius, and a workflow change wants its own review.The shape
.github/workflows/scaffold-e2e.ymlhas three boot-and-probe blocks on fixed ports:Boot from the artifact and probe health(~line 154)npx os start --port 8080http://localhost:8080/api/v1/health, then asserts/api/v1/readydocker run -d --name e2e -p 18080:8080(~line 371)18080, container namee2ehttp://localhost:18080/api/v1/healthBoot and probe health (blank only)inregistry-canary(~line 424)npx os start --port 8080Each wait loop asks one question — does the port answer 200? — and nothing more.
SERVER_PIDis captured but never checked for liveness inside the loop; it is only used for the closingkill.Why that is the same defect as #9647, and why it is also NOT the same
Different from #9647: these use
os start, notos dev.packages/cli/src/commands/serve.tsgates the port auto-shift onflags.dev, so the non-dev branch takes the loud refusal instead — run B's own server prints✗ Port 8080 is already in use.and exits 1 into its ownserver.log.Same as #9647: that refusal does not help, and this is exactly the half that surprised on #9578. Run B's server is gone, but run A keeps answering on 8080, so run B's loop still gets its 200, still sets
ok=1, and still asserts/api/v1/readyagainst run A's app. Thenkill "$SERVER_PID"kills a pid that already exited. The run reports green about an app it never booted.The docker leg has a second shared name on top of the port:
--name e2eis fixed too, so a second concurrent run'sdocker runfails on the name — loudly, which is better, but the host port18080is still shared with whatever the first container published.Severity — deliberately not graded here
Unmeasured, and the mitigating fact is real: GitHub-hosted runners give every job its own VM, so two concurrent CI runs of this workflow never share a network namespace. The exposure is self-hosted runners and agent dispatch containers, where several jobs or several agents do share one namespace. Whether this repo runs scaffold-e2e anywhere like that is the question that grades it, and I did not establish it.
What is verified: the three literals above, the absence of any liveness check inside the three wait loops, and the non-dev refusal branch in
serve.ts(read, and consistent with the measured dev-path behaviour recorded on #9647).Sweep context
scripts/was re-swept on currentmainwhile working #9647 and is clean:publish-smoke.shwas the only remaining fixed port, and every/tmpuse underscripts/ismktemp-qualified (gen-sdui-manifest.sh,publish-smoke.sh,downstream-smoke.sh,release-spec-changes.sh,scripts/pm/release-rehearsal-clone.mjs); the two other/tmphits are documentation examples..github/workflows/**had never been swept — this is what that extension found. The database service ports inci.yml(5432,3306) are GitHub service containers, which the runner allocates per job; they are not this shape.