Deploy from a push, and start keeping track of migrations
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 5m52s
CI / Integrationstests (echtes Postgres) (push) Failing after 5m13s
Deploy / Migrationen und Container (push) Has been cancelled

A push to master now builds and restarts the application on the server:
a Gitea Actions workflow on a self-hosted runner writes .env from the
repository secrets, applies pending migrations, rebuilds the compose
stack against the host's Docker daemon, and waits for the container's
healthcheck before calling the run green. Without that last step a
deploy counts as successful the moment the container *starts*, even if
the app inside it dies immediately.

Switching migrations on automatically turned up something that had to be
fixed first: supabase_migrations.schema_migrations did not exist at all.
Every one of the 65 migrations was unrecorded, because they have been
applied by hand all along. An automatic `db push` would therefore have
replayed all 65 against the live database — initial_schema and the OM
cutover included. The database was checked against a spread of
migrations first (it is at head), then baselined: all 65 recorded as
applied without executing them.

The runner is scripts/migrate.mjs rather than the Supabase CLI. It needs
only `pg`, which the project already ships, instead of downloading a CLI
whose version drifts independently of this repository; and it does one
thing — the missing files, in order, each in its own transaction — where
`db push` also diffs schemas and may do more than that. Bookkeeping goes
in the same table in the same shape the CLI uses, so `supabase db push`
from a workstation still works and still skips what already ran.

The workflow lives in .github/workflows, not .gitea/. Gitea reads
.gitea/workflows and falls back to .github/workflows only when the
former is absent — creating .gitea/ would have silently switched off
ci.yml, with the run simply never appearing.

Verified: both workflow files parse; the secret check names what is
missing and refuses; values starting with "-" or containing "=" survive
being written to .env; and the runner was exercised against the real
database with a throwaway migration — applied once, skipped on a second
run, and on a deliberate syntax error rolled back whole, recording
nothing. Both probes were removed; the count is back to 65.

Not verified: nothing has run on an actual Gitea runner — none is
registered yet. DEPLOYMENT.md §5 covers registering one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 16:00:45 +02:00
parent 19e3170b00
commit fc0989debb
3 changed files with 426 additions and 8 deletions

View File

@@ -219,10 +219,126 @@ Ohne diesen Wert baut Auth.js seine Rückruf-Adresse aus dem, was der
Container sieht — und das ist hinter dem Proxy nicht der Name, den der
Browser benutzt hat.
## 5. Updates ausrollen
## 5. Automatisch ausrollen bei einem Push (Gitea Actions)
Ein Push auf `master` baut und startet die Anwendung auf dem Server neu.
Zuständig ist [`.github/workflows/deploy.yml`](.github/workflows/deploy.yml).
> **Warum `.github/` und nicht `.gitea/`:** Gitea Actions liest
> `.gitea/workflows` — und nur wenn dieses Verzeichnis **fehlt**, ersatzweise
> `.github/workflows`. Ein neu angelegtes `.gitea/workflows/` würde also
> `ci.yml` stillschweigend abschalten. Solange beide Dateien unter `.github/`
> liegen, sieht Gitea beide.
### a) Runner registrieren
Der Runner läuft als Container **auf demselben Server** wie die Anwendung und
bekommt den Docker-Socket des Hosts eingehängt — damit baut und startet er die
Container des Hosts, statt welche in sich selbst zu erzeugen.
Registrierungs-Token holen: Gitea → Repository → *Settings* → *Actions* →
*Runners* → *Create new runner*.
```bash
mkdir -p /opt/gitea-runner && cd /opt/gitea-runner
docker run -d --name gitea-runner --restart unless-stopped \
-e GITEA_INSTANCE_URL=https://git.elycon.solutions \
-e GITEA_RUNNER_REGISTRATION_TOKEN=<token> \
-e GITEA_RUNNER_NAME=alpenwerk-deploy \
-e GITEA_RUNNER_LABELS=self-hosted,ubuntu-latest \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /opt/gitea-runner:/data \
gitea/act_runner:latest
```
Zu den Labels: `self-hosted` verlangt der Deploy-Workflow, `ubuntu-latest` die
bestehende `ci.yml`. Fehlt eines, bleibt der jeweilige Lauf ohne Fehlermeldung
in der Warteschlange — **das ist der häufigste Grund, warum „nichts passiert"**.
> Der eingehängte Docker-Socket gibt dem Runner faktisch Wurzelrechte auf dem
> Host. Das ist bei einem Deploy-Runner der Zweck der Übung, aber es heisst
> auch: wer in dieses Repository schreiben darf, darf auf diesem Server alles.
> Bei zwei Personen vertretbar; bei einem grösseren Kreis gehört der Runner auf
> eine eigene Maschine, die sich per SSH verbindet.
### b) Secrets hinterlegen
Gitea → Repository → *Settings* → *Actions* → *Secrets*. Dieselben Werte wie in
[Abschnitt 1](#1-env-anlegen); der Workflow baut daraus zur Laufzeit die `.env`
und löscht sie danach wieder.
| Secret | Anmerkung |
|---|---|
| `DATABASE_URL` | Transaktions-Pooler (Supabase: 6543) — den benutzt die Anwendung |
| `SUPABASE_DB_URL` | **Direkter** Zugang (Supabase: 5432) — nur für die Migrationen |
| `AUTH_SECRET` | |
| `AUTH_URL` | z. B. `https://hr.elycon.solutions` |
| `AUTH_MICROSOFT_ENTRA_ID_ID` | |
| `AUTH_MICROSOFT_ENTRA_ID_SECRET` | |
| `AUTH_MICROSOFT_ENTRA_ID_ISSUER` | |
| `CRON_SECRET` | |
Warum zwei Verbindungsstrings: der Transaktions-Pooler bricht bei einer
Migration ab, die mehrere Anweisungen in einer Transaktion bündelt. Migrationen
gehören über die direkte Verbindung, der laufende Betrieb über den Pooler.
Zusätzlich unter *Variables* (kein Secret, nur eine Einstellung):
| Variable | Vorgabe | Wofür |
|---|---|---|
| `COMPOSE_PROJECT_NAME` | `alpenwerk-hr` | Muss zum bestehenden Stapel passen — sonst entsteht ein zweiter daneben |
Den bestehenden Namen zeigt auf dem Server:
```bash
docker compose ls
```
### c) Was der Lauf tut
1. `.env` aus den Secrets schreiben (fehlt eines, bricht er mit Namen ab).
2. **Migrationen anwenden** — erst `--dry-run` fürs Protokoll, dann echt.
3. `docker compose build` und `up -d` gegen den Docker-Dienst des Hosts.
4. Auf `healthy` warten (bis zu zwei Minuten). Wird die Anwendung nicht gesund,
schlägt der Lauf fehl und hängt die letzten 80 Logzeilen an.
5. `.env` wieder löschen.
Der Workflow ist **nicht** an `ci.yml` gekoppelt: ein Push auf `master` rollt
aus, auch wenn die Tests parallel noch laufen. Soll erst nach grünen Tests
ausgerollt werden, gehört der `deploy`-Job mit `needs: [check]` in `ci.yml` —
dann allerdings braucht der Runner auch das Label, mit dem `check` läuft.
## 5a. Migrationen
Eingespielt werden sie von [`scripts/migrate.mjs`](scripts/migrate.mjs) —
demselben Läufer, den auch der Deploy benutzt:
```bash
node --env-file=.env scripts/migrate.mjs --dry-run # was stünde an
node --env-file=.env scripts/migrate.mjs # anwenden
```
Buch geführt wird in `supabase_migrations.schema_migrations`, derselben Tabelle
in derselben Form, die die Supabase-CLI benutzt — `supabase db push` von der
Arbeitsstation bleibt damit möglich und überspringt, was hier schon lief.
> **Einmalig, im August 2026 bereits erledigt:** Die Migrationen wurden bis
> dahin von Hand eingespielt, die Buchführungstabelle existierte gar nicht. Ein
> automatischer Lauf hätte deshalb alle 65 Dateien erneut gegen die
> produktive Datenbank gespielt — inklusive `initial_schema` und der
> OM-Umstellung. Vor dem Einschalten wurde einmal
> `node scripts/migrate.mjs --baseline` gefahren: das verbucht alles Vorhandene
> als angewendet, **ohne es auszuführen**. Wer eine weitere Umgebung aufsetzt,
> deren Datenbank schon steht, braucht denselben Schritt.
## 5b. Updates von Hand ausrollen
Falls der Runner nicht läuft oder ein Stand ausser der Reihe gebraucht wird:
```bash
git pull
node --env-file=.env scripts/migrate.mjs
docker compose build
docker compose up -d
```
@@ -231,13 +347,8 @@ Kurzer Downtime-Moment beim Neustart des `app`-Containers ist bei dieser
Single-Instance-Compose-Konfiguration normal. Für Zero-Downtime-Deployments
wäre eine zweite Instanz + Load Balancer nötig (siehe Abschnitt 6).
Datenbank-Migrationen (`supabase/migrations/*.sql`) werden weiterhin über
die Supabase CLI gegen das Supabase-Projekt gefahren, unabhängig vom
App-Deployment:
```bash
supabase db push
```
Der Migrationsschritt steht bewusst **vor** dem Neubau: der neue Code erwartet
das neue Schema, und umgekehrt liefe die neue Anwendung kurz gegen das alte.
## 6. Hinweis bei mehreren Replicas
@@ -257,6 +368,23 @@ Single-Instance-Compose-Konfiguration ist das nicht nötig.
## Troubleshooting
- **Push gemacht, aber kein Lauf startet:** meist die Labels. Der Lauf steht
dann in Gitea unter *Actions* als „Waiting" — ohne Fehlermeldung, weil kein
Runner das verlangte Label anbietet. `deploy.yml` verlangt `self-hosted`,
`ci.yml` verlangt `ubuntu-latest`. Welche Labels registriert sind, zeigt
Gitea → *Settings* → *Actions* → *Runners*; ändern lassen sie sich durch
erneutes Registrieren.
- **Deploy läuft, aber es entsteht ein zweiter Stapel:** `COMPOSE_PROJECT_NAME`
passt nicht zum bestehenden. Auf dem Server `docker compose ls` — der dort
gelistete Name gehört als Variable ins Repository. Bis dahin läuft die alte
Instanz weiter und beansprucht Port 3000; die neue scheitert daran.
- **`Cannot connect to the Docker daemon` im Deploy:** dem Runner-Container
fehlt der Socket. Er braucht `-v /var/run/docker.sock:/var/run/docker.sock`.
- **Migration schlägt im Deploy fehl:** der Lauf bricht ab, bevor Container
angefasst werden — die alte Version läuft also weiter. Die fehlgeschlagene
Datei wurde vollständig zurückgerollt und **nicht** verbucht; nach der
Korrektur reicht ein erneuter Push.
- **Anmeldung endet auf `/login?error=…`:** die Umleitungs-URI in der
Entra-Registrierung muss exakt
`https://<host>/api/auth/callback/microsoft-entra-id` lauten. Steht die