[Graphite MQ] Draft PR GROUP:spec_762ef7 (PRs 393, 403) #485
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Storybook Snapshots | |
| on: | |
| push: | |
| branches: [main, develop] | |
| pull_request: | |
| branches: [main, develop] | |
| concurrency: | |
| group: storybook-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| storybook-tests: | |
| name: Storybook Snapshot Tests | |
| runs-on: [self-hosted, linux] | |
| timeout-minutes: 25 | |
| permissions: | |
| contents: read | |
| deployments: write | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: ./.github/actions/setup | |
| with: | |
| darkmatter-cachix-auth-token: ${{ secrets.DARKMATTER_CACHIX_AUTH_TOKEN }} | |
| - name: Resolve secrets | |
| env: | |
| SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }} | |
| run: | | |
| SECRETS_FILE="ops/secrets/secrets.yaml" | |
| if [ ! -f "$SECRETS_FILE" ]; then | |
| echo "::error::SOPS secrets file not found at $SECRETS_FILE" | |
| exit 1 | |
| fi | |
| CLOUDFLARE_API_TOKEN=$(sops -d --extract '["CLOUDFLARE_API_TOKEN"]' "$SECRETS_FILE") | |
| CLOUDFLARE_ACCOUNT_ID=$(sops -d --extract '["CLOUDFLARE_ACCOUNT_ID"]' "$SECRETS_FILE") | |
| echo "::add-mask::$CLOUDFLARE_API_TOKEN" | |
| echo "::add-mask::$CLOUDFLARE_ACCOUNT_ID" | |
| { | |
| echo "CLOUDFLARE_API_TOKEN=$CLOUDFLARE_API_TOKEN" | |
| echo "CLOUDFLARE_ACCOUNT_ID=$CLOUDFLARE_ACCOUNT_ID" | |
| } >> "$GITHUB_ENV" | |
| - name: Check Playwright browser availability | |
| working-directory: apps/native | |
| run: | | |
| bunx playwright install --dry-run chromium | |
| DEBUG=pw:browser bun --eval ' | |
| const { chromium } = await import("playwright"); | |
| const browser = await chromium.launch({ | |
| args: [ | |
| "--no-sandbox", | |
| "--disable-dev-shm-usage", | |
| "--disable-gpu-sandbox", | |
| "--disable-gpu", | |
| "--no-zygote", | |
| ], | |
| }); | |
| const page = await browser.newPage(); | |
| await page.setContent("<h1>ok</h1>"); | |
| console.log(await page.textContent("h1")); | |
| await browser.close(); | |
| ' | |
| - name: Build Storybook | |
| working-directory: apps/native | |
| run: bun run build-storybook | |
| - name: Create Cloudflare Pages project (if needed) | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ env.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ env.CLOUDFLARE_ACCOUNT_ID }} | |
| run: bunx wrangler pages project create nixmac-storybook --production-branch=main 2>/dev/null || true | |
| - name: Deploy to Cloudflare Pages | |
| id: deploy | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ env.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ env.CLOUDFLARE_ACCOUNT_ID }} | |
| BRANCH: ${{ github.head_ref || github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| output=$(bunx wrangler pages deploy apps/native/storybook-static \ | |
| --project-name=nixmac-storybook \ | |
| --commit-dirty=true \ | |
| --branch="$BRANCH" 2>&1 | tee /dev/stderr) | |
| url=$(echo "$output" | grep -oE 'https://[a-zA-Z0-9.-]+\.pages\.dev' | head -n1 || true) | |
| if [ -z "$url" ]; then | |
| echo "::error::Cloudflare Pages deploy output did not include a detectable preview URL" | |
| exit 1 | |
| fi | |
| echo "Storybook preview: $url" | |
| echo "deployment-url=$url" >> "$GITHUB_OUTPUT" | |
| # Story-level digest of new/changed/removed stories vs the PR base, | |
| # rendered into the sticky comment so reviewers can see what changed. | |
| - name: Build story-change digest | |
| if: github.event_name == 'pull_request' | |
| continue-on-error: true | |
| working-directory: apps/native | |
| env: | |
| BASE_REF: origin/${{ github.base_ref }} | |
| DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} | |
| run: node scripts/build-storybook-digest.mjs | |
| - name: Publish Storybook deployment link | |
| if: steps.deploy.outputs.deployment-url != '' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} | |
| REF: ${{ github.event.pull_request.head.sha || github.sha }} | |
| REPO: ${{ github.repository }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| set -euo pipefail | |
| deployment_id=$(gh api --method POST "repos/${REPO}/deployments" --jq '.id' --input - <<EOF | |
| { | |
| "ref": "${REF}", | |
| "environment": "storybook-preview", | |
| "description": "Storybook preview", | |
| "auto_merge": false, | |
| "required_contexts": [], | |
| "transient_environment": true, | |
| "production_environment": false | |
| } | |
| EOF | |
| ) | |
| gh api --method POST "repos/${REPO}/deployments/${deployment_id}/statuses" --input - <<EOF >/dev/null | |
| { | |
| "state": "success", | |
| "environment_url": "${DEPLOY_URL}", | |
| "log_url": "${RUN_URL}", | |
| "description": "Storybook preview is ready", | |
| "auto_inactive": false | |
| } | |
| EOF | |
| - name: Comment Storybook preview URL on PR | |
| if: always() && !cancelled() && github.event_name == 'pull_request' && steps.deploy.outputs.deployment-url != '' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| MARKER='<!-- nixmac-storybook-preview -->' | |
| DIGEST="" | |
| if [ -f apps/native/test-results/storybook-digest.md ]; then | |
| DIGEST=$(cat apps/native/test-results/storybook-digest.md) | |
| fi | |
| BODY=$(cat <<EOF | |
| ${MARKER} | |
| ### 🎨 Storybook preview | |
| [Open Storybook preview](${DEPLOY_URL}) | |
| Updated for ${SHA} | |
| ${DIGEST} | |
| EOF | |
| ) | |
| # Find existing sticky comment (if any) and update; otherwise create. | |
| EXISTING_ID=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \ | |
| --paginate --jq ".[] | select(.body | startswith(\"${MARKER}\")) | .id" \ | |
| | head -n1 || true) | |
| if [ -n "${EXISTING_ID}" ]; then | |
| gh api --method PATCH "repos/${REPO}/issues/comments/${EXISTING_ID}" \ | |
| -f body="${BODY}" >/dev/null | |
| echo "Updated sticky comment ${EXISTING_ID}" | |
| else | |
| gh pr comment "${PR_NUMBER}" --repo "${REPO}" --body "${BODY}" | |
| echo "Created sticky comment" | |
| fi | |
| - name: Run Storybook snapshot tests | |
| id: snapshot_tests | |
| working-directory: apps/native | |
| run: bun run test:storybook:ci | |
| # ---- Failed-story screenshots (PR only, only when snapshots failed) ---- | |
| # Resolve the failed stories (recorded by the runner) into Storybook IDs + | |
| # deep links, and emit the Creevey skip regex used to scope capture. | |
| - name: Resolve failed stories | |
| id: resolve_failures | |
| if: failure() && steps.snapshot_tests.conclusion == 'failure' && github.event_name == 'pull_request' && steps.deploy.outputs.deployment-url != '' | |
| continue-on-error: true | |
| working-directory: apps/native | |
| env: | |
| DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} | |
| run: | | |
| node scripts/resolve-failed-stories.mjs | |
| count=$(node -e 'const r=require("./test-results/failed-stories-resolved.json"); process.stdout.write(String(r.length))') | |
| echo "count=$count" >> "$GITHUB_OUTPUT" | |
| # Rebuild Storybook with the skip regex baked in, serve it locally, and let | |
| # Creevey (Playwright, no Docker) screenshot only the failed stories. | |
| - name: Capture failed-story screenshots (Creevey) | |
| if: ${{ failure() && steps.resolve_failures.outcome == 'success' && steps.resolve_failures.outputs.count != '0' }} | |
| continue-on-error: true | |
| working-directory: apps/native | |
| run: | | |
| export VITE_CREEVEY_SKIP_REGEX="$(cat test-results/creevey-skip-regex.txt)" | |
| bun run build-storybook | |
| python3 -m http.server 6100 --directory storybook-static >/tmp/creevey-storybook.log 2>&1 & | |
| SB_PID=$! | |
| trap 'kill $SB_PID 2>/dev/null || true' EXIT | |
| for _ in $(seq 1 30); do | |
| curl -sf http://localhost:6100/index.json >/dev/null && break | |
| sleep 1 | |
| done | |
| CREEVEY_STORYBOOK_URL="http://localhost:6100" bunx creevey test --no-docker || true | |
| node scripts/harvest-creevey-shots.mjs | |
| # Host the screenshots on Cloudflare Pages so the PR comment can embed them. | |
| - name: Deploy failed-story screenshots | |
| id: deploy_shots | |
| if: ${{ failure() && steps.resolve_failures.outcome == 'success' && steps.resolve_failures.outputs.count != '0' }} | |
| continue-on-error: true | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ env.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ env.CLOUDFLARE_ACCOUNT_ID }} | |
| BRANCH: ${{ github.head_ref || github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| bunx wrangler pages project create nixmac-storybook-shots --production-branch=main 2>/dev/null || true | |
| output=$(bunx wrangler pages deploy apps/native/test-results/shots \ | |
| --project-name=nixmac-storybook-shots \ | |
| --commit-dirty=true \ | |
| --branch="$BRANCH" 2>&1 | tee /dev/stderr) | |
| url=$(echo "$output" | grep -oE 'https://[a-zA-Z0-9.-]+\.pages\.dev' | head -n1 || true) | |
| echo "shots-url=$url" >> "$GITHUB_OUTPUT" | |
| # Re-render the sticky comment to include the failed-snapshot gallery. | |
| - name: Update PR comment with failed snapshots | |
| if: ${{ failure() && steps.deploy_shots.outcome == 'success' && steps.deploy_shots.outputs.shots-url != '' }} | |
| continue-on-error: true | |
| working-directory: apps/native | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| MARKER: "<!-- nixmac-storybook-preview -->" | |
| DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} | |
| COMMIT_SHA: ${{ github.event.pull_request.head.sha }} | |
| SHOTS_BASE_URL: ${{ steps.deploy_shots.outputs.shots-url }} | |
| run: | | |
| set -euo pipefail | |
| BODY="$(node scripts/build-failed-comment.mjs)" | |
| EXISTING_ID=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \ | |
| --paginate --jq ".[] | select(.body | startswith(\"${MARKER}\")) | .id" \ | |
| | head -n1 || true) | |
| if [ -n "${EXISTING_ID}" ]; then | |
| gh api --method PATCH "repos/${REPO}/issues/comments/${EXISTING_ID}" \ | |
| -f body="${BODY}" >/dev/null | |
| echo "Updated sticky comment ${EXISTING_ID}" | |
| else | |
| gh pr comment "${PR_NUMBER}" --repo "${REPO}" --body "${BODY}" | |
| echo "Created sticky comment" | |
| fi | |
| - name: Check for snapshot staleness | |
| if: always() && !cancelled() | |
| working-directory: apps/native | |
| run: | | |
| if git diff --name-only | grep -q '__snapshots__'; then | |
| echo "::error::Storybook snapshots are out of date. Run 'bun run test:update-snapshots' locally and commit the changes." | |
| git diff --stat -- '*/__snapshots__/*' | |
| exit 1 | |
| fi | |
| - name: Upload snapshot diffs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: storybook-snapshot-diffs | |
| path: | | |
| apps/native/src/**/__snapshots__/__diff_output__/ | |
| apps/native/test-results/ | |
| retention-days: 7 | |
| - name: Post summary | |
| if: always() | |
| run: | | |
| echo "### Storybook Snapshot Tests" >> "$GITHUB_STEP_SUMMARY" | |
| DEPLOY_URL="${{ steps.deploy.outputs.deployment-url }}" | |
| if [ -n "$DEPLOY_URL" ]; then | |
| echo "[Open Storybook preview]($DEPLOY_URL)" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| if [ "${{ job.status }}" == "success" ]; then | |
| echo "All Storybook snapshot tests passed" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "Storybook snapshot tests failed" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Check the 'storybook-snapshot-diffs' artifact for visual diffs." >> "$GITHUB_STEP_SUMMARY" | |
| fi |