From 385b7f3fe3fc187e9f64104fdcb72f3b4b796e6a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 20 Aug 2026 14:54:01 +0000 Subject: [PATCH 1/2] feat: deploy Cloudflare preview URLs for pull requests Upload a Worker version (not production) when a PR is opened or updated, then comment a stable pr- preview URL like Vercel. Co-authored-by: Jay Sharma --- .github/workflows/deploy-cloudflare.yml | 159 ++++++++++++++++++++++++ README.md | 11 +- wrangler.jsonc | 1 + 3 files changed, 170 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-cloudflare.yml b/.github/workflows/deploy-cloudflare.yml index d7cbfc3..613045d 100644 --- a/.github/workflows/deploy-cloudflare.yml +++ b/.github/workflows/deploy-cloudflare.yml @@ -3,10 +3,16 @@ name: Deploy to Cloudflare Workers on: push: branches: [main] + pull_request: workflow_dispatch: +concurrency: + group: cloudflare-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: deploy: + if: github.event_name != 'pull_request' runs-on: ubuntu-latest permissions: contents: read @@ -31,3 +37,156 @@ jobs: env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + preview: + if: > + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository && + secrets.CLOUDFLARE_API_TOKEN != '' + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 11.20.0 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build OpenNext Worker + run: pnpm exec opennextjs-cloudflare build + + - name: Upload preview version + id: upload + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + WRANGLER_OUTPUT_FILE_DIRECTORY: ${{ runner.temp }}/wrangler-output + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + mkdir -p "$WRANGLER_OUTPUT_FILE_DIRECTORY" + alias="pr-${PR_NUMBER}" + # Do not put spaces or '#' in wrangler flags: OpenNext runs wrangler with shell: true. + pnpm exec opennextjs-cloudflare upload --preview-alias "$alias" + + node <<'NODE' + const fs = require("node:fs") + const path = require("node:path") + const dir = process.env.WRANGLER_OUTPUT_FILE_DIRECTORY + let previewUrl = "" + let aliasUrl = "" + for (const name of fs.readdirSync(dir)) { + const text = fs.readFileSync(path.join(dir, name), "utf8") + for (const line of text.split("\n")) { + if (!line.trim()) continue + try { + const entry = JSON.parse(line) + if (entry.type === "version-upload") { + previewUrl = entry.preview_url || previewUrl + aliasUrl = entry.preview_alias_url || aliasUrl + } + } catch { + // ignore non-JSON lines + } + } + } + const githubOutput = process.env.GITHUB_OUTPUT + if (!githubOutput) { + throw new Error("GITHUB_OUTPUT is not set") + } + fs.appendFileSync(githubOutput, `preview_url=${previewUrl}\n`) + fs.appendFileSync(githubOutput, `alias_url=${aliasUrl}\n`) + if (!previewUrl && !aliasUrl) { + console.warn( + "Wrangler did not report a preview URL. Confirm preview_urls is enabled on the Worker." + ) + } + NODE + + - name: Comment preview URL on the PR + if: steps.upload.outputs.alias_url != '' || steps.upload.outputs.preview_url != '' + uses: actions/github-script@v7 + env: + ALIAS_URL: ${{ steps.upload.outputs.alias_url }} + PREVIEW_URL: ${{ steps.upload.outputs.preview_url }} + with: + script: | + const marker = "" + const aliasUrl = process.env.ALIAS_URL + const previewUrl = process.env.PREVIEW_URL + const url = aliasUrl || previewUrl + const rows = [ + "| | |", + "| --- | --- |", + `| Preview | ${url} |`, + ] + if (previewUrl && previewUrl !== url) { + rows.push(`| This commit | ${previewUrl} |`) + } + const body = [ + marker, + "**Cloudflare preview** is ready.", + "", + ...rows, + "", + "Production ([23rd.dev](https://23rd.dev)) is unchanged. This preview URL stays the same as you push to this PR.", + ].join("\n") + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }) + const existing = comments.find((comment) => comment.body?.includes(marker)) + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }) + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }) + } + + try { + const { data: deployment } = await github.rest.repos.createDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: context.payload.pull_request.head.sha, + environment: "preview", + auto_merge: false, + required_contexts: [], + description: `PR #${context.issue.number} Cloudflare preview`, + transient_environment: true, + production_environment: false, + }) + if (deployment && deployment.id) { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: "success", + environment_url: url, + log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + description: "Cloudflare Workers preview", + }) + } + } catch (error) { + core.warning(`Could not create GitHub deployment: ${error.message}`) + } diff --git a/README.md b/README.md index 7cffed7..0302b3d 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ Useful scripts: | `pnpm build` | Build registry + production app | | `pnpm preview` | Build with OpenNext and preview in the Workers runtime | | `pnpm cf:deploy` | Build with OpenNext and deploy to Cloudflare Workers | +| `pnpm cf:upload` | Build with OpenNext and upload a preview version | | `pnpm registry:build` | Emit `public/r/*.json` from `registry/` | | `pnpm test` | Run registry tests | | `pnpm typecheck` | MDX + TypeScript check | @@ -138,7 +139,15 @@ Stack: Next.js 16, React 19, Fumadocs, Tailwind CSS 4, shadcn/ui (Base UI), Clou pnpm cf:deploy ``` -Connect the repo in the [Cloudflare dashboard](https://dash.cloudflare.com/) (Workers Builds) for Git-based deploys. Point `23rd.dev` DNS at the Worker when you're ready to cut over from Vercel. +GitHub Actions deploys production on push to `main`. Opening a PR uploads a **preview version** (production is untouched) and comments a stable URL, the same idea as Vercel preview deployments: + +`https://pr--23rd-dev..workers.dev` + +That alias stays put as you push more commits to the same PR. Preview URLs live on `workers.dev`; they are public unless you later put [Cloudflare Access](https://developers.cloudflare.com/workers/configuration/cloudflare-access/) in front of them. + +You can get the same PR comments from [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/) instead: connect the repo in the [Cloudflare dashboard](https://dash.cloudflare.com/), set the deploy command to `pnpm cf:deploy`, the non-production command to `pnpm cf:upload`, and enable **Builds for non-production branches**. Don’t run both Workers Builds and this GitHub Action for the same events or you’ll double-deploy. + +Point `23rd.dev` DNS at the Worker when you’re ready to cut over from Vercel. ## Contributing diff --git a/wrangler.jsonc b/wrangler.jsonc index 791b37d..afbda22 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -4,6 +4,7 @@ "main": ".open-next/worker.js", "compatibility_date": "2026-08-06", "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], + "preview_urls": true, "assets": { "directory": ".open-next/assets", "binding": "ASSETS", From 883d7afd6126f5dd9c58bcd8ad19acedbea13a57 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 20 Aug 2026 14:55:28 +0000 Subject: [PATCH 2/2] fix: stop referencing secrets in the preview job if GitHub Actions rejects the secrets context in job-level conditionals, which invalidated the whole workflow and failed every push with zero jobs. Co-authored-by: Jay Sharma --- .github/workflows/deploy-cloudflare.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/deploy-cloudflare.yml b/.github/workflows/deploy-cloudflare.yml index 613045d..1d55ad1 100644 --- a/.github/workflows/deploy-cloudflare.yml +++ b/.github/workflows/deploy-cloudflare.yml @@ -39,10 +39,7 @@ jobs: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} preview: - if: > - github.event_name == 'pull_request' && - github.event.pull_request.head.repo.full_name == github.repository && - secrets.CLOUDFLARE_API_TOKEN != '' + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest permissions: contents: read