Files
alpenwerk-hr/.github/workflows/ci.yml
Andrei Laas 206a7c6eba
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m36s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m6s
Zeilenschutz auf vier Tabellen nachziehen, und den Zustand pruefen
Beim Neuaufbau der Datenbank aus den Migrationen am 26.08. blieben
cost_centers, position_cost_centers, onboarding_tasks und
offboarding_tasks ohne Row Level Security: sie verlassen sich auf
den Ereignis-Trigger ensure_rls, den eine spaetere Migration erst
anlegt. Ein Ereignis-Trigger wirkt nur nach vorne.

Die Policies auf diesen Tabellen existieren und wurden nie
ausgewertet — PostgreSQL befragt sie nur bei eingeschaltetem RLS.
In jeder Aufstellung der Policies sah es richtig aus.

Der CI-Job prueft ab jetzt den Zustand nach dem Lauf, nicht nur
dass die Dateien durchlaufen. Genau in diesem Zwischenraum ist der
Fehler durchgekommen.
2026-09-08 15:36:48 +02:00

195 lines
7.0 KiB
YAML

name: CI
on:
push:
branches: [master, main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
name: Lint, Typen, Tests, Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Lint
run: npm run lint
- name: Typecheck
run: npm run typecheck
# Fängt die Abweichung, die `tsc` nicht sehen kann: lib/types.ts wird von
# Hand gepflegt, eine Migration mit einer neuen Spalte lässt sie also
# stillschweigend veralten.
- name: Schema/Typen-Abgleich
run: npm run types:check
- name: Unit- und Komponententests
run: npm test
# Ohne Umgebungsvariablen: seit dem Wegfall der Browser-Anbindung wird
# zur Bauzeit nichts mehr aus der Umgebung gelesen, und das Abbild ist
# für jede Umgebung dasselbe.
- name: Build
run: npm run build
migrationen:
name: Migrationen auf leerer Datenbank
runs-on: ubuntu-latest
# Derselbe Stand wie im Betrieb. Ein eigener Dienst statt einer fremden
# Plattform — die Datenbank gehört seit dem Umzug zum Projekt.
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_PASSWORD: ci
POSTGRES_DB: alpenwerk
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 20
env:
MIGRATE_DATABASE_URL: postgresql://postgres:ci@postgres:5432/alpenwerk
DATABASE_SSL: "false"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
# Das Abbild des Runners bringt keinen Postgres-Client mit; der
# Schritt darunter ruft aber psql. Ohne das bricht er mit
# "psql: command not found" ab, bevor die erste Datei laeuft.
- name: psql bereitstellen
run: |
apt-get update -qq
apt-get install -y -qq --no-install-recommends postgresql-client
psql --version
# Was das Schema von der Umgebung erwartet: die Anwendungsrolle ohne
# BYPASSRLS, zwei Erweiterungen und die Attrappe des `auth`-Schemas, ohne
# die sich die Migrationen von Juni 2026 nicht abspielen lassen.
- name: Rollen, Erweiterungen, auth-Attrappe
env:
PGPASSWORD: ci
APP_DB_PASSWORD: ci-anwendungsrolle
run: |
for f in deploy/db-init/*.sql; do
echo "── $f"
psql -v ON_ERROR_STOP=1 -h postgres -U postgres -d alpenwerk -f "$f"
done
# Der eigentliche Zweck dieses Jobs: jede Migration muss sich auf einer
# leeren Datenbank abspielen lassen. Eine, die das nicht kann, fällt hier
# auf — und nicht beim Wiederherstellen einer Sicherung oder beim
# Aufsetzen einer neuen Umgebung.
- name: Migrationen einspielen
run: node scripts/migrate.mjs
# Zweiter Lauf: die Buchführung muss greifen. Meldet er etwas anderes als
# „nichts anzuwenden", verbucht der Läufer nicht richtig — und ein
# Ausrollen spielte Migrationen doppelt ein.
- name: Zweiter Lauf ist ein Nichts
run: |
ausgabe=$(node scripts/migrate.mjs)
echo "$ausgabe"
echo "$ausgabe" | grep -q "^Nichts anzuwenden" \
|| { echo "Der zweite Lauf wollte erneut anwenden — die Buchführung greift nicht."; exit 1; }
# Dass die Migrationen durchlaufen, heisst nicht, dass das Ergebnis
# stimmt. Bis hierher prüft dieser Job nur, dass keine Datei abbricht —
# und genau daran ist ein Fehler vorbeigekommen: vier Tabellen aus dem
# August (Kostenstellen, On-/Offboarding) standen ohne Zeilenschutz da,
# weil sie sich auf den Ereignis-Trigger `ensure_rls` verliessen, den
# eine spätere Migration erst anlegt. Alle Dateien liefen sauber durch,
# die Policies standen im Katalog, und ausgewertet wurde keine davon.
#
# Deshalb wird ab hier der **Zustand** geprüft, nicht der Durchlauf.
# Nachgezogen hat das 20260908100000; dort steht dieselbe Gegenprobe.
# Sie läuft aber nur einmal, beim Anwenden jener Datei. Der Schritt hier
# läuft bei jedem Push gegen eine frisch aufgebaute Datenbank und fängt
# deshalb auch die *nächste* Tabelle, die es wieder vergisst.
- name: Zugriffsschutz nach dem Lauf
env:
PGPASSWORD: ci
run: |
set -euo pipefail
frage() {
psql -v ON_ERROR_STOP=1 -h postgres -U postgres -d alpenwerk -At -c "$1"
}
# relkind in ('r','p'): gewöhnliche und partitionierte Tabellen.
ohne_rls=$(frage "
select coalesce(string_agg(c.relname, ', ' order by c.relname), '')
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind in ('r', 'p')
and not c.relrowsecurity")
# RLS ohne Policy ist nicht offen, sondern leer — kein Leck, aber
# eine Tabelle, aus der nie etwas zurückkommt.
ohne_policy=$(frage "
select coalesce(string_agg(c.relname, ', ' order by c.relname), '')
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind in ('r', 'p')
and not exists (select 1 from pg_policy p where p.polrelid = c.oid)")
# Das Netz selbst: fehlt oder schläft es, ist die nächste neue
# Tabelle wieder ungeschützt.
netz=$(frage "
select count(*) from pg_event_trigger
where evtname = 'ensure_rls' and evtenabled <> 'D'")
fehler=0
if [ -n "$ohne_rls" ]; then
echo "Ohne Row Level Security: $ohne_rls"
fehler=1
fi
if [ -n "$ohne_policy" ]; then
echo "Mit Row Level Security, aber ohne jede Policy: $ohne_policy"
fehler=1
fi
if [ "$netz" != "1" ]; then
echo "Der Ereignis-Trigger ensure_rls fehlt oder ist abgeschaltet."
fehler=1
fi
if [ "$fehler" -ne 0 ]; then
echo
echo "Jede Tabelle in public braucht RLS und mindestens eine Policy."
echo "Nachziehen in einer neuen Migration, nicht in einer bereits angewendeten."
exit 1
fi
echo "Zugriffsschutz vollstaendig: jede Tabelle mit RLS und Policy, ensure_rls aktiv."
# Die Integrationstests brauchen einen befüllten Bestand; der Seed lag
# in der abgelösten Umgebung und ist noch nicht nachgezogen (siehe
# README, offene Punkte). Bis dahin laufen sie gegen eine erreichbare
# Datenbank von Hand, nicht hier.