dorfteich/.gitea/workflows/ci.yml
Claude Fable 5 c2a4dde5cc
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m50s
CI / Build container images (pull_request) Successful in 3m57s
CI / Auth e2e pack (pull_request) Failing after 6m18s
CI / Import/export fidelity gate (pull_request) Has been skipped
Invitation flow with per-user quota (#332)
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
2026-08-05 12:44:20 +02:00

823 lines
35 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 .