Skip to content

scaffold-e2e.yml hardcodes port 8080 in three boot-and-probe blocks whose wait loop accepts whatever answers #9779

Description

@os-steve

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:

stepportshape
Boot from the artifact and probe health (~line 154)npx os start --port 8080wait 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 e2ewait loop on http://localhost:18080/api/v1/health
Boot and probe health (blank only) in registry-canary (~line 424)npx os start --port 8080same 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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions