The database moved to a container of our own; the platform is gone. This takes out what was left of it — and, where the leftovers were load bearing, moves rather than deletes. Moved, not deleted: supabase/migrations/ -> db/migrations/ the schema's source of truth supabase/build-org.ts -> scripts/build-org.ts lib/supabase/types.ts -> lib/types.ts 52 import sites repointed The bookkeeping needed care. It lived in `supabase_migrations.schema_migrations`, and simply renaming the schema would have left the runner facing an empty table: it would have called all 67 migrations pending and replayed them against a database that is long since current. So the runner now creates `migrationen.schema_migrations` and, once, copies the old rows across — guarded so a second run does nothing and a fresh database skips it entirely. Only then does migration 20260907100000 drop the old schema. Deleted: the CLI config, the seed, the historical schema/function dumps (nothing read them), scripts/umzug-von-supabase.sh (the move is done), and both Supabase packages plus the CLI. Nothing in the application imported them — the build now succeeds with no environment variables at all, which is the proof. Integration tests: six of them signed in through Supabase Auth and asserted against the anon key and the service role. That model is gone, so the tests were not portable — they are deleted. session-context and employee-status-filter already ran on pg and are untouched; om-reporting is ported to a direct connection because it guards a real risk (the reporting line rule exists twice, once in SQL and once in TypeScript). CI: the integration job started a Supabase stack. It now runs a postgres service, applies deploy/db-init and every migration to an empty database — that was the valuable part, and it still holds — then checks that a second run is a no-op, which is what proves the bookkeeping works. Docs: security-review.md audited a service-role key, a cookie adapter and auth.users, none of which exist. Restating findings about removed components would suggest today's system had been reviewed; it has not. It now records what was removed and says a fresh review is due. data-model.md was already marked obsolete and described the pre-OM schema; azure-migration.md was a plan for a route not taken. Both deleted. Verified: npm ci, typecheck, lint, 445 tests, build — all clean without the packages. Integration tests skip cleanly with no database. Migration SQL and the runner are reviewed but NOT executed: no Docker here, and the old instance no longer resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
192 lines
9.0 KiB
YAML
192 lines
9.0 KiB
YAML
name: Deploy
|
|
|
|
# Warum diese Datei unter .github/workflows liegt und nicht unter .gitea/:
|
|
#
|
|
# Gitea Actions liest `.gitea/workflows`, **und nur wenn dieses Verzeichnis
|
|
# fehlt**, ersatzweise `.github/workflows`. Ein neu angelegtes `.gitea/`
|
|
# würde also ci.yml stillschweigend abschalten — der Lauf verschwände
|
|
# einfach, ohne Fehlermeldung. Solange beide Dateien hier liegen, sieht Gitea
|
|
# beide. (Auf GitHub bliebe dieser Workflow hängen: es gibt dort keinen
|
|
# Runner mit dem Label `self-hosted`. Das Repository liegt aber ohnehin auf
|
|
# git.elycon.solutions.)
|
|
|
|
# Nur von Hand auslösbar (Actions → Deploy → „Run workflow").
|
|
#
|
|
# Lief einmal bei jedem Push auf master. Das ist zurückgenommen: die
|
|
# Voraussetzungen dafür sind nicht erfüllt — es sind keine Secrets hinterlegt,
|
|
# und ob der Job-Container den Docker-Dienst des Hosts überhaupt erreicht, ist
|
|
# ungeprüft. Bei jedem Push zu scheitern erzieht dazu, rote Läufe zu
|
|
# übersehen; dann lieber gar nicht starten.
|
|
#
|
|
# Nach dem Umzug auf den eigenen db-Container stimmt hier ausserdem noch
|
|
# nicht alles: die Secrets unten kennen die neuen Werte (POSTGRES_PASSWORD,
|
|
# APP_DB_PASSWORD) nicht, und Migrationen laufen jetzt über den Dienst
|
|
# `migrate` statt über einen eigenen Node-Schritt. Wer das wieder scharf
|
|
# schaltet, muss beides nachziehen.
|
|
on:
|
|
workflow_dispatch:
|
|
|
|
# Zwei Pushes kurz hintereinander sollen nicht zwei Deploys übereinander
|
|
# fahren. Der laufende wird **nicht** abgebrochen — mitten im `docker compose
|
|
# up` abgeschnitten zu werden ist der eine Zustand, den man nicht will.
|
|
concurrency:
|
|
group: deploy-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
deploy:
|
|
name: Migrationen und Container
|
|
# `ubuntu-latest`, weil der vorhandene Runner (elycon-runner-01) genau
|
|
# diese Bezeichnung anbietet — neben `docker`. Ein Label, das kein Runner
|
|
# führt, lässt den Lauf wortlos in der Warteschlange stehen: kein Fehler,
|
|
# keine Meldung, nur „Waiting".
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
# ── Vorprüfung ──────────────────────────────────────────────────
|
|
#
|
|
# Dieser Job läuft in einem Container, den der Runner startet — nicht
|
|
# auf dem Server selbst. Ob er den Docker-Dienst des **Hosts** erreicht,
|
|
# hängt davon ab, wie der Runner konfiguriert ist; von aussen ist das
|
|
# nicht zu sehen. Statt es zu raten, wird es hier gemessen und im
|
|
# Fehlerfall gesagt, was fehlt — sonst scheitert der Lauf erst drei
|
|
# Schritte später an einer Meldung, die niemandem weiterhilft.
|
|
- name: Erreicht dieser Job den Docker-Dienst des Servers?
|
|
run: |
|
|
set -u
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo 'Im Job-Container gibt es kein "docker".'
|
|
echo
|
|
echo "Zwei Wege:"
|
|
echo " a) Im Runner (config.yaml) ein Abbild verwenden, das die Docker-CLI"
|
|
echo " mitbringt, und den Socket durchreichen — siehe DEPLOYMENT.md §5."
|
|
echo " b) Statt lokal per SSH auf den Server ausrollen."
|
|
exit 1
|
|
fi
|
|
if ! docker info >/dev/null 2>&1; then
|
|
echo "Docker-CLI vorhanden, aber kein Dienst erreichbar."
|
|
echo
|
|
echo "Dem Job-Container fehlt der Socket des Hosts. In der config.yaml des"
|
|
echo "Runners:"
|
|
echo " container:"
|
|
echo " options: -v /var/run/docker.sock:/var/run/docker.sock"
|
|
echo " valid_volumes: [/var/run/docker.sock]"
|
|
echo "Danach den Runner neu starten. Alternative: Deploy per SSH."
|
|
exit 1
|
|
fi
|
|
docker compose version
|
|
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
|
|
# Nur was der Migrationsläufer braucht (`pg`), nicht der ganze Baum:
|
|
# gebaut wird die Anwendung im Container, nicht hier.
|
|
- name: Abhängigkeiten
|
|
run: npm ci --omit=dev --ignore-scripts
|
|
|
|
# ── .env aus den Gitea-Secrets ──────────────────────────────────
|
|
#
|
|
# Nicht die .env vom Server lesen: der Job läuft in einem eigenen
|
|
# Container, und dessen Dateisystem ist nicht das des Hosts. Die Werte
|
|
# kommen deshalb aus den Repository-Secrets und werden hier
|
|
# zusammengesetzt. Sie landen in keinem Abbild — docker compose reicht
|
|
# sie zur Laufzeit an den Container weiter.
|
|
- name: .env schreiben
|
|
env:
|
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
|
|
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
|
|
AUTH_URL: ${{ secrets.AUTH_URL }}
|
|
AUTH_MICROSOFT_ENTRA_ID_ID: ${{ secrets.AUTH_MICROSOFT_ENTRA_ID_ID }}
|
|
AUTH_MICROSOFT_ENTRA_ID_SECRET: ${{ secrets.AUTH_MICROSOFT_ENTRA_ID_SECRET }}
|
|
AUTH_MICROSOFT_ENTRA_ID_ISSUER: ${{ secrets.AUTH_MICROSOFT_ENTRA_ID_ISSUER }}
|
|
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
|
run: |
|
|
set -euo pipefail
|
|
fehlend=""
|
|
for name in DATABASE_URL MIGRATE_DATABASE_URL AUTH_SECRET AUTH_URL \
|
|
AUTH_MICROSOFT_ENTRA_ID_ID AUTH_MICROSOFT_ENTRA_ID_SECRET \
|
|
AUTH_MICROSOFT_ENTRA_ID_ISSUER CRON_SECRET; do
|
|
eval "wert=\${$name:-}"
|
|
[ -n "$wert" ] || fehlend="$fehlend $name"
|
|
done
|
|
if [ -n "$fehlend" ]; then
|
|
echo "Diese Secrets fehlen im Repository (Settings → Actions → Secrets):$fehlend"
|
|
exit 1
|
|
fi
|
|
# printf statt echo: ein Wert, der mit - beginnt, wäre sonst ein Schalter.
|
|
{
|
|
printf 'DATABASE_URL=%s\n' "$DATABASE_URL"
|
|
printf 'AUTH_SECRET=%s\n' "$AUTH_SECRET"
|
|
printf 'AUTH_URL=%s\n' "$AUTH_URL"
|
|
printf 'AUTH_MICROSOFT_ENTRA_ID_ID=%s\n' "$AUTH_MICROSOFT_ENTRA_ID_ID"
|
|
printf 'AUTH_MICROSOFT_ENTRA_ID_SECRET=%s\n' "$AUTH_MICROSOFT_ENTRA_ID_SECRET"
|
|
printf 'AUTH_MICROSOFT_ENTRA_ID_ISSUER=%s\n' "$AUTH_MICROSOFT_ENTRA_ID_ISSUER"
|
|
printf 'CRON_SECRET=%s\n' "$CRON_SECRET"
|
|
} > .env
|
|
chmod 600 .env
|
|
|
|
# ── Migrationen ─────────────────────────────────────────────────
|
|
#
|
|
# Vor dem Neustart, nicht danach: der neue Code erwartet das neue
|
|
# Schema. Umgekehrt liefe die neue Anwendung kurz gegen das alte und
|
|
# fiele über fehlende Spalten.
|
|
#
|
|
# Erst zeigen, was ansteht — das steht dann im Protokoll des Laufs, auch
|
|
# wenn danach etwas schiefgeht.
|
|
- name: Ausstehende Migrationen zeigen
|
|
env:
|
|
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
|
|
run: node scripts/migrate.mjs --dry-run
|
|
|
|
- name: Migrationen anwenden
|
|
env:
|
|
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
|
|
run: node scripts/migrate.mjs
|
|
|
|
# ── Container ───────────────────────────────────────────────────
|
|
#
|
|
# Läuft gegen den Docker-Dienst des Hosts (der Runner-Container hat
|
|
# dessen Socket eingehängt). Der Projektname wird ausdrücklich gesetzt:
|
|
# sonst leitet ihn Compose vom Verzeichnisnamen ab, und der ist im
|
|
# Arbeitsverzeichnis des Runners ein anderer als bei der ersten
|
|
# Installation von Hand — es entstünde ein zweiter Stapel daneben,
|
|
# während der alte weiterläuft.
|
|
- name: Bauen und starten
|
|
env:
|
|
COMPOSE_PROJECT_NAME: ${{ vars.COMPOSE_PROJECT_NAME || 'alpenwerk-hr' }}
|
|
run: |
|
|
set -euo pipefail
|
|
docker compose build
|
|
docker compose up -d --remove-orphans
|
|
|
|
# ── Nachweis ────────────────────────────────────────────────────
|
|
#
|
|
# Ohne diesen Schritt gilt ein Deploy als erfolgreich, sobald der
|
|
# Container *gestartet* ist — auch wenn die Anwendung darin sofort
|
|
# abstürzt. Gewartet wird auf den Healthcheck aus dem Dockerfile
|
|
# (GET /login), nicht auf „läuft".
|
|
- name: Warten, bis die Anwendung antwortet
|
|
env:
|
|
COMPOSE_PROJECT_NAME: ${{ vars.COMPOSE_PROJECT_NAME || 'alpenwerk-hr' }}
|
|
run: |
|
|
set -euo pipefail
|
|
for versuch in $(seq 1 30); do
|
|
zustand=$(docker compose ps --format '{{.Health}}' app | head -1)
|
|
case "$zustand" in
|
|
healthy) echo "Gesund nach $versuch Versuchen."; exit 0 ;;
|
|
unhealthy) echo "Container meldet unhealthy."; break ;;
|
|
esac
|
|
sleep 4
|
|
done
|
|
echo "Anwendung ist nicht gesund geworden. Letzte Ausgaben:"
|
|
docker compose ps
|
|
docker compose logs --tail 80 app
|
|
exit 1
|
|
|
|
- name: Aufräumen
|
|
if: always()
|
|
run: rm -f .env
|