From 5b5bba78d79802cb44de6d4d19afcbac3d60d629 Mon Sep 17 00:00:00 2001 From: Jameson Nyeholt Date: Mon, 4 May 2026 16:50:47 -1000 Subject: [PATCH 1/5] ci(pages): isolate PR previews under gh-pages subpaths --- .../workflows/cleanup-showcase-preview.yml | 45 ++++++++++ .github/workflows/deploy-showcase-pages.yml | 90 +++++-------------- 2 files changed, 66 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/cleanup-showcase-preview.yml diff --git a/.github/workflows/cleanup-showcase-preview.yml b/.github/workflows/cleanup-showcase-preview.yml new file mode 100644 index 0000000..4fc25f9 --- /dev/null +++ b/.github/workflows/cleanup-showcase-preview.yml @@ -0,0 +1,45 @@ +name: Cleanup Showcase PR Preview + +on: + pull_request: + types: + - closed + +permissions: + contents: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Remove preview directory from gh-pages + run: | + if ! git ls-remote --exit-code --heads origin gh-pages > /dev/null 2>&1; then + echo "gh-pages does not exist; nothing to clean." + exit 0 + fi + + git fetch origin gh-pages + git worktree add --detach ./gh-pages-out FETCH_HEAD + + preview_dir="gh-pages-out/previews/pr-${{ github.event.pull_request.number }}" + if [ ! -d "$preview_dir" ]; then + echo "Preview directory does not exist; nothing to clean." + exit 0 + fi + + rm -rf "$preview_dir" + + cd gh-pages-out + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git add -A + if ! git diff --staged --quiet; then + git commit -m "Cleanup preview for PR #${{ github.event.pull_request.number }}" + git push --force origin HEAD:refs/heads/gh-pages + fi diff --git a/.github/workflows/deploy-showcase-pages.yml b/.github/workflows/deploy-showcase-pages.yml index fb6aecc..49547d8 100644 --- a/.github/workflows/deploy-showcase-pages.yml +++ b/.github/workflows/deploy-showcase-pages.yml @@ -1,9 +1,6 @@ name: Deploy Showcase to GitHub Pages -# Deploys the showcase by replacing the entire gh-pages branch on every run. -# Both main-branch pushes and PR builds land at the same root URL. -# The Pages source is configured automatically via the GitHub API — -# if the token lacks permission (e.g. Pages set to "GitHub Actions" source), -# a one-time manual change in Settings → Pages is required. +# Deploys main to the Pages root while keeping PR previews under previews/pr-/. +# This allows production and preview deployments to coexist on gh-pages. on: push: @@ -17,19 +14,22 @@ on: pull_request: branches: - main + paths: + - "showcase/**" + - "styles/**" + - "index.css" + - ".github/workflows/deploy-showcase-pages.yml" workflow_dispatch: # contents: write — push to gh-pages branch -# pages: write — configure Pages source via API # pull-requests: write — post/update preview comment permissions: contents: write - pages: write pull-requests: write concurrency: group: pages - cancel-in-progress: true + cancel-in-progress: false jobs: deploy: @@ -60,82 +60,34 @@ jobs: git worktree add --orphan -b gh-pages ./gh-pages-out fi - # Wipe and replace with the freshly-built site - rm -rf gh-pages-out/* gh-pages-out/.[!.]* - cp -R _site/. gh-pages-out/ + if [ "${{ github.event_name }}" = "pull_request" ]; then + preview_dir="gh-pages-out/previews/pr-${{ github.event.pull_request.number }}" + rm -rf "$preview_dir" + mkdir -p "$preview_dir" + cp -R _site/. "$preview_dir/" + else + # Replace root content but preserve previews so PR links remain valid. + find gh-pages-out -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'previews' -exec rm -rf {} + + cp -R _site/. gh-pages-out/ + fi cd gh-pages-out git add -A if ! git diff --staged --quiet; then - git commit -m "Deploy: ${{ github.sha }}" + git commit -m "Deploy (${{ github.event_name }}): ${{ github.sha }}" git push --force origin HEAD:refs/heads/gh-pages fi - - name: Ensure GitHub Pages serves from gh-pages branch - uses: actions/github-script@v7 - with: - script: | - let pagesReady = false; - try { - const { data: pages } = await github.rest.repos.getPages({ - owner: context.repo.owner, - repo: context.repo.repo, - }); - if (!pages.source || pages.source.branch !== 'gh-pages') { - await github.rest.repos.updateInformationAboutPagesSite({ - owner: context.repo.owner, - repo: context.repo.repo, - source: { branch: 'gh-pages', path: '/' }, - }); - console.log('Updated Pages source to gh-pages branch.'); - pagesReady = true; - } else { - pagesReady = true; - } - } catch (err) { - if (err.status === 404) { - try { - await github.rest.repos.createPagesSite({ - owner: context.repo.owner, - repo: context.repo.repo, - source: { branch: 'gh-pages', path: '/' }, - }); - console.log('Created Pages site from gh-pages branch.'); - pagesReady = true; - } catch (createErr) { - console.warn('Could not create Pages site:', createErr.message); - } - } else { - // "Resource not accessible by integration" — GITHUB_TOKEN cannot - // reconfigure Pages when it is already set to "GitHub Actions" source. - // The user must do this once manually in Settings → Pages. - console.warn('Could not configure Pages source:', err.message); - } - } - core.exportVariable('PAGES_READY', pagesReady ? 'true' : 'false'); - - name: Post PR preview comment if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | - const pagesReady = process.env.PAGES_READY === 'true'; const base = 'https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}'; - const previewUrl = `${base}/showcase/`; + const previewUrl = `${base}/previews/pr-${{ github.event.pull_request.number }}/showcase/`; const sha = '${{ github.sha }}'.substring(0, 7); - const setupNotice = pagesReady ? '' : - '\n\n> ⚠️ **One-time setup required** — GitHub Pages is currently configured to serve ' + - 'from GitHub Actions artifacts, not the `gh-pages` branch where previews are deployed.\n' + - '> \n' + - '> To enable previews:\n' + - '> 1. Go to **[Settings → Pages](https://github.com/${{ github.repository }}/settings/pages)**\n' + - '> 2. Under **Source**, select **"Deploy from a branch"**\n' + - '> 3. Choose branch **`gh-pages`** and folder **`/ (root)`**, then **Save**\n' + - '> \n' + - '> The preview URL will resolve once Pages is pointing at the `gh-pages` branch.'; - - const body = `🚀 **PR Preview deployed!**\n\n📖 [Open showcase preview](${previewUrl})${setupNotice}\n\n> ℹ️ This preview replaces the current live site. It will be overwritten by the next deployment (from any PR or main).\n\n_Run \`${{ github.run_id }}\` — commit \`${sha}\`_`; + const body = `🚀 **PR Preview deployed!**\n\n📖 [Open showcase preview](${previewUrl})\n\n> ℹ️ Production remains at root Pages URL. This preview is isolated to PR #${{ github.event.pull_request.number }}.\n\n_Run \`${{ github.run_id }}\` — commit \`${sha}\`_`; await github.rest.issues.createComment({ issue_number: context.issue.number, From 29bd5df69f18d6f5b11bf40bc19bebab90d4e6ec Mon Sep 17 00:00:00 2001 From: Jameson Nyeholt Date: Mon, 4 May 2026 16:56:57 -1000 Subject: [PATCH 2/5] ci(pages): publish PR deployment records for preview URLs --- .github/workflows/deploy-showcase-pages.yml | 65 +++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/.github/workflows/deploy-showcase-pages.yml b/.github/workflows/deploy-showcase-pages.yml index 49547d8..a0b2dc2 100644 --- a/.github/workflows/deploy-showcase-pages.yml +++ b/.github/workflows/deploy-showcase-pages.yml @@ -26,6 +26,7 @@ on: permissions: contents: write pull-requests: write + deployments: write concurrency: group: pages @@ -51,6 +52,24 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: Create PR deployment record + if: github.event_name == 'pull_request' + id: create_deployment + uses: actions/github-script@v7 + with: + script: | + const deployment = await github.rest.repos.createDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: context.payload.pull_request.head.sha, + environment: `preview/pr-${context.payload.pull_request.number}`, + description: `Showcase preview for PR #${context.payload.pull_request.number}`, + auto_merge: false, + required_contexts: [] + }); + + core.setOutput('deployment_id', String(deployment.data.id)); + - name: Deploy to gh-pages (full replace) run: | if git ls-remote --exit-code --heads origin gh-pages > /dev/null 2>&1; then @@ -95,3 +114,49 @@ jobs: repo: context.repo.repo, body, }); + + - name: Mark PR deployment success + if: github.event_name == 'pull_request' && success() + uses: actions/github-script@v7 + with: + script: | + const deploymentId = Number('${{ steps.create_deployment.outputs.deployment_id }}'); + if (!deploymentId) { + core.info('No deployment id found; skipping deployment status update.'); + return; + } + + const previewUrl = 'https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/previews/pr-${{ github.event.pull_request.number }}/showcase/'; + const runUrl = 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}'; + + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deploymentId, + state: 'success', + environment_url: previewUrl, + log_url: runUrl, + description: 'Preview deployed' + }); + + - name: Mark PR deployment failure + if: github.event_name == 'pull_request' && failure() + uses: actions/github-script@v7 + with: + script: | + const deploymentId = Number('${{ steps.create_deployment.outputs.deployment_id }}'); + if (!deploymentId) { + core.info('No deployment id found; skipping deployment status update.'); + return; + } + + const runUrl = 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}'; + + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deploymentId, + state: 'failure', + log_url: runUrl, + description: 'Preview deployment failed' + }); From 4beacc237d7035d16352499d635486ea4570c9c7 Mon Sep 17 00:00:00 2001 From: Jameson Nyeholt Date: Mon, 4 May 2026 17:05:50 -1000 Subject: [PATCH 3/5] ci(pages): publish gh-pages branch via deploy-pages workflow --- .github/workflows/publish-gh-pages-branch.yml | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/publish-gh-pages-branch.yml diff --git a/.github/workflows/publish-gh-pages-branch.yml b/.github/workflows/publish-gh-pages-branch.yml new file mode 100644 index 0000000..98d1778 --- /dev/null +++ b/.github/workflows/publish-gh-pages-branch.yml @@ -0,0 +1,41 @@ +name: Publish gh-pages branch to GitHub Pages + +on: + push: + branches: + - gh-pages + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages-publish + cancel-in-progress: true + +jobs: + publish: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Checkout gh-pages content + uses: actions/checkout@v4 + with: + ref: gh-pages + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: . + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From 3aa4f12eca88c493d8ccd2f06905d93027a00a83 Mon Sep 17 00:00:00 2001 From: Jameson Nyeholt Date: Mon, 4 May 2026 17:07:27 -1000 Subject: [PATCH 4/5] ci(pages): publish site directly from deploy-showcase workflow --- .github/workflows/deploy-showcase-pages.yml | 14 +++++++ .github/workflows/publish-gh-pages-branch.yml | 41 ------------------- 2 files changed, 14 insertions(+), 41 deletions(-) delete mode 100644 .github/workflows/publish-gh-pages-branch.yml diff --git a/.github/workflows/deploy-showcase-pages.yml b/.github/workflows/deploy-showcase-pages.yml index a0b2dc2..2e5db52 100644 --- a/.github/workflows/deploy-showcase-pages.yml +++ b/.github/workflows/deploy-showcase-pages.yml @@ -27,6 +27,8 @@ permissions: contents: write pull-requests: write deployments: write + pages: write + id-token: write concurrency: group: pages @@ -115,6 +117,18 @@ jobs: body, }); + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: gh-pages-out + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + - name: Mark PR deployment success if: github.event_name == 'pull_request' && success() uses: actions/github-script@v7 diff --git a/.github/workflows/publish-gh-pages-branch.yml b/.github/workflows/publish-gh-pages-branch.yml deleted file mode 100644 index 98d1778..0000000 --- a/.github/workflows/publish-gh-pages-branch.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Publish gh-pages branch to GitHub Pages - -on: - push: - branches: - - gh-pages - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: pages-publish - cancel-in-progress: true - -jobs: - publish: - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - - steps: - - name: Checkout gh-pages content - uses: actions/checkout@v4 - with: - ref: gh-pages - - - name: Configure Pages - uses: actions/configure-pages@v5 - - - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v3 - with: - path: . - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 From b6d7097d60e14e161ab72be11235da6cd2a49749 Mon Sep 17 00:00:00 2001 From: Jameson Nyeholt Date: Mon, 4 May 2026 17:13:35 -1000 Subject: [PATCH 5/5] ci(pages): add health check step after deploy-pages --- .github/workflows/deploy-showcase-pages.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/deploy-showcase-pages.yml b/.github/workflows/deploy-showcase-pages.yml index 2e5db52..c5574d1 100644 --- a/.github/workflows/deploy-showcase-pages.yml +++ b/.github/workflows/deploy-showcase-pages.yml @@ -129,6 +129,26 @@ jobs: id: deployment uses: actions/deploy-pages@v4 + - name: Health check + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + url="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/previews/pr-${{ github.event.pull_request.number }}/showcase/" + else + url="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/showcase/" + fi + echo "Health check URL: $url" + for i in 1 2 3 4 5; do + status=$(curl -s -o /dev/null -w "%{http_code}" -L "$url") + echo "Attempt $i: HTTP $status" + if [ "$status" = "200" ]; then + echo "Health check passed." + exit 0 + fi + sleep 10 + done + echo "Health check failed: $url did not return 200 after 5 attempts." + exit 1 + - name: Mark PR deployment success if: github.event_name == 'pull_request' && success() uses: actions/github-script@v7