trails/.github/workflows/ci.yml
2026-05-10 21:53:17 +02:00

337 lines
11 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
permissions:
contents: read
pull-requests: write
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
id: visual-tests
run: pnpm --filter @trails-cool/planner test:visual
- name: Post diff comment on PR
if: failure() && steps.visual-tests.outcome == 'failure' && github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
diffs=$(find apps/planner/.vitest-attachments -name "*-diff-*.png" 2>/dev/null | sort)
if [ -z "$diffs" ]; then exit 0; fi
artifact_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
body="## Visual regression failures"$'\n\n'
body+="The following tests produced screenshot diffs:"$'\n\n'
for diff in $diffs; do
name=$(basename "$diff" | sed 's/-diff-chromium-[a-z]*\.png//' | sed 's/-/ /g')
body+="- \`$name\`"$'\n'
done
body+=$'\n'"**[Download the \`visual-snapshots-diff\` artifact]($artifact_url)** to inspect the diffs locally."$'\n\n'
body+="To update snapshots if the change is intentional:"$'\n'
body+="\`\`\`"$'\n'
body+="pnpm --filter @trails-cool/planner test:visual:update"$'\n'
body+="\`\`\`"
gh pr comment ${{ github.event.pull_request.number }} --body "$body"
- name: Upload screenshots on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: visual-snapshots-diff
path: apps/planner/.vitest-attachments/
include-hidden-files: true
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