diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3cac9780..cdbd22fa 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -212,190 +212,3 @@ jobs:
- name: Build
run: pnpm run build
-
- visual-regression:
- name: Visual Regression
- runs-on: ubuntu-latest
- permissions:
- contents: write
- pull-requests: write
- steps:
- - name: Checkout Repo
- uses: actions/checkout@v6
-
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version: '22'
-
- - name: Setup pnpm
- uses: pnpm/action-setup@v5
-
- - name: Install Dependencies
- run: pnpm install --frozen-lockfile
-
- - name: Cache Playwright Browsers
- uses: actions/cache@v5
- with:
- path: ~/.cache/ms-playwright
- key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- restore-keys: |
- playwright-${{ runner.os }}-
-
- - name: Install Playwright Chromium
- working-directory: apps/storybook
- run: npx playwright install chromium --with-deps
-
- - name: Build Storybook
- run: pnpm turbo run build --filter=@cocso-ui/storybook
-
- - name: Run Visual Regression Tests
- id: vr-test
- continue-on-error: true
- working-directory: apps/storybook
- run: |
- npx serve storybook-static -p 6006 &
- npx wait-on http://localhost:6006 --timeout 60000
- SNAPSHOT_COUNT=$(find __snapshots__ -maxdepth 1 -name "*.png" 2>/dev/null | wc -l)
- if [ "$SNAPSHOT_COUNT" -eq 0 ]; then
- echo "::warning::No baseline snapshots found. Run the 'Update Visual Regression Baselines' workflow to generate them."
- echo "has_baselines=false" >> $GITHUB_OUTPUT
- else
- echo "has_baselines=true" >> $GITHUB_OUTPUT
- pnpm test:visual -- --ci
- fi
-
- - name: Push Screenshots to Branch
- id: push-screenshots
- if: github.event_name == 'pull_request' && steps.vr-test.outputs.has_baselines == 'true'
- continue-on-error: true
- env:
- GITHUB_TOKEN: ${{ github.token }}
- PR_NUMBER: ${{ github.event.number }}
- run: |
- BRANCH="vr-screenshots-${PR_NUMBER}"
- SNAPSHOTS="apps/storybook/__snapshots__"
-
- STAGING=$(mktemp -d)
- mkdir -p "$STAGING/screenshots"
-
- # before: committed baseline snapshots
- for f in "${SNAPSHOTS}"/*.png; do
- [ -f "$f" ] || continue
- id=$(basename "$f" .png)
- cp "$f" "$STAGING/screenshots/before-${id}.png"
- done
-
- # after: freshly captured screenshots from this run
- if [ -d "${SNAPSHOTS}/__current__" ]; then
- for f in "${SNAPSHOTS}/__current__"/*.png; do
- [ -f "$f" ] || continue
- id=$(basename "$f" .png)
- cp "$f" "$STAGING/screenshots/after-${id}.png"
- done
- fi
-
- # diff: generated by jest-image-snapshot on mismatch
- if [ -d "${SNAPSHOTS}/__diff_output__" ]; then
- for f in "${SNAPSHOTS}/__diff_output__"/*.png; do
- [ -f "$f" ] || continue
- name=$(basename "$f" -diff.png)
- cp "$f" "$STAGING/screenshots/diff-${name}.png"
- done
- fi
-
- # Prevent Vercel from deploying this screenshots-only branch
- echo '{"ignoreCommand":"exit 0"}' > "$STAGING/vercel.json"
-
- cd "$STAGING"
- git init
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
- git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
- git add .
- git commit -m "vr: screenshots for PR #${PR_NUMBER}"
- git push --force origin "HEAD:refs/heads/${BRANCH}"
-
- - name: Generate Visual Regression Report
- if: github.event_name == 'pull_request' && steps.vr-test.outputs.has_baselines == 'true' && steps.push-screenshots.outcome == 'success'
- env:
- PR_NUMBER: ${{ github.event.number }}
- run: |
- node << 'EOF'
- const fs = require('fs');
- const path = require('path');
-
- const PR_NUMBER = process.env.PR_NUMBER;
- const REPO = process.env.GITHUB_REPOSITORY;
- const BRANCH = `vr-screenshots-${PR_NUMBER}`;
- const BASE_URL = `https://raw.githubusercontent.com/${REPO}/${BRANCH}/screenshots`;
-
- const SNAPSHOTS_DIR = 'apps/storybook/__snapshots__';
- const DIFF_DIR = path.join(SNAPSHOTS_DIR, '__diff_output__');
-
- const allSnapshots = fs.readdirSync(SNAPSHOTS_DIR)
- .filter(f => f.endsWith('.png') && !f.startsWith('.'))
- .sort()
- .map(f => f.replace('.png', ''));
-
- let diffed = [];
- try {
- diffed = fs.readdirSync(DIFF_DIR)
- .filter(f => f.endsWith('-diff.png'))
- .map(f => f.replace('-diff.png', ''));
- } catch {}
-
- const changed = allSnapshots.filter(id => diffed.includes(id));
- const unchanged = allSnapshots.filter(id => !diffed.includes(id));
-
- function formatName(id) {
- return id
- .replace(/^components-/, '')
- .replace(/--/g, ' / ')
- .replace(/-/g, ' ')
- .replace(/\b\w/g, c => c.toUpperCase());
- }
-
- const lines = ['', '## Visual Regression Report', ''];
-
- if (changed.length === 0) {
- lines.push(`✅ **All ${allSnapshots.length} snapshot(s) matched — no visual regressions.**`);
- } else {
- lines.push(`⚠️ **${changed.length} of ${allSnapshots.length} screenshot(s) changed:**`, '');
- for (const id of changed) {
- lines.push(`### ${formatName(id)}`, '');
- lines.push('| Before | After | Diff |');
- lines.push('|--------|-------|------|');
- lines.push(`|  |  |  |`);
- lines.push('');
- }
- }
-
- if (unchanged.length > 0) {
- const label = changed.length === 0
- ? `${unchanged.length} screenshot(s) — all matching baseline`
- : `${unchanged.length} screenshot(s) unchanged`;
- lines.push('', `${label}
`, '');
- for (const id of unchanged) {
- lines.push(`- ${formatName(id)}`);
- }
- lines.push('', ' ');
- }
-
- lines.push('', '---', '*Generated by cocso-ui Visual Regression*');
-
- fs.writeFileSync('vr-report.md', lines.join('\n'));
- EOF
-
- - name: Post Visual Regression Comment
- if: github.event_name == 'pull_request' && steps.vr-test.outputs.has_baselines == 'true' && steps.push-screenshots.outcome == 'success'
- uses: marocchino/sticky-pull-request-comment@v2
- with:
- header: visual-regression
- path: vr-report.md
-
- - name: Fail if Visual Regressions Detected
- if: steps.vr-test.outcome == 'failure'
- run: |
- echo "::error::Visual regressions detected. See the PR comment for before/after/diff images."
- exit 1
diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml
new file mode 100644
index 00000000..974ae833
--- /dev/null
+++ b/.github/workflows/visual-regression.yml
@@ -0,0 +1,221 @@
+name: Visual Regression
+
+on:
+ push:
+ branches:
+ - main
+ - v1
+ paths:
+ - 'packages/react/**'
+ - 'packages/react-icons/**'
+ - 'packages/css/**'
+ - 'packages/recipe/**'
+ - 'packages/baseframe-sources/**'
+ - 'ecosystem/codegen/**'
+ - 'ecosystem/baseframe/**'
+ - 'ecosystem/icons/**'
+ - 'apps/storybook/**'
+ - 'pnpm-lock.yaml'
+ pull_request:
+ branches:
+ - main
+ - v1
+ paths:
+ - 'packages/react/**'
+ - 'packages/react-icons/**'
+ - 'packages/css/**'
+ - 'packages/recipe/**'
+ - 'packages/baseframe-sources/**'
+ - 'ecosystem/codegen/**'
+ - 'ecosystem/baseframe/**'
+ - 'ecosystem/icons/**'
+ - 'apps/storybook/**'
+ - 'pnpm-lock.yaml'
+
+jobs:
+ visual-regression:
+ name: Visual Regression
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '22'
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v5
+
+ - name: Install Dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Cache Playwright Browsers
+ uses: actions/cache@v5
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
+ restore-keys: |
+ playwright-${{ runner.os }}-
+
+ - name: Install Playwright Chromium
+ working-directory: apps/storybook
+ run: npx playwright install chromium --with-deps
+
+ - name: Build Storybook
+ run: pnpm turbo run build --filter=@cocso-ui/storybook
+
+ - name: Run Visual Regression Tests
+ id: vr-test
+ continue-on-error: true
+ working-directory: apps/storybook
+ run: |
+ npx serve storybook-static -p 6006 &
+ npx wait-on http://localhost:6006 --timeout 60000
+ SNAPSHOT_COUNT=$(find __snapshots__ -maxdepth 1 -name "*.png" 2>/dev/null | wc -l)
+ if [ "$SNAPSHOT_COUNT" -eq 0 ]; then
+ echo "::warning::No baseline snapshots found. Run the 'Update Visual Regression Baselines' workflow to generate them."
+ echo "has_baselines=false" >> $GITHUB_OUTPUT
+ else
+ echo "has_baselines=true" >> $GITHUB_OUTPUT
+ pnpm test:visual -- --ci
+ fi
+
+ - name: Push Screenshots to Branch
+ id: push-screenshots
+ if: github.event_name == 'pull_request' && steps.vr-test.outputs.has_baselines == 'true'
+ continue-on-error: true
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ PR_NUMBER: ${{ github.event.number }}
+ run: |
+ BRANCH="vr-screenshots-${PR_NUMBER}"
+ SNAPSHOTS="apps/storybook/__snapshots__"
+
+ STAGING=$(mktemp -d)
+ mkdir -p "$STAGING/screenshots"
+
+ # before: committed baseline snapshots
+ for f in "${SNAPSHOTS}"/*.png; do
+ [ -f "$f" ] || continue
+ id=$(basename "$f" .png)
+ cp "$f" "$STAGING/screenshots/before-${id}.png"
+ done
+
+ # after: freshly captured screenshots from this run
+ if [ -d "${SNAPSHOTS}/__current__" ]; then
+ for f in "${SNAPSHOTS}/__current__"/*.png; do
+ [ -f "$f" ] || continue
+ id=$(basename "$f" .png)
+ cp "$f" "$STAGING/screenshots/after-${id}.png"
+ done
+ fi
+
+ # diff: generated by jest-image-snapshot on mismatch
+ if [ -d "${SNAPSHOTS}/__diff_output__" ]; then
+ for f in "${SNAPSHOTS}/__diff_output__"/*.png; do
+ [ -f "$f" ] || continue
+ name=$(basename "$f" -diff.png)
+ cp "$f" "$STAGING/screenshots/diff-${name}.png"
+ done
+ fi
+
+ # Prevent Vercel from deploying this screenshots-only branch
+ echo '{"ignoreCommand":"exit 0"}' > "$STAGING/vercel.json"
+
+ cd "$STAGING"
+ git init
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
+ git add .
+ git commit -m "vr: screenshots for PR #${PR_NUMBER}"
+ git push --force origin "HEAD:refs/heads/${BRANCH}"
+
+ - name: Generate Visual Regression Report
+ if: github.event_name == 'pull_request' && steps.vr-test.outputs.has_baselines == 'true' && steps.push-screenshots.outcome == 'success'
+ env:
+ PR_NUMBER: ${{ github.event.number }}
+ run: |
+ node << 'EOF'
+ const fs = require('fs');
+ const path = require('path');
+
+ const PR_NUMBER = process.env.PR_NUMBER;
+ const REPO = process.env.GITHUB_REPOSITORY;
+ const BRANCH = `vr-screenshots-${PR_NUMBER}`;
+ const BASE_URL = `https://raw.githubusercontent.com/${REPO}/${BRANCH}/screenshots`;
+
+ const SNAPSHOTS_DIR = 'apps/storybook/__snapshots__';
+ const DIFF_DIR = path.join(SNAPSHOTS_DIR, '__diff_output__');
+
+ const allSnapshots = fs.readdirSync(SNAPSHOTS_DIR)
+ .filter(f => f.endsWith('.png') && !f.startsWith('.'))
+ .sort()
+ .map(f => f.replace('.png', ''));
+
+ let diffed = [];
+ try {
+ diffed = fs.readdirSync(DIFF_DIR)
+ .filter(f => f.endsWith('-diff.png'))
+ .map(f => f.replace('-diff.png', ''));
+ } catch {}
+
+ const changed = allSnapshots.filter(id => diffed.includes(id));
+ const unchanged = allSnapshots.filter(id => !diffed.includes(id));
+
+ function formatName(id) {
+ return id
+ .replace(/^components-/, '')
+ .replace(/--/g, ' / ')
+ .replace(/-/g, ' ')
+ .replace(/\b\w/g, c => c.toUpperCase());
+ }
+
+ const lines = ['', '## Visual Regression Report', ''];
+
+ if (changed.length === 0) {
+ lines.push(`✅ **All ${allSnapshots.length} snapshot(s) matched — no visual regressions.**`);
+ } else {
+ lines.push(`⚠️ **${changed.length} of ${allSnapshots.length} screenshot(s) changed:**`, '');
+ for (const id of changed) {
+ lines.push(`### ${formatName(id)}`, '');
+ lines.push('| Before | After | Diff |');
+ lines.push('|--------|-------|------|');
+ lines.push(`|  |  |  |`);
+ lines.push('');
+ }
+ }
+
+ if (unchanged.length > 0) {
+ const label = changed.length === 0
+ ? `${unchanged.length} screenshot(s) — all matching baseline`
+ : `${unchanged.length} screenshot(s) unchanged`;
+ lines.push('', `${label}
`, '');
+ for (const id of unchanged) {
+ lines.push(`- ${formatName(id)}`);
+ }
+ lines.push('', ' ');
+ }
+
+ lines.push('', '---', '*Generated by cocso-ui Visual Regression*');
+
+ fs.writeFileSync('vr-report.md', lines.join('\n'));
+ EOF
+
+ - name: Post Visual Regression Comment
+ if: github.event_name == 'pull_request' && steps.vr-test.outputs.has_baselines == 'true' && steps.push-screenshots.outcome == 'success'
+ uses: marocchino/sticky-pull-request-comment@v2
+ with:
+ header: visual-regression
+ path: vr-report.md
+
+ - name: Fail if Visual Regressions Detected
+ if: steps.vr-test.outcome == 'failure'
+ run: |
+ echo "::error::Visual regressions detected. See the PR comment for before/after/diff images."
+ exit 1