The five \`*.integration.test.ts\` files in apps/journal (explore, follow,
demo-bot, notifications, notifications-fanout — 31 tests total) were
gated behind \`EXPLORE_INTEGRATION=1\` / \`FOLLOW_INTEGRATION=1\` /
\`DEMO_BOT_INTEGRATION=1\` / \`NOTIFICATIONS_INTEGRATION=1\`. The unit-test
job doesn't have Postgres, so they correctly skipped there — but no
CI job set the env vars, so they were effectively dead code.
Added a step in the E2E job (which already has Postgres + schema
pushed) that flips all four gates and runs the integration files
serially (\`--no-file-parallelism\` — they share the schema and trip FK
constraints if run in parallel; ~2.5s sequential anyway).
Wiring them up surfaced two real issues, both fixed here:
1. **\`createRoute\` silently dropped \`input.visibility\`** — every
caller passing \`visibility: \"public\"\` (including the demo-bot)
was getting the column's \`private\` default. Spread now mirrors
\`createActivity\`'s pattern. Demo-bot routes have actually been
private in production all this time — they were rendering on the
home feed only via the \`activity_published\` fan-out from the
*activity*, not as visible *routes*.
2. **The demo-bot test's fetch stub was incomplete** — it stubbed
\`.text()\` but the planner-session preflight calls \`.json()\`. The
stub now branches on URL: \`/api/sessions\` returns
\`{ sessionId: 'test-session' }\` JSON, the BRouter call returns the
stub GPX text.
Full repo: pnpm typecheck, pnpm lint, pnpm test all green
(177 unit-test pass + 31 integration pass = 208 total, no skips).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
318 lines
11 KiB
YAML
318 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 BRouter segment
|
|
id: segment-cache
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: /tmp/brouter-segments
|
|
key: brouter-segment-E10_N50-v1.7.9
|
|
|
|
- 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 -s 'http://localhost:17777/brouter?lonlats=13.4,52.5|13.5,52.5&profile=trekking&format=geojson' 2>/dev/null | grep -q "FeatureCollection" && 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: Run integration tests
|
|
# These talk to real Postgres. The unit-test job has no DB so
|
|
# the `*.integration.test.ts` files skip there; this job has
|
|
# the DB up + schema pushed, so flip the gate env vars to "1"
|
|
# and let them run. Each gate is read by one file — see
|
|
# `runIntegration` in each test.
|
|
#
|
|
# --no-file-parallelism: integration tests share the journal
|
|
# schema and clean up by `DELETE FROM ... WHERE email LIKE
|
|
# '%@example.test'`. Parallel files step on each other's rows
|
|
# and trip FK constraints. Running sequentially is still <3s.
|
|
run: pnpm --filter @trails-cool/journal exec vitest run --no-file-parallelism --reporter=default app/lib/explore.integration.test.ts app/lib/follow.integration.test.ts app/lib/demo-bot.integration.test.ts app/lib/notifications.integration.test.ts app/jobs/notifications-fanout.integration.test.ts
|
|
env:
|
|
EXPLORE_INTEGRATION: "1"
|
|
FOLLOW_INTEGRATION: "1"
|
|
DEMO_BOT_INTEGRATION: "1"
|
|
NOTIFICATIONS_INTEGRATION: "1"
|
|
|
|
- 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 is the explicit opt-out from the fail-loud
|
|
# requireSecret() / getDatabaseUrl() guards — playwright boots
|
|
# the server via `react-router serve` (NODE_ENV=production) but
|
|
# against the local dev Postgres + local cookie secrets.
|
|
E2E: "true"
|
|
INTEGRATION_SECRET: ${{ secrets.INTEGRATION_SECRET }}
|
|
|
|
- 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
|