feat(docs): deploy CLI docs to Cloudflare Workers instead of GitHub Pages - #1354
feat(docs): deploy CLI docs to Cloudflare Workers instead of GitHub Pages#1354jared-outpost[bot] wants to merge 5 commits into
Conversation
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
|
|
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 + |
|
…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.
|
Applied the The 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 }}
|
|
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
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).
|
Updated the workflow patch to match BYK's Craft-based approach. Two changes since the last patch: dropped the standalone
The jared token still lacks 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 }} |
|
fix-ci: attempt 1 — the E2E failure is infra, not code. The job's "Download Linux binary" step hit |

Moves the
cli.sentry.devdocs site off GitHub Pages onto a release-gated Cloudflare Worker (static assets), and replaces thegh-pagesPR-preview flow with Cloudflare version previews. Per BYK, this uses a Worker with static assets (Pages is deprecated); DNS cutover forcli.sentry.devis handled separately.What's in this branch (pushable)
apps/cli-docs/wrangler.jsonc— assets-only Worker serving Astro'sdist/(not_found_handling: 404-page)./installships asdist/installand stays reachable atcli.sentry.dev/install.apps/cli-docs/package.json—wranglerdevDependency +deployscript; lockfile updated.The jared GitHub App token lacks the
workflowspermission, 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 withworkflowspermission needs to add it. Verified locally: docs build succeeds withDOCS_BASE_PATH=/,wrangler deploy --dry-runreads all 333 assets, all workflow YAML parses,check:stale-refs/check:depspass.Changes in that diff:
docs-deploy.yml(new) — build + Sentry sourcemap upload +wrangler deploy, gated onrelease: published(skips prereleases), keeping the "docs tied to the published release" guarantee the old Craftgh-pagestarget gave.docs-preview.yml— uploads a Cloudflare preview version (wrangler versions upload) and comments its URL; dropsrossjrw/pr-preview-action, the gh-pages umbrella,.nojekylljuggling, andclosed-event cleanup..craft.yml— removes thegh-pagestarget + artifact.ci.yml—build-docsno longer packages/uploads thegh-pagesartifact (build + sourcemap upload unchanged).cleanup-doc-previews.yml,public/CNAME, and the orphaned.nojekyll.Notes for review
CLOUDFLARE_API_TOKEN+CLOUDFLARE_ACCOUNT_IDsecrets in theproductionenvironment (already present on the MCP side per the migration plan).versions uploadpreview needs the Worker to exist; it's created on the first production release deploy (or a one-offwrangler deploy). Bootstrap alongside the DNS cutover.Refs #1245