Adds the checks.sql smoke step, the verified CLI result for generated seed content and the created_at/updated_at note for the API. Refs #2 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
383 lines
18 KiB
YAML
383 lines
18 KiB
YAML
# 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: |
|
||
arch=$(uname -m); [ "$arch" = "aarch64" ] && target=aarch64-unknown-linux-gnu || target=x86_64-unknown-linux-gnu
|
||
curl -fsSL "https://github.com/denoland/deno/releases/download/v${DENO_VERSION}/deno-${target}.zip" -o /tmp/deno.zip
|
||
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: Datenbanktests (pgTAP)
|
||
run: |
|
||
test -d supabase/migrations || exit 0
|
||
bash .forgejo/scripts/supabase-db-tests.sh
|
||
- name: Prüfabfrage checks.sql läuft (KAT-05)
|
||
# Nur "läuft ohne Fehler"; der Sollwert SB1 (mindestens 180) ist lokal und in CI nur eine Information.
|
||
run: |
|
||
test -f supabase/catalog/checks.sql || exit 0
|
||
docker exec -i supabase_db_shuffle psql -U postgres -d postgres -v ON_ERROR_STOP=1 < supabase/catalog/checks.sql
|
||
- name: Aufräumen
|
||
if: always()
|
||
run: npx --yes supabase@${{ env.SUPABASE_CLI_VERSION }} stop --no-backup || true
|
||
|
||
# Web-E2E (Playwright: api, web, throttle) gegen lokalen Supabase-Stack und gebaute Flutter-Web-App (Issue #38, Spec #1 D2).
|
||
# Läuft wie supabase-tests auf "docker-host" (Docker-CLI + dind per TCP). Der Stack liegt im dind; ein
|
||
# TCP-Relay (e2e/support/tcp-forward.mjs) macht ihn im Job unter 127.0.0.1 erreichbar.
|
||
# SICHERHEIT (ADR-0003, Issue #43): Der Job führt PR-Code mit Zugriff auf den dind-Daemon des Runners aus
|
||
# (Daemon = Root-Äquivalent). Der `if:` unten ist KEIN Schutz vor böswilligen Forks: Bei pull_request kommt die
|
||
# Workflow-Datei aus dem PR-Head, ein Fork kann die Bedingung entfernen. Er hält nur einen unveränderten, bereits
|
||
# freigegebenen Fork-PR von docker-host fern; er verhindert weder das Warten auf die Freigabe (gilt für den ganzen Lauf,
|
||
# das Job-`if` wird erst danach ausgewertet) noch einen manipulierten Lauf. Die tatsächliche Absicherung liegt außerhalb
|
||
# dieser Datei: Freigabe-Pflicht (eine Freigabe vertraut dem Nutzer dauerhaft, also Fork-PRs hier nicht freigeben) und ein isolierter
|
||
# Runner/Daemon. Schritt-für-Schritt-Anleitung und Restrisiken: docs/adr/0003-ci-runner-sicherheit.md.
|
||
# Gleiches gilt für supabase-tests.
|
||
# needs + concurrency: Beide Jobs belegen dieselben festen Container-Namen/Ports (project_id shuffle, 54321 ...) im dind.
|
||
# `needs` serialisiert innerhalb eines Laufs; die concurrency-Gruppe (auch an supabase-tests) soll Läufe verschiedener
|
||
# PRs hintereinander schalten. Ob Forgejo 8.0.3 / Runner v5.0.4 Job-concurrency auswertet, ist hier nicht verifiziert;
|
||
# sonst wird der Schlüssel ignoriert und Kollisionen bleiben möglich (dann Runner mit capacity 1 verwenden).
|
||
# Der Spike (SPIKE_GOTRUE_URL, Issue #37) läuft nicht in CI.
|
||
e2e:
|
||
runs-on: docker-host
|
||
concurrency:
|
||
group: docker-host-supabase
|
||
cancel-in-progress: false
|
||
needs: [supabase-tests]
|
||
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
|
||
PUB_CACHE: ${{ github.workspace }}/.pub-cache
|
||
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers
|
||
steps:
|
||
- uses: https://code.forgejo.org/actions/checkout@v4
|
||
|
||
- name: Werkzeuge prüfen
|
||
run: |
|
||
missing=""
|
||
for t in git curl unzip xz jq; do
|
||
command -v "$t" >/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
|