Uh oh!
There was an error while loading. Please reload this page.
fix(cli): OS_APP_NAME 压过 config.email.defaultTemplateContext.appName —— 恢复「env 逐项覆盖」契约 (#5448) - #5498
Conversation
…ppName (#5448) `resolveEmailCapabilityArg` resolved the email template context's `appName` first and then spread the whole `config.email.defaultTemplateContext` OVER it, so a config that spelled `defaultTemplateContext: { appName: 'Acme Dev' }` made `OS_APP_NAME` silently inert — the one exception to the "env overrides per setting" contract this file states and honours for every other key it reads (apiKey, defaultFrom, retries, queueDelivery, persist, SMTP). Resolve `appName` after the spread instead, keeping the context form IN the chain so a config that spells only that form is not demoted to 'ObjectStack': env > config.email.appName > defaultTemplateContext.appName > config.appName > 'ObjectStack'. Every other context key is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31015247561 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Uh oh!
There was an error while loading. Please reload this page.
#5448 已裁 direction B 并由 PR #5498 落地:`resolveEmailCapabilityArg` 现在把 `appName` 放在 context 展开之后解析,五级链为 `OS_APP_NAME` > `config.email.appName` > `defaultTemplateContext.appName` > 顶层 `appName` > `'ObjectStack'`。本分支上写于旧序之上的三处东西随之收口: - `serve-email-config-parity.contract.test.ts` 那条 pin 原本钉的是旧序 (context.appName 压过 env),现改为钉新序。它保留本文件自己的角度而非 重述 #5498 的用例:配置先过真正的 `EmailServiceConfigSchema.parse()` 再喂 读侧,因此钉住的是 #5307 新加的两个契约键既能存活 parse、又确实落在 schema 文案承诺的档位上。 - `email-config.zod.ts` 中 `appName` / `defaultTemplateContext` 的 TSDoc 与 两处 `.describe()`:旧文案写的是「写在 context 里的 appName 压过 appName 键与 OS_APP_NAME、是否合理 filed as #5448」,该事实已不成立。 `email-config.mdx` 由 `gen:docs` 整体重生成(未手改),9 个 generated 门全绿。 运行时零改动 —— `serve.ts` 未被本次改动触碰。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
…me / defaultTemplateContext (objectstack-ai#5307) (objectstack-ai#5465) * feat(spec): EmailServiceConfigSchema 补齐 CLI 实读的 queueDelivery / appName / defaultTemplateContext (objectstack-ai#5307) config.email 在全仓只有一个读者:packages/cli/src/commands/serve.ts 的 resolveEmailCapabilityArg。它读八个键,schema 只声明五个,差集三个已经被 运行时消费多时 —— queueDelivery(objectstack-ai#5160 的耐久队列开关)、appName(模板 产品名 + 兜底发件人来源)、defaultTemplateContext(自由渲染上下文)。 与 objectstack-ai#5104 完全同型的 declared != implemented,spec 在落后的一侧:用 EmailServiceConfig 标注 objectstack.config.ts 的作者写 queueDelivery: true 会拿到类型错误,而同一份配置起得来、也确实走队列。 - 三个键均为 optional 且不带 .default():默认值由 resolveEmailCapabilityArg 对着 env 与顶层 config 解析,schema 再造一个只会多出一个谁也不赢的答案 - defaultTemplateContext 保持自由 record,读侧原样透传,不发明约束 - TSDoc 写清语义、默认值与优先级,含 defaultTemplateContext.appName 压过 OS_APP_NAME 这一处实测到的例外(另立 objectstack-ai#5448) - 新增跨包契约测试把 issue 的手工 grep 机械化:读侧新增未声明键即变红, persist 作为唯一 DECLARED_BUT_UNREAD 豁免登记并指回 objectstack-ai#5447 运行时零改动。 * chore(spec): 随行生成物 + 规避 docs 生成器的花括号转义缺陷 - authorable-surface.json 记入三个新可授权键(gen:schema;check:authorable-surface 是删除棘轮,新增键不记入即"对该棘轮永久隐形") - content/docs/references/system/email-config.mdx 重新生成,属性表出现三行 - appName 的 .describe() 不再写 {{appName}}:生成器会把双花括号转义成 `{{x}` 加一个游离的 },main 上已有 3 处同样的破损(已另立 objectstack-ai#5452), 源码留注释说明为何这里绕开 check:generated 九个 gate 全绿;gen:strictness-ledger 整体重算零 diff (台账只分诊 ui/data/automation/security/studio,system/ 不在其内)。 * test(cli): objectstack-ai#5447 已由 PR objectstack-ai#5470 落地,parity 测试撤销 persist 豁免 `serve-email-config-parity.contract.test.ts` 曾注册唯一一条 `DECLARED_BUT_UNREAD` 豁免:`persist` 被 schema 声明却无人读取(objectstack-ai#5447)。cli 车道的 PR objectstack-ai#5470 (`cd2efe62a`)已合入 main —— `resolveEmailCapabilityArg` 现在经由新的 `envBooleanFlag` / `OS_EMAIL_PERSIST_ENABLED`(三态,默认仍为 ON)读取 `cfgEmail.persist`。 于是本分支的断言在 merge queue 里必然变红:它 expect `unread == ['persist']`, 而合并后的 main 上 `unread == []`。跨车道协议(objectstack-ai#5447 评论)约定后落地方对齐, 即本分支。 对齐做法:删除豁免数组,而非留一个空数组 —— 空注册表是一种邀请,下一个 declared-but-unread 键会被直接追加进去而不必辩论,正是该条目当初要防止的 「静默豁免」。断言随之收紧为两个方向都为空,即 declared 集与 read 集相等, 也就是文件注释当初许诺的 plain set equality。豁免的来龙去脉保留在注释中。 反向验证(方向预先判定为 red,结果一致):保留旧豁免数组对合并后的 main 运行, `AssertionError: expected [] to deeply equal [ 'persist' ]` —— 这正是本次预先 规避的队列失败;撤销豁免后该文件 6 个用例全绿。 另:changeset 里「它读八个键」是 objectstack-ai#5470 之前的读侧计数(现为九个),补时间 限定词「本次改动时」,以免这段 CHANGELOG 文案落地后失真。schema 中 `persist` 的 TSDoc「Persist to sys_email (default true)」经核对在 objectstack-ai#5470 之后依然成立 (默认仍为 ON),故不改。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D * docs(spec): appName 文案与 parity pin 收口到 objectstack-ai#5448 裁定的新序(PR objectstack-ai#5498) objectstack-ai#5448 已裁 direction B 并由 PR objectstack-ai#5498 落地:`resolveEmailCapabilityArg` 现在把 `appName` 放在 context 展开之后解析,五级链为 `OS_APP_NAME` > `config.email.appName` > `defaultTemplateContext.appName` > 顶层 `appName` > `'ObjectStack'`。本分支上写于旧序之上的三处东西随之收口: - `serve-email-config-parity.contract.test.ts` 那条 pin 原本钉的是旧序 (context.appName 压过 env),现改为钉新序。它保留本文件自己的角度而非 重述 objectstack-ai#5498 的用例:配置先过真正的 `EmailServiceConfigSchema.parse()` 再喂 读侧,因此钉住的是 objectstack-ai#5307 新加的两个契约键既能存活 parse、又确实落在 schema 文案承诺的档位上。 - `email-config.zod.ts` 中 `appName` / `defaultTemplateContext` 的 TSDoc 与 两处 `.describe()`:旧文案写的是「写在 context 里的 appName 压过 appName 键与 OS_APP_NAME、是否合理 filed as objectstack-ai#5448」,该事实已不成立。 `email-config.mdx` 由 `gen:docs` 整体重生成(未手改),9 个 generated 门全绿。 运行时零改动 —— `serve.ts` 未被本次改动触碰。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes#5448
按分诊裁定取方向 B(env 必须赢)。
问题
packages/cli/src/commands/serve.ts的resolveEmailCapabilityArg里,appName先算,defaultTemplateContext再整体展开覆盖在它上面:于是作者只要写了
defaultTemplateContext: { appName: 'Acme Dev' },OS_APP_NAME就静默失效。这是本文件自述契约(EmailServiceConfigSchema头部 TSDoc 与生成的references/system/email-config页:「OS_EMAIL_*环境变量逐项覆盖」)的唯一例外 ——apiKey/defaultFrom/retries/queueDelivery/persist/ SMTP 一族全都照此执行。后果不是排序细节:一份
objectstack.config.ts部署到多环境时,运维手上唯一的按环境覆盖手段什么都没做,生产发出的品牌邮件仍叫仓库里写死的名字;又因为兜底发件人是从同一个值 slug 化来的,信封上也是no-reply@acme-dev.local。全程无报错无日志。改法
appName改为在展开之后解析,链条为:env.OS_APP_NAME>cfgEmail.appName>cfgEmail.defaultTemplateContext.appName>configAppName>'ObjectStack'按 issue 正文的陷阱注记,
defaultTemplateContext.appName显式留在链条里(排在两个专用来源之后、顶层configAppName之前)。若让它直接输给两个专用来源而消失,「只写了 context.appName」的既有配置会被降级到'ObjectStack'—— 拿一个静默错值换一个更糟的。defaultTemplateContext的其它键行为完全不变:它们没有 env 或专用 config 载体,仍整体展开。只有appName一个键特判,PR 里有一条断言专门钉这一点(含一个故意与本 resolver 读的设置同名的 context 键,证明特判恰好一个键宽)。测试
新增
packages/cli/src/commands/serve-email-appname-precedence.test.ts,12 条:五级链条逐级断言(每级单独缺席时下一级接管)、防降级例(含「只写 context.appName 时 env 仍然覆盖」)、兜底发件人 slug 跟随解析后的appName、显式defaultFrom不受影响、其它 context 键不受影响、无 context 时的形状不变,以及两条 in-process 端到端:把 resolver 的真实输出喂给真的EmailServicePlugin,读回活的EmailService手上的defaultTemplateContext——sendTemplate合并的正是这个对象(plugin 侧send-template.test.ts已钉),所以它拿到什么就是运维的邮件渲染出什么。反向验证(方向先判后跑):预判把
serve.ts改回旧展开顺序应恰好红掉 6 条 —— rung 1/2、防降级例、slug 例、其它键例、第一条端到端;其余 6 条(rung 3/4/5、显式defaultFrom、无 context 形状、第二条端到端)的期望值与旧行为一致,应保持绿。实测完全吻合:Tests 6 failed | 6 passed (12),失败集合与预判集合逐条相同。恢复改动后 12/12 绿。pnpm --filter @objectstack/cli test全包 792 passed (81 files);typecheck干净;node scripts/check-nul-bytes.mjsOK。接受的代价(user-visible)
依赖「context 压 env」现状的既有部署行为会变:该环境的邮件正文、主题与派生兜底发件人会开始使用
OS_APP_NAME的值。恢复旧结果的办法是在该环境取消OS_APP_NAME,或把想要的名字写到config.email.appName。changeset 已写明。维护者若不接受此代价,可否决改方向 A(届时只改 TSDoc 标明例外)。与 #5307 的衔接
#5307 仍未合入
main(分支claude/issue-5307-email-config-keys存在,未合并),其serve-email-config-parity.contract.test.ts与钉现状的spreads defaultTemplateContext OVER the resolved appName, as documented一条不在本 PR 切出的 main 上,按裁定不碰其分支。EmailServiceConfigSchema当前也尚未声明appName/defaultTemplateContext,其头部 TSDoc 的 Resolution order 没有提到本例外(那段例外说明本就属于 #5307 的 PR),因此本 PR 无 spec 侧改动、无生成物重生成。留给 #5307 后合时收口:那条 pin 需改为钉新序,新增的键说明按新序写。本 PR 把新序写进了resolveEmailCapabilityArg自身的 TSDoc。未触碰
persist相关行(#5470 刚落地),未触碰OS_APP_NAME命名(PD #9 债务另议)。🤖 Generated with Claude Code
https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
Generated by Claude Code