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>
143 lines
6.3 KiB
YAML
143 lines
6.3 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.)
|
|
|
|
on:
|
|
push:
|
|
branches: [master]
|
|
|
|
# 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
|
|
runs-on: self-hosted
|
|
steps:
|
|
- 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 }}
|
|
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_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 SUPABASE_DB_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:
|
|
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
|
|
run: node scripts/migrate.mjs --dry-run
|
|
|
|
- name: Migrationen anwenden
|
|
env:
|
|
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_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
|