Skip to content

Commit 4b09689

Browse files
hotlongclaude
andauthored
ci: wire the Vercel turbo remote cache into Build Core, signed and inert until configured (#21191)
Part of #21186 Clause-②: no. This is CI wiring and changes no contract's acceptance or rejection behaviour. Stage 1 of the card: measure, audit, then inert wiring. The card stays OPEN for stage 2, where the maintainer sets the two missing values. The stage-2 checklist and the inertness run URLs are in the card's report comment. ## What changes - `turbo.json`: `remoteCache.signature: true`. Turbo then tags every remote-cache upload with an HMAC keyed by `TURBO_REMOTE_CACHE_SIGNATURE_KEY` and verifies the tag on every download. It is not a task hash input (measured below), and it does nothing while remote caching is off. - `.github/workflows/ci.yml`: the Build Core step `Build packages (excluding docs)` gets four env lines, and nothing else in the file changes. - `TURBO_CACHE` is `local:rw,remote:rw` on `merge_group` and on `push` / `workflow_dispatch` against `main`, and `local:rw,remote:r` for every other event. Read-only is the default branch of the expression. Both branches are non-empty literals. - `TURBO_TOKEN` is passed only when the signing-key secret is non-empty. `TURBO_TEAM` is a repository variable, and the key is a secret. - Untouched: every other job and step, `pull_request_target`, the shard and nightly workflows, the `--force` legs, and every required check name. ## Hash compatibility with cloud's framework build Cloud's `test.yml` step `Install framework deps + build` builds this repo at cloud's pin. | reading | where | result | |---|---|---| | Real CI logs at `4b4ee88fbc` (cloud's current pin), turbo 2.10.10 | objectstack `merge_group` run 36674533149, Build Core, against cloud `merge_group` run 36797594481 | **72/72 identical** task hashes | | The same, after cloud's signing config and `TURBO_CACHE` landed | against cloud `push` run 36846326339 | **72/72 identical** | | Local `--dry=json` at `2c1cef3345`, turbo 2.11.5 | Build Core's command and env against cloud's (separate checkout under a directory named `objectstack`, `.turbo/config.json` with signature on, cloud's step env with empty credentials, spec pass then full pass) | **72/72 identical**, and `globalCacheInputs` byte-identical | | Control for the row above | cloud side with `OS_SKIP_DTS=1` | 0/72 | No change is needed on either side. The one declared input that would split them is the `globalEnv` value `OS_SKIP_DTS`, which neither workflow sets. ## Signing is hash-neutral `--dry=json` over `build test test:repo typecheck`, 231 tasks with a command: - HEAD against HEAD with the `remoteCache` block removed: 231/231 identical. - Positive control, one extra `globalEnv` entry: 0/231 identical. - The mutation was restored from HEAD and verified by blob hash (`45d71bab`). ## Audit: why only build tasks are wired, and what must land before stage 2 - **`test` / `test:repo`: kept off the remote.** Their cross-package inputs are hand-declared, the detector that keeps them honest reads source spellings only, and the Test Core legs carry the hash history of #18671 and #19278. - **`@objectstack/docs#build`: kept off.** Build Docs forces execution and gets no credentials. The input gap of #11264 is declared in `turbo.json` today. - **`build`: under-declared, measured.** For each probe I appended one comment line to a tracked file outside the task's declared inputs and re-derived the plan. | file | read by | build hashes moved | what it affects | |---|---|---|---| | `scripts/tsup-drop-sources-content.mjs` | root `tsup.config.ts` and 21 package tsup configs | 0/72 | every `.map` file | | `scripts/sync-scaffold-emission-policy.mjs` | `create-objectstack` build | 0/72 | `dist/templates` | | `packages/cli/src/commands/init.ts` | the same script | `create-objectstack#build` unchanged (cli moved) | `dist/templates` | | `scripts/check-dev-prereqs.mjs` | spec, core, organizations, plugin-auth | 0/72 | the stamp in `dist/` | | `scripts/check-regen-pending.mjs` | spec `gen:schema` | 0/72 | the schema stamp | | `scripts/check-dts-emitted.mjs` | every build | 0/72 | verdict only | - Controls: the root `tsup.config.ts` moved 72/72, and `packages/create-objectstack/tsup.config.ts` moved `create-objectstack#build`. - Output effect, `create-objectstack`: I changed `SCAFFOLD_TYPESCRIPT_RANGE` in `init.ts` and ran the build's first command. The template that tsup copies into `dist/templates` went from `^5.3.0` to `^5.9.0`, while `create-objectstack#build` stayed `840dbe274b784226`. - The spec build's authorable-surface check also anchors on `merge-base(HEAD, origin/main)`. That changes the verdict only, not the outputs. - **Today**, these gaps already exist under the actions/cache replay. - **The remote cache widens them in two ways.** `merge_group` trees become writers, including trees that a lint gate then dequeues. Cloud also replays them at older pins. - **So the input declarations must land before the secrets are set.** That is a `turbo.json` change outside this PR's surface. It is reported on the card as a stage-2 precondition. Until then this wiring stays inert. ## turbo 2.11.5, re-measured against a mock remote cache The card's facts were measured on 2.9/2.10. This repo is on 2.11.5, so I measured them again. | env | summary line | requests | |---|---|---| | all three credentials empty, `remote:rw` (today) | `Remote caching disabled (remote cache requested — set TURBO_TOKEN and TURBO_TEAM, …)` | none | | `TURBO_TEAM` set, token empty | the same | none | | token set, `TURBO_TEAM` empty | `Remote caching disabled (TURBO_TOKEN set without TURBO_TEAM)` | none | | `TURBO_CACHE=''` | `WARNING no caches are enabled`, `cache bypass, force executing` | none | | all set, `remote:r` | `Remote cache is read-only, skipping upload` | GET only, no PUT | | all set, `remote:rw` | `Remote caching enabled` | PUT with `x-artifact-tag` present | | all set, `remote:r`, after that write | `cache hit, replaying logs`, summary `source: REMOTE` | GET | | wrong key | miss | GET only | | key empty, token set | `TURBO_REMOTE_CACHE_SIGNATURE_KEY is too short (0 bytes)` | GET, still signed. The wiring never passes the token in this state. | | key unset, token set, `remote:rw` | `WARNING artifact signature error`, no upload | GET | ## Acceptance notes - **`workflow_dispatch` writes only against `main`.** That is narrower than the card's list. A dispatch from a feature branch runs that branch's unreviewed tree, which is the case the read-only default exists for. - **`schedule` (the hourly full run) reads only.** The card lists three writer events, and this follows it. - **Same-repo PRs hold the token.** Turbo hands `TURBO_*` to the tasks it spawns, so a same-repo PR's build scripts hold the token on `pull_request`, exactly as in cloud. Fork PRs receive no secrets. - **Not measured: the Vercel docs build.** `apps/docs/vercel.json` runs turbo outside CI. If that Vercel project uses a remote cache, signing without a key there gives `artifact signature error` and every remote read misses. Builds still succeed. - **Option, not taken:** turbo 2.11 offers `futureFlags.longerSignatureKey` to make a key shorter than 32 bytes fatal. It is outside this PR's surface. --- _Generated by [Claude Code](https://claude.ai/code/session_57c17198-e47f-48f8-9242-3fc94c651c85)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9fb9b25 commit 4b09689

2 files changed

Lines changed: 37 additions & 0 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2047,7 +2047,41 @@ jobs:
20472047
- name: Install dependencies
20482048
run: pnpm install --frozen-lockfile
20492049

2050+
# ── Turbo remote cache (#21186) ─────────────────────────────────────
2051+
# Vercel's managed Turborepo remote cache. objectstack-ai/cloud's
2052+
# test.yml builds this repo at its pinned SHA with the same command and
2053+
# reads/writes the same Vercel team, and the task hashes are identical
2054+
# in both places (72 of 72 build tasks, read from both CI logs at one
2055+
# SHA), so a pin bump there can replay what this step built on `main`.
2056+
#
2057+
# Scope: THIS step only, i.e. `build` tasks. `test` / `test:repo` stay
2058+
# off the remote: their cross-package inputs are hand-declared, and a
2059+
# wrong hash replayed from a remote reaches every run and cloud too.
2060+
# Build Docs forces execution and carries no credentials.
2061+
#
2062+
# TURBO_CACHE is the env form of `--cache`. Writes happen on
2063+
# `merge_group` and on `push` / `workflow_dispatch` against main; every
2064+
# other event, `pull_request` and `schedule` included, only reads,
2065+
# because read-only is the default branch of the expression. This is
2066+
# turbo configuration, not a property of the credential: a same-repo PR
2067+
# receives the secrets and runs its own copy of this file; fork PRs
2068+
# receive none. ⚠️ Never let it evaluate to '': turbo then disables the
2069+
# LOCAL cache too (`no caches are enabled`) and re-executes everything.
2070+
#
2071+
# Credentials: TURBO_TOKEN (secret) is passed only when the signing key
2072+
# is non-empty, because an empty key still "signs" with a zero-length
2073+
# HMAC key; TURBO_TEAM is a repo VARIABLE so the slug is not masked out
2074+
# of the log; TURBO_REMOTE_CACHE_SIGNATURE_KEY (secret) must equal
2075+
# cloud's. turbo.json's `remoteCache.signature` makes turbo tag every
2076+
# upload and verify every download with that key. Any of the three
2077+
# unset: remote caching is off and the build is unchanged; the summary
2078+
# then reads `Remote caching disabled (remote cache requested — …)`.
20502079
- name: Build packages (excluding docs)
2080+
env:
2081+
TURBO_CACHE: ${{ (github.event_name == 'merge_group' || ((github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}
2082+
TURBO_TOKEN: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY != '' && secrets.TURBO_TOKEN || '' }}
2083+
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
2084+
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
20512085
run: pnpm build
20522086

20532087
# [#15042] The closure-wide declaration re-check. `check-dts-emitted` runs

‎turbo.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
"globalEnv": ["OS_SKIP_DTS"],
55
"globalPassThroughEnv": ["VITEST_MAX_WORKERS"],
66
"agentGuidance": false,
7+
"remoteCache": {
8+
"signature": true
9+
},
710
"tasks": {
811
"build": {
912
"dependsOn": ["^build"],

0 commit comments

Comments
 (0)