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.
195 lines
7.0 KiB
YAML
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.
|