Skip to content

Commit 03e657b

Browse files
authored
ci(docs): clean up PR previews on close + scheduled safety-net sweep (#1202)
## Problem The `sentry docs` PR-preview link was returning **404** for new PRs (e.g. #1201), while older previews still worked. Root cause: - `docs-preview.yml` triggered on `pull_request` `[opened, synchronize, reopened]` — **never `closed`** — so `rossjrw/pr-preview-action`'s auto-cleanup never ran. - **46 stale previews** (each a full copy of the docs site) accumulated in `gh-pages`, pushing the **GitHub Pages build over its size/time limits**. Builds started failing intermittently (`Page build failed`), so newly-deployed previews 404'd while previously-published ones kept serving. ## Fix (pattern ported from `getsentry/loreai`) **`docs-preview.yml`** - Add `closed` to the `pull_request` types and **remove the `pull_request` paths filter** — a `closed` event must always fire so the preview can be removed. - A `dorny/paths-filter` step + a central **`gate`** step decide: - `build` → push, or a docs PR being opened/updated (fork PRs still build to surface compile errors). - `deploy` → build, **or any `closed` event** (cleanup), never for fork PRs. - On close, `pr-preview-action` `action: auto` **removes** the preview — the actual cleanup that was missing. **`cleanup-doc-previews.yml` (new)** - Weekly (`cron`) + `workflow_dispatch` **safety net**: sweeps `gh-pages` for `_preview/pr-<n>` dirs whose PR is no longer `OPEN` and removes them. Uses a **blobless, no-checkout clone + index rewrite**, so it never downloads the (large) preview file contents. Catches anything the on-close path misses (e.g. PRs closed while the workflow was broken — exactly how we got here). ## Already done out-of-band The 46 already-stale previews were pruned from `gh-pages` directly (blobless index rewrite) to **unblock the failing Pages build immediately** — previews now serve again (verified `pr-1201` → 200). This PR prevents recurrence. ## Validation - Both workflows: valid YAML; `actionlint` clean (only a pre-existing SC2088 on the unchanged `--url-prefix "~/"` line, a deliberate Sentry URL prefix). - Gate logic walked through for push / docs-PR open+sync / non-docs PR / close / fork / fork-close.
1 parent 108d24d commit 03e657b

2 files changed

Lines changed: 160 additions & 15 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: Cleanup Doc Previews
2+
3+
# Safety net for the on-close cleanup in docs-preview.yml: sweeps gh-pages for
4+
# `_preview/pr-<n>` directories whose PR is no longer open and removes them.
5+
# Catches previews that slipped through (e.g. closed while the preview workflow
6+
# was failing) so gh-pages never bloats past GitHub Pages' build limits again.
7+
8+
on:
9+
schedule:
10+
# Every Sunday at 05:30 UTC (before the nightly-tag cleanup).
11+
- cron: "30 5 * * 0"
12+
workflow_dispatch:
13+
14+
permissions:
15+
contents: write
16+
pull-requests: read
17+
18+
concurrency:
19+
# Serialize cleanup runs with each other. Races against docs-preview deploys
20+
# are handled by the push retry loop below (a non-fast-forward push is
21+
# rejected, never applied, so it can't corrupt gh-pages).
22+
group: gh-pages-cleanup
23+
cancel-in-progress: false
24+
25+
jobs:
26+
prune:
27+
name: Prune previews for closed PRs
28+
runs-on: ubuntu-latest
29+
steps:
30+
- name: Prune stale previews
31+
env:
32+
GH_TOKEN: ${{ github.token }}
33+
REPO: ${{ github.repository }}
34+
run: |
35+
set -euo pipefail
36+
37+
# Blobless, no-checkout clone so we never download the (large) preview
38+
# file contents — we only need trees to rewrite the index.
39+
git clone --filter=blob:none --no-checkout --single-branch \
40+
--branch gh-pages \
41+
"https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" ghp
42+
cd ghp
43+
git config user.name "github-actions[bot]"
44+
git config user.email "github-actions[bot]@users.noreply.github.com"
45+
46+
# Retry loop: another gh-pages writer (a docs-preview deploy) may land
47+
# between our read and push. A non-fast-forward push is REJECTED (never
48+
# applied, so no corruption); we re-sync onto the new tip and re-prune.
49+
max_attempts=3
50+
attempt=0
51+
while :; do
52+
attempt=$((attempt + 1))
53+
git read-tree HEAD
54+
55+
removed=0
56+
# One entry per top-level _preview/<dir>.
57+
for dir in $(git ls-files _preview/ | sed -E 's#(_preview/[^/]+)/.*#\1#' | sort -u); do
58+
name="${dir#_preview/}"
59+
# Keep the production/main preview.
60+
if [ "$name" = "pr-main" ]; then
61+
continue
62+
fi
63+
num="${name#pr-}"
64+
# Skip anything that isn't a pr-<number> directory.
65+
case "$num" in
66+
''|*[!0-9]*) continue ;;
67+
esac
68+
state=$(gh pr view "$num" --repo "$REPO" --json state --jq .state 2>/dev/null || echo "UNKNOWN")
69+
# Only prune when the PR is CONFIRMED closed/merged. Keep the
70+
# preview on OPEN and on any API error/unknown state so a transient
71+
# glitch never deletes a live preview.
72+
case "$state" in
73+
CLOSED|MERGED)
74+
echo "Removing $dir (PR #$num state=$state)"
75+
git rm -r -q --cached "$dir"
76+
removed=$((removed + 1))
77+
;;
78+
esac
79+
done
80+
81+
if [ "$removed" -eq 0 ]; then
82+
echo "No stale previews to prune."
83+
exit 0
84+
fi
85+
86+
git commit -q -m "chore(docs): prune ${removed} preview(s) for closed PRs [skip ci]"
87+
if git push origin gh-pages; then
88+
echo "Pruned ${removed} stale preview(s)."
89+
exit 0
90+
fi
91+
92+
if [ "$attempt" -ge "$max_attempts" ]; then
93+
echo "gh-pages push kept failing after ${max_attempts} attempts; retrying next run." >&2
94+
exit 1
95+
fi
96+
echo "gh-pages moved underneath us; re-syncing and retrying (attempt ${attempt})…"
97+
git fetch --quiet origin gh-pages
98+
git reset --soft FETCH_HEAD
99+
done

.github/workflows/docs-preview.yml

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@ on:
1111
- 'install'
1212
- '.github/workflows/docs-preview.yml'
1313
pull_request:
14-
paths:
15-
- 'docs/**'
16-
- 'src/**'
17-
- 'script/generate-command-docs.ts'
18-
- 'script/generate-skill.ts'
19-
- 'install'
20-
- '.github/workflows/docs-preview.yml'
14+
# No paths filter here: the 'closed' event must ALWAYS fire so
15+
# pr-preview-action can remove the deployed preview. A paths-filter step
16+
# inside the job (below) skips the build/deploy for non-docs PRs. Without
17+
# this, closed PRs never clean up and gh-pages bloats until GitHub Pages
18+
# exceeds its build limits and starts failing.
19+
types: [opened, reopened, synchronize, closed]
2120

2221
permissions:
2322
contents: write
@@ -40,32 +39,77 @@ jobs:
4039
steps:
4140
- uses: actions/checkout@v6
4241

42+
# Detect whether this PR actually touches docs. Skipped on 'closed' (and
43+
# on push), where the gate below decides what to do without a diff.
44+
- name: Check for docs changes
45+
id: filter
46+
if: github.event_name == 'pull_request' && github.event.action != 'closed'
47+
uses: dorny/paths-filter@v4
48+
with:
49+
filters: |
50+
docs:
51+
- 'docs/**'
52+
- 'src/**'
53+
- 'script/generate-command-docs.ts'
54+
- 'script/generate-skill.ts'
55+
- 'install'
56+
- '.github/workflows/docs-preview.yml'
57+
58+
# Central gate. `build` = produce the site (push, or a docs PR being
59+
# opened/updated). `deploy` = publish/remove on gh-pages (build, or any
60+
# 'closed' event to clean up), but never for fork PRs which lack write
61+
# access. Fork PRs still build to surface compilation errors.
62+
- name: Compute gates
63+
id: gate
64+
env:
65+
EVENT: ${{ github.event_name }}
66+
ACTION: ${{ github.event.action }}
67+
DOCS_CHANGED: ${{ steps.filter.outputs.docs }}
68+
IS_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
69+
run: |
70+
build=false
71+
deploy=false
72+
if [ "$EVENT" = "push" ] || { [ "$ACTION" != "closed" ] && [ "$DOCS_CHANGED" = "true" ]; }; then
73+
build=true
74+
fi
75+
if { [ "$build" = "true" ] || [ "$ACTION" = "closed" ]; } && [ "$IS_FORK" != "true" ]; then
76+
deploy=true
77+
fi
78+
echo "build=$build" >> "$GITHUB_OUTPUT"
79+
echo "deploy=$deploy" >> "$GITHUB_OUTPUT"
80+
4381
- uses: pnpm/action-setup@v4
82+
if: steps.gate.outputs.build == 'true'
4483

4584
# Astro 6 requires Node >= 22.12. Pin an exact patched version (see
4685
# NODE_VERSION_24) so the docs build doesn't rely on the runner image's
4786
# default.
4887
- uses: actions/setup-node@v6
88+
if: steps.gate.outputs.build == 'true'
4989
with:
5090
node-version: ${{ env.NODE_VERSION_24 }}
5191

5292
- uses: actions/cache@v5
5393
id: cache
94+
if: steps.gate.outputs.build == 'true'
5495
with:
5596
path: node_modules
5697
key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'patches/**') }}
5798

58-
- if: steps.cache.outputs.cache-hit != 'true'
99+
- if: steps.gate.outputs.build == 'true' && steps.cache.outputs.cache-hit != 'true'
59100
run: pnpm install --frozen-lockfile
60101

61102
- name: Get CLI version
62103
id: version
104+
if: steps.gate.outputs.build == 'true'
63105
run: echo "version=$(node -p 'require("./package.json").version')" >> "$GITHUB_OUTPUT"
64106

65107
- name: Generate docs content
108+
if: steps.gate.outputs.build == 'true'
66109
run: pnpm run generate:schema && pnpm run generate:docs
67110

68111
- name: Build Docs for Preview
112+
if: steps.gate.outputs.build == 'true'
69113
working-directory: docs
70114
env:
71115
DOCS_BASE_PATH: ${{ github.event_name == 'push'
@@ -79,7 +123,7 @@ jobs:
79123
pnpm run build
80124
81125
- name: Inject debug IDs and upload sourcemaps
82-
if: env.SENTRY_AUTH_TOKEN != ''
126+
if: steps.gate.outputs.build == 'true' && env.SENTRY_AUTH_TOKEN != ''
83127
env:
84128
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
85129
SENTRY_ORG: sentry
@@ -92,13 +136,13 @@ jobs:
92136
93137
# Remove .map files — uploaded to Sentry but shouldn't be deployed.
94138
- name: Remove sourcemaps from output
139+
if: steps.gate.outputs.build == 'true'
95140
run: find docs/dist -name '*.map' -delete
96141

97142
- name: Ensure .nojekyll at gh-pages root
98-
# Fork PRs can't push to the base repo (GITHUB_TOKEN is read-only on
99-
# pull_request from forks). Skip the deploy but still run the build
100-
# above so docs compilation errors surface.
101-
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request'
143+
# Runs whenever we deploy (build or a 'closed' cleanup), but never for
144+
# fork PRs which lack write access to the base repo's gh-pages branch.
145+
if: steps.gate.outputs.deploy == 'true'
102146
run: |
103147
git config user.name "github-actions[bot]"
104148
git config user.email "github-actions[bot]@users.noreply.github.com"
@@ -130,8 +174,10 @@ jobs:
130174
fi
131175
132176
- name: Deploy Preview
133-
# Same fork-PR guard as the .nojekyll step above.
134-
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request'
177+
# Deploys on build; on a 'closed' event `auto` removes the preview
178+
# instead — this is what keeps gh-pages from accumulating stale
179+
# previews. Skipped for fork PRs (no write access).
180+
if: steps.gate.outputs.deploy == 'true'
135181
uses: rossjrw/pr-preview-action@v1
136182
with:
137183
source-dir: docs/dist/

0 commit comments

Comments
 (0)