Any authenticated user can invite an e-mail address; the mailed single-use token lets exactly one signup through even while registration is closed. Open (pending, unexpired) invitations count against the new instance setting invitations.maxOpenPerUser (default 5, 0 disables inviting) — plus a 20/day per-user rate limit so a revoke-and-recreate loop cannot become a mail cannon. Only the SHA-256 token hash is stored (auth-tokens pattern); a failed signup (taken username) un-redeems the token so the invitee can retry. Surfaces: invitations section in the user settings (list, invite, revoke, quota line; wide table in a focusable .table-scroll region), signup page reads ?invitation=<token> (preview banner, e-mail prefill, closed-mode gate opens only for a previewed-valid token), admin general card gets the quota field (flat RHF name per #322; VS-NfD marked and hideable). Governance: audit actions invitation.created/revoked/accepted (catalogue 1.10), VS-NfD profile entry (compliant: 0) + hardening-guide row, i18n de+en including the invitation mail template. Tests: api e2e-db (mail link, closed-mode single-use signup with un-redeem on failure, quota + revoke frees slot, quota 0 = 403, auth matrix), new web e2e pack invitations.spec.ts (full UI loop through Mailpit, wired into ci.yml with its own rate-limit reset), a11y scan waits for the new section. Full api suite (107 files / 607 tests), auth/admin-settings/a11y packs green against a fresh local stack. Closes #332
823 lines
35 KiB
YAML
823 lines
35 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:
|
||
# The pnpm jobs are chained (checks -> auth-e2e -> fidelity) so no two run
|
||
# at once. At runner capacity > 1 they otherwise start together and extract
|
||
# the same actions (setup-node, pnpm/action-setup) into the shared offline
|
||
# cache (`/run/act/actions`) concurrently, reading it half-written — a flaky
|
||
# "Cannot find module .../dist/…" in "Set up Node.js"/"Set up pnpm". Warming
|
||
# the cache first did not help (concurrent extraction still corrupts it);
|
||
# only serialization does. The CD image builds still run in parallel, so the
|
||
# runner's spare capacity is not wasted.
|
||
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
|
||
|
||
# Fails if a real .env (anything but .env.example) is ever tracked, or
|
||
# if a tracked file matches an obvious secret pattern (issue #198).
|
||
# .env.example is the authoritative reference; real values never enter
|
||
# the repository (docs/self-hosting/README.md).
|
||
- name: No tracked .env files or secret material
|
||
run: |
|
||
set -euo pipefail
|
||
bad_env=$(git ls-files | grep -E '(^|/)\.env(\.[^/]*)?$' | grep -v '\.env\.example$' || true)
|
||
if [ -n "$bad_env" ]; then
|
||
echo "tracked .env file(s) — only .env.example may be tracked:"
|
||
echo "$bad_env"
|
||
exit 1
|
||
fi
|
||
secrets=$(git grep -nIE -e '-----BEGIN [A-Z ]*PRIVATE KEY-----|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|glpat-[A-Za-z0-9_-]{20}|xox[baprs]-[0-9A-Za-z-]{10}' -- . || true)
|
||
if [ -n "$secrets" ]; then
|
||
echo "tracked file matches a secret pattern:"
|
||
echo "$secrets"
|
||
exit 1
|
||
fi
|
||
|
||
# One authoritative Node version (issue #236): `.node-version` is the
|
||
# pin; every Dockerfile image tag and the engines floor must match it
|
||
# exactly, and workflows select Node only through node-version-file.
|
||
# Raising Node = update .node-version, every `FROM node:` tag and the
|
||
# engines floor in ONE commit (procedure: docs/architecture/operations.md
|
||
# §Update strategy). The bracketed grep pattern keeps this step from
|
||
# matching its own source (same trick as the secret fence above).
|
||
- name: Node version pin is consistent
|
||
run: |
|
||
set -euo pipefail
|
||
ver="$(cat .node-version)"
|
||
echo "pinned Node version: $ver"
|
||
bad=0
|
||
for f in apps/*/Dockerfile; do
|
||
if grep '^FROM node:' "$f" | grep -v "node:${ver}-alpine"; then
|
||
echo "$f pins a different Node image than node:${ver}-alpine"
|
||
bad=1
|
||
fi
|
||
done
|
||
if grep -rn "node-version[:] " .gitea/workflows; then
|
||
echo "workflows must use node-version-file, not a literal version"
|
||
bad=1
|
||
fi
|
||
if grep -rnE 'node:[0-9][^ ]*-alpine' .gitea/workflows | grep -v "node:${ver}-alpine"; then
|
||
echo "a workflow references a different node image than node:${ver}-alpine"
|
||
bad=1
|
||
fi
|
||
if ! grep -q "\"node\": \">=${ver}\"" package.json; then
|
||
echo "package.json engines floor does not match ${ver}"
|
||
bad=1
|
||
fi
|
||
exit "$bad"
|
||
|
||
# Third-party deploy images are pinned by digest (issue #203): every
|
||
# image in the deploy compose that is not one of our own
|
||
# (${IMAGE_PREFIX}…) must carry @sha256 — the tag stays for
|
||
# readability, the digest decides what runs. Update procedure:
|
||
# deploy/stages.md §Third-party image digests. compose.dev.yml is a
|
||
# local convenience, deliberately not held to this.
|
||
- name: Third-party compose images are digest-pinned
|
||
run: |
|
||
set -euo pipefail
|
||
bad=$(grep -hE '^ *image: ' deploy/compose/docker-compose.yml | grep -v 'IMAGE_PREFIX' | grep -v '@sha256:' || true)
|
||
if [ -n "$bad" ]; then
|
||
echo "third-party image reference(s) without a digest:"
|
||
echo "$bad"
|
||
exit 1
|
||
fi
|
||
|
||
# A fresh named volume inherits the ownership of the image directory it
|
||
# is mounted over. Every /data/… path the api image defaults to must
|
||
# therefore be pre-created AND chowned to `node`, or the non-root user
|
||
# cannot write to it — found on a real deploy in #303, where the env
|
||
# entry was added but the mkdir/chown line was not.
|
||
- name: api image pre-creates its data directories node-owned
|
||
run: |
|
||
set -euo pipefail
|
||
dirs=$(grep -oE '[A-Z_]+_DIR=/data/[a-z]+' apps/api/Dockerfile | cut -d= -f2 | sort -u)
|
||
bad=0
|
||
for d in $dirs; do
|
||
grep -q "mkdir -p .*$d" apps/api/Dockerfile || {
|
||
echo "$d is not pre-created in apps/api/Dockerfile"; bad=1; }
|
||
grep -q "chown -R node:node .*$d" apps/api/Dockerfile || {
|
||
echo "$d is not chowned to node in apps/api/Dockerfile"; bad=1; }
|
||
done
|
||
exit "$bad"
|
||
|
||
- name: Set up pnpm
|
||
uses: pnpm/action-setup@v4
|
||
|
||
- name: Set up Node.js
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version-file: .node-version
|
||
cache: pnpm
|
||
|
||
- name: Install dependencies
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
# License allowlist gate (issue #202): fails when any dependency's
|
||
# license falls outside the documented policy in
|
||
# scripts/check-licenses.mjs (which is also where the reasoning and
|
||
# per-package exceptions live).
|
||
- name: License allowlist
|
||
run: pnpm licenses list --json | node scripts/check-licenses.mjs
|
||
|
||
# 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
|
||
needs: checks
|
||
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-file: .node-version
|
||
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: |
|
||
# VS_NFD_MODE=marked: the marking pack and the a11y admin scan
|
||
# cover the marked state (issue #244); mode off is covered by
|
||
# local full runs and the marking pack's off-assertions there.
|
||
(cd apps/api && PORT=3001 VS_NFD_MODE=marked node dist/main.js > /tmp/api.log 2>&1 &)
|
||
# Second api on the SAME database with VS_NFD_MODE=hidden: the
|
||
# marking pack's hidden half runs against it via its own static
|
||
# server (issue #245); the mode is env-only, so sharing the db is
|
||
# exactly the deploy semantics.
|
||
(cd apps/api && PORT=3006 VS_NFD_MODE=hidden MIGRATE_ON_START=false node dist/main.js > /tmp/api-hidden.log 2>&1 &)
|
||
(PORT=5176 API_TARGET=http://127.0.0.1:3006 node scripts/e2e-static-server.mjs > /tmp/web-hidden.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
|
||
|
||
# Three contexts per test (owner + editor + viewer) → reset first.
|
||
- name: Reset login rate limit before access-rules 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 access-rules pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/access-rules.spec.ts
|
||
|
||
- name: Reset login rate limit before public 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 public pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/public.spec.ts
|
||
|
||
- name: Run legal pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/legal.spec.ts
|
||
|
||
- name: Reset login rate limit before system 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 system pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/system.spec.ts
|
||
|
||
- name: Reset login rate limit before comments 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 comments pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/comments.spec.ts
|
||
|
||
- name: Reset login rate limit before social 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 social pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/social.spec.ts
|
||
|
||
- name: Reset login rate limit before admin-settings 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 admin-settings pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/admin-settings.spec.ts
|
||
|
||
- name: Reset login rate limit before admin-quotas 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 admin-quotas pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/admin-quotas.spec.ts
|
||
|
||
- name: Reset login rate limit before admin-users 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 admin-users pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/admin-users.spec.ts
|
||
|
||
- name: Reset login rate limit before invitations pack
|
||
run: |
|
||
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
|
||
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
|
||
|
||
# Invitations (issue #332) need the mail catcher like the auth pack:
|
||
# the invite link and the follow-up verification both travel by mail.
|
||
- name: Run invitations pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 E2E_MAILPIT_URL=http://mailpit:8025 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/invitations.spec.ts
|
||
|
||
- name: Reset login rate limit before permission-matrix 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 permission-matrix pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/permission-matrix.spec.ts
|
||
|
||
- name: Reset login rate limit before attachments 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 attachments pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/attachments.spec.ts
|
||
|
||
- name: Reset login rate limit before import pack
|
||
run: |
|
||
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
|
||
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
|
||
|
||
# The .docx case self-skips without a reachable pandoc sidecar (no
|
||
# E2E_PANDOC here); the .md, failure, and concurrent cases run (#64).
|
||
- name: Run import pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/import.spec.ts
|
||
|
||
- name: Reset login rate limit before export pack
|
||
run: |
|
||
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
|
||
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
|
||
|
||
# The ZIP case runs; the .docx case self-skips without a pandoc sidecar (#65).
|
||
- name: Run export pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/export.spec.ts
|
||
|
||
- name: Reset login rate limit before fonts pack
|
||
run: |
|
||
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
|
||
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
|
||
|
||
# Pond fonts (#66): the GDPR no-off-origin assertion + apply/persist.
|
||
- name: Run fonts pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/fonts.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 page-tree 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 page-tree pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/page-tree.spec.ts
|
||
|
||
- name: Reset login rate limit before graph 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 graph pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/graph.spec.ts
|
||
|
||
- name: Reset login rate limit before favorites 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 favorites pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/favorites.spec.ts
|
||
|
||
- name: Reset login rate limit before settings-nav 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 settings-nav pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/settings-nav.spec.ts
|
||
|
||
- name: Reset login rate limit before tasks 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 tasks pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/tasks.spec.ts
|
||
|
||
- name: Reset login rate limit before create-missing-page 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 create-missing-page pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/create-missing-page.spec.ts
|
||
|
||
- name: Reset login rate limit before vault-import 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 vault-import pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/import-vault.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: Run plugin sandbox pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/plugins.spec.ts
|
||
|
||
- name: Run plugin admin pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/plugin-admin.spec.ts
|
||
|
||
# Several contexts per test across these two packs → reset first.
|
||
- name: Reset login rate limit before plugin feature 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 section-styles pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/section-styles.spec.ts
|
||
|
||
- name: Run plugin blocks pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/plugin-blocks.spec.ts
|
||
|
||
# Owner + admin + restricted viewer contexts → reset first.
|
||
- name: Reset login rate limit before page-tools 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 page-tools pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/page-tools.spec.ts
|
||
|
||
# Owner + admin contexts in the collab case → reset first.
|
||
- name: Reset login rate limit before mermaid 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 mermaid pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/mermaid.spec.ts
|
||
|
||
# The setup wizard (issue #81) needs an instance where setup is still
|
||
# pending — the main stack is seeded and long past it. Provision a
|
||
# second api + static web against a virgin database on their own ports.
|
||
# SMTP_HOST/PORT are forced empty so the fresh api boots unconfigured
|
||
# like a real first run (loadApiEnv drops empty strings, issue #80).
|
||
- name: Provision fresh setup stack
|
||
run: |
|
||
(cd apps/api && node -e '
|
||
const { PrismaClient } = require("@prisma/client");
|
||
const admin = new PrismaClient();
|
||
admin.$executeRawUnsafe("CREATE DATABASE setup_e2e")
|
||
.finally(() => admin.$disconnect());
|
||
')
|
||
DATABASE_URL=postgresql://e2e:e2e@postgres:5432/setup_e2e \
|
||
pnpm --filter @dorfteich/api exec prisma migrate deploy
|
||
(cd apps/api && PORT=3005 \
|
||
DATABASE_URL=postgresql://e2e:e2e@postgres:5432/setup_e2e \
|
||
APP_BASE_URL=http://localhost:5175 SMTP_HOST= SMTP_PORT= \
|
||
SECRETS_FILE=/tmp/setup-secrets.env \
|
||
node dist/main.js > /tmp/api-setup.log 2>&1 &)
|
||
(PORT=5175 API_TARGET=http://127.0.0.1:3005 \
|
||
node scripts/e2e-static-server.mjs > /tmp/web-setup.log 2>&1 &)
|
||
for i in $(seq 1 30); do
|
||
curl -sf http://localhost:3005/api/v1/readyz >/dev/null && break
|
||
sleep 2
|
||
done
|
||
|
||
# Six logins per run since #180 doubled the scans (3 contexts × light/
|
||
# dark, limit is 10/min) → reset first (see note above).
|
||
- name: Reset login rate limit before a11y pack
|
||
run: |
|
||
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
|
||
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
|
||
|
||
# WCAG-A/AA-Regressionsschutz (issue #171): axe-Scan der Kernscreens,
|
||
# seit #180 in beiden Farbschemata.
|
||
- name: Run a11y pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/a11y.spec.ts
|
||
|
||
# Das a11y-Pack kostet seit #301 einen Login mehr (der Reflow-Zaun);
|
||
# damit reicht das Budget nicht mehr bis in die VS-NfD-Packs → hier
|
||
# zusätzlich zurücksetzen (siehe Hinweis oben).
|
||
- name: Reset login rate limit before the VS-NfD packs
|
||
run: |
|
||
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
|
||
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
|
||
|
||
# VS-NfD-Markierungen im Modus `marked` (issue #244).
|
||
- name: Run VS-NfD marking pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 E2E_VS_NFD_MODE=marked \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/vs-nfd-marking.spec.ts
|
||
|
||
# Ausblendung + Policy-Hinweis im Modus `hidden` (issue #245).
|
||
- name: Run VS-NfD hidden pack
|
||
run: |
|
||
for i in $(seq 1 30); do
|
||
curl -sf http://localhost:3006/api/v1/readyz >/dev/null && break
|
||
sleep 2
|
||
done
|
||
E2E_BASE_URL=http://localhost:5176 E2E_VS_NFD_MODE=hidden \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/vs-nfd-marking.spec.ts
|
||
|
||
# The marking pack's extra login on top of the six a11y logins pushes
|
||
# the theme pack over the 10/min login limit — reset again (#244).
|
||
- name: Reset login rate limit before theme pack
|
||
run: |
|
||
echo "DELETE FROM rate_limits WHERE key LIKE 'login%';" | \
|
||
pnpm --filter @dorfteich/api exec prisma db execute --stdin --url "$DATABASE_URL"
|
||
|
||
# Hell/Dunkel/System-Umschalter (issue #180).
|
||
- name: Run theme pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5173 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/theme.spec.ts
|
||
|
||
- name: Run setup wizard pack
|
||
run: |
|
||
E2E_BASE_URL=http://localhost:5175 E2E_SETUP=1 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/setup.spec.ts
|
||
|
||
- name: Dump server logs on failure
|
||
if: failure()
|
||
run: tail -50 /tmp/api.log /tmp/collab.log /tmp/web.log /tmp/api-setup.log /tmp/web-setup.log || true
|
||
|
||
# The import/export fidelity gate (issue #69, ADR 0009): runs the corpus
|
||
# snapshot suites and the PDF smoke check against the *pinned* sidecar images
|
||
# (the same versions the stages run), so a structural regression — ours or a
|
||
# pandoc/Gotenberg version bump that drifts the output — fails the pipeline
|
||
# instead of degrading "best effort" silently. Kept a small, separate job so
|
||
# it stays well under five minutes; the suites self-skip in the main `checks`
|
||
# job (no sidecars there).
|
||
fidelity:
|
||
name: Import/export fidelity gate
|
||
needs: auth-e2e
|
||
runs-on: ubuntu-latest
|
||
# A guard so a hung sidecar can never keep the job (and its containers)
|
||
# alive on the shared runner host; the suite itself finishes in ~1 min.
|
||
timeout-minutes: 10
|
||
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-file: .node-version
|
||
cache: pnpm
|
||
|
||
- name: Install dependencies
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
# The fidelity tests import from @dorfteich/shared's built dist; they run
|
||
# TypeScript directly (vitest) so only shared needs building, not the api.
|
||
- name: Build shared package
|
||
run: pnpm --filter @dorfteich/shared build
|
||
|
||
# The import corpus test reaches through import.service, which imports the
|
||
# generated Prisma client — generate it (no DB or migration needed; these
|
||
# suites never touch a database).
|
||
- name: Generate Prisma client
|
||
run: pnpm --filter @dorfteich/api exec prisma generate
|
||
|
||
# Start the pinned sidecars with `docker run` (pandoc-server needs the
|
||
# `server` arg, which Actions `services:` cannot pass) and attach them to
|
||
# THIS job container's network namespace, so they are reachable at
|
||
# localhost — no dependency on `ip`/gateway routing (the minimal runner
|
||
# image has no iproute2). Sharing the netns means no published ports.
|
||
- name: Start pinned pandoc + Gotenberg sidecars
|
||
run: |
|
||
# Sidecar names carry THIS job container's id: parallel runs on the
|
||
# shared host must not collide on a fixed name (a fixed-name rm -f
|
||
# here even killed a sibling run's live sidecars — run 547).
|
||
JOB_ID=$(cat /etc/hostname)
|
||
echo "PANDOC_NAME=fidelity-pandoc-${JOB_ID}" >> "$GITHUB_ENV"
|
||
echo "GOTENBERG_NAME=fidelity-gotenberg-${JOB_ID}" >> "$GITHUB_ENV"
|
||
docker rm -f "fidelity-pandoc-${JOB_ID}" "fidelity-gotenberg-${JOB_ID}" 2>/dev/null || true
|
||
docker run -d --name "fidelity-pandoc-${JOB_ID}" \
|
||
--network "container:${JOB_ID}" pandoc/core:3.6 server
|
||
docker run -d --name "fidelity-gotenberg-${JOB_ID}" \
|
||
--network "container:${JOB_ID}" gotenberg/gotenberg:8
|
||
for i in $(seq 1 30); do
|
||
curl -sf http://localhost:3030/version >/dev/null && break
|
||
sleep 1
|
||
done
|
||
for i in $(seq 1 30); do
|
||
curl -sf http://localhost:3000/health >/dev/null && break
|
||
sleep 1
|
||
done
|
||
curl -sf http://localhost:3030/version
|
||
curl -sf http://localhost:3000/health
|
||
|
||
- name: Run fidelity suite (import + export snapshots, PDF smoke)
|
||
run: |
|
||
PANDOC_URL=http://localhost:3030 GOTENBERG_URL=http://localhost:3000 \
|
||
pnpm --filter @dorfteich/api exec vitest run \
|
||
src/import-export/import.fixtures.test.ts \
|
||
src/import-export/export.fidelity.test.ts \
|
||
src/import-export/pdf.fidelity.test.ts
|
||
|
||
- name: Dump sidecar logs on failure
|
||
if: failure()
|
||
run: |
|
||
echo '--- pandoc ---'; docker logs "$PANDOC_NAME" 2>&1 | tail -30 || true
|
||
echo '--- gotenberg ---'; docker logs "$GOTENBERG_NAME" 2>&1 | tail -30 || true
|
||
|
||
# Always tear the sidecars down — they run on the shared runner host, so a
|
||
# leaked (especially Chromium-backed Gotenberg) container would waste its
|
||
# memory until the next run.
|
||
- name: Stop sidecars
|
||
if: always()
|
||
run: docker rm -f "$PANDOC_NAME" "$GOTENBERG_NAME" 2>/dev/null || true
|
||
|
||
images:
|
||
name: Build container images
|
||
needs: checks
|
||
# 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 .
|
||
|
||
- name: Build backup image
|
||
run: docker build -f apps/backup/Dockerfile --build-arg APP_VERSION=${{ github.sha }} -t dorfteich-backup:ci .
|