From 7affb1776cdea6cc9cb90a301bf63081503f5195 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:46:12 +1100 Subject: [PATCH 01/14] ci: add Cloudflare Pages deployment to workflow Adds wrangler.toml at repo root declaring project identity (name=chinmina, pages_build_output_dir=dist) and a deploy-cloudflare job that downloads the shared dist artifact and deploys via wrangler-action on all branches. Production deploys on main; PRs get preview URLs. Deployment URL written to workflow summary. --- .github/workflows/deploy.yaml | 24 ++++++++++++++++++++++++ wrangler.toml | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 wrangler.toml diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 735ce85..671e431 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -41,6 +41,30 @@ jobs: name: dist path: dist/ + deploy-cloudflare: + needs: build + runs-on: ubuntu-latest + steps: + - name: Download dist artifact + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Deploy to Cloudflare Pages + id: deploy + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy dist --project-name=chinmina + - name: Add deployment summary + run: | + echo "### Cloudflare Pages Deployment" >> $GITHUB_STEP_SUMMARY + echo "URL: ${{ steps.deploy.outputs.deployment-url }}" >> $GITHUB_STEP_SUMMARY + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "Preview: ${{ steps.deploy.outputs.deployment-url }}" >> $GITHUB_STEP_SUMMARY + fi + deploy: needs: build if: github.event_name == 'push' && github.ref == 'refs/heads/main' diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..c40a95f --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,2 @@ +name = "chinmina" +pages_build_output_dir = "dist" From a57aa920908c6ed9522bd558cb52b38c6fcf77b0 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:50:38 +1100 Subject: [PATCH 02/14] ci: update download-artifact to v8 to match upload-artifact v7 --- .github/workflows/deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 671e431..8ab05e4 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -46,7 +46,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download dist artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: dist path: dist/ From bc2085e900815ade240701e0a447922bd29df2e4 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:55:44 +1100 Subject: [PATCH 03/14] ci: add cloudflare-pages environment to deploy-cloudflare job --- .github/workflows/deploy.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 8ab05e4..92868d5 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -44,12 +44,16 @@ jobs: deploy-cloudflare: needs: build runs-on: ubuntu-latest + environment: + name: cloudflare-pages + url: ${{ steps.deploy.outputs.deployment-url }} steps: - name: Download dist artifact uses: actions/download-artifact@v8 with: name: dist path: dist/ + - name: Deploy to Cloudflare Pages id: deploy uses: cloudflare/wrangler-action@v3 @@ -57,6 +61,7 @@ jobs: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy dist --project-name=chinmina + - name: Add deployment summary run: | echo "### Cloudflare Pages Deployment" >> $GITHUB_STEP_SUMMARY From 64c812703440c57d72c0fc549d291be07ca043e9 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:28:38 +1100 Subject: [PATCH 04/14] ci: replace wrangler-action with direct wrangler CLI and deploy script Use the package.json wrangler CLI instead of cloudflare/wrangler-action. The deploy job now checks out the repo, installs deps, then runs pnpm wrangler pages deploy directly. Post-deploy GitHub integration (PR comment, deployment record, step summary) is handled by .github/scripts/cloudflare-deploy.sh. --- .github/workflows/deploy.yaml | 49 +++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 92868d5..64e4f78 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -11,6 +11,8 @@ permissions: contents: read pages: write id-token: write + deployments: write + pull-requests: write jobs: build: @@ -46,8 +48,21 @@ jobs: runs-on: ubuntu-latest environment: name: cloudflare-pages - url: ${{ steps.deploy.outputs.deployment-url }} steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .tool-versions + + - name: Enable corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Download dist artifact uses: actions/download-artifact@v8 with: @@ -56,19 +71,27 @@ jobs: - name: Deploy to Cloudflare Pages id: deploy - uses: cloudflare/wrangler-action@v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy dist --project-name=chinmina + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + WRANGLER_OUTPUT_FILE_DIRECTORY: .wrangler-output + run: pnpm wrangler pages deploy dist --project-name=chinmina - - name: Add deployment summary - run: | - echo "### Cloudflare Pages Deployment" >> $GITHUB_STEP_SUMMARY - echo "URL: ${{ steps.deploy.outputs.deployment-url }}" >> $GITHUB_STEP_SUMMARY - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - echo "Preview: ${{ steps.deploy.outputs.deployment-url }}" >> $GITHUB_STEP_SUMMARY - fi + - name: Post PR comment + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WRANGLER_OUTPUT_FILE_DIRECTORY: .wrangler-output + PR_NUMBER: ${{ github.event.pull_request.number }} + run: .github/scripts/cloudflare-deploy.sh comment + + - name: Create GitHub deployment + if: always() && steps.deploy.outcome == 'success' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WRANGLER_OUTPUT_FILE_DIRECTORY: .wrangler-output + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: .github/scripts/cloudflare-deploy.sh deployment deploy: needs: build From b2eb1b172c1f1d9d6366c656452424d33f7d497e Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:31:12 +1100 Subject: [PATCH 05/14] chore: add wrangler dep, deploy script, example workflow, and PRD --- .github/scripts/cloudflare-deploy.sh | 237 ++++++++++++++++++++++++ docs/prd-cloudflare-pages-migration.md | 91 +++++++++ package.json | 3 +- pnpm-lock.yaml | 246 +++++++++++++++++++++++++ 4 files changed, 576 insertions(+), 1 deletion(-) create mode 100755 .github/scripts/cloudflare-deploy.sh create mode 100644 docs/prd-cloudflare-pages-migration.md diff --git a/.github/scripts/cloudflare-deploy.sh b/.github/scripts/cloudflare-deploy.sh new file mode 100755 index 0000000..139af56 --- /dev/null +++ b/.github/scripts/cloudflare-deploy.sh @@ -0,0 +1,237 @@ +#!/usr/bin/env bash +# +# cloudflare-deploy.sh — post-deploy GitHub integration for Cloudflare Workers/Pages +# +# Usage: +# cloudflare-deploy.sh comment Post or update a PR comment with the preview URL +# cloudflare-deploy.sh deployment Create a GitHub Deployment and job summary +# +# Required environment: +# WRANGLER_OUTPUT_FILE_DIRECTORY Directory where wrangler wrote its output artifacts +# GH_TOKEN GitHub token (usually secrets.GITHUB_TOKEN) +# GITHUB_REPOSITORY owner/repo (set automatically by Actions) +# +# For 'comment': +# PR_NUMBER Pull request number +# +# For 'deployment': +# GITHUB_HEAD_REF / GITHUB_REF_NAME Branch ref (set automatically by Actions) +# CLOUDFLARE_ACCOUNT_ID Cloudflare account ID (for dashboard link) + +set -euo pipefail + +die() { + echo "error: $*" >&2 + exit 1 +} + +# Read the first wrangler output entry matching one of the supported types. +# +# Wrangler writes newline-delimited JSON files named +# wrangler-output--.json into WRANGLER_OUTPUT_FILE_DIRECTORY. +# We read all files once and search in priority order: +# pages-deploy-detailed > deploy > version-upload +read_deploy_output() { + local dir="${WRANGLER_OUTPUT_FILE_DIRECTORY:?WRANGLER_OUTPUT_FILE_DIRECTORY must be set}" + + # Gather all matching files. Use nullglob-safe find to avoid errors on + # empty directories. + local files + files=$(find "$dir" -maxdepth 1 -name 'wrangler-output-*.json' 2>/dev/null | sort) + + if [[ -z "${files}" ]]; then + die "no wrangler output files found in ${dir}" + fi + + # Slurp all lines from all output files into a single stream, then filter. + # This avoids re-reading the directory for each entry type. + local -a file_list + mapfile -t file_list <<< "${files}" + + local all_entries + all_entries=$(cat "${file_list[@]}" 2>/dev/null) + + local entry_type + local match + for entry_type in "pages-deploy-detailed" "deploy" "version-upload"; do + match=$(jq -c "select(.type == \"${entry_type}\")" <<< "${all_entries}" 2>/dev/null | head -n1) + if [[ -n "${match}" ]]; then + echo "${match}" + return + fi + done + + die "no deployment output entry found in wrangler artifacts" +} + +# Extract the deployment URL from whichever entry type we found. +extract_url() { + local entry="$1" + local entry_type + entry_type=$(jq -r '.type' <<< "${entry}") + + case "${entry_type}" in + pages-deploy-detailed) + jq -r '.url // empty' <<< "${entry}" + ;; + deploy) + jq -r '.targets[0] // empty' <<< "${entry}" + ;; + version-upload) + jq -r '.preview_url // empty' <<< "${entry}" + ;; + *) + die "unknown entry type: ${entry_type}" + ;; + esac +} + +# Post or update a PR comment with the preview URL. +cmd_comment() { + local pr="${PR_NUMBER:?PR_NUMBER must be set}" + + local entry + entry=$(read_deploy_output) + + local url + url=$(extract_url "${entry}") + [[ -z "${url}" ]] && die "could not extract deployment URL from wrangler output" + + local body + body="**Cloudflare Preview**"$'\n\n'"🔗 ${url}" + + # Include alias URL for Pages deployments. + local alias_url + alias_url=$(jq -r '.alias // empty' <<< "${entry}" 2>/dev/null) + if [[ -n "${alias_url}" ]]; then + body+=$'\n'"🔀 ${alias_url} (branch alias)" + fi + + # Look for an existing comment to update (avoids spamming on repeated pushes). + local existing_comment + existing_comment=$( + gh api "repos/${GITHUB_REPOSITORY}/issues/${pr}/comments" \ + --jq '.[] | select(.body | startswith("**Cloudflare Preview**")) | .id' \ + 2>/dev/null | head -n1 + ) || true + + if [[ -n "${existing_comment}" ]]; then + gh api "repos/${GITHUB_REPOSITORY}/issues/comments/${existing_comment}" \ + -X PATCH -f body="${body}" --silent + echo "Updated existing comment ${existing_comment}" + else + gh api "repos/${GITHUB_REPOSITORY}/issues/${pr}/comments" \ + -f body="${body}" --silent + echo "Posted new comment on PR #${pr}" + fi +} + +# Create a GitHub Deployment + status and write a job summary. +cmd_deployment() { + local entry + entry=$(read_deploy_output) + + local url + url=$(extract_url "${entry}") + [[ -z "${url}" ]] && die "could not extract deployment URL from wrangler output" + + local entry_type + entry_type=$(jq -r '.type' <<< "${entry}") + + local ref="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME:?}}" + local environment="preview" + local log_url="" + + # Pages deployments have richer metadata. + if [[ "${entry_type}" == "pages-deploy-detailed" ]]; then + environment=$(jq -r '.environment // "preview"' <<< "${entry}") + + local project_name + project_name=$(jq -r '.pages_project // empty' <<< "${entry}") + + local cf_deployment_id + cf_deployment_id=$(jq -r '.deployment_id // empty' <<< "${entry}") + + local account_id="${CLOUDFLARE_ACCOUNT_ID:-}" + + if [[ -n "${account_id}" && -n "${project_name}" && -n "${cf_deployment_id}" ]]; then + log_url="https://dash.cloudflare.com/${account_id}/pages/view/${project_name}/${cf_deployment_id}" + fi + fi + + # Create the deployment. + # Passing an empty JSON array for required_contexts disables commit status + # checks on the deployment object. The gh cli -f flag cannot represent an + # empty array, so we pipe raw JSON via --input. + local gh_deployment_id + gh_deployment_id=$( + jq -n \ + --arg ref "${ref}" \ + --arg env "${environment}" \ + --arg desc "Cloudflare Deploy" \ + '{ + ref: $ref, + environment: $env, + auto_merge: false, + description: $desc, + required_contexts: [] + }' \ + | gh api "repos/${GITHUB_REPOSITORY}/deployments" \ + --method POST --input - --jq '.id' + ) + + if [[ -z "${gh_deployment_id}" ]]; then + die "failed to create GitHub deployment" + fi + + # Set deployment status to success. + local status_body + status_body=$( + jq -n \ + --arg env "${environment}" \ + --arg url "${url}" \ + --arg desc "Cloudflare Deploy" \ + --arg log_url "${log_url}" \ + '{ + state: "success", + environment: $env, + environment_url: $url, + description: $desc, + auto_inactive: false + } + | if $log_url != "" then . + {log_url: $log_url} else . end' + ) + + gh api "repos/${GITHUB_REPOSITORY}/deployments/${gh_deployment_id}/statuses" \ + --method POST --input - --silent <<< "${status_body}" + + echo "Created GitHub deployment ${gh_deployment_id} → ${url}" + + # Write job summary if the variable is available. + if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then + { + echo "### Cloudflare Deploy" + echo "" + echo "| | |" + echo "|---|---|" + echo "| **URL** | ${url} |" + echo "| **Environment** | ${environment} |" + if [[ -n "${log_url}" ]]; then + echo "| **Dashboard** | [View](${log_url}) |" + fi + } >> "${GITHUB_STEP_SUMMARY}" + fi +} + +main() { + case "${1:-}" in + comment) cmd_comment ;; + deployment) cmd_deployment ;; + *) + echo "Usage: $(basename "$0") {comment|deployment}" >&2 + exit 1 + ;; + esac +} + +main "$@" diff --git a/docs/prd-cloudflare-pages-migration.md b/docs/prd-cloudflare-pages-migration.md new file mode 100644 index 0000000..37ad850 --- /dev/null +++ b/docs/prd-cloudflare-pages-migration.md @@ -0,0 +1,91 @@ +# Migrate Hosting to Cloudflare Pages + +## Problem Statement + +The documentation site is currently hosted on GitHub Pages at `chinmina.github.io`. The `chinmina.dev` domain has been registered with Cloudflare, and the canonical home for the docs should be `docs.chinmina.dev`. GitHub Pages does not integrate with the Cloudflare-managed domain, and the two deployments need to coexist during the transition while search engines index the new URL. + +## Solution + +Deploy the site to Cloudflare Pages (project: `chinmina`) via the existing GitHub Actions pipeline. The Astro `site` config is updated to `https://docs.chinmina.dev`, which causes all builds — both Cloudflare Pages and GitHub Pages — to emit canonical links pointing to the new domain. GitHub Pages remains live and up to date throughout, serving as a fallback and preserving the old URL until a redirect strategy is decided separately. + +## Requirements + +### Canonical URL and SEO + +1. The site shall set `https://docs.chinmina.dev` as the canonical base URL in `astro.config.mjs`. +2. The site shall emit a `` tag on every page, resolving to the corresponding URL under `https://docs.chinmina.dev`. +3. When a build is deployed to GitHub Pages, the system shall emit canonical links pointing to `https://docs.chinmina.dev`, not to `chinmina.github.io`. + +### Cloudflare Pages Deployment + +4. When a commit is pushed to `main`, the CI shall build the site and deploy it to Cloudflare Pages as a production deployment under `https://chinmina.pages.dev`. +5. When a pull request is opened or updated, the CI shall build the site and deploy it to a Cloudflare Pages preview URL. +6. While a pull request is open, its Cloudflare Pages preview deployment shall remain accessible at its preview URL. +7. When a Cloudflare Pages deployment completes, the CI shall surface the deployment URL in the workflow summary. +8. If the Cloudflare Pages deployment step fails, then the CI shall fail and not mark the workflow as successful. + +### GitHub Pages Deployment (Continued) + +9. When a commit is pushed to `main`, the CI shall also deploy the same build to GitHub Pages. +10. If the GitHub Pages deployment step fails, then the CI shall fail and not mark the workflow as successful. + +### Build Pipeline + +11. The CI shall install the D2 diagramming tool before running the Astro build. +12. The CI shall produce a single build artifact shared by both the Cloudflare Pages and GitHub Pages deployment jobs. +13. If the build step fails, then the CI shall not attempt either deployment. + +### DNS and Domain + +14. The system shall serve the Cloudflare Pages production deployment at `https://docs.chinmina.dev` via a DNS CNAME record in Cloudflare. +15. The Cloudflare Pages project shall enforce HTTPS for all requests to `docs.chinmina.dev`. + +### Optional + +16. Where a pull request triggers a CI build, the CI shall output the Cloudflare Pages preview URL as a GitHub Actions step summary. + +## Implementation Decisions + +**Workflow restructure**: The current `withastro/action` couples the build to GitHub Pages artifact upload. To share one build between two deploy targets, the build must be extracted into explicit steps: install D2, set up Node.js (via `actions/setup-node` with `enable-corepack: true` — corepack reads the `packageManager` field in `package.json` and provisions the pinned pnpm version automatically), run `pnpm install`, run `pnpm run build`, then upload two artifacts — one as a GitHub Pages artifact (`actions/upload-pages-artifact`) and one as a generic `dist/` artifact (`actions/upload-artifact`). The two deploy jobs run in parallel after the build job completes. + +**GitHub Pages deploy job**: Unchanged in behaviour. Conditional on `github.ref == 'refs/heads/main'`. Consumes the GitHub Pages artifact via `actions/deploy-pages`. + +**Cloudflare Pages deploy job**: Runs on all branches (for preview support). Downloads the `dist/` artifact and deploys via `cloudflare/wrangler-action` with `command: pages deploy dist --project-name=chinmina`. Requires `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` as GitHub Actions secrets. + +**`wrangler.toml`**: A minimal `wrangler.toml` at the repo root declares `name = "chinmina"` and `pages_build_output_dir = "dist"`. This makes the project identity explicit and removes the need to pass flags in the workflow command. + +**Canonical link handling**: Astro emits `` automatically based on the `site` config. Changing `site` to `https://docs.chinmina.dev` is sufficient — no changes to `Head.astro` are required. The `site` change and the Cloudflare Pages go-live must ship in the same merge to avoid a window where GitHub Pages serves canonicals pointing to a domain not yet live. + +**Cloudflare Pages project**: Must be created in the Cloudflare dashboard (or via Wrangler) before the first deployment. Custom domain `docs.chinmina.dev` is configured in the Cloudflare Pages project settings. DNS is a CNAME record: `docs.chinmina.dev` → `chinmina.pages.dev`. + +**GitHub Pages remains live**: GitHub Pages is not disabled as part of this work. It continues to receive deployments from `main` and serves the site at `chinmina.github.io` with canonical links pointing to `docs.chinmina.dev`. Decommissioning GitHub Pages is deferred to a future redirect-strategy workstream. + +**GitHub Actions secrets needed**: +- `CLOUDFLARE_API_TOKEN` — scoped to Cloudflare Pages edit permissions +- `CLOUDFLARE_ACCOUNT_ID` — the Cloudflare account hosting the `chinmina` project + +## Testing Decisions + +This is an infrastructure and configuration change. There are no unit tests. All requirements map to manual acceptance checks performed after deployment: + +| Requirement | Verification | +|---|---| +| 1–3 (canonical) | View page source on both `docs.chinmina.dev` and `chinmina.github.io`; confirm canonical tag resolves to `docs.chinmina.dev` | +| 4 (production deploy) | Merge to `main`; confirm Cloudflare Pages production deployment succeeds and site is reachable at `chinmina.pages.dev` | +| 5–6 (preview deploy) | Open a PR; confirm a preview URL appears in the workflow summary and is reachable | +| 9 (GH Pages continued) | Merge to `main`; confirm GitHub Pages deployment succeeds and `chinmina.github.io` reflects the change | +| 11 (D2) | Confirm a page containing a D2 diagram renders correctly on `docs.chinmina.dev` | +| 14–15 (DNS + HTTPS) | `curl -I https://docs.chinmina.dev`; confirm 200 and valid TLS certificate | + +## Out of Scope + +- Redirecting `chinmina.github.io` to `docs.chinmina.dev` (deferred to a separate workstream) +- Disabling GitHub Pages +- Redirecting the apex domain `chinmina.dev` to `docs.chinmina.dev` +- Any changes to site content or structure + +## Further Notes + +The `withastro/action` action is a convenience wrapper that bundles Node.js setup, pnpm detection, build, and GitHub Pages artifact upload in one step. Replacing it with explicit steps adds a few lines to the workflow but gives full control over the build environment — necessary here because of the D2 pre-install step and the need to share the build output with a second deploy target. + +The Cloudflare Pages project must exist before the first pipeline run. Creating it via the Cloudflare dashboard (connect to GitHub, select repo, set build command to `pnpm run build` and output dir to `dist`) is the recommended path, but with `wrangler.toml` in place, `wrangler pages project create chinmina` also works. diff --git a/package.json b/package.json index 36ba78e..84fd490 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.5", - "vitest": "^3.0.0" + "vitest": "^3.0.0", + "wrangler": "^4.77.0" }, "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4613381..fef5a61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: vitest: specifier: ^3.0.0 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(yaml@2.8.2) + wrangler: + specifier: ^4.77.0 + version: 4.77.0 packages: @@ -193,6 +196,53 @@ packages: '@clack/prompts@1.1.0': resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==} + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.16.0': + resolution: {integrity: sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260317.1': + resolution: {integrity: sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260317.1': + resolution: {integrity: sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260317.1': + resolution: {integrity: sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260317.1': + resolution: {integrity: sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260317.1': + resolution: {integrity: sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@ctrl/tinycolor@4.2.0': resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==} engines: {node: '>=14'} @@ -682,9 +732,16 @@ packages: cpu: [x64] os: [win32] + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -724,6 +781,15 @@ packages: cpu: [x64] os: [win32] + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -907,6 +973,13 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.15': + resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} + '@terrastruct/d2@0.1.33': resolution: {integrity: sha512-eK5hyfGIJFolC7sUsiKvWdY9xGFctTe3d+PSijo09IYDso8psztC+A4SammizXtlwYZpnnW0AtDjfBYauceSeA==} @@ -1096,6 +1169,9 @@ packages: bcp-47@2.1.0: resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -1282,6 +1358,9 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -1720,6 +1799,11 @@ packages: micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + miniflare@4.20260317.2: + resolution: {integrity: sha512-qNL+yWAFMX6fr0pWU6Lx1vNpPobpnDSF1V8eunIckWvoIQl8y1oBjL2RJFEGY3un+l3f9gwW9dirDPP26usYJQ==} + engines: {node: '>=18.0.0'} + hasBin: true + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -1801,6 +1885,9 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2037,6 +2124,10 @@ packages: style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + svgo@4.0.0: resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==} engines: {node: '>=16'} @@ -2117,6 +2208,13 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@7.24.4: + resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -2452,10 +2550,37 @@ packages: engines: {node: '>=8'} hasBin: true + workerd@1.20260317.1: + resolution: {integrity: sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.77.0: + resolution: {integrity: sha512-E2Gm69+K++BFd3QvoWjC290RPQj1vDOUotA++sNHmtKPb7EP6C8Qv+1D5Ii73tfZtyNgakpqHlh8lBBbVWTKAQ==} + engines: {node: '>=20.3.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260317.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} @@ -2493,6 +2618,12 @@ packages: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -2712,6 +2843,33 @@ snapshots: '@clack/core': 1.1.0 sisteransi: 1.0.5 + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@cloudflare/unenv-preset@2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260317.1 + + '@cloudflare/workerd-darwin-64@1.20260317.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260317.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260317.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260317.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260317.1': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@ctrl/tinycolor@4.2.0': {} '@emmetio/abbreviation@2.3.3': @@ -3019,8 +3177,15 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.8 @@ -3073,6 +3238,18 @@ snapshots: '@pagefind/windows-x64@1.4.0': optional: true + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + '@rollup/pluginutils@5.3.0(rollup@4.59.0)': dependencies: '@types/estree': 1.0.8 @@ -3227,6 +3404,10 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.15': {} + '@terrastruct/d2@0.1.33': {} '@types/chai@5.2.3': @@ -3529,6 +3710,8 @@ snapshots: is-alphanumerical: 2.0.1 is-decimal: 2.0.1 + blake3-wasm@2.1.5: {} + boolbase@1.0.0: {} cac@6.7.14: {} @@ -3682,6 +3865,8 @@ snapshots: entities@6.0.1: {} + error-stack-parser-es@1.0.5: {} + es-module-lexer@1.7.0: {} es-module-lexer@2.0.0: {} @@ -4574,6 +4759,18 @@ snapshots: transitivePeerDependencies: - supports-color + miniflare@4.20260317.2: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.24.4 + workerd: 1.20260317.1 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + mrmime@2.0.1: {} ms@2.1.3: {} @@ -4663,6 +4860,8 @@ snapshots: path-browserify@1.0.1: {} + path-to-regexp@6.3.0: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -5024,6 +5223,8 @@ snapshots: dependencies: inline-style-parser: 0.2.7 + supports-color@10.2.2: {} + svgo@4.0.0: dependencies: commander: 11.1.0 @@ -5082,6 +5283,12 @@ snapshots: undici-types@7.16.0: {} + undici@7.24.4: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -5371,12 +5578,38 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + workerd@1.20260317.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260317.1 + '@cloudflare/workerd-darwin-arm64': 1.20260317.1 + '@cloudflare/workerd-linux-64': 1.20260317.1 + '@cloudflare/workerd-linux-arm64': 1.20260317.1 + '@cloudflare/workerd-windows-64': 1.20260317.1 + + wrangler@4.77.0: + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260317.2 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260317.1 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 + ws@8.18.0: {} + xxhash-wasm@1.1.0: {} y18n@5.0.8: {} @@ -5415,6 +5648,19 @@ snapshots: yocto-queue@1.2.2: {} + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.15 + cookie: 1.1.1 + youch-core: 0.3.3 + zod@4.3.6: {} zwitch@2.0.4: {} From 712a1f9a160700c565e6cd2c8284a604cd766b88 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:44:43 +1100 Subject: [PATCH 06/14] ci: use cloudflare environment for secrets; remove example workflow --- .github/workflows/deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 64e4f78..1f00df6 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -47,7 +47,7 @@ jobs: needs: build runs-on: ubuntu-latest environment: - name: cloudflare-pages + name: cloudflare steps: - name: Checkout repository uses: actions/checkout@v6 From fb0a5b589075d276354fa8aca0a69e1b55069064 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:12:50 +1100 Subject: [PATCH 07/14] ci: remove duplicate deployment; surface URL via environment block The job environment: block auto-creates a GitHub Deployment named 'cloudflare', so cloudflare-deploy.sh deployment was redundant and producing a second record named 'preview'. Replace it with an inline URL extraction step that sets environment.url and writes the step summary. PR comments via cloudflare-deploy.sh comment are unchanged. --- .github/workflows/deploy.yaml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 1f00df6..3b7b974 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -48,6 +48,7 @@ jobs: runs-on: ubuntu-latest environment: name: cloudflare + url: ${{ steps.cf-url.outputs.value }} steps: - name: Checkout repository uses: actions/checkout@v6 @@ -77,6 +78,17 @@ jobs: WRANGLER_OUTPUT_FILE_DIRECTORY: .wrangler-output run: pnpm wrangler pages deploy dist --project-name=chinmina + - name: Extract deployment URL + id: cf-url + if: always() && steps.deploy.outcome == 'success' + run: | + url=$(cat .wrangler-output/wrangler-output-*.json 2>/dev/null \ + | jq -r 'select(.type == "pages-deploy-detailed") | .url // empty' \ + | head -1) + echo "value=${url}" >> "$GITHUB_OUTPUT" + echo "### Cloudflare Pages" >> "$GITHUB_STEP_SUMMARY" + echo "**URL:** ${url}" >> "$GITHUB_STEP_SUMMARY" + - name: Post PR comment if: github.event_name == 'pull_request' env: @@ -85,14 +97,6 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} run: .github/scripts/cloudflare-deploy.sh comment - - name: Create GitHub deployment - if: always() && steps.deploy.outcome == 'success' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - WRANGLER_OUTPUT_FILE_DIRECTORY: .wrangler-output - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - run: .github/scripts/cloudflare-deploy.sh deployment - deploy: needs: build if: github.event_name == 'push' && github.ref == 'refs/heads/main' From 242a682852494f547d997c9fea367fbdd9d90bfc Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:47:05 +1100 Subject: [PATCH 08/14] ci: pass --branch to wrangler pages deploy for correct preview alias --- .github/workflows/deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 3b7b974..5230b6f 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -76,7 +76,7 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} WRANGLER_OUTPUT_FILE_DIRECTORY: .wrangler-output - run: pnpm wrangler pages deploy dist --project-name=chinmina + run: pnpm wrangler pages deploy dist --project-name=chinmina --branch=${{ github.head_ref || github.ref_name }} - name: Extract deployment URL id: cf-url From 41a63952f7164f201a89dd5e0e2801f15851f1c6 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:57:37 +1100 Subject: [PATCH 09/14] ci: drop redundant dist and --project-name from wrangler deploy (set in wrangler.toml) --- .github/workflows/deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 5230b6f..5e80de9 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -76,7 +76,7 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} WRANGLER_OUTPUT_FILE_DIRECTORY: .wrangler-output - run: pnpm wrangler pages deploy dist --project-name=chinmina --branch=${{ github.head_ref || github.ref_name }} + run: pnpm wrangler pages deploy --branch=${{ github.head_ref || github.ref_name }} - name: Extract deployment URL id: cf-url From af8b60bc2b113faeefa366e16f2d83cd816403c1 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:58:49 +1100 Subject: [PATCH 10/14] ci: pass branch via env var to avoid command injection --- .github/workflows/deploy.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 5e80de9..00ef7ae 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -76,7 +76,8 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} WRANGLER_OUTPUT_FILE_DIRECTORY: .wrangler-output - run: pnpm wrangler pages deploy --branch=${{ github.head_ref || github.ref_name }} + CF_BRANCH: ${{ github.head_ref || github.ref_name }} + run: pnpm wrangler pages deploy --branch="$CF_BRANCH" - name: Extract deployment URL id: cf-url From 31a70e9945a4ad5dcbfd5483b7ade7c7932cafa7 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:06:19 +1100 Subject: [PATCH 11/14] ci: use in PR comment for new-window links --- .github/scripts/cloudflare-deploy.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/cloudflare-deploy.sh b/.github/scripts/cloudflare-deploy.sh index 139af56..2b894e7 100755 --- a/.github/scripts/cloudflare-deploy.sh +++ b/.github/scripts/cloudflare-deploy.sh @@ -98,13 +98,13 @@ cmd_comment() { [[ -z "${url}" ]] && die "could not extract deployment URL from wrangler output" local body - body="**Cloudflare Preview**"$'\n\n'"🔗 ${url}" + body="**Cloudflare Preview**"$'\n\n'"🔗 ${url}" # Include alias URL for Pages deployments. local alias_url alias_url=$(jq -r '.alias // empty' <<< "${entry}" 2>/dev/null) if [[ -n "${alias_url}" ]]; then - body+=$'\n'"🔀 ${alias_url} (branch alias)" + body+=$'\n'"🔀 ${alias_url} (branch alias)" fi # Look for an existing comment to update (avoids spamming on repeated pushes). From 1dfa4dcecce6f19bb42f177875c6c43eeff6808e Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:20:31 +1100 Subject: [PATCH 12/14] ci: fix PR comment detection to match updated heading --- .github/scripts/cloudflare-deploy.sh | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/scripts/cloudflare-deploy.sh b/.github/scripts/cloudflare-deploy.sh index 2b894e7..097b642 100755 --- a/.github/scripts/cloudflare-deploy.sh +++ b/.github/scripts/cloudflare-deploy.sh @@ -91,27 +91,27 @@ cmd_comment() { local pr="${PR_NUMBER:?PR_NUMBER must be set}" local entry - entry=$(read_deploy_output) - local url + local alias_url + local body + + entry=$(read_deploy_output) url=$(extract_url "${entry}") [[ -z "${url}" ]] && die "could not extract deployment URL from wrangler output" - local body - body="**Cloudflare Preview**"$'\n\n'"🔗 ${url}" - - # Include alias URL for Pages deployments. - local alias_url alias_url=$(jq -r '.alias // empty' <<< "${entry}" 2>/dev/null) - if [[ -n "${alias_url}" ]]; then - body+=$'\n'"🔀 ${alias_url} (branch alias)" - fi + + body=" +### Branch preview + +🔗 [${alias_url}](${alias_url}) ([direct commit link](${url})) +" # Look for an existing comment to update (avoids spamming on repeated pushes). local existing_comment existing_comment=$( gh api "repos/${GITHUB_REPOSITORY}/issues/${pr}/comments" \ - --jq '.[] | select(.body | startswith("**Cloudflare Preview**")) | .id' \ + --jq '.[] | select(.body | contains("### Branch preview")) | .id' \ 2>/dev/null | head -n1 ) || true From 988e70ea8ee0b4be8a4b636d494c8a1420dd111f Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:22:07 +1100 Subject: [PATCH 13/14] ci: use HTML comment marker for robust PR comment detection --- .github/scripts/cloudflare-deploy.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/cloudflare-deploy.sh b/.github/scripts/cloudflare-deploy.sh index 097b642..fc0a820 100755 --- a/.github/scripts/cloudflare-deploy.sh +++ b/.github/scripts/cloudflare-deploy.sh @@ -101,7 +101,7 @@ cmd_comment() { alias_url=$(jq -r '.alias // empty' <<< "${entry}" 2>/dev/null) - body=" + body=" ### Branch preview 🔗 [${alias_url}](${alias_url}) ([direct commit link](${url})) @@ -111,7 +111,7 @@ cmd_comment() { local existing_comment existing_comment=$( gh api "repos/${GITHUB_REPOSITORY}/issues/${pr}/comments" \ - --jq '.[] | select(.body | contains("### Branch preview")) | .id' \ + --jq '.[] | select(.body | contains("")) | .id' \ 2>/dev/null | head -n1 ) || true From ecd1625024b7ac51925e57fe84ab2c313951ab20 Mon Sep 17 00:00:00 2001 From: James Telfer <792299+jamestelfer@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:24:33 +1100 Subject: [PATCH 14/14] ci: skip Cloudflare deploy for forked PRs and Dependabot --- .github/workflows/deploy.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 00ef7ae..22aa2b2 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -45,6 +45,7 @@ jobs: deploy-cloudflare: needs: build + if: github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]') runs-on: ubuntu-latest environment: name: cloudflare