deckhearth/.github/workflows/visual-diff.yml
Randall Stillwell 1cc2e28423 Migrate Deck Hearth off Vercel/Neon to homelab Dokploy stack.
Replace @vercel/postgres, Blob, and Upstash with lib/sql.js, MinIO object
storage, and CT 102 Redis rate limits. Add Dockerfile for Dokploy deploy,
homelab runbooks, Neon data-copy helper, and point CI smoke/visual at the
homelab URL instead of Vercel previews.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-15 09:32:13 -05:00

162 lines
6.1 KiB
YAML

name: Visual diff
# Same as preview-smoke — hits the homelab deployment at deckhearth.stillwell.cloud.
# Paths are tcg-vault-specific (pages router, JS).
on:
pull_request:
branches: [main]
paths:
- 'pages/**'
# Exclude API-only edits — they don't render UI, so they can't move
# any visual-diff pixels. Order matters: GitHub Actions evaluates the
# `paths:` list with minimatch and applies `!`-prefixed exclusions
# only after they've already matched a prior include. Keep this entry
# immediately AFTER `pages/**`.
# Surfaced by `tighten-visual-diff-path-filter` after PR #19
# (cors-tighten) and PR #20 (add-rate-limiting) both falsely
# triggered Screenshot diff at ~55s/PR.
- '!pages/api/**'
- 'components/**'
- 'styles/**'
- 'tailwind.config.js'
- 'postcss.config.js'
# Baseline-only PRs (specs + committed snapshots) should still run
# visual diff now that home.png is in tests/visual/__screenshots__/.
- 'tests/visual/**'
concurrency:
group: visual-diff-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
jobs:
gate:
name: Should run?
runs-on: [self-hosted, axiom]
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: check
env:
# See preview-smoke.yml for the rationale: ${{ github.event.* }}
# inlined into shell is a syntax-error + injection vector.
PR_BODY: ${{ github.event.pull_request.body }}
PR_IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
run: |
if [[ "$PR_IS_FORK" == "true" ]]; then
echo "should_run=false" >> $GITHUB_OUTPUT
echo "::notice::Visual diff skipped on fork PR (bypass secret unavailable to forks)"
elif echo "$PR_BODY" | grep -qE 'pipeline:.*skip.*\bvisual\b'; then
echo "should_run=false" >> $GITHUB_OUTPUT
echo "::notice::Visual diff skipped via pipeline directive"
else
echo "should_run=true" >> $GITHUB_OUTPUT
fi
visual:
name: Screenshot diff
needs: gate
if: needs.gate.outputs.should_run == 'true'
runs-on: [self-hosted, axiom]
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Wait for homelab app health
run: |
BASE_URL="${SMOKE_BASE_URL:-https://deckhearth.stillwell.cloud}"
for i in $(seq 1 30); do
if curl -fsS "${BASE_URL}/api/health" >/dev/null; then
echo "Homelab app healthy at ${BASE_URL}"
exit 0
fi
echo "Waiting for ${BASE_URL}/api/health (${i}/30)…"
sleep 10
done
echo "::error::Homelab app did not become healthy in time"
exit 1
env:
SMOKE_BASE_URL: ${{ vars.SMOKE_BASE_URL }}
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- name: Cache node_modules
id: cache-node-modules
uses: actions/cache@v4
with:
path: node_modules
key: node-modules-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
- run: npm ci
if: steps.cache-node-modules.outputs.cache-hit != 'true'
# Cache Playwright browsers — see preview-smoke.yml for rationale.
- name: Resolve Playwright version
id: pw-version
run: |
VERSION=$(node -p "require('./package-lock.json').packages['node_modules/@playwright/test'].version")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: cache-playwright
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}-chromium
- name: Install Playwright browsers
run: |
if [ "${{ steps.cache-playwright.outputs.cache-hit }}" = "true" ]; then
npx playwright install-deps chromium
else
npx playwright install --with-deps chromium
fi
# Hard-gating after `harden-visual-diff-gate` brief 2 (PR #140).
# The prior `continue-on-error: true` was a documented end state of
# `adopt-playwright-smoke` (Decision 4) — necessary while no Linux
# baseline existed. PR #58 seeded the first baseline, and PR #139
# refreshed it against post-glass-redesign main. Visual diff is now
# a real merge gate.
#
# If this step fails on a UI-touching PR, two paths:
# (a) Intentional change: dispatch
# `.github/workflows/seed-visual-baselines.yml` against this
# PR's preview URL, merge the resulting baseline-refresh PR,
# then re-run this workflow.
# (b) Unintentional regression: open the artifact bundle from
# this run, inspect the diff PNG, fix the regression in
# source, push.
- name: Capture screenshots (PR)
run: npx playwright test --project=visual --update-snapshots=none
env:
BASE_URL: ${{ vars.SMOKE_BASE_URL || 'https://deckhearth.stillwell.cloud' }}
- name: Upload screenshots + diffs
if: always()
uses: actions/upload-artifact@v4
with:
name: visual-diff
path: |
tests/visual/__screenshots__/
test-results/
retention-days: 7
- name: Comment on PR with diff link
if: always()
uses: actions/github-script@v7
with:
script: |
const run = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Visual Diff\n\nScreenshots and diffs uploaded as artifacts: [view run](${run})\n\nIf intentional changes: update snapshots locally with \`npx playwright test --project=visual --update-snapshots\` and commit.`
});