Skip to content

fix(i18n): fallback interpolation replaces all placeholder occurrences - #3510

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-3418-fallback-interpolation
Aug 7, 2026
Merged

fix(i18n): fallback interpolation replaces all placeholder occurrences#3510
yinlianghui merged 2 commits into
mainfrom
claude/issue-3418-fallback-interpolation

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3418

问题

createSafeTranslation 的无 provider 回退插值用的是 value.replace('{{k}}', String(v))String.prototype.replace 传字符串 needle 时只替换第一次出现;而挂了 provider 的路径走 i18next 自己的插值,替换全部出现

于是任何一条在同一句里重复用同一个占位符的默认文案(Selected {{count}} of {{count}} items —— 在很多语言里都很自然,RTL / 黏着语为了语序还经常必须这么写),在没有挂 I18nProvider 的宿主(独立 / 嵌入式渲染)上会把第二次及以后的 {{k}} 原样吐给用户。不抛错、不打日志,专挑我们最看不到的运行环境 —— 一条静默的语义分叉

改法

value.split(needle).join(String(v)),一行。

裁定见 issue:修插值语义,不加守卫。为绕开实现 bug 去限制文案作者能写的句式,是把 bug 固化成契约;语义修齐后,重复占位符在两条路径上同样合法。

为什么是 split/join 而不是 replaceAll:replacereplaceAll 都会解释替换串里的 $&$'$$ 和反引号形式,i18next 不会。这里插进去的是运行时数据(记录 label、搜索词),所以这第二条分叉今天就够得着 —— 一个名字里带 $& 的记录在回退路径上会被改坏('Hello {{name}}' + '$& raw' 旧实现给出 'Hello {{name}} raw')。split/join 两侧都是字面量,顺带也省掉了占位符名的正则转义。

defaults[key] || key 的取值、String(v) 强转、以及「没有对应 option 时占位符保持字面量」的行为,全部原样保留。

前提核验

  • 一行 replaceorigin/main 上确实还活着 —— 前提 live。
  • issue 说的「休眠」也仍然成立:扫过 packages/ + apps/ 全部单行字符串字面量,0 条重复使用同一个占位符。所以 (1) 类分叉今天没有触发点,(2) 类($ 替换模式)有。

测试

新增断言的期望值不是猜的 —— 每一条都先在一个按 createI18n 同样配置(interpolation: { escapeValue: false })的真实 i18next 实例上跑过,测里写的就是 i18next 自己的输出:

输入i18next旧回退新回退
selection + {count: 3}Selected 3 of 3 itemsSelected 3 of {{count}} items✅ 一致
pair + {a:'A', b:'B'}A/B — A again, B againA/B — {{a}} again, {{b}} again✅ 一致
greeting + {name:'$& raw'}Hello $& rawHello {{name}} raw✅ 一致
greeting + {other:'x'}Hello {{name}}✅ 不变
greeting(无 options)Hello {{name}}✅ 不变

反向验证(先定方向再跑):预测「两条重复占位符用例 + $ 模式用例 RED,其余 GREEN」。测试先写、源码未动时跑:

Tests 3 failed | 5 passed (8)
AssertionError: expected 'Selected 3 of {{count}} items' to be 'Selected 3 of 3 items'
AssertionError: expected 'A/B — {{a}} again, {{b}} again' to be 'A/B — A again, B again'
AssertionError: expected 'Hello {{name}} raw' to be 'Hello $& raw'

与预测逐条吻合。改完后 Tests 8 passed (8)

消费半径清扫(这条规则跑在哪就要扫到哪,不是只扫被改的包):createSafeTranslation 有 ~15 个包在用,root vitest 把 @object-ui/i18n alias 到 src,所以下游 no-provider 测试直接吃到这次改动。

pnpm exec vitest run packages/i18n → 22 files / 261 tests passed
pnpm exec vitest run <18 个下游 no-provider 测试文件> → 18 files / 100 tests passed
pnpm --filter @object-ui/i18n type-check → exit 0
pnpm --filter @object-ui/i18n lint → 0 errors(28 条既有 warning,均不在本次改的文件里)
node scripts/check-control-bytes.mjs → OK
node scripts/check-changeset-no-major.mjs → OK

下游那 100 条断言没有一条方向翻转,与前提核验一致:仓库里既没有重复占位符的文案,也没有把 $ 替换模式当值插进 t() 的地方。

Changeset:.changeset/safe-translation-fallback-replace-all.md(@object-ui/i18n patch)。


Generated by Claude Code

`createSafeTranslation`'s no-provider fallback used
`value.replace('{{k}}', String(v))`, which substitutes only the FIRST
occurrence of each placeholder. i18next — which serves the provider path —
substitutes every one, so a default string repeating a placeholder leaked
literal braces to users on hosts with no `I18nProvider` mounted.
Switches to `value.split(needle).join(String(v))`. Not `replaceAll`: both
`replace` and `replaceAll` interpret `$&`, `` $` ``, `$'` and `$$` in the
*replacement* string, which i18next does not — and interpolated values are
runtime data (record labels, search terms), so that divergence was already
reachable. split/join is literal on both sides and needs no regex escaping.
Key resolution (`defaults[key] || key`), the `String(v)` coercion and the
leave-it-literal behaviour for an unmatched placeholder are unchanged.
Fixes objectui#3418.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercelBot commented Aug 7, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 7, 2026 1:59am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-DTS-8mLG.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)24.58KB7.04KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)479.63KB105.43KB
core (index.js)2.47KB0.91KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)136.30KB34.76KB
fields (index.js)229.82KB56.53KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.05KB1.53KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)112.03KB28.88KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)232.79KB57.42KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)111.54KB26.97KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)185.25KB49.08KB
plugin-kanban (index.js)48.03KB13.22KB
plugin-list (index.js)104.72KB25.36KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.46KB1.21KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)0.20KB0.18KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

AGENTS.md commandment #-1: code comments are English. The repeated-placeholder
example carried a CJK sentence to illustrate the case; describe the word-order
motivation instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-DTS-8mLG.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)24.58KB7.04KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)479.63KB105.43KB
core (index.js)2.47KB0.91KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)136.30KB34.76KB
fields (index.js)229.82KB56.53KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.05KB1.53KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)112.03KB28.88KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)232.79KB57.42KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)111.54KB26.97KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)185.25KB49.08KB
plugin-kanban (index.js)48.03KB13.22KB
plugin-list (index.js)104.72KB25.36KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.46KB1.21KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)0.20KB0.18KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 7, 2026 02:11
@yinlianghui
yinlianghui added this pull request to the merge queueAug 7, 2026
Merged via the queue into main with commit a6ec93dAug 7, 2026
18 of 19 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3418-fallback-interpolation branch August 7, 2026 02:11
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…t path filtering into the jobs (objectstack-ai#3523 steps 1-2) (objectstack-ai#3722)
* ci(step 1): subscribe ci/lint/control-bytes/docs-links to merge_group (objectstack-ai#3523)
Step 1 of objectui#3523, on its own commit as the issue's ruling requires:
this is a pure addition and changes nothing about which pull requests or
pushes run CI.
The merge queue is enforced on this repository by a ruleset (objectstack-ai#3243 measured a
direct push to `main` returning 405 `Changes must be made through the merge
queue`), but not one of the 22 workflows subscribed `merge_group` — repo-wide
`event=merge_group` runs stood at total_count = 0. A queue nothing subscribes
to can only carry an empty required-check set, so it rebuilt each PR on the
current `main` and let it through without validating anything. On 2026-08-07
that was cashed in: objectstack-ai#3503 / objectstack-ai#3510 / objectstack-ai#3516 merged between 02:11Z and 02:15Z
with `Type Check` at conclusion=failure, on a `main` poisoned by objectstack-ai#3498, and
objectstack-ai#3505 had to hot-fix it.
The four workflows named by the issue now carry the trigger. It is spelled
`merge_group: types: [checks_requested]`; `checks_requested` is the only
activity type GitHub defines for this event today, so this is equivalent to
objectstack's bare `merge_group:` and merely refuses to inherit a second type
added later.
Two `ci.yml` jobs additionally needed to be told the third event exists —
both changes are no-ops for `pull_request` and `push`:
- `test` moves from `if: github.event_name == 'pull_request'` to
`!= 'push'`, so a queue build actually runs the suite instead of skipping
every shard.
- `docs`'s `should_run` treated anything that is not `push` as a pull request
and diffed `github.event.pull_request.*`, which is null on a queue build —
an empty revision range, i.e. the site build silently skipped on the last
check before `main`.
Verified rather than assumed: `concurrency` needs no merge-queue case. On
`merge_group` the group expression falls back to `github.ref`, which is the
queue's own generation — measured on objectstack, whose 3552 queue runs report
head_branch `gh-readonly-queue/main/pr-6594-251e888a…`. That can collide with
neither a PR group (a bare number) nor a push group (`refs/heads/main`).
Refs objectstack-ai#3523
* ci(step 2): move ci/lint path filtering out of the PR trigger into the jobs (objectstack-ai#3523)
Step 2 of objectui#3523, deliberately a separate commit from step 1: step 1
adds runs that did not exist, this one changes which pull requests start CI,
and mixing them would make that impossible to review apart.
`on.pull_request.paths-ignore` skips the WHOLE workflow when every changed
file matches, and GitHub has no per-job path filter. A docs-only or
changeset-only PR therefore started neither `ci.yml` nor `lint.yml` — objectstack-ai#3509
measured zero check runs from them. A check that is never *created* does not
fail a required-status-check rule, it leaves the pull request pending; inside
the merge queue it fails on the ruleset's 60-minute status-check timeout. So
none of `Lint`, `Type Check`, `Test (shard N/4)`, `Build & E2E` or
`Changeset Fixed Group Check` could be made required while the filter lived
on the trigger — which is why the queue's required set was empty to begin
with.
The filter is not deleted, it moves. `type-check`, `test`, `e2e` (ci.yml) and
`lint` (lint.yml) each open with a `Decide whether this change needs a full
run` step, and every step after it carries
`if: steps.relevant.outputs.should_run == 'true'`. The job always runs and
always reports; the paths decide only whether it does any work. That is the
shape `ci.yml`'s `docs` job has used since objectstack-ai#3450 — not a new mechanism — and
the exclusion lists are byte-for-byte the `paths-ignore` they replace, so
which PRs pay for a full run is exactly as before.
Two deliberate narrowings, both stated so they can be argued with:
- `paths-ignore` stays on the `push` trigger. Branch protection and the merge
queue judge pull requests and queue builds, never pushes to `main`, so the
push lane gains nothing from losing it and would cost a full CI run on
every docs merge. It also gives the ignore list one authored home, which
`merge-queue-reporting.test.ts` then pins the in-job copies against.
- `changeset-check` is not gated. It is a checkout and one `node` call, so
short-circuiting it would cost more in complexity than it saves.
The gate fails OPEN: an unresolvable diff runs everything rather than
reporting green having built nothing (objectstack#4928's filter contract).
Measured both ways against a fixture repository — as shipped an unreachable
base sha yields should_run=true; rewritten with the `|| echo ""` spelling
`ci.yml`'s older `docs` gate still uses, the same input yields
should_run=false, i.e. a silent full skip. That pre-existing `docs` gate is
reported separately rather than changed here.
`scripts/__tests__/merge-queue-reporting.test.ts` pins both steps; against
the pre-change workflows 9 of its 10 assertions go red, each naming its own
regression. `lint-workflow.test.ts`'s TypeScript-exclusion tripwire now reads
the in-job list as well as the trigger, which is where that list now lives.
Refs objectstack-ai#3523
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

createSafeTranslation 的无 provider 回退只替换每个占位符的第一次出现

2 participants

@yinlianghui@claude