Skip to content

feat(cache): authenticate remote cache uploads with GitHub Actions OIDC - #774

Draft
wan9chi wants to merge 3 commits into
mainfrom
feat/remote-cache-github-oidc
Draft

wan9chi wants to merge 3 commits into
mainfrom
feat/remote-cache-github-oidc

Conversation

@wan9chi

@wan9chi wan9chi commented Sep 28, 2026

Copy link
Copy Markdown
Member

Motivation

The self-hosted Cloudflare Worker from #718 accepts /store only with a GitHub Actions OIDC token for the namespace, but vp run never sends Authorization. #718's e2e wrapper hides this by injecting a signed token itself. With this change, vp run requests the token itself in the job, so publishing to the Worker only needs permissions: id-token: write and VP_REMOTE_CACHE=read-write, with no token to configure. This follows RFC #716, section 5. Once this lands, #718's e2e wrapper can serve a fake token endpoint instead of injecting its own token.

Changes

  • vt_remote_cache: Client::new takes a StoreAuth: Anonymous, GithubOidc (the job's ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN), or GithubActionsWithoutOidc.
    • Before the first store, the client requests a token whose audience is the endpoint without its query, userinfo, or trailing slash, and sends it as Authorization: Bearer <token> on /store only. Fetches and downloads stay anonymous, and the request token goes only to GitHub's token endpoint.
    • The token is cached per endpoint and reused until it expires within 60 seconds, reading exp without verifying it. Concurrent stores share one token request. A failed token request is kept and returned for later stores.
    • Errors leave out the token endpoint's URL and response body, and Debug leaves out the tokens and the endpoint.
    • With GithubActionsWithoutOidc, a 401 keeps the message HTTP status 401 and gets the cause "grant id-token: write to this job".
  • vt: Session::cache() chooses the StoreAuth from the session envs. Once an upload is unauthorized (no token, or a 401 or 403 response), uploads to that endpoint stop for the rest of the run, and later ones return the same error without a request, so the summary shows one warning. The task's exit status doesn't change.
  • The OIDC variables are still passed through to tasks (fix(env): pass through GitHub Actions OIDC variables #691), so npm Trusted Publishing keeps working. The RFC proposes removing them.
  • Tests: unit tests in both crates, and two e2e cases with a new vtt oidc-remote-cache helper that fakes the token endpoint and a store that checks the bearer token. Neither needs Node, so they run on all platforms.
  • Updated the vt_remote_cache README, the cache.remote doc comments, and the remote caching changelog entry.

wan9chi and others added 3 commits September 28, 2026 11:49
In a GitHub Actions job granted `id-token: write`, `vp run` now requests a
GitHub OIDC token for the remote cache endpoint before the first upload and
sends it as `Authorization: Bearer <token>` on `/store`. Fetches and
downloads stay anonymous, and the runner's request token goes only to
GitHub's token endpoint.

The audience is the endpoint without its query, fragment, userinfo, or
trailing slash. The token is kept in memory per endpoint and reused until
it expires within 60 seconds; concurrent uploads share one token request.

If the token request fails, or `/store` responds with 401 or 403, uploads to
that endpoint stop for the rest of the run, and every skipped upload reports
the same reason, so the summary shows one warning. A 401 from a GitHub
Actions job without the OIDC variables adds the cause "grant `id-token:
write` to this job". Without the variables elsewhere, uploads are
anonymous as before.

The OIDC variables are still passed through to tasks, unlike the RFC
proposes, so npm Trusted Publishing keeps working.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… client

Keep the token state on `Client` instead of mirroring `StoreAuth` in an
internal enum, merge `OidcTokenError` variants that only differ in wording,
read only the JWT payload, and fold overlapping tests together.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

fspy benchmark

linux

dynamic/launch             change  -0.06%  [ -9.68% .. +10.16%]  overhead  +262.23%
dynamic/access             change  -0.31%  [ -1.84% ..  +1.24%]  overhead   +14.35%
dynamic/access-relative    change  +0.11%  [ -0.99% ..  +1.12%]  overhead   +60.16%
dynamic/access-contended   change  +0.14%  [ -1.74% ..  +1.67%]  overhead   +13.71%
static/launch              change  +0.96%  [ -4.55% ..  +5.81%]  overhead  +730.97%
static/access              change  +0.06%  [ -1.47% ..  +1.08%]  overhead  +805.08%
static/access-relative     change  +0.02%  [ -0.82% ..  +0.92%]  overhead +1384.91%
static/access-contended    change  -0.08%  [ -1.20% ..  +0.79%]  overhead +3135.40%

macos

dynamic/launch             change  -0.21%  [ -4.61% ..  +4.64%]  overhead  +215.70%
dynamic/access             change  -0.70%  [ -4.10% ..  +2.45%]  overhead    -0.33%
dynamic/access-relative    change  -0.30%  [ -2.77% ..  +2.42%]  overhead  +251.77%
dynamic/access-contended   change  +3.76%  [ -5.97% .. +11.76%]  overhead    +5.37%

windows

dynamic/launch             change  +0.03%  [ -4.67% ..  +4.48%]  overhead   +26.34%
dynamic/access             change  -0.32%  [ -5.83% ..  +3.12%]  overhead    +0.75%
dynamic/access-relative    change  +0.35%  [ -7.35% .. +14.71%]  overhead    +1.97%
dynamic/access-contended   change  +0.18%  [ -1.77% ..  +4.88%]  overhead    +1.46%

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant