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>
162 lines
6.1 KiB
YAML
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.`
|
|
});
|