Skip to content

feat(remote-cache): add self-hosted public cache server - #718

Draft
fengmk2 wants to merge 16 commits into
remote-cache-e2e-rejected-uploadsfrom
feat/public-remote-cache
Draft

fengmk2 wants to merge 16 commits into
remote-cache-e2e-rejected-uploadsfrom
feat/public-remote-cache

Conversation

@fengmk2

@fengmk2 fengmk2 commented Sep 12, 2026 •

Copy link
Copy Markdown
Member

Add packages/remote-cache for the server in #716, using Cloudflare Workers, primary D1 metadata, and a private R2 bucket. The server supports public reads and GitHub Actions OpenID Connect checks for writes. Stores use streaming uploads, atomic publication, storage limits, and automatic data expiry.

One workflow deploys related changes to a persistent staging Worker, D1 database, and R2 bucket. Internal PRs, main pushes, and manual runs share these resources. The workflow runs deployments and smoke tests in sequence, then updates PR comments with the tested revision and manual instructions. Each deployment replaces the previous revision. Closing a PR keeps staging available. Setup uses repository secrets and variables. Repository maintainers can configure staging without a GitHub environment.

The e2e plan defines automated checks and manual exercises. PR runs check public reads and rejected writes. Pushes to main also check authorized uploads, multipart storage, replacement, concurrency, and quotas. Local tests repeat smoke checks against reused storage and retain maximum-payload and scheduled-handler coverage. Real Cron execution and maximum payloads on Cloudflare require separate release exercises.

/fetch returns only { kind: "fallback", key } for fallback matches and does not read R2. Exact matches return the value and blob_id. A missing or unreadable exact value returns 503. Fetch misses and unavailable blobs return plain-text 404, as specified in the local RFC.

Setup rejects incompatible origins and repositories before it changes existing policies or saved configuration. Each Worker has separate rate-limit counters that remain stable across revisions. Workers Free CPU support remains unverified. The guide recommends Workers Paid for the full payload limits.

Motivation

Maintainers need a cache service in their own Cloudflare account. Developers and fork contributors must reuse public task results without login. Only trusted jobs on main should publish those results. Reviewers need one persistent staging environment and a clear verification result after each related change.

@socket-security

socket-security Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

fspy benchmark

linux

dynamic/launch             change  +0.07%  [ -8.97% ..  +8.57%]  overhead  +264.05%
dynamic/access             change  +0.02%  [ -1.08% ..  +1.26%]  overhead   +15.12%
dynamic/access-relative    change  +0.01%  [ -0.89% ..  +1.00%]  overhead   +60.60%
dynamic/access-contended   change  -0.15%  [ -1.23% ..  +1.99%]  overhead   +13.66%
static/launch              change  +0.67%  [ -5.51% ..  +6.65%]  overhead  +729.40%
static/access              change  +0.08%  [ -0.88% ..  +1.42%]  overhead  +807.35%
static/access-relative     change  +0.21%  [ -0.79% ..  +1.44%]  overhead +1396.86%
static/access-contended    change  +0.03%  [ -0.71% ..  +0.64%]  overhead +3157.85%

macos

dynamic/launch             change  -0.34%  [ -2.98% ..  +3.05%]  overhead  +213.39%
dynamic/access             change  -0.78%  [ -3.50% ..  +2.41%]  overhead    +6.35%
dynamic/access-relative    change  +0.10%  [ -1.96% ..  +1.95%]  overhead  +262.54%
dynamic/access-contended   change  +1.26%  [ -3.03% ..  +5.95%]  overhead    +2.86%

windows

dynamic/launch             change  -0.06%  [ -8.88% .. +10.39%]  overhead   +22.04%
dynamic/access             change  +0.37%  [ -4.17% ..  +9.20%]  overhead    +1.70%
dynamic/access-relative    change  +0.18%  [ -2.63% ..  +3.81%]  overhead    +1.85%
dynamic/access-contended   change  -0.53%  [-11.13% .. +21.27%]  overhead    +1.07%

@github-actions

github-actions Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

Remote cache staging

Commit: b30dcd5b6d1943459a16d20bcbdc1d3a28d9a627

Cloudflare staging deployment and smoke tests passed. You can now perform manual verification.

Endpoint: https://vp-cache-ci-staging.voidzero-docs.workers.dev/projects/manual

Download the remote-cache-e2e-37270179886-1 artifact from the workflow run. It contains manual-fetch.cbor, manual-manifest.json, and report.json.

PR checks cover public reads and rejected writes. Main-branch push checks also cover authorized HTTP stores. This staging endpoint is shared by all PRs and main. A later deployment replaces its code and manual fixture. Compare the deployment ID in the response with the artifact before manual verification. Closing this PR does not remove staging.

Manual checks and complete e2e plan.

@fengmk2
fengmk2 force-pushed the feat/public-remote-cache branch from 194d39f to e461d47 Compare September 14, 2026 15:16
@fengmk2

fengmk2 commented Sep 16, 2026

Copy link
Copy Markdown
Member Author

Deploy prompt:

Refer to the deployment instructions at https://github.com/voidzero-dev/vite-task/blob/feat/public-remote-cache/packages/remote-cache/docs/self-hosting.md to deploy a remote cache service for the current repo.

@wan9chi
wan9chi force-pushed the feat/public-remote-cache branch from df2d129 to e127e9b Compare September 27, 2026 16:04
@wan9chi
wan9chi changed the base branch from main to remote-cache-hardening September 27, 2026 16:04
@wan9chi
wan9chi added this pull request to stack #759 September 27, 2026 16:04
Base automatically changed from remote-cache-hardening to main September 27, 2026 16:32
@wan9chi
wan9chi force-pushed the feat/public-remote-cache branch from e127e9b to d4b0fea Compare September 27, 2026 16:32
@wan9chi
wan9chi force-pushed the feat/public-remote-cache branch from 58347c9 to c2f00bd Compare September 27, 2026 17:15
@wan9chi
wan9chi removed this pull request from stack #759 September 27, 2026 17:15
@wan9chi
wan9chi changed the base branch from main to remote-cache-fetch-404 September 27, 2026 17:15
@wan9chi
wan9chi added this pull request to stack #773 September 27, 2026 17:16
Base automatically changed from remote-cache-fetch-404 to main September 28, 2026 02:52
wan9chi added a commit that referenced this pull request Sep 28, 2026
## Motivation

The public cache service (#718) follows its RFC and answers a fetch that
matches neither key with HTTP 404 and a plain-text body. It never sends
`kind: "not_found"`. The client and the Node test backend still used a
200 response with `kind: "not_found"`, so a miss meant different things
depending on the server. This switches both to 404 and drops the
`not_found` kind, giving the client and both servers one miss contract.

## Changes

- `vt_remote_cache`: `Client::fetch` returns `Result<Option<Fetched>,
Error>`, with `None` for a 404 response. `Fetched` only describes the
body of a 200 response, so its `NotFound` variant is removed. A 200
response with `kind: "not_found"` is now a malformed response. Every
other non-200 status is still an error, and a 404 download still fails.
- Test backend (`packages/tools`): a fetch miss gets a 404 with the body
`Not found`, logged as `POST /fetch 404`.
- The remote cache e2e snapshots change only in those backend lines and
responses. The `vp run` output is unchanged.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@wan9chi
wan9chi force-pushed the feat/public-remote-cache branch from c2f00bd to 0c55376 Compare September 28, 2026 02:52
wan9chi added a commit that referenced this pull request Oct 4, 2026
This reverts a463bde. The stall cases test how vp run handles a backend
that never answers, not the backend itself, and the real cache service
from #718 will replace remote-cache-server in these tests. That service
can't stall, so --stall would have to move into its proxy, and the cases
would stay skipped on Windows and musl. vtt stalled-remote-cache
--fetch-miss runs on every platform without Node.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wan9chi added a commit that referenced this pull request Oct 4, 2026
…795)

## Motivation

The e2e cases for a remote cache that never answers need some requests
to stall while others behave normally. For example, an upload case needs
its fetch to reach a backend and miss before the upload stalls. `vtt
stalled-remote-cache` was its own endpoint and stalled every request, so
it couldn't do that. #718 will replace `remote-cache-server` with the
real cache service, which can't stall requests itself. So the stalling
has to happen in front of whatever backend a test runs.

## Changes

- **Proxy.** `vtt stalled-remote-cache [--stall ROUTE]... COMMAND
[ARGS...]` is now a proxy for the endpoint in `VP_REMOTE_CACHE_URL`,
which must be `http://<host>:<port>/<path>`. It runs the command with
`VP_REMOTE_CACHE_URL` set to the proxy.
- A request to a stalled route below the endpoint, such as `/store`, is
never answered, and it emits a `stalled` milestone.
- Other requests are forwarded with `connection: close`, so each one
comes in on its own connection and is checked on its own.
  - A forwarded request gets a 502 if the endpoint can't be reached.
- **Fetch cases.** `ctrl_c_during_fetch` and `fast_fail_during_fetch`
stall `/fetch`, with an unreachable endpoint behind the proxy. They
still run on every platform without Node.js.
- **New `ctrl_c_during_upload` case.** It runs `remote-cache-server vtt
stalled-remote-cache --stall /store vt run build`. The fetch reaches the
backend and misses, the upload stalls, and Ctrl-C cancels it.
- **`remote-cache-server`** now ignores Ctrl-C and leaves it to its
command, so it still prints its request log afterwards.

## Notes for reviewers

- **Lost milestones on Windows.** A milestone there is the console
title, and ConPTY sends it on its next render. If the command sets a
milestone at about the same time as the proxy, the earlier one can be
lost. The doc comment says so. Cases that need `remote-cache-server` are
skipped on Windows anyway.
- **Conflict with #718.** #718 rewrites
`packages/tools/src/remote-cache/cli.ts`, so the line that ignores
Ctrl-C needs to move into its version.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@wan9chi
wan9chi force-pushed the feat/public-remote-cache branch from 0c55376 to c4d5b0b Compare October 5, 2026 01:39
@wan9chi
wan9chi removed this pull request from stack #773 October 5, 2026 01:40
@wan9chi
wan9chi changed the base branch from main to claude/github-oidc-auth-headers-18e511 October 5, 2026 01:40
@wan9chi
wan9chi added this pull request to stack #799 October 5, 2026 01:40
Base automatically changed from claude/github-oidc-auth-headers-18e511 to main October 5, 2026 02:14
wan9chi added a commit that referenced this pull request Oct 5, 2026
## Motivation

The self-hosted remote cache server in #718, designed in #716, serves
reads to anyone but accepts a store only with a GitHub Actions OIDC
token. Other servers will need other credentials. For example, a private
cache behind Cloudflare Access could need headers on every request. This
adds one general hook so each kind of credentials is a separate
implementation, and the client doesn't need to know about any of them.

## Changes

- `vt_remote_cache::auth::Auth` supplies the headers for each request,
given its operation: fetch, download, or store. It can use the client's
HTTP client to get credentials, such as a token, and that client doesn't
follow redirects. If it fails, the request isn't sent, and the operation
fails with the new `Error::Auth` ("failed to authenticate").
- `Client::new(endpoint, auth)` takes the auth. `Anonymous` adds no
headers.
- Planning resolves how requests authenticate into `remote_cache.auth`,
next to the access mode and endpoint. The result holds everything needed
to build the credentials, so nothing reads envs after planning. Choosing
the auth from `cache.remote` config or envs later only changes this
step. For now, the only kind is `anonymous`.
- `vt` turns the resolved auth into a `vt_remote_cache` auth with
`build_auth`, a single `match`, and caches clients by endpoint and auth.

Requests don't change. Plan snapshots gain `"auth": {"kind":
"anonymous"}`. The next PR in this stack adds GitHub Actions OIDC as the
first auth with credentials.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
wan9chi added a commit that referenced this pull request Oct 5, 2026
…DC (#798)

## Motivation

The self-hosted remote cache server in #718, designed in #716, accepts
uploads only with a GitHub Actions OIDC token. The token's audience must
be the namespace endpoint, and it must come from a push job on the main
branch. `vp run` sends stores without credentials today, so that server
rejects every upload with 401.

## Changes

- Planning resolves `remote_cache.auth` to `github-oidc` when
`ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` are
set in the envs visible at the `vp run` level. That happens in jobs with
`permissions: id-token: write`; otherwise the auth stays `anonymous`.
- It holds the request URL, the request token, and the audience, which
is the endpoint without a trailing slash.
- The request token is a `Secret`, which debug output and serialized
plans redact.
- `build_auth` turns `github-oidc` into
`vt_remote_cache::auth::GithubOidc`, which adds `Authorization: Bearer
<token>` to stores only. Fetches and downloads stay anonymous.
  - It requests a token when the first store needs one.
- Later stores reuse the token until two minutes before its `exp`.
Cloudflare receives a store's whole body before the Worker checks the
token, so the token has to outlast the upload. A token without `exp` is
a malformed response.
  - Concurrent stores wait for the same request.
- A failed request is remembered, so later stores fail right away
without making more requests. Each task with a failed upload shows the
existing "Not uploaded to the remote cache" warning.
  - Neither token appears in debug output or errors.
- Its state is a single enum: ready with a request and an optional
cached token, or failed. A token can't stay cached after a failure.
- Tasks still receive the two env vars as untracked envs, as in #691, so
npm trusted publishing through `vp run` keeps working.

Stacked on #797, which adds the `Auth` hook and the resolved auth
config.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@wan9chi
wan9chi force-pushed the feat/public-remote-cache branch from c4d5b0b to 172ad91 Compare October 5, 2026 02:23
fengmk2 and others added 16 commits October 5, 2026 12:45
Co-authored-by: GPT-6 Codex <codex@openai.com>
Co-authored-by: GPT-6 Codex <codex@openai.com>
Co-authored-by: GPT-6 Codex <codex@openai.com>
Co-authored-by: GPT-6 Codex <codex@openai.com>
Copy RFC #716 from c201f8e and adjust relative paths.

Co-authored-by: GPT-6 Codex <codex@openai.com>
Co-authored-by: GPT-6 Codex <codex@openai.com>
Co-authored-by: GPT-6 Codex <codex@openai.com>
Co-authored-by: GPT-6 Codex <codex@openai.com>
Co-authored-by: GPT-6 Codex <codex@openai.com>
Co-authored-by: GPT-6 Codex <codex@openai.com>
Remove the obsolete cache size study and its RFC link.

Co-authored-by: GPT-6 Codex <codex@openai.com>
Co-authored-by: GPT-6 Codex <codex@openai.com>
Co-authored-by: GPT-6 Codex <codex@openai.com>
`npm_execpath` points to pnpm's standalone executable on the Linux and
Windows runners, so running it through `node` failed. Run it directly
unless it's a JavaScript entry point.

The remote cache package also brought esbuild and workerd into the
workspace, and a root `pnpm install` without `--ignore-scripts` failed
on their unapproved build scripts. Allow them, as the standalone package
already does.

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

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.

2 participants