Skip to content

ci(test-core): a shard whose tests read no .d.ts builds its closure with OS_SKIP_DTS=1 - #25465

Merged
os-zhuang merged 6 commits into
mainfrom
claude/project-thread-qwwbnr
Oct 23, 2026
Merged

os-zhuang merged 6 commits into
mainfrom
claude/project-thread-qwwbnr

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Fixes #25375
Clause-②: no
Metadata-protocol: none

维护者速读(草稿)

改了什么:Test Core 六个分片在建依赖闭包前多一步「Choose this shard's declaration build」。分片里没有 @objectstack/cli、@objectstack/metadata 时,整个分片设 OS_SKIP_DTS=1,构建只出 JavaScript,不再生成 .d.ts(spec 那次 6 GB 堆的声明构建、cli 的完整 tsc 都省掉)。带这两个包的分片照旧。只改 .github/workflows/ci.yml。

为什么改:vitest 只加载 JS,大多数分片生成的声明文件没人读。本地 4 核冷构建同一闭包(65 个包):开声明 5m59s,关声明 1m24s。卡面原方案是六个分片全关,但实测全关会让 cli(4 个测试文件,24 例)和 metadata(1 个文件)红,原因都是 TS7016 找不到 spec / cli 的 .d.ts,这些测试本来就是在编译已发布的声明。所以这两个包所在的分片保留声明。

风险与代价(含回滚):① 关声明的分片和 Build Core 不在同一个 turbo 哈希空间(turbo.json 的 globalEnv),读不到 Build Core 写进远端缓存的带声明产物,只能读本分片在合并队列和 main 上写的无声明产物;落地后头几轮会偏冷,之后自愈。② 以后若有新测试读已建好的 .d.ts 却落在关声明的分片,会以 TS7016 明确变红,修法是把它的包加进 DTS_READERS,不会静默放过。③ 全量跑时 cli 切 3 片、metadata 占 1 片,当前切法下只有分片 1、6 省;PR 只跑受影响包时省的分片更多。回滚:删掉这一步即可。

席位意见:同意落地(devx 席位已在 head e62c37b 复核,ACCEPT 见 #25375)。机制早已在 main 上(turbo.json 的 globalEnv 和各包 tsup 配置),本 PR 只决定哪些分片打开它。收窄到不含 cli、metadata 的分片是对的;漏掉的读者会以 TS7016 明确变红,不会静默放过。落地后头几轮偏冷属预期。

你要做的:以 GOVERNED_APPROVERS 账户审批本 PR(受管面 Tier H)。

What changed

One new step in the test job, right after "Compute this shard's package set":
it writes OS_SKIP_DTS=1 to $GITHUB_ENV unless the shard's package list
carries a package in DTS_READERS (@objectstack/cli @objectstack/metadata).
Through $GITHUB_ENV it reaches both closure builds, the test step and the
main-push seed, so all of them hash in one namespace (turbo.json globalEnv).
turbo.json is untouched.

The card's step 2: tests that read a .d.ts

The card's premise "vitest never reads a .d.ts" is false for two packages.
The static grep the card names gives 70 test files that mention .d.ts, so I
measured instead. Under OS_SKIP_DTS=1, the whole Test Core list (dogfood
excluded, OS_TEST_TIERS=queue) ran on 830a63a in a cloud container. These
went red:

Package Test file Reads Disposition
@objectstack/cli test/published-subpath-console.pin.test.ts (6 tests) cli's own dist/console.d.ts shard keeps DTS
@objectstack/cli test/published-subpath-hook-body.pin.test.ts (4) cli's own dist/hook-body.d.ts shard keeps DTS
@objectstack/cli test/scaffold-emission-typechecks.test.ts (12) spec's dist/**/*.d.ts (TS7016) shard keeps DTS
@objectstack/cli test/scaffold-test-script-passes.test.ts (2) spec's dist/**/*.d.ts (TS7016) shard keeps DTS
@objectstack/metadata src/serializers/typescript-serializer-annotation.test.ts (1) spec's dist/**/*.d.ts (TS7016) shard keeps DTS

@objectstack/example-showcase was killed (exit 137) in that run because the
local container ran out of memory. Run alone under OS_SKIP_DTS=1 it passed:
37 files, 443 tests.

Silent-skip check: 32 packages own a test that compiles a TS program or names a
.d.ts. I ran them again with declarations on and compared the vitest totals
per task. All 38 test/test:repo totals were identical except one.
@objectstack/spec:test ran 21634 passed with DTS and 21632 passed plus 1
skipped without it. The difference is
scripts/root-entry-type-nameability.pin.test.ts, which skips by its own design
when spec's dist declarations are absent. That pin runs with
OS_EXPECT_ROOT_NAMEABILITY=1 against a built dist in lint.yml's
root-entry-type-nameability-pin-built-dist step, so its coverage holds.

Measurement

Local, cloud container, 4 cores, the same 65-package closure (every package
whose build the full Test Core list schedules), turbo run build --force --concurrency=4:

Wall Σ task time @objectstack/spec#build @objectstack/cli#build
declarations on 5m59s 838s 92.2s 18.5s
OS_SKIP_DTS=1 1m24s 146s 47.8s 7.6s

That is 23% of the DTS-on wall time, against the card's ceiling of 60%.

Today's baseline on CI, from the card: cold closure step 3.1 to 6.0 min per
shard on PR run 39891259937, about 31 runner-minutes in total.

On main after the shard-timings refresh (#25409), the full list puts metadata
on shard 2 and the three cli slices on shards 3 to 5, so shards 1 and 6 skip
on a full run. On a PR the affected set decides. This PR's own diff selects
only @objectstack/driver-sql (a declared ci.yml input), so its first run
measures a cold DTS-off closure. Nothing in the remote cache has DTS-off
hashes yet. The CI step timings from this PR, both ways, are posted as a PR
comment.

Replay trade-off: a skipping shard cannot replay Build Core's DTS-on remote
artifacts. In steady state that costs nothing. Every package hash a merge
introduces is built in the queue run's shard closures, which write the remote
(remote:rw on merge_group and main pushes). That gives later PRs a DTS-off
hit for every package they do not change. The packages a PR does change miss
both ways, and they build faster without declarations. The cost is real only
in the first runs after landing.

Gates

node scripts/pm/dispatch-gates.mjs --commands lists 54 commands. All 54 exit 0
on 8d58beb. check:dts-closure and check:dual-build-cjs-loads were re-run
after a full DTS-on pnpm build, because they read a built tree. actionlint
is not installed here. The workflow parses as YAML, and the job's step order
is unchanged around the new step.

Acceptance notes

  • Scope deviation from the card's step 1: two packages' shards keep their
    declarations, for the reason above. The other way to save on those shards is
    to keep only spec's and cli's declarations under a per-package variable.
    That needs tsup.config.*, cli's build script and turbo.json
    globalEnv, which is outside this card's file surface, so it is not done
    here.
  • .github/workflows/test-nightly-tiers.yml builds its own shard closure and
    is not changed here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VqzDQEbeHPVJoaEoutGsTi


Generated by Claude Code

…ith OS_SKIP_DTS=1

A new step after "Compute this shard's package set" writes OS_SKIP_DTS=1
to $GITHUB_ENV unless the shard carries @objectstack/cli or
@objectstack/metadata, so the closure build, the slice build, the test
step and the main seed hash in one namespace (turbo.json globalEnv).

Those two are kept on DTS because their tests compile against built
declarations: under OS_SKIP_DTS=1 the whole Test Core list on 830a63a
went red only in cli's published-subpath-console/-hook-body pins and
scaffold typecheck tests, and in metadata's
typescript-serializer-annotation test (TS7016). Every other package's
test counts matched the DTS-on run except spec's
root-entry-type-nameability pin, which skips by design and runs in
lint.yml's built-dist step.

Local cold closure, 65 packages, 4 cores, --force: 5m59s with
declarations, 1m24s without.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqzDQEbeHPVJoaEoutGsTi
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 23, 2026
…is PR's closure

Measures the cold DTS-on closure build for the same closure the previous
push built cold with OS_SKIP_DTS=1. Reverted by the next push.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqzDQEbeHPVJoaEoutGsTi
…RS and --force

The measurement it ran is posted on the PR. ci.yml is back to 8d58beb's.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqzDQEbeHPVJoaEoutGsTi
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Measurement on this PR, as the card's step 3 asks: the same closure, cold both ways.

This PR's diff selects one package, @objectstack/driver-sql (a declared ci.yml input), on Test Core (1/6). Its ^build closure is 6 packages: spec, formula, types, observability, metadata-core and core.

Push Head Declarations Closure step turbo Cache
1 7b998bb OS_SKIP_DTS=1 (the change) 31s Time: 29.747s 0 cached, 6 total, all cache miss (no DTS-off hash existed in the remote yet)
2 a0b56bd on, plus --force (temporary) 74s Time: 1m13.866s 0 cached, 6 total, all cache bypass, force executing

Push 1 took 40% of push 2's closure time, against the card's ceiling of 60%. @objectstack/driver-sql:test passed in both runs, and every check was green on both heads. Jobs: 120488980779 (push 1) and 120493083606 (push 2).

The local full-closure reading in the PR body says the same thing at larger scale. The 65-package closure took 5m59s with declarations and 1m24s without.

Push 2 existed only to measure. The next commit, be7daa3, reverts it, so ci.yml on head e62c37b is byte-identical to 8d58beb's.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 23, 2026 17:50
@os-zhuang
os-zhuang enabled auto-merge October 23, 2026 17:50
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit f7b6019 Oct 23, 2026
48 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-qwwbnr branch October 23, 2026 18:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants