mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
441 lines
19 KiB
YAML
441 lines
19 KiB
YAML
name: Deploy Examples
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request: # run on PRs against any base branch, not just main
|
|
|
|
concurrency:
|
|
group: deploy-${{ github.event_name }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions: {}
|
|
|
|
jobs:
|
|
response_store:
|
|
# Skip entirely for fork PRs — they don't have access to deploy secrets.
|
|
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
|
|
name: Deploy and test Workers Response Store
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
deployments: write
|
|
outputs:
|
|
service_name: ${{ steps.names.outputs.service_name }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
- uses: ./.github/actions/setup
|
|
|
|
- name: Build Workers Response Store
|
|
run: vp run @cloudflare/workers-response-store#build
|
|
|
|
- name: Select per-run Worker names
|
|
id: names
|
|
shell: bash
|
|
run: |
|
|
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
|
|
prefix="pr-${{ github.event.pull_request.number }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
|
service_name="${prefix}-rs-service"
|
|
else
|
|
prefix="main"
|
|
service_name="vinext-workers-response-store-service-poc"
|
|
fi
|
|
{
|
|
echo "service_name=$service_name"
|
|
echo "client_name=${prefix}-rs-client"
|
|
echo "inline_name=${prefix}-rs-inline"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Deploy cache Worker
|
|
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
|
with:
|
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
workingDirectory: packages/workers-response-store
|
|
command: deploy --config example/service-binding/wrangler.cache.jsonc --name ${{ steps.names.outputs.service_name }}
|
|
|
|
- name: Configure live client Worker
|
|
working-directory: packages/workers-response-store
|
|
env:
|
|
CLIENT_NAME: ${{ steps.names.outputs.client_name }}
|
|
SERVICE_NAME: ${{ steps.names.outputs.service_name }}
|
|
run: |
|
|
# Node reads the values from process.env.
|
|
# shellcheck disable=SC2016
|
|
node -e '
|
|
const fs = require("node:fs");
|
|
const source = fs.readFileSync("example/service-binding/wrangler.user.jsonc", "utf8");
|
|
const config = source
|
|
.replace(/"name": "[^"]+"/, `"name": "${process.env.CLIENT_NAME}"`)
|
|
.replace(/"main": "user-worker\.ts"/, `"main": "example/service-binding/user-worker.ts"`)
|
|
.replace(/"service": "[^"]+"/, `"service": "${process.env.SERVICE_NAME}"`);
|
|
fs.writeFileSync(".wrangler-e2e.user.jsonc", config);
|
|
'
|
|
|
|
- name: Deploy live client Worker
|
|
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
|
with:
|
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
workingDirectory: packages/workers-response-store
|
|
command: deploy --config .wrangler-e2e.user.jsonc
|
|
|
|
- name: Deploy live self-contained Worker
|
|
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
|
with:
|
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
workingDirectory: packages/workers-response-store
|
|
command: deploy --config wrangler.jsonc --name ${{ steps.names.outputs.inline_name }}
|
|
|
|
- name: Run live Response Store E2E
|
|
working-directory: packages/workers-response-store
|
|
env:
|
|
LIVE_CACHE_BASE: https://${{ steps.names.outputs.inline_name }}.vinext.workers.dev
|
|
LIVE_RESPONSE_STORE_SERVICE_BASE: https://${{ steps.names.outputs.client_name }}.vinext.workers.dev
|
|
run: |
|
|
for url in "$LIVE_CACHE_BASE" "$LIVE_RESPONSE_STORE_SERVICE_BASE"; do
|
|
ready=false
|
|
consecutive=0
|
|
for _ in {1..60}; do
|
|
if node -e 'fetch(process.argv[1]).then(async response => process.exit(response.ok && (await response.text()).includes("\"status\":\"ready\"") ? 0 : 1)).catch(() => process.exit(1))' "$url"; then
|
|
consecutive=$((consecutive + 1))
|
|
if [[ "$consecutive" -eq 5 ]]; then
|
|
ready=true
|
|
break
|
|
fi
|
|
else
|
|
consecutive=0
|
|
fi
|
|
sleep 2
|
|
done
|
|
if [[ "$ready" != true ]]; then
|
|
echo "$url did not become ready"
|
|
exit 1
|
|
fi
|
|
done
|
|
vp run test:live
|
|
|
|
- name: Clean up live Response Store Workers
|
|
if: always() && github.event_name == 'pull_request'
|
|
continue-on-error: true
|
|
working-directory: packages/workers-response-store
|
|
env:
|
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
CLIENT_NAME: ${{ steps.names.outputs.client_name }}
|
|
INLINE_NAME: ${{ steps.names.outputs.inline_name }}
|
|
run: |
|
|
set +e
|
|
for worker in "$CLIENT_NAME" "$INLINE_NAME"; do
|
|
vp exec wrangler delete "$worker" --force
|
|
done
|
|
|
|
deploy:
|
|
# Skip entirely for fork PRs — they don't have access to deploy secrets.
|
|
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
|
|
name: Deploy ${{ matrix.example.name }}
|
|
needs: response_store
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
deployments: write
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
example:
|
|
- name: app-router-cloudflare
|
|
project: app-router-cloudflare
|
|
working_directory: examples/app-router-cloudflare
|
|
wrangler_config: dist/server/wrangler.json
|
|
- name: pages-router-cloudflare
|
|
project: pages-router-cloudflare
|
|
working_directory: examples/pages-router-cloudflare
|
|
wrangler_config: dist/pages_router_cloudflare/wrangler.json
|
|
- name: app-router-playground
|
|
project: app-router-playground
|
|
working_directory: examples/app-router-playground
|
|
wrangler_config: dist/server/wrangler.json
|
|
- name: realworld-api-rest
|
|
project: realworld-api-rest
|
|
working_directory: examples/realworld-api-rest
|
|
wrangler_config: dist/realworld_api_rest/wrangler.json
|
|
- name: nextra-docs-template
|
|
project: nextra-docs-template
|
|
working_directory: examples/nextra-docs-template
|
|
wrangler_config: dist/server/wrangler.json
|
|
- name: benchmarks
|
|
project: benchmarks
|
|
working_directory: examples/benchmarks
|
|
wrangler_config: dist/server/wrangler.json
|
|
- name: hackernews
|
|
project: hackernews
|
|
working_directory: examples/hackernews
|
|
wrangler_config: dist/server/wrangler.json
|
|
- name: response-store-demo
|
|
project: response-store-demo
|
|
working_directory: examples/response-store-demo
|
|
wrangler_config: dist/server/wrangler.json
|
|
warm_cdn_cache: true
|
|
- name: static-export
|
|
project: static-export
|
|
working_directory: examples/static-export
|
|
wrangler_config: deploy/wrangler.jsonc
|
|
static_export: true
|
|
- name: web
|
|
project: vinext-web
|
|
working_directory: apps/web
|
|
wrangler_config: dist/server/wrangler.json
|
|
warm_cdn_cache: true
|
|
warm_cdn_target: https://vinext.dev
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
- uses: ./.github/actions/setup
|
|
|
|
- name: Build vinext plugin
|
|
run: vp run build
|
|
|
|
- name: Build example
|
|
if: matrix.example.static_export != true
|
|
run: vp build
|
|
working-directory: ${{ matrix.example.working_directory }}
|
|
|
|
- name: Bind Response Store demo to this run's cache Worker
|
|
if: matrix.example.name == 'response-store-demo'
|
|
working-directory: ${{ matrix.example.working_directory }}
|
|
env:
|
|
RESPONSE_STORE_SERVICE: ${{ needs.response_store.outputs.service_name }}
|
|
run: |
|
|
# Node reads the value from process.env.
|
|
# shellcheck disable=SC2016
|
|
node -e '
|
|
const fs = require("node:fs");
|
|
const path = "dist/server/wrangler.json";
|
|
const config = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
const binding = config.services?.find(({ binding }) => binding === "RESPONSE_STORE");
|
|
if (!binding) throw new Error("Missing RESPONSE_STORE service binding");
|
|
binding.service = process.env.RESPONSE_STORE_SERVICE;
|
|
fs.writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
'
|
|
|
|
- name: Build static export
|
|
if: matrix.example.static_export == true
|
|
run: vp exec vinext build
|
|
working-directory: ${{ matrix.example.working_directory }}
|
|
|
|
- name: Verify static export artifact
|
|
if: matrix.example.static_export == true
|
|
run: node scripts/verify-output.mjs
|
|
working-directory: ${{ matrix.example.working_directory }}
|
|
|
|
- name: Apply D1 migrations (benchmarks only)
|
|
if: matrix.example.name == 'benchmarks' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
|
with:
|
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
workingDirectory: examples/benchmarks
|
|
command: d1 migrations apply vinext-benchmarks --remote
|
|
|
|
- name: Apply D1 migrations (web only)
|
|
if: matrix.example.name == 'web' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
|
with:
|
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
workingDirectory: apps/web
|
|
command: d1 migrations apply vinext-metrics --remote
|
|
|
|
- name: Deploy to Cloudflare Workers (Production)
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.example.warm_cdn_cache != true
|
|
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
|
with:
|
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
workingDirectory: ${{ matrix.example.working_directory }}
|
|
command: deploy --config ${{ matrix.example.wrangler_config }}
|
|
|
|
- name: Deploy to Cloudflare Workers with CDN warmup (Production)
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.example.warm_cdn_cache == true
|
|
working-directory: ${{ matrix.example.working_directory }}
|
|
env:
|
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
WARM_CDN_TARGET: ${{ matrix.example.warm_cdn_target }}
|
|
run: |
|
|
args=(--skip-build --config "${{ matrix.example.wrangler_config }}" --experimental-warm-cdn-cache)
|
|
if [[ -n "$WARM_CDN_TARGET" ]]; then
|
|
args+=(--warm-cdn-target "$WARM_CDN_TARGET")
|
|
fi
|
|
vp exec vinext-cloudflare deploy "${args[@]}"
|
|
|
|
- name: Deploy Preview Version
|
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
|
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
|
with:
|
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
workingDirectory: ${{ matrix.example.working_directory }}
|
|
command: versions upload --config ${{ matrix.example.wrangler_config }} --preview-alias pr-${{ github.event.pull_request.number }}
|
|
|
|
# `versions upload` does not apply non-versioned workers.dev/preview URL
|
|
# settings. A newly added assets-only Worker has no routes until this runs.
|
|
- name: Apply Static Export Preview Routes
|
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && matrix.example.static_export == true
|
|
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
|
with:
|
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
workingDirectory: ${{ matrix.example.working_directory }}
|
|
command: triggers deploy --config ${{ matrix.example.wrangler_config }}
|
|
|
|
smoke-test:
|
|
name: Smoke Test Deployments
|
|
runs-on: ubuntu-latest
|
|
needs: deploy
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Smoke test (production)
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
run: ./scripts/smoke-test.sh
|
|
|
|
- name: Smoke test (preview)
|
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
|
run: ./scripts/smoke-test.sh --preview pr-${{ github.event.pull_request.number }}
|
|
|
|
deployed-e2e:
|
|
name: E2E Deployed Example
|
|
runs-on: ubuntu-latest
|
|
needs: deploy
|
|
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
- uses: ./.github/actions/setup
|
|
|
|
- name: Cache Playwright browsers
|
|
id: playwright-cache
|
|
uses: ./.github/actions/playwright-cache
|
|
with:
|
|
browser: chromium
|
|
mode: cache
|
|
restore-keys: |
|
|
playwright-chromium-${{ runner.os }}-
|
|
|
|
- name: Install Playwright Chromium
|
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
run: vp exec playwright install chromium
|
|
|
|
- name: Test deployed Web Worker
|
|
env:
|
|
PLAYWRIGHT_PROJECT: cloudflare-workers
|
|
VINEXT_E2E_BASE_URL: ${{ github.event_name == 'pull_request' && format('https://pr-{0}-app-router-cloudflare.vinext.workers.dev', github.event.pull_request.number) || 'https://app-router-cloudflare.vinext.workers.dev' }}
|
|
run: >-
|
|
vp exec playwright test
|
|
tests/e2e/cloudflare-workers/web-worker.spec.ts
|
|
tests/e2e/cloudflare-workers/optimistic-search-navigation.spec.ts
|
|
|
|
cleanup-response-store:
|
|
name: Clean up Workers Response Store service
|
|
if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
|
needs: [response_store, deploy, smoke-test, deployed-e2e]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
- uses: ./.github/actions/setup
|
|
|
|
- name: Delete cache Worker
|
|
working-directory: packages/workers-response-store
|
|
env:
|
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
SERVICE_NAME: ${{ needs.response_store.outputs.service_name }}
|
|
run: |
|
|
if [[ -n "$SERVICE_NAME" ]]; then
|
|
vp exec wrangler delete "$SERVICE_NAME" --force
|
|
fi
|
|
|
|
comment:
|
|
name: Comment Preview URLs
|
|
runs-on: ubuntu-latest
|
|
needs: deploy
|
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
|
permissions:
|
|
pull-requests: write
|
|
steps:
|
|
- name: Comment PR with preview URLs
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
script: |
|
|
const prNumber = context.issue.number;
|
|
const alias = `pr-${prNumber}`;
|
|
|
|
const examples = [
|
|
{ name: 'app-router-cloudflare', project: 'app-router-cloudflare' },
|
|
{ name: 'pages-router-cloudflare', project: 'pages-router-cloudflare' },
|
|
{ name: 'app-router-playground', project: 'app-router-playground', original: 'https://app-router.vercel.app' },
|
|
{ name: 'realworld-api-rest', project: 'realworld-api-rest' },
|
|
{ name: 'nextra-docs-template', project: 'nextra-docs-template' },
|
|
{ name: 'benchmarks', project: 'benchmarks' },
|
|
{ name: 'hackernews', project: 'hackernews', original: 'https://next-react-server-components.vercel.app' },
|
|
{ name: 'response-store-demo', project: 'response-store-demo' },
|
|
{ name: 'static-export', project: 'static-export' },
|
|
{ name: 'web', project: 'vinext-web', production: 'https://vinext.dev' },
|
|
];
|
|
|
|
const { data: comments } = await github.rest.issues.listComments({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: prNumber,
|
|
});
|
|
|
|
const marker = '<!-- vinext-deploy-previews -->';
|
|
const existingComment = comments.find(c => c.body?.includes(marker));
|
|
|
|
const rows = examples
|
|
.map(e => {
|
|
const orig = e.original ? `[original](${e.original})` : '';
|
|
const production = e.production || `https://${e.project}.vinext.workers.dev`;
|
|
return `| ${e.name} | [preview](https://${alias}-${e.project}.vinext.workers.dev) | [production](${production}) | ${orig} |`;
|
|
})
|
|
.join('\n');
|
|
|
|
const body = [
|
|
marker,
|
|
'| Example | Preview | Production | Original |',
|
|
'|---------|---------|------------|----------|',
|
|
rows,
|
|
].join('\n');
|
|
|
|
if (existingComment) {
|
|
await github.rest.issues.updateComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: existingComment.id,
|
|
body,
|
|
});
|
|
} else {
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: prNumber,
|
|
body,
|
|
});
|
|
}
|