# CI für Pull Requests und main – läuft auf einem Forgejo-Runner mit Docker-Label. # Flutter-Projekt liegt in app/, Supabase in supabase/. name: CI on: pull_request: push: branches: [main] env: SUPABASE_CLI_VERSION: 3.0.0 # Deno für das Startbestand-Werkzeug (supabase/catalog/, KAT-05); lokal getestet mit 2.9.8. DENO_VERSION: 2.9.8 # Einzige Stelle für die Flutter-Version (= lokale Version, Dart 3.13.7; muss zu app/pubspec.yaml passen). # Muss ein existierendes Tag in github.com/flutter/flutter sein. Ein Cirrus-Image mit dieser Version gibt es nicht. FLUTTER_VERSION: 3.47.9 FLUTTER_HOME: /opt/flutter jobs: # Format, Analyse, Tests. Generierte Dateien (*.g.dart, gen-l10n) sind eingecheckt und # werden hier neu erzeugt; ein Diff bedeutet: Generierung im PR vergessen. flutter: runs-on: docker container: image: node:22.12-bookworm # gepinnt; enthält git, curl, unzip, xz und Node (für JS-Actions) env: PUB_CACHE: ${{ github.workspace }}/.pub-cache defaults: run: working-directory: app steps: - uses: https://code.forgejo.org/actions/checkout@v4 - name: Flutter-SDK-Cache id: flutter-cache uses: https://code.forgejo.org/actions/cache@v4 with: path: ${{ env.FLUTTER_HOME }} key: flutter-${{ env.FLUTTER_VERSION }}-node22.12-bookworm - name: Flutter installieren run: | if [ ! -x "$FLUTTER_HOME/bin/flutter" ]; then rm -rf "$FLUTTER_HOME" git clone --depth 1 --branch "$FLUTTER_VERSION" https://github.com/flutter/flutter.git "$FLUTTER_HOME" fi echo "$FLUTTER_HOME/bin" >> "$GITHUB_PATH" export PATH="$FLUTTER_HOME/bin:$PATH" export CI=true flutter config --no-analytics --enable-web flutter --version flutter precache --web working-directory: . - name: Pub-Cache uses: https://code.forgejo.org/actions/cache@v4 with: path: ${{ github.workspace }}/.pub-cache key: pub-${{ env.FLUTTER_VERSION }}-${{ hashFiles('app/pubspec.lock') }} restore-keys: pub-${{ env.FLUTTER_VERSION }}- - name: Abhängigkeiten (erzeugt auch gen-l10n) run: flutter pub get - name: Code-Generierung run: dart run build_runner build --delete-conflicting-outputs - name: Generierte Dateien aktuell? run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" if ! git diff --exit-code -- . ; then echo "::error::Generierte Dateien sind nicht aktuell. Lokal 'dart run build_runner build --delete-conflicting-outputs' und 'flutter gen-l10n' ausführen und committen." exit 1 fi untracked=$(git ls-files --others --exclude-standard -- lib test) if [ -n "$untracked" ]; then echo "Nicht eingecheckte generierte Dateien:"; echo "$untracked"; exit 1; fi - name: Format run: dart format --set-exit-if-changed . - name: Analyse run: flutter analyze --fatal-infos - name: Tests run: flutter test # Web-Build parallel zu den Prüfungen; Platzhalterwerte aus app/env/ci.example.json (keine Secrets). web-build: runs-on: docker container: image: node:22.12-bookworm # gepinnt; enthält git, curl, unzip, xz und Node (für JS-Actions) env: PUB_CACHE: ${{ github.workspace }}/.pub-cache defaults: run: working-directory: app steps: - uses: https://code.forgejo.org/actions/checkout@v4 - name: Flutter-SDK-Cache id: flutter-cache uses: https://code.forgejo.org/actions/cache@v4 with: path: ${{ env.FLUTTER_HOME }} key: flutter-${{ env.FLUTTER_VERSION }}-node22.12-bookworm - name: Flutter installieren run: | if [ ! -x "$FLUTTER_HOME/bin/flutter" ]; then rm -rf "$FLUTTER_HOME" git clone --depth 1 --branch "$FLUTTER_VERSION" https://github.com/flutter/flutter.git "$FLUTTER_HOME" fi echo "$FLUTTER_HOME/bin" >> "$GITHUB_PATH" export PATH="$FLUTTER_HOME/bin:$PATH" export CI=true flutter config --no-analytics --enable-web flutter --version flutter precache --web working-directory: . - name: Pub-Cache uses: https://code.forgejo.org/actions/cache@v4 with: path: ${{ github.workspace }}/.pub-cache key: pub-${{ env.FLUTTER_VERSION }}-${{ hashFiles('app/pubspec.lock') }} restore-keys: pub-${{ env.FLUTTER_VERSION }}- - name: Abhängigkeiten run: flutter pub get - name: Web-Build run: flutter build web --release --dart-define-from-file=env/ci.example.json supabase: runs-on: docker container: image: node:22.12-bookworm steps: - uses: https://code.forgejo.org/actions/checkout@v4 with: fetch-depth: 0 - name: Migrationen prüfen (ohne laufende DB) run: | test -d supabase/migrations || { echo "Keine Migrationen – übersprungen"; exit 0; } # Bereits gemergte Migrationen dürfen nicht verändert werden if [ "${{ github.event_name }}" = "pull_request" ]; then git fetch origin "${{ github.base_ref }}" --depth=1 changed=$(git diff --name-only --diff-filter=M "origin/${{ github.base_ref }}"...HEAD -- supabase/migrations || true) if [ -n "$changed" ]; then echo "Geänderte bestehende Migrationen: $changed"; exit 1; fi fi # Startbestand-Werkzeug (KAT-05, Spec 2 D1): Deno-Tests, erzeugte Dateien aktuell, Schlüsselliste wächst nur. - name: Deno installieren run: | # SHA256 fest eingetragen (aus den veröffentlichten .sha256sum-Dateien von v2.9.8); bei neuer DENO_VERSION mit ändern. if [ "$(uname -m)" = "aarch64" ]; then target=aarch64-unknown-linux-gnu; sha=d49596c20b9788cf8f5d8ddf644103d4bde56ed239753f0bdb92ba9361d51905 else target=x86_64-unknown-linux-gnu; sha=e890975bc2549807bbcc0b584ffafda72cc7c93349dd99c80483dd784f6c4609 fi curl -fsSL "https://github.com/denoland/deno/releases/download/v${DENO_VERSION}/deno-${target}.zip" -o /tmp/deno.zip echo "${sha} /tmp/deno.zip" | sha256sum -c - unzip -q -o /tmp/deno.zip -d /usr/local/bin deno --version - name: Startbestand-Werkzeug testen run: deno test --allow-read=supabase supabase/catalog/ - name: Erzeugte Katalogdateien aktuell? run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" deno run --allow-read=supabase --allow-write=supabase supabase/catalog/build_startbestand.ts if ! git diff --exit-code -- supabase/catalog supabase/tests/support; then echo "::error::Erzeugte Dateien sind nicht aktuell. Lokal 'deno run --allow-read=supabase --allow-write=supabase supabase/catalog/build_startbestand.ts' ausführen und committen." exit 1 fi untracked=$(git ls-files --others --exclude-standard -- supabase/catalog supabase/tests/support) if [ -n "$untracked" ]; then echo "Nicht eingecheckte erzeugte Dateien:"; echo "$untracked"; exit 1; fi - name: startbestand-keys.csv wächst nur if: github.event_name == 'pull_request' run: | # Jede Zeile der Schlüsselliste aus dem Basis-Branch muss unverändert erhalten bleiben (Schlüssel sind unveränderlich). git fetch origin "${{ github.base_ref }}" --depth=1 if git cat-file -e "origin/${{ github.base_ref }}:supabase/catalog/startbestand-keys.csv" 2>/dev/null; then git show "origin/${{ github.base_ref }}:supabase/catalog/startbestand-keys.csv" > /tmp/keys-base.csv missing=$(grep -vxFf supabase/catalog/startbestand-keys.csv /tmp/keys-base.csv || true) if [ -n "$missing" ]; then echo "::error::Zeilen aus startbestand-keys.csv fehlen oder wurden geändert:"; echo "$missing"; exit 1; fi fi # RLS-/pgTAP-Tests: braucht einen Runner mit Docker-Zugriff (Label "docker-host"). # Der Job-Container hat kein Docker-CLI und keinen Socket (Isolation, Repo ist öffentlich). Stattdessen # wird ein gepinntes Docker-CLI installiert und der dind-Daemon des Runners per TCP angesprochen # (erreichbar über das Default-Gateway des Job-Netzes). Supabase-Ports liegen dann im dind; # die Supabase-CLI nimmt den Host aus DOCKER_HOST automatisch. # Nutzt die eingecheckte supabase/config.toml. Ohne passenden Runner Job bei Bedarf deaktivieren. # SICHERHEIT: siehe Kommentar am Job e2e und docs/adr/0003-ci-runner-sicherheit.md. Der `if:` ist kein Schutz. supabase-tests: runs-on: docker-host concurrency: group: docker-host-supabase cancel-in-progress: false if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository env: DOCKER_CLI_VERSION: 27.3.1 DIND_PORT: 2375 steps: - uses: https://code.forgejo.org/actions/checkout@v4 - name: Docker-CLI installieren und dind anbinden run: | arch=$(uname -m) curl -fsSL "https://download.docker.com/linux/static/stable/${arch}/docker-${DOCKER_CLI_VERSION}.tgz" \ | tar -xz -C /usr/local/bin --strip-components=1 docker/docker # Default-Gateway des Job-Containers (= dind-Host) aus /proc/net/route; `ip` fehlt im Image hex=$(awk '$2=="00000000"{print $3; exit}' /proc/net/route) gw=$(printf '%d.%d.%d.%d' "0x${hex:6:2}" "0x${hex:4:2}" "0x${hex:2:2}" "0x${hex:0:2}") echo "DOCKER_HOST=tcp://${gw}:${DIND_PORT}" >> "$GITHUB_ENV" DOCKER_HOST="tcp://${gw}:${DIND_PORT}" docker version --format 'dind {{.Server.Version}}' - name: Supabase starten run: | test -f supabase/config.toml || { echo "supabase/config.toml fehlt"; exit 1; } test -d supabase/migrations || { echo "Keine Migrationen – übersprungen"; exit 0; } npx --yes supabase@${{ env.SUPABASE_CLI_VERSION }} start -x studio,imgproxy,edge-runtime,logflare,vector - name: Katalog-Vokabular eingespielt? (KAT-05) # Schlägt fehl, wenn der Seed (sql_paths) das Vokabular nicht geladen hat, zum Beispiel wegen eines Pfad-Tippfehlers # (die CLI überspringt eine fehlende Datei nur mit Warnung). IDs werden aus vocabulary.csv abgeleitet. run: | expected=$(awk -F, 'NR>1 && NF>=4 {printf "%s(\x27%s\x27,\x27%s\x27)", sep, $1, $2; sep=","}' supabase/catalog/vocabulary.csv) test -n "$expected" || { echo "vocabulary.csv ist leer"; exit 1; } missing=$(docker exec -i supabase_db_shuffle psql -U postgres -d postgres -At -v ON_ERROR_STOP=1 </dev/null || { [ "$t" = xz ] && missing="$missing xz-utils" || missing="$missing $t"; } done if [ -n "$missing" ]; then apt-get update -qq && apt-get install -y -qq --no-install-recommends $missing ca-certificates fi node --version - name: Docker-CLI installieren und dind anbinden run: | arch=$(uname -m) curl -fsSL "https://download.docker.com/linux/static/stable/${arch}/docker-${DOCKER_CLI_VERSION}.tgz" \ | tar -xz -C /usr/local/bin --strip-components=1 docker/docker hex=$(awk '$2=="00000000"{print $3; exit}' /proc/net/route) gw=$(printf '%d.%d.%d.%d' "0x${hex:6:2}" "0x${hex:4:2}" "0x${hex:2:2}" "0x${hex:0:2}") echo "DOCKER_GW=${gw}" >> "$GITHUB_ENV" echo "DOCKER_HOST=tcp://${gw}:${DIND_PORT}" >> "$GITHUB_ENV" DOCKER_HOST="tcp://${gw}:${DIND_PORT}" docker version --format 'dind {{.Server.Version}}' - name: Mailvorlagen im dind anlegen # Unter dind existiert der Bind-Mount-Pfad der Vorlagen nicht; sie müssen VOR `supabase start` dort liegen, # sonst entstehen leere Verzeichnis-Mounts und GoTrue verschickt still eine Fehlerseite. Details im Skript. run: bash .forgejo/scripts/remote-templates.sh seed - name: Supabase starten (mit Mailpit, ohne Studio u. a.) # Ausgabe nicht ins Log: Die Zusammenfassung von `start` enthält die lokalen Schlüssel (noch nicht maskiert). run: | if ! npx --yes supabase@${{ env.SUPABASE_CLI_VERSION }} start -x studio,imgproxy,edge-runtime,logflare,vector >"${RUNNER_TEMP:-/tmp}/supabase-start.log" 2>&1; then echo "supabase start fehlgeschlagen, letzte Zeilen (Schlüssel gefiltert):" tail -n 40 "${RUNNER_TEMP:-/tmp}/supabase-start.log" | grep -viE 'key|secret|token|jwt' exit 1 fi echo "supabase start ok" - name: Datenbank zurücksetzen run: npx --yes supabase@${{ env.SUPABASE_CLI_VERSION }} db reset - name: Mailvorlagen im dind prüfen run: bash .forgejo/scripts/remote-templates.sh verify - name: Lokale Schlüssel lesen # Öffentliche Demo-Schlüssel der CLI; trotzdem wird die Maske gesetzt, bevor sie irgendwo ausgegeben werden. run: | status=$(npx --yes supabase@${{ env.SUPABASE_CLI_VERSION }} status -o env 2>/dev/null) get() { printf '%s\n' "$status" | sed -n "s/^$1=\"\{0,1\}\([^\"]*\)\"\{0,1\}$/\1/p" | head -n1; } anon=$(get ANON_KEY); service=$(get SERVICE_ROLE_KEY) [ -z "$anon" ] || echo "::add-mask::$anon" [ -z "$service" ] || echo "::add-mask::$service" test -n "$anon" && test -n "$service" || { echo "ANON_KEY/SERVICE_ROLE_KEY nicht in 'supabase status -o env' gefunden"; exit 1; } echo "SUPABASE_ANON_KEY=${anon}" >> "$GITHUB_ENV" echo "SUPABASE_SERVICE_ROLE_KEY=${service}" >> "$GITHUB_ENV" # Env-Datei für den Web-Build: Platzhalter-Datei bleibt unverändert, der Schlüssel kommt zur Laufzeit. jq --arg k "$anon" '.SUPABASE_ANON_KEY = $k | .SUPABASE_URL = "http://127.0.0.1:54321"' \ app/env/ci.example.json > "${RUNNER_TEMP:-/tmp}/ci.e2e.json" echo "E2E_ENV_FILE=${RUNNER_TEMP:-/tmp}/ci.e2e.json" >> "$GITHUB_ENV" - name: Extra-GoTrue für Drosselung (Krit. 7) starten run: | BIND_ADDR=0.0.0.0 HEALTH_HOST="$DOCKER_GW" bash e2e/support/start-throttle-gotrue.sh start throttle - name: Flutter-SDK-Cache uses: https://code.forgejo.org/actions/cache@v4 with: path: ${{ env.FLUTTER_HOME }} key: flutter-${{ env.FLUTTER_VERSION }}-docker-host - name: Flutter installieren run: | if [ ! -x "$FLUTTER_HOME/bin/flutter" ]; then rm -rf "$FLUTTER_HOME" git clone --depth 1 --branch "$FLUTTER_VERSION" https://github.com/flutter/flutter.git "$FLUTTER_HOME" fi echo "$FLUTTER_HOME/bin" >> "$GITHUB_PATH" export PATH="$FLUTTER_HOME/bin:$PATH" export CI=true flutter config --no-analytics --enable-web flutter precache --web - name: Pub-Cache uses: https://code.forgejo.org/actions/cache@v4 with: path: ${{ github.workspace }}/.pub-cache key: pub-${{ env.FLUTTER_VERSION }}-${{ hashFiles('app/pubspec.lock') }} restore-keys: pub-${{ env.FLUTTER_VERSION }}- - name: Web-Build mit lokalem Anon-Key working-directory: app run: | flutter pub get flutter build web --release --dart-define-from-file="$E2E_ENV_FILE" - name: Playwright-Cache uses: https://code.forgejo.org/actions/cache@v4 with: path: ${{ github.workspace }}/.playwright-browsers key: playwright-${{ hashFiles('e2e/package-lock.json') }} - name: E2E-Abhängigkeiten und Browser working-directory: e2e run: | npm ci npx playwright install --with-deps chromium - name: Playwright (api, web, throttle) run: bash .forgejo/scripts/e2e-run.sh - name: Artefakte hochladen (Report, Traces) if: failure() uses: https://code.forgejo.org/forgejo/upload-artifact@v4 with: name: playwright-${{ github.run_number }} path: | e2e/playwright-report e2e/test-results retention-days: 7 if-no-files-found: ignore - name: Aufräumen if: always() run: | bash e2e/support/start-throttle-gotrue.sh stop throttle || true npx --yes supabase@${{ env.SUPABASE_CLI_VERSION }} stop --no-backup || true