Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4ed0c4a
feat(remote-cache): add self-hosted public cache server
fengmk2 Sep 12, 2026
6249808
docs: link remote cache changelog to pull request
fengmk2 Sep 12, 2026
8ae2f70
ci(remote-cache): deploy previews and verify Cloudflare
fengmk2 Sep 12, 2026
099a946
fix(remote-cache): initialize CI types and revise cleanup trigger
fengmk2 Sep 12, 2026
443fe7f
test(remote-cache): synchronize upload cancellation with stream reads
fengmk2 Sep 12, 2026
5d22b21
docs(remote-cache): include RFC and supporting material
fengmk2 Sep 12, 2026
d21e046
docs(remote-cache): use the local RFC status contract
fengmk2 Sep 12, 2026
54c4870
fix(remote-cache): align RFC responses and isolate deployments
fengmk2 Sep 14, 2026
ab68928
ci(remote-cache): use one persistent staging deployment
fengmk2 Sep 15, 2026
117a7ff
ci(remote-cache): use repository secrets for staging
fengmk2 Sep 15, 2026
d7534cd
fix(remote-cache): preserve trigger bodies in D1 migrations
fengmk2 Sep 15, 2026
311ba63
refactor(remote-cache): simplify service and deployment code
fengmk2 Sep 16, 2026
d34945f
chore(remote-cache): document self-hosting and default staging to free
fengmk2 Sep 16, 2026
598003c
feat(remote-cache): add Deploy to Cloudflare button
fengmk2 Sep 16, 2026
1b47bfe
ci(remote-cache): guard temporary deployment button URLs
fengmk2 Sep 16, 2026
a069da2
ci(remote-cache): run the standalone check with a native pnpm executable
wan9chi Sep 27, 2026
0c55376
test(cache): run the remote cache e2e tests against the public cache …
wan9chi Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/scripts/check-remote-cache-standalone.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { spawn } from 'node:child_process';
import { cp, mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { basename, join } from 'node:path';

const pnpm = process.env.npm_execpath;
if (!pnpm) throw new Error('Run this check through pnpm check-remote-cache-standalone');
// pnpm can be a JavaScript entry point or a standalone executable.
const [command, prefix] = /\.[cm]?js$/.test(pnpm) ? [process.execPath, [pnpm]] : [pnpm, []];
const directory = await mkdtemp(join(tmpdir(), 'remote-cache-standalone-'));
const excluded = new Set([
'node_modules',
'.wrangler',
'dist',
'wrangler.operator.json',
'operator-state.json',
'benchmark-results.json',
'e2e-results',
]);
try {
await cp(new URL('../../packages/remote-cache/', import.meta.url), directory, {
recursive: true,
filter(source) {
const name = basename(source);
return !excluded.has(name) && !name.startsWith('.dev.vars') && !name.startsWith('.env');
},
});
for (const args of [['install', '--frozen-lockfile'], ['check'], ['build']]) {
await new Promise((resolve, reject) => {
const child = spawn(command, [...prefix, ...args], {
cwd: directory,
stdio: 'inherit',
shell: false,
});
child.once('error', reject);
child.once('exit', (code) =>
code === 0 ? resolve() : reject(new Error(`Standalone ${args[0]} failed (${code})`)),
);
});
}
} finally {
await rm(directory, { recursive: true, force: true });
}
63 changes: 53 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ permissions:
on:
workflow_dispatch:
pull_request:
types: [opened, synchronize]
types: [opened, synchronize, ready_for_review, converted_to_draft]
push:
branches:
- main
Expand All @@ -23,6 +23,35 @@ defaults:
shell: bash

jobs:
deploy-button-links:
name: Deploy button links
runs-on: namespace-profile-linux-x64-default
steps:
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { readFile } = require('node:fs/promises');
const main = 'https://github.com/voidzero-dev/vite-task/tree/main/packages/remote-cache';
const pr = context.payload.pull_request;
const temporary = pr?.draft
? `https://github.com/${pr.head.repo.full_name}/tree/${pr.head.ref}/packages/remote-cache`
: undefined;
for (const file of ['README.md', 'packages/remote-cache/README.md', 'packages/remote-cache/docs/self-hosting.md']) {
const content = await readFile(file, 'utf8');
const buttons = [...content.matchAll(/\]\((https:\/\/deploy\.workers\.cloudflare\.com\/\?url=[^)]+)\)/g)];
if (!buttons.length) core.setFailed(`${file}: Deploy to Cloudflare button is missing.`);
for (const [, button] of buttons) {
const source = new URL(button).searchParams.get('url');
if (source === main) continue;
if (temporary && source === temporary) {
core.warning(`${file}: Temporary PR branch URL. Restore the button to main before marking this PR ready for review.`);
} else {
core.setFailed(`${file}: Restore the Deploy to Cloudflare source URL to ${main} before merge.`);
}
}
}

detect-changes:
runs-on: namespace-profile-linux-x64-default
permissions:
Expand Down Expand Up @@ -148,9 +177,7 @@ jobs:
run: pnpm --filter vite-task-tools exec playwright install-deps chromium

- name: Run ignored tests
run: |
pnpm --filter vite-task-tools test
${{ matrix.cargo_cmd }} test --target ${{ matrix.build_target }} -- --ignored
run: ${{ matrix.cargo_cmd }} test --target ${{ matrix.build_target }} -- --ignored

# Windows tests are cross-compiled on a fast Linux runner with cargo-xwin
# (clang-cl + lld-link against the xwin-downloaded MSVC CRT/Windows SDK)
Expand Down Expand Up @@ -283,9 +310,7 @@ jobs:

- name: Run ignored tests
if: matrix.mode == 'ignored'
run: |
pnpm --filter vite-task-tools test
cargo-nextest nextest run --archive-file windows-tests.tar.zst --workspace-remap . --run-ignored ignored-only
run: cargo-nextest nextest run --archive-file windows-tests.tar.zst --workspace-remap . --run-ignored ignored-only

test-musl:
needs: detect-changes
Expand Down Expand Up @@ -339,9 +364,7 @@ jobs:
- uses: oxc-project/setup-node@f46a72f95efdc55273fcd042d61c84e723b2892c # v1.4.1

- name: Run ignored tests
run: |
pnpm --filter vite-task-tools test
cargo test -- --ignored
run: cargo test -- --ignored

fmt:
name: Format and Check Deps
Expand Down Expand Up @@ -383,16 +406,36 @@ jobs:
- name: Check test utility types
run: pnpm --filter vite-task-tools check

remote-cache:
needs: detect-changes
if: needs.detect-changes.outputs.code-changed == 'true'
name: Remote cache (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os:
- namespace-profile-linux-x64-default
- namespace-profile-mac-default
- namespace-profile-windows-4c-8g
runs-on: ${{ matrix.os }}
steps:
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
- uses: oxc-project/setup-node@f46a72f95efdc55273fcd042d61c84e723b2892c # v1.4.1
- run: pnpm check-remote-cache
- run: pnpm check-remote-cache-standalone

done:
runs-on: namespace-profile-linux-x64-default
if: always()
needs:
- deploy-button-links
- clippy
- test
- test-musl
- build-windows-tests
- test-windows
- fmt
- remote-cache
steps:
- run: exit 1
# Thank you, next https://github.com/vercel/next.js/blob/canary/.github/workflows/build_and_test.yml#L379
Expand Down
164 changes: 164 additions & 0 deletions .github/workflows/remote-cache-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
name: Remote cache staging

on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- packages/remote-cache/**
- .github/workflows/remote-cache-*.yml
- package.json
- pnpm-lock.yaml
- pnpm-workspace.yaml
- vite.config.ts
push:
branches: [main]
paths:
- packages/remote-cache/**
- .github/workflows/remote-cache-*.yml
- package.json
- pnpm-lock.yaml
- pnpm-workspace.yaml
- vite.config.ts
workflow_dispatch:

permissions:
contents: read

# All PRs and main share staging. Finish deployment, smoke tests, and notification before replacing it.
concurrency:
group: remote-cache-staging
cancel-in-progress: false

env:
REMOTE_CACHE_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
REMOTE_CACHE_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
REMOTE_CACHE_RESOURCE_PREFIX: ${{ vars.REMOTE_CACHE_RESOURCE_PREFIX || 'vp-cache-ci' }}
REMOTE_CACHE_WORKERS_SUBDOMAIN: ${{ vars.REMOTE_CACHE_WORKERS_SUBDOMAIN }}
REMOTE_CACHE_PROFILE: ${{ vars.REMOTE_CACHE_PROFILE || 'free' }}

jobs:
check:
name: Check deployment source
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ env.REMOTE_CACHE_SOURCE_SHA }}
persist-credentials: false
- uses: oxc-project/setup-node@f46a72f95efdc55273fcd042d61c84e723b2892c # v1.4.1
- run: pnpm check-remote-cache
- name: Explain staging deployment availability
if: >-
always() && github.event_name == 'pull_request' &&
(github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
await core.summary.addHeading('Remote cache staging').addRaw(
'Fork and Dependabot PRs run local checks without Cloudflare credentials. ' +
'A maintainer can copy the reviewed commit to an internal branch and open a PR to deploy to staging. ' +
'No live deployment passed verification for this PR.'
).write();

deploy:
name: Deploy staging and run smoke tests
needs: check
if: >-
vars.REMOTE_CACHE_DEPLOY_ENABLED == 'true' &&
github.event.pull_request.user.login != 'dependabot[bot]' &&
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&
(github.event_name != 'workflow_dispatch' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch))
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write
pull-requests: read
outputs:
endpoint: ${{ steps.deploy.outputs.endpoint }}
steps:
- name: Check that the PR still selects this commit
if: github.event_name == 'pull_request'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: context.issue.number });
if (pr.state !== 'open' || pr.head.sha !== process.env.REMOTE_CACHE_SOURCE_SHA) {
core.setFailed('This PR deployment is obsolete.');
}
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ env.REMOTE_CACHE_SOURCE_SHA }}
persist-credentials: false
- uses: oxc-project/setup-node@f46a72f95efdc55273fcd042d61c84e723b2892c # v1.4.1
- name: Create or update persistent staging resources
id: deploy
run: pnpm --filter @voidzero-dev/remote-cache ci:deploy
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Run smoke tests against the staging Worker, D1, and R2
run: pnpm --filter @voidzero-dev/remote-cache e2e
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Save verification results and manual fixtures
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: remote-cache-e2e-${{ github.run_id }}-${{ github.run_attempt }}
path: packages/remote-cache/e2e-results/
retention-days: 7
if-no-files-found: warn

notify:
name: Update PR staging verification instructions
if: >-
always() && github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]'
needs: [check, deploy]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
pull-requests: write
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
DEPLOY_RESULT: ${{ needs.deploy.result }}
CHECK_RESULT: ${{ needs.check.result }}
DEPLOY_ENDPOINT: ${{ needs.deploy.outputs.endpoint }}
with:
script: |
// Reuse the marker so existing PR comments are updated.
const marker = '<!-- remote-cache-preview -->';
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: context.issue.number });
if (pr.state !== 'open' || pr.head.sha !== process.env.REMOTE_CACHE_SOURCE_SHA) return;
const run = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const docs = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/blob/${pr.head.sha}/packages/remote-cache/docs/e2e-plan.md`;
const passed = process.env.DEPLOY_RESULT === 'success';
let status;
if (passed) {
const endpoint = process.env.DEPLOY_ENDPOINT;
const expected = `https://${process.env.REMOTE_CACHE_RESOURCE_PREFIX}-staging.${process.env.REMOTE_CACHE_WORKERS_SUBDOMAIN}.workers.dev/projects/manual`;
if (endpoint !== expected) throw new Error('Unexpected deployment endpoint');
status = `Cloudflare staging deployment and smoke tests passed. You can now perform manual verification.\n\n` +
`Endpoint: ${endpoint}\n\n` +
`Download the **remote-cache-e2e-${context.runId}-${process.env.GITHUB_RUN_ATTEMPT}** artifact from the [workflow run](${run}). ` +
`It contains \`manual-fetch.cbor\`, \`manual-manifest.json\`, and \`report.json\`.\n\n` +
`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.`;
} else if (process.env.CHECK_RESULT !== 'success') {
status = `The source checks did not pass. No verified Cloudflare staging deployment is ready. See the [workflow run](${run}).`;
} else if (process.env.DEPLOY_RESULT === 'skipped') {
status = `Cloudflare deployment is disabled. Configure the repository secrets and variables described in the [e2e plan](${docs}), ` +
`then set \`REMOTE_CACHE_DEPLOY_ENABLED=true\` and rerun this workflow. No live deployment passed verification.`;
} else {
status = `Cloudflare deployment or e2e verification failed. The staging deployment is not marked ready. See the [workflow run](${run}).`;
}
const body = `${marker}\n### Remote cache staging\n\nCommit: \`${pr.head.sha}\`\n\n${status}\n\n[Manual checks and complete e2e plan](${docs}).`;
const comments = await github.paginate(github.rest.issues.listComments, { ...context.repo, issue_number: pr.number, per_page: 100 });
const previous = comments.find(c => c.user?.login === 'github-actions[bot]' && c.body?.includes(marker));
if (previous) await github.rest.issues.updateComment({ ...context.repo, comment_id: previous.id, body });
else await github.rest.issues.createComment({ ...context.repo, issue_number: pr.number, body });
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

- **Added** A self-hosted public remote cache service with a Deploy to Cloudflare button, GitHub Actions write authorization, storage limits, and automatic cleanup ([#718](https://github.com/voidzero-dev/vite-task/pull/718)).
- **Fixed** An invalid glob in `--filter` no longer shows its error message twice ([#763](https://github.com/voidzero-dev/vite-task/pull/763)).
- **Changed** The detailed summary from `vp run --verbose` and `vp run --last-details` now shows each underlying cause of an error on its own line ([#761](https://github.com/voidzero-dev/vite-task/pull/761)).
- **Added** Remote caching. Configure an endpoint with the workspace's `cache: { remote: { url } }` or `VP_REMOTE_CACHE_URL`, and choose access with `--remote-cache=off|read|read-write` or `VP_REMOTE_CACHE`. The default is `read` with an endpoint and `off` without one. After a local cache miss, `vp run` looks the task up in the remote cache and, on a hit, restores its outputs and caches it locally. The task output and the run summary show which hits came from the remote cache. A failed read is just a cache miss, with the failure as its reason. In `read-write` mode, `vp run` also uploads the results of successful, cacheable tasks after caching them locally. A failed upload doesn't fail the task; the run summary shows a warning instead. Ctrl-C, or a failing task, stops remote cache requests right away, and a task still being looked up doesn't start. Tasks can opt out with `cache: { remote: false }`. Requests use the proxy environment variables or, on macOS and Windows, the system proxy settings ([#727](https://github.com/voidzero-dev/vite-task/pull/727), [#755](https://github.com/voidzero-dev/vite-task/pull/755), [#756](https://github.com/voidzero-dev/vite-task/pull/756), [#757](https://github.com/voidzero-dev/vite-task/pull/757), [#764](https://github.com/voidzero-dev/vite-task/pull/764), [#771](https://github.com/voidzero-dev/vite-task/pull/771), [#772](https://github.com/voidzero-dev/vite-task/pull/772)).
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ vp run -t @my/app#build # run in a package and its transitive dependencies
vp run --cache build # run with caching enabled
```

## Remote cache service

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https%3A%2F%2Fgithub.com%2Fvoidzero-dev%2Fvite-task%2Ftree%2Ffeat%2Fpublic-remote-cache%2Fpackages%2Fremote-cache)

To deploy the public remote cache service in your own Cloudflare account and bind it to a GitHub repository, follow the [self-hosting guide](packages/remote-cache/docs/self-hosting.md). The service is available in this source tree; the `vp run` remote-cache client adapter is not yet implemented here.

## Sponsors

Thanks to [namespace.so](https://namespace.so) for powering our CI/CD pipelines with fast, free macOS and Linux runners.
Expand Down
Loading
Loading