Skip to content

fix(cli): os doctoros serve 的顺序读 .env*,并逐值注明来源 (#5387) - #5398

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5387-doctor-dotenv-parity
Aug 5, 2026
Merged

fix(cli): os doctoros serve 的顺序读 .env*,并逐值注明来源 (#5387)#5398
baozhoutao merged 1 commit into
mainfrom
claude/issue-5387-doctor-dotenv-parity

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes#5387

前提核验:成立(#5390 合入后仍然成立)

issue 是在 #5382 的实施过程中顺带记下的,而 PR #5390 在本单派发之前已经合入 main。所以第一件事是在 origin/main(58975525c)上重跑一遍它的判断,而不是照抄 issue:

$ grep -n dotenv packages/cli/src/commands/doctor.ts
114: // opposite of serve's gate, which runs after `dotenv-flow` has loaded.
$ grep -n 'process\.env' packages/cli/src/commands/doctor.ts
(无匹配)

唯一一处 dotenv#5390 写下的那句「我不载入」的注释;env 的读取全部经由 resolveTenancyPosture()(@objectstack/types,自己读 process.env)。issue 的事实面完全成立,PR #5390 文案里那段自陈的残留也如实。

改了什么

按本单已留的 PM 裁定的两半实现,两半缺一都是缺陷。

1)读取顺序对齐 serve。 doctor 用 dotenv-flow 自己的listFiles() 取到 os serve 会载入的同一份文件清单 —— 不是重写一遍命名约定,所以像「node_env=test 时跳过 .env.local」这种 dotenv-flow 的既有例外是继承来的,不是记住的。node_env 按 serve 同款推导:serve 的表达式是 flags.dev ? 'development' : (NODE_ENV === 'test' ? 'test' : (NODE_ENV || 'production')),doctor 没有 --dev,在 flags.dev 为假时那个 'test' 分支是恒等的,所以 NODE_ENV || 'production' 与它逐字等价(首版不新增旗标,按裁定)。

2)逐值注明来源,不静默合并。 文件里的值不写进process.env 供整轮运行使用:overlay 只在需要它的那一次读取周围套上(withDotenvOverlay),finally 里摘掉,用的是 dotenv-flow 自己 unload() 的判定(只删掉仍然等于写入值的那些)。报告新增一行常驻体检项:

 ✓ Environment files .env, .env.production (node_env=production), the cascade `os serve` loads — OS_TENANCY_POSTURE from .env.production, OS_MULTI_ORG_ENABLED from .env

为什么必须有这一行:只静默 dotenvFlow.config() 会把盲区从「doctor 没读我的 .env」平移成「doctor 到底信了我四个 .env* 里的哪一个」,同一类缺陷往前挪一层。该行只报来源、从不报值(有测试钉住),所以 DOCTOR_ENV_INPUTS 将来加入带密钥的变量也不会因此泄露;唯一被打印的值是非法的 posture,由 #5382 那条 finding 原样引回,让作者看见自己的拼写。

withDotenvOverlay 而不是「在 doctor 里重写一遍 posture 解析」的理由:resolveTenancyPosture() 自己读 process.env,绕开它就等于让 doctor 持有第二份 @objectstack/types 拥有的词表,可以与 os serve 实际执行的那份跑偏 —— 正是 #5382 引用解析器原话、不改写措辞时刻意避开的那个坑。

3)posture finding 的 .env 文案按裁定改写。#5382 写的是「unlike os serve, os doctor does not load .env* files」—— 在当时如实,也正是本单被开出来的原因;现在同一个位置说的是它读到了什么

真机运行(bin/run.js doctor,临时 cwd)

.env 里放非法 posture、shell 什么都不设:

 ✓ Environment files .env (node_env=production), the cascade `os serve` loads — OS_TENANCY_POSTURE from .env
✗ Tenancy posture OS_TENANCY_POSTURE="isolatd" is not a recognized tenancy posture — `os serve` refuses to boot this environment
→ Set one of the accepted values:
• OS_TENANCY_POSTURE=single — one organization, no organization wall — the default
…
Read from .env — `os doctor` loaded the same `.env*` cascade
`os serve` does (node_env=production: .env).
cause: Invalid OS_TENANCY_POSTURE="isolatd". Expected one of: single, group, isolated …
❌ Some critical issues found. Please fix them before continuing.
EXIT=1

同目录、同一份破 .env,但 shell 里设了合法值(serve 的优先级:shell 胜过文件):

 ✓ Environment files .env (node_env=production), the cascade `os serve` loads — OS_TENANCY_POSTURE from this process's environment
⚠️ Environment is functional but has some warnings.

两个文件都设时,归因给胜出的那个(指向 .env 会把人送去改一个到不了服务器的值):

 ✓ Environment files .env, .env.production (node_env=production), … — OS_TENANCY_POSTURE from .env.production, OS_MULTI_ORG_ENABLED from .env
Read from .env.production — `os doctor` loaded the same `.env*` cascade

测试

新增 packages/cli/src/commands/doctor-env-provenance.test.ts(24 例):文件清单与优先级、逐键归因、node_env=test 跳过 .env.local、不可读文件降级为 warning(serve 用 silent: true 静默忽略,doctor 说出来)、shell 优先(含 hasOwnProperty 而非真值判定,所以 shell 里显式空值也胜过 .env)、overlay 的施加/还原/抛出时仍还原/不误删回调改写的值、报告只报来源不报值、DOCTOR_ENV_INPUTS 的漂移守卫(doctor.ts 里出现的每个 OS_* 都必须被声明,否则下一个 env 派生检查会重新长出无归因的静默合并),以及进程内跑真实命令的端到端差分。

doctor-tenancy-posture-report.test.ts(#5390 的 11 例)按裁定调整前提、不削断言:

  • 所有调用点改为传入一份真实readDotenvFiles() 读数(对一个真空目录),这些用例本来测的就是 shell 来源的值,继续测它;
  • 那条钉住「doctor 不加载 .env*」的用例,钉的句子现在是假的 —— 换成它的反面(finding 必须说出读了什么),并保留反过度声称的断言、把它指向已退役的那句话,防止它回潮。诊断说自己没看某处而其实看了,与声称覆盖而其实没有,是同一种不可信。

反向验证(方向在跑之前就定了:标准的 before-red / after-green)。readDotenvFiles() 打桩成「直接返回空读数」—— 这正是 #5387 之前 doctor 的行为(看不到任何文件),而不是删掉整个 API(那只会得到一堆 import 错误,证明不了行为)。预测:新文件里所有依赖文件来源的用例转红、端到端用例以缺陷本来的样子转红,而 #5382 的 11 例一例不动。实跑:

 Test Files 1 failed | 1 passed (2)
Tests 13 failed | 22 passed (35)

红的方式正是缺陷本身 —— 端到端用例打印出的正是 #5387 描述的那份报告:

+ ✓ Environment files No .env* files here (node_env=test) — environment read from this process only — no environment input set
+ ⚠️ Environment is functional but has some warnings.

整包(桩已撤除):

$ pnpm --filter @objectstack/cli typecheck # tsc --noEmit,干净
$ pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2
Test Files 75 passed (75)
Tests 705 passed (705)
Duration 142.04s

另跑:check:nul-bytes OK、check:role-word OK、check:error-code-casing OK、check:wildcard-fallthrough OK、check:durability-log-level OK、check:startup-registry-verdict OK、check:adr-anchors OK、eslint 干净;控制字符自查(grep -naP\x00-\x08\x0b\x0c\x0e-\x1f)四个文件均无裸控制字节。

一个必须说清的影响面(不是副作用,是本单的内容)

本改动改变的是 doctor 每一项 env 派生检查的输入,今天落在两处:

  1. posture 报告(os doctor 对非法 OS_TENANCY_POSTURE 退出码 0 并报告「环境功能正常」—— 抛错被 config 分析的宽 catch 吞成一句「Could not load config」 #5382);
  2. ADR-0120 D5e 的 unique-scope 建议 —— 它只在 posture 为 isolated 时运行,所以一个只写在 .env 里的 isolated 现在会让它在 doctor 里跑起来,与 os serve 一致。

这与派发口径里「若改变既有检查的判定超出预期就停下报告」并不冲突:让 env 派生检查看见 serve 看见的那份环境,正是本单要修的东西;它不是被顺手带进来的。反过来,用户配置文件自己读 process.env 的那条路径没有套 overlay,是刻意的克制(见下)。

顺带发现(已另开 issue,未在本 PR 修)

另一处看过、判断为不需要动:dev.ts / start.ts 各自的 dotenvFlow.config() 用的是硬编码的 'development' / mode,与 doctor 的推导不同 —— 但 doctor 的定位是回答「os serve 将看到什么」,裁定也只要求对齐 serve,dev.env.development 是它自己声明的模式,不构成不一致。

🤖 Generated with Claude Code

https://claude.ai/code/session_01VkPSGsX9o17MsGv3Lbxu2w


Generated by Claude Code

…ery value (#5387)
`serve` / `dev` / `start` load `.env*` through dotenv-flow before reading a
single `OS_*` variable (serve.ts:520, dev.ts, start.ts); `doctor` loaded none,
so a posture committed to a shared `.env` reached the server and never reached
the diagnostic — doctor green, `os serve` refusing to boot the same directory.
Doctor now resolves the same cascade dotenv-flow picks for serve (node_env
derived as `NODE_ENV || production`, serve's expression minus the `--dev` flag
doctor does not have), without merging it into `process.env`: the overlay is
applied around the read that needs it and removed in a `finally`. A new
always-on `Environment files` check reports which files were loaded and where
each declared input came from — source only, never the value — and the posture
finding's `.env` sentence now states what was read instead of what was skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkPSGsX9o17MsGv3Lbxu2w
@vercel

vercelBot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 5, 2026 7:30am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/xl labels Aug 5, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx(via packages/cli)
  • content/docs/api/client-sdk.mdx(via @objectstack/cli)
  • content/docs/api/data-flow.mdx(via @objectstack/cli)
  • content/docs/api/environment-routing.mdx(via @objectstack/cli)
  • content/docs/api/error-catalog.mdx(via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx(via packages/cli)
  • content/docs/deployment/backup-restore.mdx(via @objectstack/cli)
  • content/docs/deployment/cli.mdx(via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx(via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx(via packages/cli)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx(via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx(via packages/cli)
  • content/docs/permissions/authentication.mdx(via @objectstack/cli)
  • content/docs/plugins/index.mdx(via @objectstack/cli)
  • content/docs/plugins/packages.mdx(via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx(via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx(via @objectstack/cli)
  • content/docs/releases/v16.mdx(via @objectstack/cli)
  • content/docs/releases/v17.mdx(via @objectstack/cli)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

2 participants

@baozhoutao@claude