dorfteich/.gitea/workflows/ci.yml
Claude Opus 4.8 7f1c49db53
All checks were successful
CD / Build and push images (push) Successful in 3m3s
CI / Lint, typecheck, test (push) Successful in 2m29s
CI / Auth e2e pack (push) Successful in 3m8s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m12s
CD / Promote to Int (push) Successful in 12s
Add pond member management UI (#54)
Pond Admins manage who participates in a pond, by role, with editor/reader
seat quotas — the member-facing layer over the grant model (#51/#52).

- shared: `MemberView`/`PondMembersView` + add/change-role schemas
  (`members.ts`), a `members` i18n namespace (de+en), and member error codes.
- api `members/`: a member-centric API over pond-scope user grants —
  `GET /ponds/:id/members` (any member, for transparency: list grouped by
  effective role + seat usage + `canManage`), `POST` (add by exact username or
  e-mail — no directory browsing), `PATCH :userId` (change role), `DELETE
  :userId` (remove), all Pond-Admin-gated by the guard. Editor/reader seats are
  enforced against `editors_per_pond`/`readers_per_pond` (#22) inside a
  per-pond advisory-locked transaction so counts cannot race; the owner's
  membership is protected, personal ponds refuse a second admin (shared grant
  rule), and the last Pond Admin cannot be dropped. Every change invalidates
  the pond permission cache and fires the access NOTIFY (#39/#53).
- web `members/`: `MemberManager` in Pond Settings — list grouped by role with
  a search filter and seat usage, add-by-identifier form (disabled with a
  localized explanation when the chosen role's seats are full), per-member role
  change and remove; read-only for non-admins; the personal-pond rule is
  surfaced. There is no invitation flow (v1): adding is immediate, and the copy
  says so.
- tests: `members.e2e.db.test.ts` (add/change/remove, seat exhaustion,
  personal-pond and owner rules, read-only transparency, last-admin) and a
  `members` browser pack (immediate second-browser access, quota disables the
  add action, non-admin read-only) with its own CI step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-09 20:22:52 +02:00

263 lines
9.5 KiB
YAML

# CI: every pull request and every push to main must pass these checks
# (ADR 0014). The deploy pipeline (CD) lives in cd.yml and only runs on
# main after this workflow's quality bar.
#
# Runner requirements: an act_runner with the `ubuntu-latest` label and
# Docker access (see deploy/stages.md, issue #9).
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
checks:
name: Lint, typecheck, test
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17.5-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
env:
# Enables the database-backed test suites (vitest.global-setup.ts).
TEST_DATABASE_URL: postgresql://test:test@postgres:5432/test
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Build first: package type checks resolve @dorfteich/shared through
# its built dist, and i18n:check imports the built helpers.
- name: Build all packages
run: pnpm build
- name: Lint (ESLint + Prettier)
run: pnpm lint
- name: Typecheck
run: pnpm typecheck
- name: Unit and integration tests
run: pnpm test
- name: Translation key parity (de/en)
run: pnpm i18n:check
# Job id/display name kept stable (branch-protection/status-check
# matching uses the reported "CI / Auth e2e pack" context) even though
# it now also runs the content pack (issue #32) against the same
# already-built-and-seeded stack, instead of spinning up a second one.
auth-e2e:
name: Auth e2e pack
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17.5-alpine
env:
POSTGRES_USER: e2e
POSTGRES_PASSWORD: e2e
POSTGRES_DB: e2e
mailpit:
image: axllent/mailpit:latest
env:
DATABASE_URL: postgresql://e2e:e2e@postgres:5432/e2e
APP_BASE_URL: http://localhost:5173
SMTP_HOST: mailpit
SMTP_PORT: '1025'
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm build
- name: Apply migrations
run: pnpm --filter @dorfteich/api exec prisma migrate deploy
- name: Seed fixtures
run: pnpm --filter @dorfteich/api db:seed
- name: Start api, collab, and static web server
run: |
(cd apps/api && PORT=3001 node dist/main.js > /tmp/api.log 2>&1 &)
(cd apps/collab && PORT=3002 node dist/index.js > /tmp/collab.log 2>&1 &)
(PORT=5173 COLLAB_TARGET=http://127.0.0.1:3002 node scripts/e2e-static-server.mjs > /tmp/web.log 2>&1 &)
for i in $(seq 1 30); do
curl -sf http://localhost:3001/api/v1/readyz >/dev/null && break
sleep 2
done
for i in $(seq 1 30); do
curl -sf http://localhost:3002/healthz >/dev/null && break
sleep 2
done
curl -sf http://localhost:5173/ >/dev/null
- name: Install Playwright browser
run: pnpm --filter @dorfteich/web exec playwright install --with-deps chromium
- name: Run auth pack
run: |
E2E_BASE_URL=http://localhost:5173 E2E_MAILPIT_URL=http://mailpit:8025 \
pnpm --filter @dorfteich/web exec playwright test e2e/auth.spec.ts
# auth.spec.ts's own logins (contextForUser per test) already spend
# a good chunk of the login rate limit (10/min/IP, operations.md) —
# reset it before content.spec.ts's five more, or its later tests
# 429 (found by reproducing this job's failure locally, issue #32).
- name: Reset login rate limit between e2e packs
run: |
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
- name: Run content pack
run: |
E2E_BASE_URL=http://localhost:5173 \
pnpm --filter @dorfteich/web exec playwright test e2e/content.spec.ts
# The collab pack opens two browser contexts per test (more logins),
# so reset the login rate limit before it as well (see note above).
- name: Reset login rate limit before collab pack
run: |
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
- name: Run collab pack
run: |
E2E_BASE_URL=http://localhost:5173 \
pnpm --filter @dorfteich/web exec playwright test e2e/collab.spec.ts
# Two contexts per test (owner + a second regular account) and grant
# changes → reset the login rate limit first (see note above).
- name: Reset login rate limit before collab-permissions pack
run: |
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
- name: Run collab-permissions pack
run: |
E2E_BASE_URL=http://localhost:5173 \
pnpm --filter @dorfteich/web exec playwright test e2e/collab-permissions.spec.ts
# Two contexts per test (owner + a second regular account) → reset first.
- name: Reset login rate limit before members pack
run: |
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
- name: Run members pack
run: |
E2E_BASE_URL=http://localhost:5173 \
pnpm --filter @dorfteich/web exec playwright test e2e/members.spec.ts
- name: Reset login rate limit before offline pack
run: |
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
- name: Run offline pack
run: |
E2E_BASE_URL=http://localhost:5173 \
pnpm --filter @dorfteich/web exec playwright test e2e/offline.spec.ts
- name: Reset login rate limit before labels pack
run: |
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
- name: Run labels pack
run: |
E2E_BASE_URL=http://localhost:5173 \
pnpm --filter @dorfteich/web exec playwright test e2e/labels.spec.ts
- name: Reset login rate limit before reorder pack
run: |
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
- name: Run reorder pack
run: |
E2E_BASE_URL=http://localhost:5173 \
pnpm --filter @dorfteich/web exec playwright test e2e/reorder.spec.ts
- name: Reset login rate limit before wikilink pack
run: |
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
- name: Run wikilink pack
run: |
E2E_BASE_URL=http://localhost:5173 \
pnpm --filter @dorfteich/web exec playwright test e2e/wikilink.spec.ts
- name: Reset login rate limit before backlinks pack
run: |
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
- name: Run backlinks pack
run: |
E2E_BASE_URL=http://localhost:5173 \
pnpm --filter @dorfteich/web exec playwright test e2e/backlinks.spec.ts
- name: Reset login rate limit before search pack
run: |
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
- name: Run search pack
run: |
E2E_BASE_URL=http://localhost:5173 \
pnpm --filter @dorfteich/web exec playwright test e2e/search.spec.ts
- name: Dump server logs on failure
if: failure()
run: tail -50 /tmp/api.log /tmp/collab.log /tmp/web.log || true
images:
name: Build container images
# PR-only: on main the CD workflow builds and pushes the same images —
# building twice would waste the runner (ADR 0014: build once, promote).
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
# PRs prove the Dockerfiles still build; pushing happens in cd.yml.
- name: Build web image
run: docker build -f apps/web/Dockerfile --build-arg APP_VERSION=${{ github.sha }} -t dorfteich-web:ci .
- name: Build api image
run: docker build -f apps/api/Dockerfile --build-arg APP_VERSION=${{ github.sha }} -t dorfteich-api:ci .
- name: Build collab image
run: docker build -f apps/collab/Dockerfile --build-arg APP_VERSION=${{ github.sha }} -t dorfteich-collab:ci .