Files
CapaKraken/.github/workflows/ci.yml
T
Hartmut 0b2d263d30
CI / Architecture Guardrails (push) Successful in 2m54s
CI / Typecheck (push) Successful in 3m38s
CI / Lint (push) Successful in 3m56s
CI / Assistant Split Regression (push) Successful in 4m17s
CI / Unit Tests (push) Successful in 6m32s
CI / Build (push) Successful in 6m8s
CI / E2E Tests (push) Failing after 4m37s
CI / Fresh-Linux Docker Deploy (push) Failing after 6m7s
CI / Release Images (push) Has been skipped
ci: use prisma db execute (no psql dep); baseline migrations after push
- e2e: switch schema reset + sanity check from psql (not installed in
  act_runner's catthehacker/ubuntu image) to `prisma db execute --stdin`
  which is already a dev dep.
- docker-deploy: after `db push` the schema matches schema.prisma but
  _prisma_migrations is empty, so the follow-up `migrate deploy` fails
  with P3005. Baseline each migration directory as applied via
  `prisma migrate resolve --applied` before deploy; the migrations
  themselves are idempotent supplements, so marking-as-applied is safe.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 23:01:51 +02:00

485 lines
17 KiB
YAML

name: CI
# Retrigger marker: da0d69c (QNAP runner DNS fix applied)
on:
push:
branches: [main]
paths-ignore:
- "docs/**"
- ".gitea/**"
- "**/*.md"
- "LICENSE"
pull_request:
branches: [main]
paths-ignore:
- "docs/**"
- ".gitea/**"
- "**/*.md"
- "LICENSE"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: "20"
PNPM_VERSION: "9.14.2"
CI_AUTH_URL: http://localhost:3100
# Placeholder for CI — real secret only matters at deploy time.
# next build collects page data for auth routes and aborts if empty.
CI_AUTH_SECRET: ci-test-secret-minimum-32-chars-xx
jobs:
guardrails:
name: Architecture Guardrails
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
run: npm install -g pnpm@${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4.0.4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run repo script tests
run: pnpm test:scripts
- name: Check architecture guardrails
run: pnpm check:architecture
- name: Check workspace exports
run: pnpm check:exports
- name: Check workspace imports
run: pnpm check:imports
- name: Security audit (high+ severity)
run: pnpm audit --audit-level=high
# ──────────────────────────────────────────────
# Typecheck — ~40s, no services needed
# ──────────────────────────────────────────────
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
run: npm install -g pnpm@${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4.0.4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate Prisma client
run: pnpm db:generate
- name: Cache Turborepo
uses: actions/cache@v4
continue-on-error: true
with:
path: .turbo
key: turbo-typecheck-${{ github.sha }}
restore-keys: turbo-typecheck-
- name: Run typecheck
run: pnpm typecheck
assistant-split:
name: Assistant Split Regression
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
run: npm install -g pnpm@${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4.0.4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate Prisma client
run: pnpm db:generate
- name: Run assistant split regression
run: pnpm --filter @capakraken/api test:assistant-split
# ──────────────────────────────────────────────
# Lint — ~20s, no services needed
# ──────────────────────────────────────────────
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
run: npm install -g pnpm@${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4.0.4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate Prisma client
run: pnpm db:generate
- name: Cache Turborepo
uses: actions/cache@v4
continue-on-error: true
with:
path: .turbo
key: turbo-lint-${{ github.sha }}
restore-keys: turbo-lint-
- name: Run lint
run: pnpm lint
# ──────────────────────────────────────────────
# Unit tests — needs PostgreSQL + Redis
# ──────────────────────────────────────────────
test:
name: Unit Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: capakraken_test
POSTGRES_USER: capakraken
POSTGRES_PASSWORD: capakraken_test
options: >-
--health-cmd="pg_isready -U capakraken -d capakraken_test"
--health-interval=10s
--health-timeout=5s
--health-retries=5
redis:
image: redis:7
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
env:
DATABASE_URL: postgresql://capakraken:capakraken_test@postgres:5432/capakraken_test
REDIS_URL: redis://redis:6379
# Force in-memory rate limiter to avoid cross-test state when Redis drops.
# Redis fallback downgrades to max/10 limits which rate-limits unit tests.
RATE_LIMIT_BACKEND: memory
# Tests assume Europe/Berlin for month-boundary math (new Date(y,m,1)).
TZ: Europe/Berlin
NEXTAUTH_URL: ${{ env.CI_AUTH_URL }}
AUTH_URL: ${{ env.CI_AUTH_URL }}
NEXTAUTH_SECRET: ${{ env.CI_AUTH_SECRET }}
AUTH_SECRET: ${{ env.CI_AUTH_SECRET }}
steps:
- uses: actions/checkout@v4
- name: Install pnpm
run: npm install -g pnpm@${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4.0.4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate Prisma client
run: pnpm db:generate
- name: Run unit tests with coverage
run: |
pnpm --filter @capakraken/web test:unit -- --coverage
pnpm --filter @capakraken/engine exec vitest run --coverage
pnpm --filter @capakraken/staffing exec vitest run --coverage
pnpm --filter @capakraken/api exec vitest run --coverage
pnpm --filter @capakraken/application exec vitest run --coverage
pnpm --filter @capakraken/shared exec vitest run --coverage
pnpm --filter @capakraken/db test:unit
- name: Upload coverage reports
uses: actions/upload-artifact@v4
continue-on-error: true # upload-artifact@v4 unsupported on Gitea (GHES) runner
if: ${{ !cancelled() }}
with:
name: coverage-reports
path: |
apps/web/coverage/
packages/engine/coverage/
packages/staffing/coverage/
packages/api/coverage/
packages/application/coverage/
packages/shared/coverage/
retention-days: 14
# ──────────────────────────────────────────────
# Build — depends on typecheck passing
# ──────────────────────────────────────────────
build:
name: Build
needs: [guardrails, typecheck]
runs-on: ubuntu-latest
env:
DATABASE_URL: postgresql://placeholder:placeholder@localhost:5432/placeholder
REDIS_URL: redis://placeholder:6379
NEXTAUTH_URL: ${{ env.CI_AUTH_URL }}
AUTH_URL: ${{ env.CI_AUTH_URL }}
NEXTAUTH_SECRET: ${{ env.CI_AUTH_SECRET }}
AUTH_SECRET: ${{ env.CI_AUTH_SECRET }}
steps:
- uses: actions/checkout@v4
- name: Install pnpm
run: npm install -g pnpm@${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4.0.4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate Prisma client
run: pnpm db:generate
- name: Cache Turborepo
uses: actions/cache@v4
continue-on-error: true
with:
path: .turbo
key: turbo-build-${{ github.sha }}
restore-keys: turbo-build-
- name: Cache Next.js build
uses: actions/cache@v4
continue-on-error: true
with:
path: apps/web/.next/cache
key: nextjs-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: nextjs-${{ hashFiles('pnpm-lock.yaml') }}-
- name: Build
run: pnpm --filter @capakraken/web exec next build
# ──────────────────────────────────────────────
# E2E — depends on build, needs PostgreSQL + Redis
# ──────────────────────────────────────────────
e2e:
name: E2E Tests
needs: [build]
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: capakraken_test
POSTGRES_USER: capakraken
POSTGRES_PASSWORD: capakraken_test
options: >-
--health-cmd="pg_isready -U capakraken -d capakraken_test"
--health-interval=10s
--health-timeout=5s
--health-retries=5
redis:
image: redis:7
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
env:
DATABASE_URL: postgresql://capakraken:capakraken_test@postgres:5432/capakraken_test
# Playwright test-server.mjs requires an explicit test DB URL.
PLAYWRIGHT_DATABASE_URL: postgresql://capakraken:capakraken_test@postgres:5432/capakraken_test
# prisma-with-env.mjs refuses to run unless DATABASE_URL's db name matches
# the expected target; default is "capakraken", CI uses capakraken_test.
CAPAKRAKEN_EXPECTED_DB_NAME: capakraken_test
ALLOW_DESTRUCTIVE_DB_TOOLS: "true"
CONFIRM_DESTRUCTIVE_DB_NAME: capakraken_test
REDIS_URL: redis://redis:6379
PORT: 3100
NEXTAUTH_URL: ${{ env.CI_AUTH_URL }}
AUTH_URL: ${{ env.CI_AUTH_URL }}
NEXTAUTH_SECRET: ${{ env.CI_AUTH_SECRET }}
AUTH_SECRET: ${{ env.CI_AUTH_SECRET }}
steps:
- uses: actions/checkout@v4
- name: Install pnpm
run: npm install -g pnpm@${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4.0.4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate Prisma client
run: pnpm db:generate
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v4
continue-on-error: true
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('apps/web/package.json') }}
restore-keys: playwright-
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm --filter @capakraken/web exec playwright install --with-deps chromium
- name: Install Playwright system deps
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: pnpm --filter @capakraken/web exec playwright install-deps chromium
- name: Push DB schema & seed
run: |
# Nuke any leftover schema state from a previous job that shared the
# postgres service container (act_runner reuses service volumes).
# --force-reset alone proved unreliable: push reported "in sync" but
# audit_logs ended up missing. Use prisma db execute (no psql dep).
printf '%s\n' \
'DROP SCHEMA IF EXISTS public CASCADE;' \
'CREATE SCHEMA public;' \
'GRANT ALL ON SCHEMA public TO capakraken;' \
'GRANT ALL ON SCHEMA public TO public;' \
| pnpm --filter @capakraken/db exec prisma db execute --stdin --schema ./prisma/schema.prisma
pnpm --filter @capakraken/db exec prisma db push --schema ./prisma/schema.prisma --accept-data-loss --skip-generate
# Sanity check: confirm the schema actually landed before seeding.
printf "SELECT to_regclass('public.audit_logs'), to_regclass('public.users');\n" \
| pnpm --filter @capakraken/db exec prisma db execute --stdin --schema ./prisma/schema.prisma
pnpm db:seed
- name: Run E2E tests
run: pnpm test:e2e
- name: Upload Playwright report
uses: actions/upload-artifact@v4
continue-on-error: true # upload-artifact@v4 unsupported on Gitea (GHES) runner
if: ${{ !cancelled() }}
with:
name: playwright-report
path: apps/web/playwright-report/
retention-days: 14
# ──────────────────────────────────────────────
# Fresh Docker Compose deploy test — validates
# that the prod compose bundle comes up clean
# from scratch and the smoke tests pass.
# ──────────────────────────────────────────────
docker-deploy-test:
name: Fresh-Linux Docker Deploy
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create minimal .env
run: |
cat <<'EOF' > .env
NEXTAUTH_URL=http://localhost:3100
NEXTAUTH_SECRET=ci-test-secret-minimum-32-chars-xx
PGADMIN_PASSWORD=ci-pgadmin
EOF
- name: Tear down any stale stack & volumes
# act_runner on self-hosted QNAP keeps named compose volumes between
# runs. A previous run's failed migration entry in _prisma_migrations
# causes P3009 on the next migrate deploy; wipe volumes for a truly
# fresh deploy test every time.
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml down -v --remove-orphans || true
- name: Start infrastructure (postgres + redis)
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d postgres redis
- name: Wait for postgres
run: |
for i in $(seq 1 20); do
docker compose -f docker-compose.yml -f docker-compose.ci.yml exec -T postgres pg_isready -U capakraken -d capakraken && break
sleep 3
done
- name: Build and start app (full profile)
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile full up -d --build app
- name: Wait for /api/health (up to 3 minutes)
run: |
for i in $(seq 1 36); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3100/api/health || echo "000")
echo "Attempt $i: HTTP $STATUS"
if [ "$STATUS" = "200" ]; then exit 0; fi
sleep 5
done
echo "Health check timed out"
docker compose -f docker-compose.yml -f docker-compose.ci.yml logs app --tail=50
exit 1
- name: Verify health response contains status ok
run: |
BODY=$(curl -sf http://localhost:3100/api/health)
echo "$BODY"
echo "$BODY" | grep '"status":"ok"'
- name: Seed admin user
run: |
docker compose -f docker-compose.yml -f docker-compose.ci.yml exec -T app node /app/scripts/setup-admin.mjs \
--email admin@capakraken.dev \
--name "Admin" \
--password admin123
- name: Set up Node.js 20
uses: actions/setup-node@v4.0.4
with:
node-version: "20"
- name: Install Playwright and Chromium
run: |
npm install -g @playwright/test@1.49
playwright install chromium --with-deps
- name: Run smoke tests
run: npx playwright test --config apps/web/playwright.ci.config.ts
- name: Upload Playwright report
if: failure()
continue-on-error: true # upload-artifact@v4 unsupported on Gitea (GHES) runner
uses: actions/upload-artifact@v4
with:
name: playwright-smoke-report
path: apps/web/playwright-report/
retention-days: 7
- name: Show logs on failure
if: failure()
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml logs --tail=100
# ──────────────────────────────────────────────
# Release images — only on push to main, after
# every check has passed. Calls the reusable
# release-image.yml workflow.
# ──────────────────────────────────────────────
release-images:
name: Release Images
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [lint, test, e2e, assistant-split, docker-deploy-test]
uses: ./.github/workflows/release-image.yml
secrets: inherit