Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,23 @@ jobs:
pnpm build-vite-task-client-types
git diff --exit-code packages/vite-task-client/src/index.d.ts

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

done:
runs-on: namespace-profile-linux-x64-default
if: always()
Expand All @@ -384,6 +401,7 @@ jobs:
- 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
171 changes: 171 additions & 0 deletions .github/workflows/remote-cache-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
name: Remote cache deployment

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:
inputs:
full:
description: Check real Cron cleanup (manual runs cannot authorize HTTP stores)
type: boolean
default: true

permissions:
contents: read

# Share the group with PR teardown. Do not cancel a partially completed deployment.
concurrency:
group: remote-cache-cloudflare-${{ github.event.pull_request.number || 'main' }}
cancel-in-progress: false

env:
REMOTE_CACHE_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
REMOTE_CACHE_PR_NUMBER: ${{ github.event.pull_request.number }}
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 }}

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 fork preview 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 preview').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 for a preview. ' +
'No live deployment passed verification for this PR.'
).write();

deploy:
name: Deploy and verify
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: 40
environment:
name: remote-cache-staging
url: ${{ steps.deploy.outputs.endpoint }}
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: Deploy isolated Cloudflare 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: Test the deployed 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 }}
REMOTE_CACHE_E2E_FULL: ${{ github.event_name == 'push' || inputs.full == true }}
- 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 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: |
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}-pr-${pr.number}.${process.env.REMOTE_CACHE_WORKERS_SUBDOMAIN}.workers.dev/projects/manual`;
if (endpoint !== expected) throw new Error('Unexpected deployment endpoint');
status = `Cloudflare deployment and PR e2e checks 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 and real Cron cleanup. ` +
`The endpoint remains available until this PR closes or a later commit replaces it.`;
} else if (process.env.CHECK_RESULT !== 'success') {
status = `The source checks did not pass. No verified Cloudflare preview is ready. See the [workflow run](${run}).`;
} else if (process.env.DEPLOY_RESULT === 'skipped') {
status = `Cloudflare deployment is disabled. Configure the staging environment 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 preview is not marked ready. See the [workflow run](${run}).`;
}
const body = `${marker}\n### Remote cache preview\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 });
91 changes: 91 additions & 0 deletions .github/workflows/remote-cache-preview-events.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Remote cache preview cleanup

on:
pull_request:
types: [closed]
paths:
- packages/remote-cache/**
- .github/workflows/remote-cache-*.yml
- package.json
- pnpm-lock.yaml
- pnpm-workspace.yaml
- vite.config.ts
workflow_dispatch:
inputs:
pr:
description: Closed PR number whose preview resources should be removed
type: string
required: true

permissions: {}

jobs:
cleanup:
name: Remove closed PR resources
if: >-
vars.REMOTE_CACHE_DEPLOY_ENABLED == 'true' &&
((github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.login != 'dependabot[bot]') ||
(github.event_name == 'workflow_dispatch' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)))
runs-on: ubuntu-latest
timeout-minutes: 35
concurrency:
group: remote-cache-cloudflare-${{ github.event.pull_request.number || inputs.pr }}
cancel-in-progress: false
environment: remote-cache-staging
permissions:
contents: read
pull-requests: read
env:
REMOTE_CACHE_PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr }}
REMOTE_CACHE_SOURCE_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 }}
steps:
- name: Verify that the PR is closed
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const number = process.env.REMOTE_CACHE_PR_NUMBER;
if (!/^[1-9][0-9]{0,9}$/.test(number)) throw new Error('Invalid PR number');
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: Number(number) });
if (pr.state !== 'closed') throw new Error('Only closed PR previews can be removed');
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- uses: oxc-project/setup-node@f46a72f95efdc55273fcd042d61c84e723b2892c # v1.4.1
- run: pnpm --filter @voidzero-dev/remote-cache ci:cleanup
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

cleanup-notice:
if: always() && needs.cleanup.result != 'skipped'
needs: cleanup
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
pull-requests: write
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr }}
CLEANUP_RESULT: ${{ needs.cleanup.result }}
with:
script: |
if (!/^[1-9][0-9]{0,9}$/.test(process.env.PR_NUMBER)) return;
const number = Number(process.env.PR_NUMBER);
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: number });
if (pr.state !== 'closed') return;
const marker = '<!-- remote-cache-preview -->';
const comments = await github.paginate(github.rest.issues.listComments, { ...context.repo, issue_number: number, per_page: 100 });
const previous = comments.find(c => c.user?.login === 'github-actions[bot]' && c.body?.includes(marker));
if (!previous) return;
const run = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const status = process.env.CLEANUP_RESULT === 'success'
? 'The PR is closed. Its Cloudflare preview resources were removed.'
: 'The PR is closed, but preview cleanup did not finish. Check the workflow and rerun cleanup after Cron or lifecycle completes.';
await github.rest.issues.updateComment({ ...context.repo, comment_id: previous.id,
body: `${marker}\n### Remote cache preview\n\n${status}\n\n[Cleanup run](${run}).` });
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 GitHub Actions write authorization, storage limits, and automatic cleanup ([#718](https://github.com/voidzero-dev/vite-task/pull/718)).
- **Fixed** `vp run` no longer hangs or fails when a task leaves a process running behind it, such as a dev server or a background helper, or when one of a task's processes is killed. The run finishes as soon as the task itself does, and the files the task used are still recorded ([#544](https://github.com/voidzero-dev/vite-task/issues/544), [#675](https://github.com/voidzero-dev/vite-task/pull/675)).
- **Fixed** A task that reads or writes an unusually large number of files now runs to the end instead of being killed partway through. Vite+ reports the run as not cached, because it could not record every file the task used ([#533](https://github.com/voidzero-dev/vite-task/issues/533), [#675](https://github.com/voidzero-dev/vite-task/pull/675)).
- **Fixed** Vite+ diagnostics now display individual paths and working directories without Rust debug formatting such as quoted paths or escaped Windows backslashes ([#534](https://github.com/voidzero-dev/vite-task/pull/534)).
Expand Down
3 changes: 3 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ watch-check:
test:
cargo test

remote-cache:
pnpm check-remote-cache

lint:
cargo clippy --workspace --all-targets --all-features -- --deny warnings

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"prepare": "vp config",
"build-vite-task-client-types": "tsc -p packages/vite-task-client/tsconfig.json"
"build-vite-task-client-types": "tsc -p packages/vite-task-client/tsconfig.json",
"check-remote-cache": "pnpm --filter @voidzero-dev/remote-cache check && pnpm --filter @voidzero-dev/remote-cache smoke"
},
"devDependencies": {
"@tsconfig/strictest": "catalog:",
Expand Down
6 changes: 6 additions & 0 deletions packages/remote-cache/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.wrangler/
.dev.vars*
operator-state.json
wrangler.operator.json
benchmark-results.json
e2e-results/
Loading
Loading