name: CI on: push: branches: [main] pull_request: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: security: name: Security Scan runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Gitleaks uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: pnpm/action-setup@v4 - 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 typecheck: name: Typecheck runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 - 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@v4 - 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@v4 - 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@v4 - uses: actions/setup-node@v6 with: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm build 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@v4 - 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@v4 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@v4 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@v4 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@v4 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 - 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