Repository navigation
fix(cli): os dev -p and os serve --port export the port as OS_PORT/PORT, so a config reads the port the server listens on - #26268
Conversation
… config's environment `os dev -p N` and `os serve --port N` handed the flag to the listener only, so a config reading OS_PORT/PORT at module evaluation (the showcase's self URL) saw the inherited value or the 3000 default: the self-ping connector failed `fetch failed` and /meta served :3000. `os start` already exported its flag as OS_PORT/PORT; that writer moves into utils/port-contract.ts as `portFlagEnv`, and dev (into its own environment, before the compile) and serve (before it loads the config) call it too. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrUJpT4UGJT1bwN5uvT8dh
…nvironment Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrUJpT4UGJT1bwN5uvT8dh
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 31 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 49c0e3f2ba3b8faf21c5778bd0324bb179079676 && git checkout 49c0e3f2ba3b8faf21c5778bd0324bb179079676
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0453fabef70c1b2a8276508d37c350d3487badce 45116cbc104801a811f4ee0399e3fef69fe5f806 && git checkout -B drift-repro 0453fabef70c1b2a8276508d37c350d3487badce && git merge --no-ff 45116cbc104801a811f4ee0399e3fef69fe5f806
node scripts/docs-audit/affected-docs.mjs --json 0453fabef70c1b2a8276508d37c350d3487badce
|
席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
Requested by Leehom · project thread
Fixes #26243
Clause-②: no
Metadata-protocol: none
What was wrong
os dev -p Nforwarded the port to itsservechild as--porton argv, andos serve --port Nhanded it to its listener. Neither put it in the environment, so a config that readsOS_PORT/PORTwhile it loads saw the inherited value or the 3000 default.examples/app-showcase's self URL is such a config.os start --portwas already right, because it hands the flag to its child asOS_PORT+PORT(#12992).Measured on
examples/app-showcaseat81bba6c3(main), with noOS_PORT/PORT/SHOWCASE_SELF_URLin the environment. "ping" isPOST /api/v1/automation/showcase_declarative_connector_ping/trigger, and "meta" isproviderConfig.baseUrlfromGET /api/v1/meta/connector/showcase_status_api.af26658b)os dev --fresh -p 41302http://127.0.0.1:3000FLOW_FAILED … fetch failedhttp://127.0.0.1:41302status: okos serve --port 41303 objectstack.config.ts(production)http://127.0.0.1:3000FLOW_FAILED … fetch failedhttp://127.0.0.1:41303status: okos start --port 41304(control)http://127.0.0.1:41304os serve --portshowed the same symptom, so it is fixed through the same source, per the ruling.The fix
There is one writer for the port's environment, and all three doors call it. No door restates the precedence.
packages/cli/src/utils/port-contract.ts:os start'schildPortEnvmoves here asportFlagEnv(port), unchanged in behaviour. It writes{ OS_PORT, PORT }with one value when a flag was given, and nothing otherwise. Its docblock moves with it and now names all three callers.packages/cli/src/commands/dev.ts: the port door (resolution plus refusal) moves ahead of the compile, because the compile is the first child that evaluates the config. Once a-ppasses the door,os devmergesportFlagEnv(parseRequestedPort(port))into its ownprocess.env. The compile, the watch recompiles and theservechild all run in that environment (the one fix(cli): os dev compiles the config before it boots, so served connector metadata matches dispatch #26240'sdevCompilesAtBootrelies on), so both copies of the stack read the flag's port. An invalid-pis now refused before the compile instead of after it. The argv forwarding is unchanged.packages/cli/src/commands/serve.ts: once the port is validated, a--portgiven on argv (oclif'ssetFromDefaultis nottrue) is merged intoprocess.envthe same way, before the config is loaded. A port that came fromOS_PORT/PORTor the default leaves the environment as it is.packages/cli/src/commands/start.ts: importsportFlagEnv; its two comment references are re-pointed. The behaviour is unchanged.examples/app-showcase/src/system/self-url.ts: header comment only. Its claim that it reads "the same names … the CLI itself reads" is now true for the flag too, and it says how.The exported value is the parsed port, not the text:
--port 3e3selects port 3, so it exports3.os devkeeps forwarding its flag text on argv, so the child's text-read notice still names--port "3e3". Both channels come from one variable at one door. TheportFlagEnvdocblock says why that is deliberate, and whyos start, whose integer flag has no such text, stays on the environment alone.Tests
At
45116cbc(the head;af26658bplus the changeset):packages/cli/test/dev-connector-env-parity.integration.test.ts(fix(cli): os dev compiles the config before it boots, so served connector metadata matches dispatch #26240's file, reshaped): two boots of the builtbin/run.json the minimal host config.os serve objectstack.config.ts --port N, production posture, with noOS_PORT. The materialized connectorbaseUrlmust behttp://127.0.0.1:N.os dev -p Nover an inherited staleOS_PORT=41302and an artifact built with:3000. The served and the materializedbaseUrlmust both be:N. This one boot holds both cards: before fix(cli): os dev compiles the config before it boots, so served connector metadata matches dispatch #26240 the served copy read:3000, and before this PR both copies read:41302.#26130's old boot was-p 0withOS_PORT=41302as the config's value. This PR makes-p 0win for the config too, so that shape no longer holds. Each boot now draws a non-zero port withreservePort(), because the config has to know the number before the boot. The draw is ledgered inreleased-port-production-spawn.pin.test.ts(2 draws, with the reason), and a lost race reads asportDriftError/portContentionError.start-port-forwarding-channel.pin.test.tsnow importsportFlagEnvfrom the contract module. Its five behavioural cases are unchanged.vitest runon the integration file,released-port-production-spawn.pin.test.ts,start-port-forwarding-channel.pin.test.tsandport-contract-single-source.test.tsgaveTest Files 4 passed (4),Tests 21 passed (21).pnpm --filter @objectstack/cli exec vitest run --project unitgaveTest Files 334 passed (334)andTests 4878 passed (4878)(lock VERDICT command-exit 0).pnpm --filter @objectstack/cli typecheckexited 0. The integration tier ran locally for the touched file only; the rest is declared to CI.scripts/ablation-replace.mjs. In each case the CLI was rebuilt andablation-dist-preflightfound the marker inpackages/cli/dist/commands/*.js.expected 'http://127.0.0.1:41302' to be 'http://127.0.0.1:33115') and the materialized leg stayed green. That shows why the parent has to export: theservechild re-exports from its own argv, but the compile only sees the parent's environment.expected 'http://127.0.0.1:3000' to be 'http://127.0.0.1:44503') and both dev legs stayed green.git status --porcelainempty), the CLI was rebuilt, andablation-dist-preflight --absentexited 0 for both markers.TS2367on the literal comparison). Nothing ran, and it is not counted.Gates
Local subset:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat45116cbcderived 73 families.--ranreconciliation: 72 ran with exit 0, 1 NOT MEASURED, 0 unrun.pnpm check:dual-build-cjs-loadsreads a full-repo build that this container did not make. CI runs it.node scripts/check-plugin-teardown-shape.mjs --self-testexited 3 on the shallow clone. Aftergit fetch origin 621a4876…(its positive control), it passed 48 cases.pnpm check:cli-test-child-envpassed (exit 0). The reshaped test is already in its census, and the new serve leg also spawnsbin/run.jswithNODE_ENVunset.pnpm exec eslint --no-inline-configon the 8 touched source and test files exited 0. Neither@objectstack/clinor the showcase has alintscript.Acceptance notes
af26658bwith41310held by another listener:os dev --fresh -p 41310printedPort 41310 is in use — serving on 41311 instead. Its/metaserved:41310and the ping answered400 FLOW_FAILED … fetch failed. The config is evaluated before the bind, so it reads the requested port, not the shifted one. A bareos devwith 3000 busy has the same shape onmain. This is the Showcase REST connectors hard-wirebaseUrltohttp://127.0.0.1:3000, so self-ping flows failfetch failedon any isolated instance — a port mismatch, not an egress block #7538 family, and this PR does not widen to it, per the ruling. It goes to the seat as a finding.--port 0now exports0, so a config like the showcase's builds:0where it used to build:3000(or the inheritedOS_PORT). Neither address serves anything. A kernel-assigned port cannot be known before the bind.OS_PORT=3e3) is not normalized. Only a flag is exported, asos startalready did, so an operator's own variables reach the config exactly as set.#26242 remains open; this PR does not address it.
Generated by Claude Code