Uh oh!
There was an error while loading. Please reload this page.
fix(cli): os start / os dev stop injecting OS_ARTIFACT_PATH into the child serve environment - #9365
Conversation
…an internal channel (#8985) `os start` and `os dev` wrote their own resolved artifact path into the child `serve` environment as `OS_ARTIFACT_PATH` — the same variable an operator sets. The write happens before the downstream `objectstack.config.ts` is evaluated, so a config saw the variable set on every boot and could not tell an operator's instruction from the CLI's plumbing. The resolved path now travels on `OS_INTERNAL_ARTIFACT_PATH`, owned by `packages/cli/src/utils/internal-artifact-channel.ts`. `serve` reads it strictly between `OS_ARTIFACT_URL` and `OS_ARTIFACT_PATH`, which preserves every answer the resolution ladder gives today: it must beat the operator knob so `--artifact X` still wins over an exported `OS_ARTIFACT_PATH=Y`, and it must lose to the reference because `dev` sends its answer unconditionally. Unchanged and now pinned: the ladder itself, `start`'s refusal to set `OS_BOOT_EMPTY` on a reference boot, and the named-vs-conventional distinction that keeps a missing resolved artifact a loud refusal. Co-Authored-By: Claude <noreply@anthropic.com>
…t scan (#8985) Reverse verification caught the pin under-reporting: with the old injection restored in both commands, only `dev.ts` went red. `start.ts`'s `--auth-secret` flag description contains the literal `/api/v1/auth/*`, and that `/*` opened a phantom block comment which the regex comment-stripper closed against a docblock 250 lines later — swallowing the reinstated write along with it. The pin now parses the file and walks for real `OS_ARTIFACT_PATH` writes (object property, property assignment, indexed assignment), so strings and comments cannot lie to it, and carries a self-test over the exact specimen that defeated the text scan. Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop):
|
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32049971906 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Queue-eviction triage — unrelated harness flake, re-queuing oncePM seat, session This is NOT the infrastructure signature from earlier todayQueue build 32049971906, Every test passedThe run was failed by a single teardown-time rejection in A worker's RPC channel closing while a pending console-log delivery is in flight — timing- and log-volume-dependent by construction, which fits its appearing in the full-suite queue build rather than the PR-side affected-subset run. Running the workflow's own triage checklist
Filed as #9371 ( Action: re-queue onceJustified because zero tests failed, the failure is in a package this diff does not touch, and it is a first sighting rather than a known-recurring signature. Re-entry goes to the back of the queue, behind #9368 and #9369, so it does not force a rebuild of either. ⛔ If this same signature evicts this PR again, it stops being a re-queue case: that second data point confirms the flaky, and the correct move then is to fix or isolate the race under #9371 rather than burn another full-queue rebuild. ⛔ No change is being pushed here — all five required checks are green on Generated by Claude Code |
Fixes#8985
os startandos devare supervisors: each resolves an artifact, then spawnsos serveto boot it. Both handed the resolved path down by writingOS_ARTIFACT_PATHinto the child environment — the same variable an operator sets to name an artifact — and both writes land before the downstreamobjectstack.config.tsis evaluated. So inside any config that variable was set on every boot, including boots where no operator ever mentioned it, and a config could not answer "did a human ask for this, or did the CLI put it here?"The CLI's own plumbing now travels on
OS_INTERNAL_ARTIFACT_PATH, and the property the downstream retirement needs is restored:The dispatch scoped this card to
start.ts+dev.ts+ sibling tests, and told me to stop on breach and explain. I could not deliver the ruling inside that surface, and this PR touches two more files. The measurement that forces it:os servehas no--artifactflag. The--artifactinserve.ts's own precedence comment isstart/dev's flag, upstream — so "pass it as an argument instead of the environment" (one of the suggested routes) is not reachable without adding a flag toserve.resolveDefaultArtifactPath()inpackages/runtime/src/default-host.ts, called fromserve.tsin three places.So every route — internal env var, CLI flag, structural pass-through — needs a change on the child side. The suggested route ("a distinct env var the CLI owns") is not implementable within
start.ts+dev.tsalone. I kept the breach minimal and chose the smaller of the two possible hosts:resolveDefaultArtifactPath(packages/runtime)serve.ts(chosen)Two further notes for the ruling: the PM's claim comment records
packages/cli/src/commands/serve.tsas freed at ~16:18Z from the three-way exclusive slot (#8368 / #8985 / #8978) that this card was held behind — i.e. this card's own serialization slot already contemplatedserve.ts. And the newpackages/cli/src/utils/internal-artifact-channel.tsexists so the channel has exactly one definition rather than three literals drifting apart.If the surface ruling goes the other way, the alternative is to leave
start'shome/dist/objectstack.jsonfallback and both commands'--artifactflag unable to reach the child — a behaviour regression, not a smaller fix.Precedence is unchanged — this is a rename of the channel, not a change of answers
servereads the new channel strictly between the reference and the operator knob:That exact position is what preserves today's answers, in both directions:
OS_ARTIFACT_PATH.os start --artifact Xrun with an operator'sOS_ARTIFACT_PATH=Yexported boots X today, because the parent overwrote the variable on the way down. The parent now inheritsYuntouched, so only a higher rung keepsXwinning.OS_ARTIFACT_URL.os devwrites the channel unconditionally, exactly as unconditionally as it wroteOS_ARTIFACT_PATH, and the reference has always outranked the path inserve.Each command's own resolution ladder is untouched, and so is the value: the child receives the path the parent resolved, still "named" in the sense
resolveDefaultArtifactPathmeans it, so a resolved-but-missing artifact stays a loud refusal instead of degrading to an empty boot.The two load-bearing behaviours, checked
start's no-OS_BOOT_EMPTY-on-reference refusal survives. The guard at the oldstart.ts:207(!process.env.OS_ARTIFACT_PATH) reads the parent environment, and this command never mutatesprocess.env— it composes a separate child env — so that expression sees exactly what it saw before. Worth noting for the record that this guard suppresses auto-compile; the reference refusal itself is theartifactUrl ? {} : ...ladder in the child env block, and both are pinned now.OS_BOOT_EMPTYstays add-only. An operator-exported value is still inherited rather than cleared — the helper only ever adds it, so no behaviour changes there either.start.ts:194's existing comment already said the child env must carry nothing that contradicts anOS_ARTIFACT_URLboot. That partial intent is extended, not contradicted: it is now general rather than special-cased to the reference branch.Tests
packages/cli/src/commands/artifact-child-env.pin.test.ts(19 tests) pins the property from both sides, per the card's "pin the property, not just the plumbing":OS_ARTIFACT_PATHwhen the operator set none — for all three decisions (resolved / reference / empty);$OS_ARTIFACT_PATHstill beats both auto-detected locations;OS_BOOT_EMPTYrefusal, in all four combinations;OS_ARTIFACT_PATHanywhere — the pin that stops a future edit from simply re-adding the injection beside the helper.Reverse verification found a defect in the pin itself, which is why the second commit exists. With the old injection restored in both commands, only
dev.tswent red.start.ts's--auth-secretflag description contains the literal/api/v1/auth/*, and that/*opened a phantom block comment which the regex comment-stripper closed against a docblock 250 lines later — swallowing the reinstated write. The pin now walks the TypeScript AST for real writes, and carries a self-test over the exact specimen that defeated the text scan. Re-verified after the fix: both structural pins red with the injection restored, all 19 green with it removed.Verification
All of the below on
8705d5700, the final commit:pnpm --filter @objectstack/cli test— 127 files / 1394 tests passedpnpm --filter @objectstack/cli typecheck(tsc --noEmit) — cleaneslinton all five changed files clean, pluscheck:cross-package-test-inputs,check:where-matcher,check:engine-double-contract,check:query-options-erasure,check:type-check-coverage,check:nul-bytes— all passcheck:changeset-gate-self-tests,check:objectui-changeset,check-adr-0087-registration,check-changeset-no-major,check-empty-changeset— all passscripts/docs-audit/check-affected-docs.mjs— passnode scripts/pm/dispatch-gates.mjsre-derived from the actual changed paths named no family beyond the set above.check:type-check-debt --re-measurewas not run:packages/clicarries no ledger entry and itstsconfig.jsonincludes all ofsrcwith no test exclusion, so the new test file is already compiled by thetypecheckabove and cannot move that ratchet.Downstream
This is what makes cloud#1292's tightening from a value-based to a presence-based refusal possible. Not touched here — the consumer-side change is filed there, and cloud#1292 stays open on the cloud side.
Generated by Claude Code