Skip to content

fix(cli): OS_APP_NAME 压过 config.email.defaultTemplateContext.appName —— 恢复「env 逐项覆盖」契约 (#5448) - #5498

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5448-appname-env-wins
Aug 5, 2026
Merged

fix(cli): OS_APP_NAME 压过 config.email.defaultTemplateContext.appName —— 恢复「env 逐项覆盖」契约 (#5448)#5498
baozhoutao merged 1 commit into
mainfrom
claude/issue-5448-appname-env-wins

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5448

按分诊裁定取方向 B(env 必须赢)

问题

packages/cli/src/commands/serve.tsresolveEmailCapabilityArg 里,appName 先算,defaultTemplateContext 再整体展开覆盖在它上面:

const defaultTemplateContext = {
  appName: env.OS_APP_NAME || cfgEmail.appName || configAppName || 'ObjectStack',
  ...(cfgEmail.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.mjs OK。

接受的代价(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

…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
@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 2:22pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling 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.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31015247561 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Temporal Conformance (live PG + MySQL) — 失败步骤: Run the non-SQL temporal backends under the skewed process zone

    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mretires a legacy global unique index and replaces it with the composite
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mretires the legacy `uniq_<table>_<col>` index left by the drift rebuild path
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mbare-composite tightening + duplicate pre-flight (ADR-0120 D4)�[2m > �[22m�[2ma
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mbare-composite tightening + duplicate pre-flight (ADR-0120 D4)�[2m > �[22m�[2mB
    �[22m�[39m[schema-drift] product: cannot tighten 'uniq_product_organization_id_code' as UNIQUE (COALESCE(organization_id, '__global__'), code) — existing rows already violate the NULL-safe unique cons
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mbare-composite tightening + duplicate pre-flight (ADR-0120 D4)�[2m > �[22m�[2mB
    �[22m�[39m[schema-drift] REFUSING to rebuild 'uniq_product_organization_id_code' on 'product' as a NULL-safe unique — 1 duplicate group(s) violate it (e.g. organization_id="__global__", code="DUP" × 2
    

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 6 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 736519d Aug 5, 2026
24 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5448-appname-env-wins branch August 5, 2026 14:38
os-zhuang pushed a commit that referenced this pull request Aug 5, 2026
#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
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cli: config.email.defaultTemplateContext.appName 压过 OS_APP_NAME —— 与「env 逐项覆盖」的声明相反

2 participants