ci(forgejo): port full CI + CD workflows to Forgejo Actions
Some checks failed
CI / Dockerfile Package Check (push) Successful in 42s
CI / Security Scan (push) Failing after 1m7s
CI / OpenSpec Validate (push) Successful in 2m31s
CI / Typecheck (push) Successful in 5m16s
CI / Unit Tests (push) Failing after 5m50s
CI / Build (push) Successful in 4m30s
CI / Lint (push) Successful in 19m28s
CI / Journal Image Smoke Test (push) Failing after 9m17s
CI / E2E Tests (push) Failing after 2m15s
CI / Visual Tests (push) Successful in 16m22s
Some checks failed
CI / Dockerfile Package Check (push) Successful in 42s
CI / Security Scan (push) Failing after 1m7s
CI / OpenSpec Validate (push) Successful in 2m31s
CI / Typecheck (push) Successful in 5m16s
CI / Unit Tests (push) Failing after 5m50s
CI / Build (push) Successful in 4m30s
CI / Lint (push) Successful in 19m28s
CI / Journal Image Smoke Test (push) Failing after 9m17s
CI / E2E Tests (push) Failing after 2m15s
CI / Visual Tests (push) Successful in 16m22s
- ci.yml: all 10 jobs (security/dockerfile/openspec/typecheck/lint/test/ build/visual/e2e/image-smoke); drop merge_group, gitleaks via full URL, visual-diff PR comment via Forgejo API. - cd-apps/cd-brouter/cd-infra/cd-staging: drop GitHub Environments, GHCR push auth via DEPLOY_GHCR_TOKEN. cd-staging flags Forgejo PR-event and peter-evans comment-action risks for validation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
db8a3bf2ad
commit
7e6d4cbcb2
5 changed files with 1177 additions and 4 deletions
|
|
@ -1,15 +1,69 @@
|
|||
name: CI
|
||||
# Forgejo Actions port — proof workflow. Once green, the remaining jobs from
|
||||
# .github/workflows/ci.yml (lint, test, build, e2e, visual, image-smoke, security)
|
||||
# get ported here. NOTE: Forgejo uses .forgejo/workflows in preference to
|
||||
# .github/workflows, so the GitHub copies do not double-run here.
|
||||
# Ported from .github/workflows/ci.yml for Forgejo Actions.
|
||||
# Differences from the GitHub version:
|
||||
# - no `merge_group` trigger (Forgejo has no merge queue)
|
||||
# - gitleaks action referenced by full GitHub URL (custom action, not on
|
||||
# the Forgejo actions mirror)
|
||||
# - the visual-diff PR comment posts via the Forgejo API instead of `gh`
|
||||
# The `ci/forgejo-actions` push branch is temporary (lets this branch re-run
|
||||
# CI pre-merge); drop it once merged to main.
|
||||
on:
|
||||
push:
|
||||
branches: [main, ci/forgejo-actions]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
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@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Gitleaks
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
uses: https://github.com/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@v7
|
||||
- run: bash scripts/check-dockerfiles.sh
|
||||
|
||||
openspec:
|
||||
name: OpenSpec Validate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- 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
|
||||
|
|
@ -22,3 +76,302 @@ jobs:
|
|||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm typecheck
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- 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@v7
|
||||
- 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@v7
|
||||
- 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@v7
|
||||
- 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@v6
|
||||
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:
|
||||
TOKEN: ${{ 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="${{ github.server_url }}/${{ 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+="\`\`\`"
|
||||
|
||||
# Forgejo API (GitHub-compatible issues/comments endpoint).
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${{ github.api_url }}/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
|
||||
-d "$(jq -n --arg b "$body" '{body:$b}')" || true
|
||||
|
||||
- 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@v7
|
||||
- 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@v6
|
||||
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
|
||||
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@v6
|
||||
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"
|
||||
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
|
||||
|
||||
journal-image-smoke:
|
||||
name: Journal Image Smoke Test
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: imresamu/postgis:16-3.4
|
||||
env:
|
||||
POSTGRES_USER: trails
|
||||
POSTGRES_PASSWORD: trails
|
||||
POSTGRES_DB: trails
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U trails"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 20
|
||||
env:
|
||||
DATABASE_URL: postgres://trails:trails@localhost:5432/trails
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Push database schema
|
||||
run: pnpm db:push
|
||||
|
||||
- name: Build journal runtime image
|
||||
run: docker build --target runtime -f apps/journal/Dockerfile -t journal-smoke .
|
||||
|
||||
- name: Boot image and wait for healthy
|
||||
run: |
|
||||
docker run -d --name journal-smoke --network host \
|
||||
-e NODE_ENV=production -e E2E=true \
|
||||
-e DATABASE_URL="$DATABASE_URL" \
|
||||
journal-smoke
|
||||
code=000
|
||||
for i in $(seq 1 30); do
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 http://localhost:3000/api/health || echo 000)
|
||||
echo "attempt $i: /api/health -> $code"
|
||||
[ "$code" = "200" ] && break
|
||||
if [ "$(docker inspect -f '{{.State.Running}}' journal-smoke 2>/dev/null)" != "true" ]; then
|
||||
echo "::error::journal container exited during boot"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [ "$code" != "200" ]; then
|
||||
echo "::error::journal production image failed to boot healthy (see logs below)"
|
||||
docker logs journal-smoke 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "journal production image booted healthy"
|
||||
|
||||
- name: Container logs
|
||||
if: always()
|
||||
run: docker logs journal-smoke 2>&1 | tail -40 || true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue