Skip to content

feat(docs): deploy CLI docs to Cloudflare Workers instead of GitHub Pages - #1354

Open
jared-outpost[bot] wants to merge 5 commits into
mainfrom
issue-1245-cli-docs-cloudflare-base
Open

feat(docs): deploy CLI docs to Cloudflare Workers instead of GitHub Pages#1354
jared-outpost[bot] wants to merge 5 commits into
mainfrom
issue-1245-cli-docs-cloudflare-base

Conversation

@jared-outpost

@jared-outpost jared-outpost Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Moves the cli.sentry.dev docs site off GitHub Pages onto a release-gated Cloudflare Worker (static assets), and replaces the gh-pages PR-preview flow with Cloudflare version previews. Per BYK, this uses a Worker with static assets (Pages is deprecated); DNS cutover for cli.sentry.dev is handled separately.

What's in this branch (pushable)

  • apps/cli-docs/wrangler.jsonc — assets-only Worker serving Astro's dist/ (not_found_handling: 404-page). /install ships as dist/install and stays reachable at cli.sentry.dev/install.
  • apps/cli-docs/package.jsonwrangler devDependency + deploy script; lockfile updated.

⚠️ Workflow changes need a maintainer to apply

The jared GitHub App token lacks the workflows permission, so it cannot push .github/workflows/* or .craft.yml. The full, tested diff for those is in the hidden comment at the bottom of this description — a maintainer with workflows permission needs to add it. Verified locally: docs build succeeds with DOCS_BASE_PATH=/, wrangler deploy --dry-run reads all 333 assets, all workflow YAML parses, check:stale-refs/check:deps pass.

Changes in that diff:

  • docs-deploy.yml (new) — build + Sentry sourcemap upload + wrangler deploy, gated on release: published (skips prereleases), keeping the "docs tied to the published release" guarantee the old Craft gh-pages target gave.
  • docs-preview.yml — uploads a Cloudflare preview version (wrangler versions upload) and comments its URL; drops rossjrw/pr-preview-action, the gh-pages umbrella, .nojekyll juggling, and closed-event cleanup.
  • .craft.yml — removes the gh-pages target + artifact.
  • ci.ymlbuild-docs no longer packages/uploads the gh-pages artifact (build + sourcemap upload unchanged).
  • Deletes cleanup-doc-previews.yml, public/CNAME, and the orphaned .nojekyll.

Notes for review

  • Requires CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID secrets in the production environment (already present on the MCP side per the migration plan).
  • The first versions upload preview needs the Worker to exist; it's created on the first production release deploy (or a one-off wrangler deploy). Bootstrap alongside the DNS cutover.

Refs #1245

Adds the wrangler config (assets-only Worker serving Astro's dist/) and
the wrangler devDependency for the cli.sentry.dev docs site, in
preparation for moving off GitHub Pages onto a release-gated Cloudflare
deploy.

The workflow changes that wire this up (release-gated docs-deploy.yml,
Cloudflare PR previews, and dropping the gh-pages Craft target) require
the `workflows` GitHub-App permission to push and are attached to the PR
for a maintainer to apply — see the PR description.

Refs #1245
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://cli.sentry.dev/_preview/pr-1354/

Built to branch gh-pages at 2026-08-03 20:56 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@jared-outpost
jared-outpost Bot marked this pull request as ready for review August 3, 2026 17:24
@jared-outpost
jared-outpost Bot requested a review from BYK August 3, 2026 17:24
@github-actions github-actions Bot added the risk: high PR risk score: high label Aug 3, 2026
@jared-outpost

jared-outpost Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Out of draft — CI is green across the board (lint & typecheck, unit tests, docs build/preview, CodeQL, warden, socket, dependency review) and self-review turned up nothing. The pushable half of the migration (wrangler config + deploy script + lockfile) is all that's in here; the workflow/.craft.yml changes still need a maintainer with workflows permission to apply from the diff in the description. @BYK for review.

@BYK

BYK commented Aug 3, 2026

Copy link
Copy Markdown
Member

Out of draft — CI is green across the board (lint & typecheck, unit tests, docs build/preview, CodeQL, warden, socket, dependency review) and self-review turned up nothing. The pushable half of the migration (wrangler config + deploy script + lockfile) is all that's in here; the workflow/.craft.yml changes still need a maintainer with workflows permission to apply from the diff in the description. @BYK for review.

.craft.yml changes should not require workflow permissions so push those. For everything else, push the patch as a comment and I'll get it applied

@BYK BYK added the jared Trigger the Jared agent to work on stuff label Aug 3, 2026
jared-outpost Bot added 2 commits August 3, 2026 20:44
…ories

wrangler pulls in undici 7.28.0, which now trips GHSA-4cwx-7wf7-3272 (high)
plus four moderates — all patched in 7.29.0. undici is a dev-only transitive
dep (docs deploy), never shipped in the CLI.
@jared-outpost

jared-outpost Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Applied the .craft.yml changes directly (no workflows permission needed there), so that half is on the branch now. Also fixed the dependency-review failure: wrangler pulls in undici@7.28.0, which now trips GHSA-4cwx-7wf7-3272 (high) + 4 moderates — pinned undici to 7.29.0 via a pnpm override (dev-only transitive dep, never ships in the CLI).

The .github/workflows/* changes still need someone with workflows permission. Diff to apply (.craft.yml excluded since it is already in):

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 45b0956..d02896e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -758,15 +758,6 @@ jobs:
       # be deployed to production.
       - name: Remove sourcemaps from output
         run: find apps/cli-docs/dist -name '*.map' -delete
-      - name: Package Docs
-        run: |
-          cp .nojekyll apps/cli-docs/dist/
-          cd apps/cli-docs/dist && zip -r "$GITHUB_WORKSPACE/gh-pages.zip" .
-      - name: Upload docs artifact
-        uses: actions/upload-artifact@v7
-        with:
-          name: gh-pages
-          path: gh-pages.zip
 
   ci-status:
     name: CI Status
diff --git a/.github/workflows/cleanup-doc-previews.yml b/.github/workflows/cleanup-doc-previews.yml
deleted file mode 100644
index 6a59b85..0000000
--- a/.github/workflows/cleanup-doc-previews.yml
+++ /dev/null
@@ -1,99 +0,0 @@
-name: Cleanup Doc Previews
-
-# Safety net for the on-close cleanup in docs-preview.yml: sweeps gh-pages for
-# `_preview/pr-<n>` directories whose PR is no longer open and removes them.
-# Catches previews that slipped through (e.g. closed while the preview workflow
-# was failing) so gh-pages never bloats past GitHub Pages' build limits again.
-
-on:
-  schedule:
-    # Every Sunday at 05:30 UTC (before the nightly-tag cleanup).
-    - cron: "30 5 * * 0"
-  workflow_dispatch:
-
-permissions:
-  contents: write
-  pull-requests: read
-
-concurrency:
-  # Serialize cleanup runs with each other. Races against docs-preview deploys
-  # are handled by the push retry loop below (a non-fast-forward push is
-  # rejected, never applied, so it can't corrupt gh-pages).
-  group: gh-pages-cleanup
-  cancel-in-progress: false
-
-jobs:
-  prune:
-    name: Prune previews for closed PRs
-    runs-on: ubuntu-latest
-    steps:
-      - name: Prune stale previews
-        env:
-          GH_TOKEN: ${{ github.token }}
-          REPO: ${{ github.repository }}
-        run: |
-          set -euo pipefail
-
-          # Blobless, no-checkout clone so we never download the (large) preview
-          # file contents — we only need trees to rewrite the index.
-          git clone --filter=blob:none --no-checkout --single-branch \
-            --branch gh-pages \
-            "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" ghp
-          cd ghp
-          git config user.name "github-actions[bot]"
-          git config user.email "github-actions[bot]@users.noreply.github.com"
-
-          # Retry loop: another gh-pages writer (a docs-preview deploy) may land
-          # between our read and push. A non-fast-forward push is REJECTED (never
-          # applied, so no corruption); we re-sync onto the new tip and re-prune.
-          max_attempts=3
-          attempt=0
-          while :; do
-            attempt=$((attempt + 1))
-            git read-tree HEAD
-
-            removed=0
-            # One entry per top-level _preview/<dir>.
-            for dir in $(git ls-files _preview/ | sed -E 's#(_preview/[^/]+)/.*#\1#' | sort -u); do
-              name="${dir#_preview/}"
-              # Keep the production/main preview.
-              if [ "$name" = "pr-main" ]; then
-                continue
-              fi
-              num="${name#pr-}"
-              # Skip anything that isn't a pr-<number> directory.
-              case "$num" in
-                ''|*[!0-9]*) continue ;;
-              esac
-              state=$(gh pr view "$num" --repo "$REPO" --json state --jq .state 2>/dev/null || echo "UNKNOWN")
-              # Only prune when the PR is CONFIRMED closed/merged. Keep the
-              # preview on OPEN and on any API error/unknown state so a transient
-              # glitch never deletes a live preview.
-              case "$state" in
-                CLOSED|MERGED)
-                  echo "Removing $dir (PR #$num state=$state)"
-                  git rm -r -q --cached "$dir"
-                  removed=$((removed + 1))
-                  ;;
-              esac
-            done
-
-            if [ "$removed" -eq 0 ]; then
-              echo "No stale previews to prune."
-              exit 0
-            fi
-
-            git commit -q -m "chore(docs): prune ${removed} preview(s) for closed PRs [skip ci]"
-            if git push origin gh-pages; then
-              echo "Pruned ${removed} stale preview(s)."
-              exit 0
-            fi
-
-            if [ "$attempt" -ge "$max_attempts" ]; then
-              echo "gh-pages push kept failing after ${max_attempts} attempts; retrying next run." >&2
-              exit 1
-            fi
-            echo "gh-pages moved underneath us; re-syncing and retrying (attempt ${attempt})…"
-            git fetch --quiet origin gh-pages
-            git reset --soft FETCH_HEAD
-          done
diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml
new file mode 100644
index 0000000..51ec6b1
--- /dev/null
+++ b/.github/workflows/docs-deploy.yml
@@ -0,0 +1,82 @@
+name: Docs Deploy
+
+# Release-gated production deploy of the CLI docs site to Cloudflare Workers
+# (static assets). Fires when a release is published so the deployed docs always
+# track the latest published CLI version — the same "docs tied to release"
+# guarantee the old Craft gh-pages target provided, just on Cloudflare instead
+# of GitHub Pages. Pre-releases (nightlies, -dev versions) are skipped.
+on:
+  release:
+    types: [published]
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "Release version to deploy (e.g., 0.24.0)"
+        required: true
+        type: string
+
+permissions:
+  contents: read
+
+concurrency:
+  group: docs-deploy
+  cancel-in-progress: false
+
+env:
+  # Pin Node to an exact patched version (single source of truth). A floating
+  # "24" can reuse the runner's pre-cached 24.17.0, which carries the
+  # ERR_STREAM_PREMATURE_CLOSE regression on keep-alive fetch reuse
+  # (nodejs/node#64004, fixed in 24.18.0).
+  NODE_VERSION_24: "24.18.0"
+
+jobs:
+  deploy:
+    name: Deploy docs to Cloudflare
+    runs-on: ubuntu-latest
+    environment: production
+    # Skip pre-releases (nightlies, dev versions) on automatic trigger; always
+    # run on manual dispatch.
+    if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
+    env:
+      # Both secrets are scoped to the "production" environment.
+      SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+      CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+      CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+      # Bare semver tag (e.g. "0.24.0", no "v" prefix), matching the
+      # Sentry.init() release value and sourcemap uploads.
+      VERSION: ${{ github.event.release.tag_name || inputs.version }}
+    steps:
+      - uses: actions/checkout@v6
+      - uses: pnpm/action-setup@v4
+      # Astro 6 requires Node >= 22.12. Pin an exact patched version (see
+      # NODE_VERSION_24) so the docs build doesn't rely on the runner default.
+      - uses: actions/setup-node@v6
+        with:
+          node-version: ${{ env.NODE_VERSION_24 }}
+      - run: pnpm install --frozen-lockfile
+      - name: Generate docs content
+        run: pnpm run generate:schema && pnpm run generate:docs
+      - name: Build Docs
+        working-directory: apps/cli-docs
+        env:
+          PUBLIC_SENTRY_ENVIRONMENT: production
+          SENTRY_RELEASE: ${{ env.VERSION }}
+          PUBLIC_SENTRY_RELEASE: ${{ env.VERSION }}
+        run: pnpm run build
+      # Inject debug IDs and upload sourcemaps to Sentry (see ci.yml build-docs).
+      - name: Inject debug IDs and upload sourcemaps
+        if: env.SENTRY_AUTH_TOKEN != ''
+        env:
+          SENTRY_ORG: sentry
+          SENTRY_PROJECT: cli-website
+        run: |
+          pnpm run cli sourcemap inject "$GITHUB_WORKSPACE/apps/cli-docs/dist/"
+          pnpm run cli sourcemap upload "$GITHUB_WORKSPACE/apps/cli-docs/dist/" \
+            --release "$VERSION" \
+            --url-prefix "~/"
+      # Remove .map files — uploaded to Sentry but shouldn't be deployed.
+      - name: Remove sourcemaps from output
+        run: find apps/cli-docs/dist -name '*.map' -delete
+      - name: Deploy to Cloudflare
+        working-directory: apps/cli-docs
+        run: pnpm run deploy
diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml
index cf83134..71c7606 100644
--- a/.github/workflows/docs-preview.yml
+++ b/.github/workflows/docs-preview.yml
@@ -1,5 +1,10 @@
 name: Docs Preview
 
+# Builds the CLI docs on every docs-touching PR and uploads a Cloudflare Workers
+# preview version, then comments the preview URL on the PR. Replaces the old
+# gh-pages + rossjrw/pr-preview-action flow: Cloudflare version previews are
+# isolated per upload and expire on their own, so there is no umbrella branch to
+# clean up and no `closed`-event handling.
 on:
   push:
     branches: [main]
@@ -11,15 +16,16 @@ on:
       - 'packages/cli/install'
       - '.github/workflows/docs-preview.yml'
   pull_request:
-    # No paths filter here: the 'closed' event must ALWAYS fire so
-    # pr-preview-action can remove the deployed preview. A paths-filter step
-    # inside the job (below) skips the build/deploy for non-docs PRs. Without
-    # this, closed PRs never clean up and gh-pages bloats until GitHub Pages
-    # exceeds its build limits and starts failing.
-    types: [opened, reopened, synchronize, closed]
+    paths:
+      - 'apps/cli-docs/**'
+      - 'packages/cli/src/**'
+      - 'packages/cli/script/generate-command-docs.ts'
+      - 'packages/cli/script/generate-skill.ts'
+      - 'packages/cli/install'
+      - '.github/workflows/docs-preview.yml'
 
 permissions:
-  contents: write
+  contents: read
   pull-requests: write
 
 concurrency:
@@ -35,63 +41,29 @@ env:
 
 jobs:
   preview:
+    name: Build and upload docs preview
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v6
 
-      # Detect whether this PR actually touches docs. Skipped on 'closed' (and
-      # on push), where the gate below decides what to do without a diff.
-      - name: Check for docs changes
-        id: filter
-        if: github.event_name == 'pull_request' && github.event.action != 'closed'
-        uses: dorny/paths-filter@v4
-        with:
-          filters: |
-            docs:
-              - 'apps/cli-docs/**'
-              - 'packages/cli/src/**'
-              - 'packages/cli/script/generate-command-docs.ts'
-              - 'packages/cli/script/generate-skill.ts'
-              - 'packages/cli/install'
-              - '.github/workflows/docs-preview.yml'
-
-      # Central gate. `build` = produce the site (push, or a docs PR being
-      # opened/updated). `deploy` = publish/remove on gh-pages (build, or any
-      # 'closed' event to clean up), but never for fork PRs which lack write
-      # access. Fork PRs still build to surface compilation errors.
-      - name: Compute gates
+      # Fork PRs have no access to the Cloudflare secrets, so they build only
+      # (to surface compilation errors) and skip the upload/comment steps.
+      - name: Compute deploy gate
         id: gate
         env:
-          EVENT: ${{ github.event_name }}
-          ACTION: ${{ github.event.action }}
-          DOCS_CHANGED: ${{ steps.filter.outputs.docs }}
           IS_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
-        run: |
-          build=false
-          deploy=false
-          if [ "$EVENT" = "push" ] || { [ "$ACTION" != "closed" ] && [ "$DOCS_CHANGED" = "true" ]; }; then
-            build=true
-          fi
-          if { [ "$build" = "true" ] || [ "$ACTION" = "closed" ]; } && [ "$IS_FORK" != "true" ]; then
-            deploy=true
-          fi
-          echo "build=$build" >> "$GITHUB_OUTPUT"
-          echo "deploy=$deploy" >> "$GITHUB_OUTPUT"
+        run: echo "deploy=$([ "$IS_FORK" = "true" ] && echo false || echo true)" >> "$GITHUB_OUTPUT"
 
       - uses: pnpm/action-setup@v4
-        if: steps.gate.outputs.build == 'true'
 
       # Astro 6 requires Node >= 22.12. Pin an exact patched version (see
-      # NODE_VERSION_24) so the docs build doesn't rely on the runner image's
-      # default.
+      # NODE_VERSION_24) so the docs build doesn't rely on the runner default.
       - uses: actions/setup-node@v6
-        if: steps.gate.outputs.build == 'true'
         with:
           node-version: ${{ env.NODE_VERSION_24 }}
 
       - uses: actions/cache@v5
         id: cache
-        if: steps.gate.outputs.build == 'true'
         with:
           path: |
             node_modules
@@ -99,94 +71,46 @@ jobs:
             apps/*/node_modules
           key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }}
 
-      - if: steps.gate.outputs.build == 'true' && steps.cache.outputs.cache-hit != 'true'
+      - if: steps.cache.outputs.cache-hit != 'true'
         run: pnpm install --frozen-lockfile
 
       - name: Get CLI version
         id: version
-        if: steps.gate.outputs.build == 'true'
         run: echo "version=$(node -p 'require("./packages/cli/package.json").version')" >> "$GITHUB_OUTPUT"
 
       - name: Generate docs content
-        if: steps.gate.outputs.build == 'true'
         run: pnpm run generate:schema && pnpm run generate:docs
 
       - name: Build Docs for Preview
-        if: steps.gate.outputs.build == 'true'
         working-directory: apps/cli-docs
         env:
-          DOCS_BASE_PATH: ${{ github.event_name == 'push'
-            && '/_preview/pr-main'
-            || format('/_preview/pr-{0}', github.event.pull_request.number) }}
           PUBLIC_SENTRY_ENVIRONMENT: staging
           SENTRY_RELEASE: ${{ steps.version.outputs.version }}
           PUBLIC_SENTRY_RELEASE: ${{ steps.version.outputs.version }}
-        run: |
-          pnpm install --frozen-lockfile
-          pnpm run build
+        run: pnpm run build
 
-      - name: Inject debug IDs and upload sourcemaps
-        if: steps.gate.outputs.build == 'true' && env.SENTRY_AUTH_TOKEN != ''
-        env:
-          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
-          SENTRY_ORG: sentry
-          SENTRY_PROJECT: cli-website
-        run: |
-          pnpm run cli sourcemap inject "$GITHUB_WORKSPACE/apps/cli-docs/dist/"
-          pnpm run cli sourcemap upload "$GITHUB_WORKSPACE/apps/cli-docs/dist/" \
-            --release "${{ steps.version.outputs.version }}" \
-            --url-prefix "~/"
-
-      # Remove .map files — uploaded to Sentry but shouldn't be deployed.
+      # Remove .map files — sourcemaps aren't uploaded for previews and
+      # shouldn't ship in the preview deployment.
       - name: Remove sourcemaps from output
-        if: steps.gate.outputs.build == 'true'
         run: find apps/cli-docs/dist -name '*.map' -delete
 
-      - name: Ensure .nojekyll at gh-pages root
-        # Runs whenever we deploy (build or a 'closed' cleanup), but never for
-        # fork PRs which lack write access to the base repo's gh-pages branch.
+      - name: Upload Cloudflare preview
+        id: preview
         if: steps.gate.outputs.deploy == 'true'
-        run: |
-          git config user.name "github-actions[bot]"
-          git config user.email "github-actions[bot]@users.noreply.github.com"
-          
-          # Try to fetch the gh-pages branch
-          if git fetch origin gh-pages:gh-pages 2>/dev/null; then
-            # Branch exists remotely, check if .nojekyll is present
-            if git show gh-pages:.nojekyll &>/dev/null; then
-              echo ".nojekyll already exists at gh-pages root"
-            else
-              echo "Adding .nojekyll to existing gh-pages branch"
-              git checkout gh-pages
-              touch .nojekyll
-              git add .nojekyll
-              git commit -m "Add .nojekyll to disable Jekyll processing"
-              git push origin gh-pages
-              git checkout -
-            fi
-          else
-            # Branch doesn't exist, create it as an orphan branch
-            echo "Creating gh-pages branch with .nojekyll"
-            git checkout --orphan gh-pages
-            git rm -rf .
-            touch .nojekyll
-            git add .nojekyll
-            git commit -m "Initialize gh-pages with .nojekyll"
-            git push origin gh-pages
-            git checkout -
-          fi
-
-      - name: Deploy Preview
-        # Deploys on build; on a 'closed' event `auto` removes the preview
-        # instead — this is what keeps gh-pages from accumulating stale
-        # previews. Skipped for fork PRs (no write access).
-        if: steps.gate.outputs.deploy == 'true'
-        uses: rossjrw/pr-preview-action@v1
+        uses: cloudflare/wrangler-action@v4
+        with:
+          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+          workingDirectory: apps/cli-docs
+          # Uses the workspace-pinned wrangler (apps/cli-docs devDependency)
+          # rather than letting the action install its own copy.
+          wranglerVersion: "4"
+          command: versions upload
+
+      - name: Comment preview URL
+        if: steps.gate.outputs.deploy == 'true' && github.event_name == 'pull_request'
+        uses: marocchino/sticky-pull-request-comment@v2
         with:
-          source-dir: apps/cli-docs/dist/
-          preview-branch: gh-pages
-          umbrella-dir: _preview
-          pages-base-url: cli.sentry.dev
-          action: ${{ github.event_name == 'push' && 'deploy' || 'auto' }}
-          pr-number: ${{ github.event_name == 'push' && 'main' || github.event.pull_request.number }}
-          comment: ${{ github.event_name != 'push' }}
+          header: docs-preview
+          message: |
+            Docs preview for this PR: ${{ steps.preview.outputs.deployment-url }}

@BYK

BYK commented Aug 3, 2026

Copy link
Copy Markdown
Member

Jared, we're supposed to deploy the site using Craft again: https://craft.sentry.dev/targets/cloudflare/

Per BYK, the docs site should ship through Craft's cloudflare target
(deployType: worker) rather than a standalone GitHub Actions deploy —
same release-gated model the old gh-pages target used. Adds the
cloudflare artifact + target; CI packages the worker as cloudflare.zip.
Comment thread .craft.yml
Comment thread apps/cli-docs/wrangler.jsonc Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit fc3a67c. Configure here.

Comment thread .craft.yml Outdated
BYK: the Cloudflare worker is named just `cli` (cli.getsentry.workers.dev),
not sentry-cli-docs. Also drop `workingDir` from the craft cloudflare target
and package cloudflare.zip with wrangler.jsonc + dist/ at the archive root, so
Craft deploys from where wrangler.jsonc lives after extraction (Craft flattens
a single wrapping dir, which would have broken a workingDir: apps/cli-docs).
@jared-outpost

jared-outpost Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Updated the workflow patch to match BYK's Craft-based approach. Two changes since the last patch: dropped the standalone docs-deploy.yml (release deploy now runs through Craft's cloudflare worker target), and ci.yml's build-docs job now packages the worker as cloudflare.zip instead of gh-pages.zip.

.craft.yml is already on the branch (a49dfb1) with the cloudflare target — no maintainer action needed there. The previous patch comment (5171545134) is superseded by this one.

The jared token still lacks workflows permission, so a maintainer needs to apply the .github/workflows/* changes below. Verified: ci.diff applies cleanly, docs build + wrangler deploy --dry-run pass, all YAML parses.

Workflow patch to apply (needs `workflows` permission)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -758,15 +758,19 @@ jobs:
       # be deployed to production.
       - name: Remove sourcemaps from output
         run: find apps/cli-docs/dist -name '*.map' -delete
+      # Package the built site plus its wrangler config as cloudflare.zip so
+      # Craft's cloudflare target (deployType: worker) can run `wrangler deploy`
+      # from the extracted artifact on release. Zip from inside apps/cli-docs so
+      # wrangler.jsonc and dist/ sit at the archive root (no wrapping dir), which
+      # is where wrangler expects to run.
       - name: Package Docs
         run: |
-          cp .nojekyll apps/cli-docs/dist/
-          cd apps/cli-docs/dist && zip -r "$GITHUB_WORKSPACE/gh-pages.zip" .
+          cd apps/cli-docs && zip -r "$GITHUB_WORKSPACE/cloudflare.zip" wrangler.jsonc dist
       - name: Upload docs artifact
         uses: actions/upload-artifact@v7
         with:
-          name: gh-pages
-          path: gh-pages.zip
+          name: cloudflare
+          path: cloudflare.zip
 
   ci-status:
     name: CI Status

diff --git a/.github/workflows/cleanup-doc-previews.yml b/.github/workflows/cleanup-doc-previews.yml
deleted file mode 100644
index 6a59b85..0000000
--- a/.github/workflows/cleanup-doc-previews.yml
+++ /dev/null
@@ -1,99 +0,0 @@
-name: Cleanup Doc Previews
-
-# Safety net for the on-close cleanup in docs-preview.yml: sweeps gh-pages for
-# `_preview/pr-<n>` directories whose PR is no longer open and removes them.
-# Catches previews that slipped through (e.g. closed while the preview workflow
-# was failing) so gh-pages never bloats past GitHub Pages' build limits again.
-
-on:
-  schedule:
-    # Every Sunday at 05:30 UTC (before the nightly-tag cleanup).
-    - cron: "30 5 * * 0"
-  workflow_dispatch:
-
-permissions:
-  contents: write
-  pull-requests: read
-
-concurrency:
-  # Serialize cleanup runs with each other. Races against docs-preview deploys
-  # are handled by the push retry loop below (a non-fast-forward push is
-  # rejected, never applied, so it can't corrupt gh-pages).
-  group: gh-pages-cleanup
-  cancel-in-progress: false
-
-jobs:
-  prune:
-    name: Prune previews for closed PRs
-    runs-on: ubuntu-latest
-    steps:
-      - name: Prune stale previews
-        env:
-          GH_TOKEN: ${{ github.token }}
-          REPO: ${{ github.repository }}
-        run: |
-          set -euo pipefail
-
-          # Blobless, no-checkout clone so we never download the (large) preview
-          # file contents — we only need trees to rewrite the index.
-          git clone --filter=blob:none --no-checkout --single-branch \
-            --branch gh-pages \
-            "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" ghp
-          cd ghp
-          git config user.name "github-actions[bot]"
-          git config user.email "github-actions[bot]@users.noreply.github.com"
-
-          # Retry loop: another gh-pages writer (a docs-preview deploy) may land
-          # between our read and push. A non-fast-forward push is REJECTED (never
-          # applied, so no corruption); we re-sync onto the new tip and re-prune.
-          max_attempts=3
-          attempt=0
-          while :; do
-            attempt=$((attempt + 1))
-            git read-tree HEAD
-
-            removed=0
-            # One entry per top-level _preview/<dir>.
-            for dir in $(git ls-files _preview/ | sed -E 's#(_preview/[^/]+)/.*#\1#' | sort -u); do
-              name="${dir#_preview/}"
-              # Keep the production/main preview.
-              if [ "$name" = "pr-main" ]; then
-                continue
-              fi
-              num="${name#pr-}"
-              # Skip anything that isn't a pr-<number> directory.
-              case "$num" in
-                ''|*[!0-9]*) continue ;;
-              esac
-              state=$(gh pr view "$num" --repo "$REPO" --json state --jq .state 2>/dev/null || echo "UNKNOWN")
-              # Only prune when the PR is CONFIRMED closed/merged. Keep the
-              # preview on OPEN and on any API error/unknown state so a transient
-              # glitch never deletes a live preview.
-              case "$state" in
-                CLOSED|MERGED)
-                  echo "Removing $dir (PR #$num state=$state)"
-                  git rm -r -q --cached "$dir"
-                  removed=$((removed + 1))
-                  ;;
-              esac
-            done
-
-            if [ "$removed" -eq 0 ]; then
-              echo "No stale previews to prune."
-              exit 0
-            fi
-
-            git commit -q -m "chore(docs): prune ${removed} preview(s) for closed PRs [skip ci]"
-            if git push origin gh-pages; then
-              echo "Pruned ${removed} stale preview(s)."
-              exit 0
-            fi
-
-            if [ "$attempt" -ge "$max_attempts" ]; then
-              echo "gh-pages push kept failing after ${max_attempts} attempts; retrying next run." >&2
-              exit 1
-            fi
-            echo "gh-pages moved underneath us; re-syncing and retrying (attempt ${attempt})…"
-            git fetch --quiet origin gh-pages
-            git reset --soft FETCH_HEAD
-          done
diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml
index cf83134..71c7606 100644
--- a/.github/workflows/docs-preview.yml
+++ b/.github/workflows/docs-preview.yml
@@ -1,5 +1,10 @@
 name: Docs Preview
 
+# Builds the CLI docs on every docs-touching PR and uploads a Cloudflare Workers
+# preview version, then comments the preview URL on the PR. Replaces the old
+# gh-pages + rossjrw/pr-preview-action flow: Cloudflare version previews are
+# isolated per upload and expire on their own, so there is no umbrella branch to
+# clean up and no `closed`-event handling.
 on:
   push:
     branches: [main]
@@ -11,15 +16,16 @@ on:
       - 'packages/cli/install'
       - '.github/workflows/docs-preview.yml'
   pull_request:
-    # No paths filter here: the 'closed' event must ALWAYS fire so
-    # pr-preview-action can remove the deployed preview. A paths-filter step
-    # inside the job (below) skips the build/deploy for non-docs PRs. Without
-    # this, closed PRs never clean up and gh-pages bloats until GitHub Pages
-    # exceeds its build limits and starts failing.
-    types: [opened, reopened, synchronize, closed]
+    paths:
+      - 'apps/cli-docs/**'
+      - 'packages/cli/src/**'
+      - 'packages/cli/script/generate-command-docs.ts'
+      - 'packages/cli/script/generate-skill.ts'
+      - 'packages/cli/install'
+      - '.github/workflows/docs-preview.yml'
 
 permissions:
-  contents: write
+  contents: read
   pull-requests: write
 
 concurrency:
@@ -35,63 +41,29 @@ env:
 
 jobs:
   preview:
+    name: Build and upload docs preview
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v6
 
-      # Detect whether this PR actually touches docs. Skipped on 'closed' (and
-      # on push), where the gate below decides what to do without a diff.
-      - name: Check for docs changes
-        id: filter
-        if: github.event_name == 'pull_request' && github.event.action != 'closed'
-        uses: dorny/paths-filter@v4
-        with:
-          filters: |
-            docs:
-              - 'apps/cli-docs/**'
-              - 'packages/cli/src/**'
-              - 'packages/cli/script/generate-command-docs.ts'
-              - 'packages/cli/script/generate-skill.ts'
-              - 'packages/cli/install'
-              - '.github/workflows/docs-preview.yml'
-
-      # Central gate. `build` = produce the site (push, or a docs PR being
-      # opened/updated). `deploy` = publish/remove on gh-pages (build, or any
-      # 'closed' event to clean up), but never for fork PRs which lack write
-      # access. Fork PRs still build to surface compilation errors.
-      - name: Compute gates
+      # Fork PRs have no access to the Cloudflare secrets, so they build only
+      # (to surface compilation errors) and skip the upload/comment steps.
+      - name: Compute deploy gate
         id: gate
         env:
-          EVENT: ${{ github.event_name }}
-          ACTION: ${{ github.event.action }}
-          DOCS_CHANGED: ${{ steps.filter.outputs.docs }}
           IS_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
-        run: |
-          build=false
-          deploy=false
-          if [ "$EVENT" = "push" ] || { [ "$ACTION" != "closed" ] && [ "$DOCS_CHANGED" = "true" ]; }; then
-            build=true
-          fi
-          if { [ "$build" = "true" ] || [ "$ACTION" = "closed" ]; } && [ "$IS_FORK" != "true" ]; then
-            deploy=true
-          fi
-          echo "build=$build" >> "$GITHUB_OUTPUT"
-          echo "deploy=$deploy" >> "$GITHUB_OUTPUT"
+        run: echo "deploy=$([ "$IS_FORK" = "true" ] && echo false || echo true)" >> "$GITHUB_OUTPUT"
 
       - uses: pnpm/action-setup@v4
-        if: steps.gate.outputs.build == 'true'
 
       # Astro 6 requires Node >= 22.12. Pin an exact patched version (see
-      # NODE_VERSION_24) so the docs build doesn't rely on the runner image's
-      # default.
+      # NODE_VERSION_24) so the docs build doesn't rely on the runner default.
       - uses: actions/setup-node@v6
-        if: steps.gate.outputs.build == 'true'
         with:
           node-version: ${{ env.NODE_VERSION_24 }}
 
       - uses: actions/cache@v5
         id: cache
-        if: steps.gate.outputs.build == 'true'
         with:
           path: |
             node_modules
@@ -99,94 +71,46 @@ jobs:
             apps/*/node_modules
           key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }}
 
-      - if: steps.gate.outputs.build == 'true' && steps.cache.outputs.cache-hit != 'true'
+      - if: steps.cache.outputs.cache-hit != 'true'
         run: pnpm install --frozen-lockfile
 
       - name: Get CLI version
         id: version
-        if: steps.gate.outputs.build == 'true'
         run: echo "version=$(node -p 'require("./packages/cli/package.json").version')" >> "$GITHUB_OUTPUT"
 
       - name: Generate docs content
-        if: steps.gate.outputs.build == 'true'
         run: pnpm run generate:schema && pnpm run generate:docs
 
       - name: Build Docs for Preview
-        if: steps.gate.outputs.build == 'true'
         working-directory: apps/cli-docs
         env:
-          DOCS_BASE_PATH: ${{ github.event_name == 'push'
-            && '/_preview/pr-main'
-            || format('/_preview/pr-{0}', github.event.pull_request.number) }}
           PUBLIC_SENTRY_ENVIRONMENT: staging
           SENTRY_RELEASE: ${{ steps.version.outputs.version }}
           PUBLIC_SENTRY_RELEASE: ${{ steps.version.outputs.version }}
-        run: |
-          pnpm install --frozen-lockfile
-          pnpm run build
+        run: pnpm run build
 
-      - name: Inject debug IDs and upload sourcemaps
-        if: steps.gate.outputs.build == 'true' && env.SENTRY_AUTH_TOKEN != ''
-        env:
-          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
-          SENTRY_ORG: sentry
-          SENTRY_PROJECT: cli-website
-        run: |
-          pnpm run cli sourcemap inject "$GITHUB_WORKSPACE/apps/cli-docs/dist/"
-          pnpm run cli sourcemap upload "$GITHUB_WORKSPACE/apps/cli-docs/dist/" \
-            --release "${{ steps.version.outputs.version }}" \
-            --url-prefix "~/"
-
-      # Remove .map files — uploaded to Sentry but shouldn't be deployed.
+      # Remove .map files — sourcemaps aren't uploaded for previews and
+      # shouldn't ship in the preview deployment.
       - name: Remove sourcemaps from output
-        if: steps.gate.outputs.build == 'true'
         run: find apps/cli-docs/dist -name '*.map' -delete
 
-      - name: Ensure .nojekyll at gh-pages root
-        # Runs whenever we deploy (build or a 'closed' cleanup), but never for
-        # fork PRs which lack write access to the base repo's gh-pages branch.
+      - name: Upload Cloudflare preview
+        id: preview
         if: steps.gate.outputs.deploy == 'true'
-        run: |
-          git config user.name "github-actions[bot]"
-          git config user.email "github-actions[bot]@users.noreply.github.com"
-          
-          # Try to fetch the gh-pages branch
-          if git fetch origin gh-pages:gh-pages 2>/dev/null; then
-            # Branch exists remotely, check if .nojekyll is present
-            if git show gh-pages:.nojekyll &>/dev/null; then
-              echo ".nojekyll already exists at gh-pages root"
-            else
-              echo "Adding .nojekyll to existing gh-pages branch"
-              git checkout gh-pages
-              touch .nojekyll
-              git add .nojekyll
-              git commit -m "Add .nojekyll to disable Jekyll processing"
-              git push origin gh-pages
-              git checkout -
-            fi
-          else
-            # Branch doesn't exist, create it as an orphan branch
-            echo "Creating gh-pages branch with .nojekyll"
-            git checkout --orphan gh-pages
-            git rm -rf .
-            touch .nojekyll
-            git add .nojekyll
-            git commit -m "Initialize gh-pages with .nojekyll"
-            git push origin gh-pages
-            git checkout -
-          fi
-
-      - name: Deploy Preview
-        # Deploys on build; on a 'closed' event `auto` removes the preview
-        # instead — this is what keeps gh-pages from accumulating stale
-        # previews. Skipped for fork PRs (no write access).
-        if: steps.gate.outputs.deploy == 'true'
-        uses: rossjrw/pr-preview-action@v1
+        uses: cloudflare/wrangler-action@v4
+        with:
+          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+          workingDirectory: apps/cli-docs
+          # Uses the workspace-pinned wrangler (apps/cli-docs devDependency)
+          # rather than letting the action install its own copy.
+          wranglerVersion: "4"
+          command: versions upload
+
+      - name: Comment preview URL
+        if: steps.gate.outputs.deploy == 'true' && github.event_name == 'pull_request'
+        uses: marocchino/sticky-pull-request-comment@v2
         with:
-          source-dir: apps/cli-docs/dist/
-          preview-branch: gh-pages
-          umbrella-dir: _preview
-          pages-base-url: cli.sentry.dev
-          action: ${{ github.event_name == 'push' && 'deploy' || 'auto' }}
-          pr-number: ${{ github.event_name == 'push' && 'main' || github.event.pull_request.number }}
-          comment: ${{ github.event_name != 'push' }}
+          header: docs-preview
+          message: |
+            Docs preview for this PR: ${{ steps.preview.outputs.deployment-url }}

@jared-outpost
jared-outpost Bot requested a review from BYK August 3, 2026 20:57
@jared-outpost

jared-outpost Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

fix-ci: attempt 1 — the E2E failure is infra, not code. The job's "Download Linux binary" step hit digest-mismatch, so the CLI bundle never got built and library.test.ts bailed with "Bundle not built." This sha (a49dfb1) already passed a Build run earlier, and my diff only touches .craft.yml + apps/cli-docs/ + the undici pin — nothing under packages/cli. Re-running the failed jobs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jared Trigger the Jared agent to work on stuff risk: high PR risk score: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant