You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit ba9e0c3
Browse filesBrowse the repository at this point in the historyBrowse files
ci: pin credential-holding third-party actions by sha, and pass the Turbo cache credentials only to merge-queue and main runs (#25559)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoRyAStcu3CV5kiXeCX564xD)_
CI supply-chain hardening, two commits that can land or be dropped
independently:
1. **Pin the credential-holding third-party actions by commit sha**
(`96212c2`).
2. **Pass the Turbo remote-cache credentials only to merge-queue and
main runs** (`7ea2af7`).
## 1. Third-party actions pinned by sha
Each names the commit its floating major resolved to when this was
written (read with `git ls-remote` against the upstream repository),
with a trailing version comment, the form `release.yml` already uses for
`changesets/action`.
| workflow | action | was | now | credential in reach |
|---|---|---|---|---|
| `docker-publish.yml` | `docker/setup-qemu-action` | `@v4` |
`99012661954931238ded8c8b007157a8430204e1` # v4.4.0 | `packages: write`
job |
| `docker-publish.yml` | `docker/setup-buildx-action` | `@v4` |
`f87e5991a6d7451dcb8d9637bfbc97413f497069` # v4.4.1 | `packages: write`
job |
| `docker-publish.yml` | `docker/login-action` | `@v4` |
`81c6f93d4bc991e510b342a00b419b3bb4efc5c6` # v4.7.0 | `GITHUB_TOKEN` as
registry password |
| `docker-publish.yml` | `docker/metadata-action` | `@v6` |
`dc802804100637a589fabce1cb79ff13a1411302` # v6.2.0 | `packages: write`
job |
| `docker-publish.yml` | `docker/build-push-action` | `@v7` |
`c3c9e263c25d99ce0380d002d59b67737d91b0dc` # v7.4.0 | pushes with the
logged-in registry credential |
| `check-links.yml` (2 steps) | `lycheeverse/lychee-action` | `@v2` |
`e7477775783ea5526144ba13e8db5eec57747ce8` # v2.9.0 | `token` input
defaults to the job token |
| `ci.yml` (filter job) | `dorny/paths-filter` | `@v4` |
`ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d` # v4.0.3 | `token` input
defaults to the job token |
Already pinned and untouched: `changesets/action` (`release.yml`),
`google/osv-scanner-action` (`validate-deps.yml`). Left on tags:
first-party `actions/*` and `github/codeql-action` (GitHub-owned);
`pnpm/action-setup` in `showcase-smoke.yml` receives no credential. The
readers of the paths-filter ref (`check-ci-filter-parity.mjs`,
`dispatch-gates.mjs`) match the action by prefix, so a pinned ref reads
the same.
## 2. Turbo remote-cache credentials scoped by event
Before: every same-repo `pull_request` run received `TURBO_TOKEN` and
`TURBO_REMOTE_CACHE_SIGNATURE_KEY`. The read-only `remote:r` setting is
turbo client configuration; the token itself can write and the key can
sign, so any code a PR runs during its build could upload a signed
artifact that a later queue or main build replays.
After, in all twelve carriers (`ci.yml` ×7, `lint.yml` ×5), both secrets
are gated on the same condition:
```yaml
TURBO_TOKEN: ${{ (github.event_name == 'merge_group' || github.ref == 'refs/heads/main') && secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY != '' && secrets.TURBO_TOKEN || '' }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ (github.event_name == 'merge_group' || github.ref == 'refs/heads/main') && secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY || '' }}
```
- Credentialed: `merge_group`, and every run on `refs/heads/main` (push,
the hourly schedule, `workflow_dispatch` on main). Not credentialed:
`pull_request`, and a dispatch on any other branch. `TURBO_CACHE` and
`TURBO_TEAM` are unchanged, so the write rule is exactly as before.
- **Measured question, PR reads with or without the secrets:** Vercel's
remote cache authenticates reads as well as writes, and with
`remoteCache.signature: true` turbo needs the key to verify a download,
which is the same key that signs. So a PR run cannot read the remote
without holding both; the answer is **not at all**. A PR run builds from
the restored Actions-cache seed (`local:rw`), which `main` saves. ⚠️ I
could not reach `api.vercel.com` from this container (egress refused),
so the "reads need the token" half is read from turbo's behaviour as
`assert-turbo-remote-cache.mjs` already documents it (any of the three
unset turns the remote off), not from a live probe; this PR's own CI run
is the first live reading.
- Cost, measured on this PR's own runs. With the remote withheld, a PR
build reads only the Actions seed `main` saves:
- At `7ea2af7` (00:27Z, before PR #25523 landed): Build Core `76 build
task(s) -- 0 remote hit(s), 0 local hit(s), 76 miss(es)`, build step 6 m
4 s. A credentialed PR in the same window read 53 remote / 0 local / 23
miss (1 m 40 s), so the seed was contributing nothing to anyone, most
likely because the 2.4 GB Test Core seed snowball (#25452) was flushing
the 10 GB pool on every merge.
- At `945502c` (01:34Z, after PR #25523, the merge of main): Build Core
`76 build task(s) -- 0 remote hit(s), 76 local hit(s), 0 miss(es)`,
build step 911 ms, entirely from the seed.
- So with the pool no longer churned, a PR build no longer needs the
remote. What remains is seed lag: tasks whose inputs changed in merges
that landed after the newest seed was saved rebuild cold. The
`assert-turbo-remote-cache` notice prints the local-hit count on every
PR run, so a regression is visible in the log.
- `assert-turbo-remote-cache.mjs` gains `isCredentialedRun()` (same
condition) and reports a disabled remote on a non-credentialed run as a
`notice` ("withheld by design"), not a warning. Writing runs still red
when the remote is off. New self-test battery `credentialed run` (5
cases): 37 cases across 7 batteries, exit 0.
- Build Core's "Credentials" comment, the authority the other carriers
point back to, is rewritten to the new rule.
## Verification
Gates derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `7ea2af7`; results under "Gates" below.
### Gates (local, head `7ea2af7`)
- `node scripts/pm/dispatch-gates.mjs --commands` derived **70**
families from the diff; **66 exit 0**; **4 NOT MEASURED** (exit 3,
prerequisite: they read built `dist/`, and no package source is in this
diff): `check:dts-closure`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure`, `check:sourcemap-no-sources-content`. CI
runs them on a real build.
- Reconciled: `dispatch-gates.mjs --ran` reports "70 derived families
accounted for — 66 run, 4 NOT-MEASURED", exit 0.
- Among the 66 green: `check:ci-filter-parity` (+ self-test),
`check:required-contexts`, `check:pm-governed-merges`,
`check:select-gate-families`, `check:self-test-wired`,
`check:workflow-step-name-quoting`, `check:nul-bytes`, and
`assert-turbo-remote-cache.mjs --self-test` (37 cases, 7 batteries).
- No changeset: the diff touches `.github/workflows/**` and
`scripts/ci/**`, which no package publishes (`skip-changeset`).
## 维护者速读(草稿)
- **改了什么**:①CI 里拿得到凭据的第三方 action(docker 发布五个、lychee、paths-filter)改为按提交
sha 钉死;②Turbo 远程缓存的 token 和签名密钥只给合并队列和 main 上的运行,PR 运行不再拿到。
- **为什么改**:浮动标签可以被上游重指,下一次运行就把凭据交给未经审阅的代码;PR 里跑的任何代码(包括新加依赖的安装脚本)此前都握着
main 信任的缓存签名密钥。
- **风险与代价(含回滚)**:PR 构建不再读远程缓存,只读 main 存下的 Actions 缓存种子。实测:#25523
落地前种子被挤掉,PR 的 Build Core 0 命中、6 分 4 秒;#25523 落地后本 PR 的 Build Core 76/76
全部命中种子、构建 0.9 秒。剩余代价是种子之后新合并改动过的包需要冷构建。回滚:单独 revert 第二个提交即可,第一个提交独立成立。
- **席位意见**:
- **你要做的**:第②条已于 2026-10-24 裁定采用(收回凭据并依赖 Actions 种子);批准后由席位落地。本 PR 保持
draft。
Session: `https://claude.ai/code/session_01W9H2WdXx3kPkf6moDnRdRE`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01W9H2WdXx3kPkf6moDnRdRE
---------
Co-authored-by: Claude <noreply@anthropic.com>
0 commit comments