The bind mount override (docker-compose.ci.yml) failed because file permissions on the cached segment prevented the entrypoint from seeing the file, causing a fresh download on every CI run. Instead: create the named volume and copy the cached segment into it before compose starts, using alpine with explicit chmod. The entrypoint then finds the segment and skips the download, so BRouter starts in ~4s instead of ~2min. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
295 lines
9.2 KiB
YAML
295 lines
9.2 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 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: Pre-seed BRouter segment volume
|
|
run: |
|
|
docker volume create trails_brouter_segments
|
|
docker run --rm \
|
|
-v /tmp/brouter-segments:/src:ro \
|
|
-v trails_brouter_segments:/dst \
|
|
alpine sh -c "cp /src/*.rd5 /dst/ && chmod a+r /dst/*.rd5"
|
|
|
|
- name: Start services
|
|
run: docker compose -f docker-compose.dev.yml up -d --wait --build
|
|
env:
|
|
BROUTER_URL: http://localhost:17777
|
|
|
|
- name: Wait for BRouter routing
|
|
run: |
|
|
for i in $(seq 1 60); do
|
|
curl -sf 'http://localhost:17777/brouter?lonlats=13.4,52.5|13.5,52.5&profile=trekking&format=geojson' > /dev/null 2>&1 && echo "BRouter ready" && break
|
|
[ "$i" = "60" ] && echo "BRouter not ready after 120s" && exit 1
|
|
sleep 2
|
|
done
|
|
|
|
- name: Push database schema
|
|
run: pnpm db:push
|
|
|
|
- name: Seed database
|
|
run: pnpm db:seed
|
|
|
|
- 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
|