Files
alpenwerk-hr/.github/workflows/deploy.yml
Maximilian Stubhan b87c8ad64c
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 5m40s
CI / Migrationen auf leerer Datenbank (push) Has been cancelled
Remove Supabase
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>
2026-09-07 10:43:22 +02:00

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