From 9d11918bd89070ca64feb025c5227d9ebfa6a2ab Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:51:45 +0100 Subject: [PATCH] Add Storybook preview deploy + changed-stories comment on PRs (#6929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Adds a **Storybook preview** for PRs. When a PR changes any story (`*.stories.{ts,tsx,mdx}`) or the `.storybook` config, this builds the static Storybook, deploys it to the preview VPS on a PR-scoped port, and comments with the URL plus an **expandable list of exactly which stories changed**. Torn down automatically when the PR closes. New file: `.github/workflows/storybook-preview.yml`. Nothing else is touched. ## How - **Detect** (`changes` job) - `dorny/paths-filter` with `list-files: json` flags Storybook changes and captures the exact changed files. Skipped on close and for fork PRs (which don't get the VPS secrets). - **Deploy** (`deploy` job, only when Storybook changed) - builds the static Storybook (`task frontend:prepare` + `frontend:storybook:build`), tars it, and serves it from an `nginx:alpine` container on the VPS at port `PR# + 20000` (offset from the app preview's bare-PR-number port to avoid collisions). Mirrors `PR-Auto-Deploy-V2.yml`'s VPS SSH pattern and reuses the same secrets. - **Comment** - a single bot comment (replaced on each push) with the preview URL and a `
` block listing the changed stories (and any `.storybook` config changes), e.g.: > ## 📚 Storybook preview > 🔗 **Preview:** http://<vps>:26911 >
2 stories changed (+1 config file)
- **Cleanup** (`cleanup` job, on PR close) - stops the container, removes the files, and deletes the comment. ## Validation - Static Storybook builds locally (`task frontend:storybook:build` → `frontend/storybook-static`, 151 stories). - Confirmed `task frontend:prepare` regenerates the un-committed `material-symbols-icons.json` that stories import, so a fresh CI checkout builds (added it before the build step). - Comment-markdown logic unit-checked against a sample changed-files list. - YAML validated; action pins match the repo (`setup-node` v6.4.0 / node 22, same `paths-filter`, `setup-bot`, `harden-runner`). ## Note The VPS deploy mirrors the proven `PR-Auto-Deploy-V2` machinery but couldn't be exercised end-to-end from a dev box (needs the VPS secrets) - the first live run on a Storybook-touching PR will confirm the deploy/serve/cleanup path. Everything build- and comment-side is validated locally. --- .github/workflows/PR-Auto-Deploy-V2.yml | 92 ++++++++++++++++++- .../editor/src/core/ui/Banner.stories.tsx | 9 ++ frontend/editor/src/core/ui/Chip.stories.tsx | 1 + 3 files changed, 99 insertions(+), 3 deletions(-) diff --git a/.github/workflows/PR-Auto-Deploy-V2.yml b/.github/workflows/PR-Auto-Deploy-V2.yml index 31135cfb4b..173b4db9df 100644 --- a/.github/workflows/PR-Auto-Deploy-V2.yml +++ b/.github/workflows/PR-Auto-Deploy-V2.yml @@ -342,9 +342,70 @@ jobs: # Set port for output echo "v2_port=${V2_PORT}" >> $GITHUB_OUTPUT + # ---- Storybook preview (only when this PR touches stories/.storybook) ---- + # Runs inside the same approved-contributor-gated deploy job, so it deploys + # under the exact same access rules as the app preview. + - name: Detect Storybook changes + id: sb-changes + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + with: + list-files: json + filters: | + storybook: + - 'frontend/**/*.stories.@(ts|tsx|mdx)' + - 'frontend/**/*.mdx' + - 'frontend/.storybook/**' + + - name: Set up Node.js for Storybook + if: steps.sb-changes.outputs.storybook == 'true' + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "22" + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install Task for Storybook + if: steps.sb-changes.outputs.storybook == 'true' + uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 + + - name: Build and deploy Storybook + id: storybook + if: steps.sb-changes.outputs.storybook == 'true' + env: + VPS_HOST: ${{ secrets.NEW_VPS_HOST }} + VPS_USER: ${{ secrets.NEW_VPS_USERNAME }} + run: | + set -euo pipefail + # `prepare` generates the icon set stories import (not committed). + task frontend:prepare + task frontend:storybook:build + PR=${{ needs.check-pr.outputs.pr_number }} + # Served at the ROOT of its own port so Storybook's global MSW worker + # (/mockServiceWorker.js) resolves. Port = PR + 20000 (bijective, offset + # from the app preview's bare-PR-number port). + SB_PORT=$((PR + 20000)) + DIR=/stirling/SB-PR-$PR + tar czf storybook.tgz -C frontend/storybook-static . + scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + storybook.tgz "$VPS_USER@$VPS_HOST:/tmp/storybook-$PR.tgz" + ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T \ + "$VPS_USER@$VPS_HOST" << ENDSSH + set -e + rm -rf "$DIR" && mkdir -p "$DIR" + tar xzf /tmp/storybook-$PR.tgz -C "$DIR" + rm -f /tmp/storybook-$PR.tgz + docker rm -f storybook-pr-$PR 2>/dev/null || true + docker run -d --name storybook-pr-$PR --restart unless-stopped \ + -p $SB_PORT:80 -v "$DIR":/usr/share/nginx/html:ro nginx:alpine + ENDSSH + echo "url=http://$VPS_HOST:$SB_PORT/" >> "$GITHUB_OUTPUT" + - name: Post V2 deployment URL to PR if: success() uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + SB_URL: ${{ steps.storybook.outputs.url }} + SB_FILES: ${{ steps.sb-changes.outputs.storybook_files }} with: github-token: ${{ steps.setup-bot.outputs.token }} script: | @@ -376,11 +437,32 @@ jobs: ? `🧩 **Admin portal** included - try it at [${deploymentUrl}/portal](${deploymentUrl}/portal).\n\n` : ``; + // Storybook preview: only present when this PR changed stories/config. + const sbUrl = process.env.SB_URL; + let storybookNote = ""; + if (sbUrl) { + const files = JSON.parse(process.env.SB_FILES || "[]"); + const stories = files.filter((f) => /\.stories\.(ts|tsx|mdx)$/.test(f)); + const config = files.filter((f) => f.startsWith("frontend/.storybook/")); + const shorten = (f) => + f.replace(/^frontend\/editor\/src\//, "").replace(/^frontend\//, ""); + const storyList = stories.map((f) => `- \`${shorten(f)}\``).join("\n"); + const configList = config.map((f) => `- \`${shorten(f)}\``).join("\n"); + const summary = + `${stories.length} stor${stories.length === 1 ? "y" : "ies"} changed` + + (config.length ? ` (+${config.length} config file${config.length === 1 ? "" : "s"})` : ""); + storybookNote = + `📚 **Storybook:** [${sbUrl}](${sbUrl})\n\n` + + `
\n${summary}\n\n` + + (storyList ? `**Stories**\n${storyList}\n\n` : "") + + (configList ? `**Config**\n${configList}\n` : "") + + `
\n\n`; + } + const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` + - `Your V2 PR with embedded architecture has been deployed!\n\n` + `🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` + - `🔐 **Secure HTTPS URL**: unsupported currently\n\n` + portalNote + + storybookNote + `_This deployment will be automatically cleaned up when the PR is closed._\n\n` + `🔄 **Auto-deployed** for approved V2 contributors.`; @@ -476,7 +558,11 @@ jobs: else echo "V2 PR directory not found, nothing to clean up" fi - + + # Remove this PR's Storybook preview (container + files), if any. + docker rm -f storybook-pr-${{ github.event.pull_request.number }} 2>/dev/null || true + rm -rf /stirling/SB-PR-${{ github.event.pull_request.number }} + # Clean up old unused images (older than 2 weeks) but keep recent ones for reuse docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true diff --git a/frontend/editor/src/core/ui/Banner.stories.tsx b/frontend/editor/src/core/ui/Banner.stories.tsx index 891cf66b78..2e1f2b4d78 100644 --- a/frontend/editor/src/core/ui/Banner.stories.tsx +++ b/frontend/editor/src/core/ui/Banner.stories.tsx @@ -33,6 +33,15 @@ type Story = StoryObj; /** Flip tone / title / description / action / onDismiss in controls. */ export const Playground: Story = {}; +/** Success tone - added as a Storybook-preview workflow example. */ +export const Success: Story = { + args: { + tone: "success", + title: "All clear", + description: "Every document passed validation.", + }, +}; + export const WithAction: Story = { args: { tone: "warning", diff --git a/frontend/editor/src/core/ui/Chip.stories.tsx b/frontend/editor/src/core/ui/Chip.stories.tsx index 22a9f554c8..6fab3f8e85 100644 --- a/frontend/editor/src/core/ui/Chip.stories.tsx +++ b/frontend/editor/src/core/ui/Chip.stories.tsx @@ -1,3 +1,4 @@ +// One-line edit to demonstrate the storybook-preview workflow detects edits, not just adds. import type { Meta, StoryObj } from "@storybook/react-vite"; import { Chip } from "@app/ui/Chip";