Runs Vitest browser visual regression tests (drawElevationChart) on every PR and push to main. Uses the same Playwright/Chromium cache as the e2e job. On first run (no committed snapshots yet) the tests create the snapshots and pass. On subsequent runs they compare against committed snapshots and fail on visual regression. Failed runs upload a visual-snapshots-diff artifact so the diff is visible in the Actions UI. To update snapshots after an intentional visual change: Actions → "Update visual snapshots" → Run workflow (or add the `update-snapshots` label to the PR). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
309 lines
9.3 KiB
YAML
309 lines
9.3 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
merge_group:
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
security:
|
|
name: Security Scan
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
- name: Gitleaks
|
|
if: github.actor != 'dependabot[bot]'
|
|
uses: trails-cool/gitleaks-action@4cbc857b9cfa2a3297fe2be1078e196d30d1b424
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
|
- uses: pnpm/action-setup@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
- run: pnpm install --frozen-lockfile
|
|
- name: Dependency audit
|
|
run: pnpm audit --audit-level=high
|
|
continue-on-error: true
|
|
|
|
dockerfile-check:
|
|
name: Dockerfile Package Check
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- run: bash scripts/check-dockerfiles.sh
|
|
|
|
openspec:
|
|
name: OpenSpec Validate
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: pnpm/action-setup@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
- run: pnpm install --frozen-lockfile
|
|
- run: pnpm openspec validate --all --strict --no-interactive
|
|
|
|
typecheck:
|
|
name: Typecheck
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: pnpm/action-setup@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
- run: pnpm install --frozen-lockfile
|
|
- run: pnpm typecheck
|
|
|
|
lint:
|
|
name: Lint
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: pnpm/action-setup@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
- run: pnpm install --frozen-lockfile
|
|
- run: pnpm lint
|
|
|
|
test:
|
|
name: Unit Tests
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: pnpm/action-setup@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
- run: pnpm install --frozen-lockfile
|
|
- run: pnpm test
|
|
|
|
build:
|
|
name: Build
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: pnpm/action-setup@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
- run: pnpm install --frozen-lockfile
|
|
- run: pnpm build
|
|
|
|
visual-tests:
|
|
name: Visual Tests
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: pnpm/action-setup@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
- run: pnpm install --frozen-lockfile
|
|
|
|
- name: Cache Playwright browsers
|
|
id: playwright-cache
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: ~/.cache/ms-playwright
|
|
key: playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
|
|
|
- name: Install Playwright Chromium
|
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
run: pnpm exec playwright install --with-deps chromium
|
|
|
|
- name: Install Playwright deps only
|
|
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
|
run: pnpm exec playwright install-deps chromium
|
|
|
|
- name: Run visual regression tests
|
|
run: pnpm --filter @trails-cool/planner test:visual
|
|
|
|
- name: Upload screenshots on failure
|
|
if: failure()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: visual-snapshots-diff
|
|
path: apps/planner/app/**/__screenshots__/
|
|
retention-days: 7
|
|
|
|
e2e:
|
|
name: E2E Tests
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
DATABASE_URL: postgres://trails:trails@localhost:5432/trails
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: pnpm/action-setup@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
- run: pnpm install --frozen-lockfile
|
|
|
|
- name: Cache PostGIS Docker image
|
|
id: postgis-cache
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: /tmp/postgis-image.tar
|
|
key: postgis-16-3.4
|
|
|
|
- name: Load or pull PostGIS image
|
|
run: |
|
|
if [ -f /tmp/postgis-image.tar ]; then
|
|
docker load < /tmp/postgis-image.tar
|
|
else
|
|
docker pull postgis/postgis:16-3.4
|
|
docker save postgis/postgis:16-3.4 > /tmp/postgis-image.tar
|
|
fi
|
|
|
|
- name: Start PostgreSQL
|
|
run: |
|
|
docker run -d --name postgres \
|
|
-e POSTGRES_USER=trails \
|
|
-e POSTGRES_PASSWORD=trails \
|
|
-e POSTGRES_DB=trails \
|
|
-p 5432:5432 \
|
|
postgis/postgis:16-3.4
|
|
# Wait for pg_isready
|
|
for i in $(seq 1 30); do
|
|
docker exec postgres pg_isready -U trails > /dev/null 2>&1 && break
|
|
sleep 1
|
|
done
|
|
# Wait for PostGIS extension to be ready
|
|
for i in $(seq 1 10); do
|
|
docker exec postgres psql -U trails -c "SELECT PostGIS_Version();" > /dev/null 2>&1 && break
|
|
sleep 1
|
|
done
|
|
|
|
- name: Push database schema
|
|
run: pnpm db:push
|
|
|
|
- name: Build and cache BRouter
|
|
id: brouter-cache
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: /tmp/brouter
|
|
key: brouter-1.7.8
|
|
|
|
- name: Download BRouter
|
|
if: steps.brouter-cache.outputs.cache-hit != 'true'
|
|
run: |
|
|
mkdir -p /tmp/brouter
|
|
wget -q "https://github.com/abrensch/brouter/releases/download/v1.7.8/brouter-1.7.8.zip" -O /tmp/brouter/brouter.zip
|
|
cd /tmp/brouter && unzip -o brouter.zip && mv brouter-1.7.8/* . && rmdir brouter-1.7.8 && rm brouter.zip
|
|
|
|
- name: Cache BRouter segment
|
|
id: segment-cache
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: /tmp/brouter-segments
|
|
key: brouter-segment-E10_N50
|
|
|
|
- name: Download Berlin segment
|
|
if: steps.segment-cache.outputs.cache-hit != 'true'
|
|
run: |
|
|
mkdir -p /tmp/brouter-segments
|
|
wget -q "https://brouter.de/brouter/segments4/E10_N50.rd5" -O /tmp/brouter-segments/E10_N50.rd5
|
|
|
|
- name: Start BRouter
|
|
run: |
|
|
cd /tmp/brouter
|
|
java -Xmx256M -Xms64M \
|
|
-DmaxRunningTime=300 \
|
|
-cp brouter-1.7.8-all.jar \
|
|
btools.server.RouteServer \
|
|
/tmp/brouter-segments profiles2 profiles2 \
|
|
17777 2 &
|
|
# Wait for BRouter to start
|
|
for i in $(seq 1 30); do
|
|
curl -sf http://localhost:17777/brouter?lonlats=13.4,52.5\|13.5,52.5\&profile=trekking\&format=geojson > /dev/null 2>&1 && break
|
|
sleep 2
|
|
done
|
|
env:
|
|
BROUTER_URL: http://localhost:17777
|
|
|
|
- name: Cache Playwright browsers
|
|
id: playwright-cache
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: ~/.cache/ms-playwright
|
|
key: playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
|
|
|
- name: Install Playwright
|
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
run: pnpm exec playwright install --with-deps chromium
|
|
|
|
- name: Install Playwright deps only
|
|
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
|
run: pnpm exec playwright install-deps chromium
|
|
|
|
- name: Build for production
|
|
run: pnpm build
|
|
env:
|
|
VITE_SENTRY_ENVIRONMENT: ci
|
|
|
|
- name: Run E2E tests
|
|
run: pnpm test:e2e
|
|
env:
|
|
BROUTER_URL: http://localhost:17777
|
|
E2E: "true"
|
|
|
|
- name: Playwright job summary
|
|
if: ${{ !cancelled() }}
|
|
run: |
|
|
if [ -f playwright-results.json ]; then
|
|
node -e "
|
|
const r = require('./playwright-results.json');
|
|
const s = r.stats;
|
|
const dur = (s.duration / 1000).toFixed(1);
|
|
let md = '## Playwright E2E Results\n\n';
|
|
md += '| Status | Count |\n|--------|-------|\n';
|
|
md += '| :white_check_mark: Passed | ' + s.expected + ' |\n';
|
|
if (s.unexpected > 0) md += '| :x: Failed | ' + s.unexpected + ' |\n';
|
|
if (s.flaky > 0) md += '| :warning: Flaky | ' + s.flaky + ' |\n';
|
|
if (s.skipped > 0) md += '| :fast_forward: Skipped | ' + s.skipped + ' |\n';
|
|
md += '| :stopwatch: Duration | ' + dur + 's |\n\n';
|
|
for (const file of r.suites) {
|
|
for (const describe of (file.suites || [])) {
|
|
md += '### ' + describe.title + '\n\n';
|
|
for (const spec of (describe.specs || [])) {
|
|
const icon = spec.ok ? ':white_check_mark:' : ':x:';
|
|
const t = spec.tests?.[0]?.results?.[0]?.duration;
|
|
md += '- ' + icon + ' ' + spec.title + (t ? ' (' + t + 'ms)' : '') + '\n';
|
|
}
|
|
md += '\n';
|
|
}
|
|
}
|
|
require('fs').appendFileSync(process.env.GITHUB_STEP_SUMMARY, md);
|
|
"
|
|
fi
|
|
|
|
- uses: actions/upload-artifact@v7
|
|
if: ${{ !cancelled() }}
|
|
with:
|
|
name: playwright-report
|
|
path: playwright-report/
|
|
retention-days: 30
|