Repository navigation
Commit 4b09689
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2047 | 2047 | | |
2048 | 2048 | | |
2049 | 2049 | | |
| 2050 | + | |
| 2051 | + | |
| 2052 | + | |
| 2053 | + | |
| 2054 | + | |
| 2055 | + | |
| 2056 | + | |
| 2057 | + | |
| 2058 | + | |
| 2059 | + | |
| 2060 | + | |
| 2061 | + | |
| 2062 | + | |
| 2063 | + | |
| 2064 | + | |
| 2065 | + | |
| 2066 | + | |
| 2067 | + | |
| 2068 | + | |
| 2069 | + | |
| 2070 | + | |
| 2071 | + | |
| 2072 | + | |
| 2073 | + | |
| 2074 | + | |
| 2075 | + | |
| 2076 | + | |
| 2077 | + | |
| 2078 | + | |
2050 | 2079 | | |
| 2080 | + | |
| 2081 | + | |
| 2082 | + | |
| 2083 | + | |
| 2084 | + | |
2051 | 2085 | | |
2052 | 2086 | | |
2053 | 2087 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
7 | 10 | | |
8 | 11 | | |
9 | 12 | | |
| |||
0 commit comments