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

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 5, 2026 7:30am

Request Review

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

github-actions Bot commented Aug 5, 2026

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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os doctor 不加载 .env*,读到的环境与 serve/dev/start 实际运行的不是同一份 —— 写在 .env 里的配置错误 doctor 一律看不见

2 participants