Add release/dev build scripts and branch-based deploy workflow

- scripts/release.sh: build from main, tag :latest + short SHA, push
  both. Aborts if not on main or working tree is dirty; warns on
  unpushed commits. Prevents dev states from becoming :latest.
- scripts/dev-build.sh: local dev image (tagged per branch), never
  pushed -- keeps development builds off the registry.
- docs/deployment.md: document branch/PR workflow, replace manual
  build/push steps with the scripts, SHA-based rollback now standard.
- .gitattributes: force LF on *.sh so the shebang works on Windows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tleiningerandClaude Opus 4.8 committed 2026-09-21 07:58:46 +02:00
1 parent 99dd052929
commit 94d9cac77b
4 files changed
+119 -30

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# Shell-Skripte brauchen Unix-Zeilenenden (LF), sonst scheitert der #!-Aufruf
# in bash. Unabhaengig von core.autocrlf auf Windows erzwingen.
*.sh text eol=lf
+39 -30
View File
@@ -35,35 +35,44 @@ eingegeben werden muss.
---
## Arbeitsweise: Branches, nie direkt auf main
`main` ist der **veröffentlichte** Stand — nur daraus wird `:latest` gebaut, und
nur `:latest` zieht Unraid. Deshalb wird nie direkt auf `main` committet:
1. Feature-Branch anlegen: `git switch -c <bereich>/<kurz>` (z.B.
`deployment/sha-tagging`, `content/pipeline`).
2. Dort committen, Branch pushen: `git push -u origin <branch>`.
3. In Gitea einen **Pull Request** gegen `main` öffnen und dort mergen.
4. Erst danach aus `main` das Release-Image bauen (unten).
**Dev-Images bleiben lokal.** Zum Ausprobieren auf dem eigenen Rechner:
```bash
scripts/dev-build.sh # baut elternbeirat-web:dev-<branch>, pusht NICHTS
```
So kann ein Entwicklungsstand nie versehentlich als `:latest` in der Registry
landen.
## Neuen Stand ausrollen (Handbetrieb)
Alle Befehle auf dem Entwicklungsrechner (Docker installiert), im Repo-Root.
### 1. Image bauen
Voraussetzung: einmalig an der Registry angemeldet (siehe unten). Dann, **auf
main** und mit sauberem Arbeitsverzeichnis:
```bash
docker build -t gitea.anticarnist.de/tom/elternbeirat:latest .
scripts/release.sh
```
Der Image-Pfad ist **kleingeschrieben** — Container-Registries verlangen das im
Pfad, obwohl Benutzer (`Tom`) und Repo (`Elternbeirat`) großgeschrieben sind.
Das Skript baut das Image, taggt es mit `:latest` **und** dem Commit-Kurz-SHA
(für Rollback) und pusht beide. Es **bricht ab**, wenn du nicht auf `main` bist
oder uncommittete Änderungen hast, und warnt bei ungepushten Commits.
### 2. An der Registry anmelden (einmalig, bis das Token wechselt)
> Der Image-Pfad `gitea.anticarnist.de/tom/elternbeirat` ist **kleingeschrieben**
> — Container-Registries verlangen das im Pfad, obwohl Benutzer (`Tom`) und Repo
> (`Elternbeirat`) großgeschrieben sind.
```bash
docker login gitea.anticarnist.de
```
- **Username:** `Tom`
- **Password:** das `package`-Token (nicht das Kontopasswort).
### 3. Image in die Registry pushen
```bash
docker push gitea.anticarnist.de/tom/elternbeirat:latest
```
### 4. Unraid einmalig an der Registry anmelden
### Unraid einmalig an der Registry anmelden
Das Image ist **privat**, deshalb muss Unraid sich einmal anmelden, bevor es
ziehen kann. Das **Compose Manager Plus**-Plugin hat dafür kein UI-Feld — der
@@ -90,7 +99,7 @@ wechselt. Zwei erfahrungsgemäße Stolpersteine:
`/root/.docker/config.json`. Auf dem eigenen Server für den Anfang okay.
*Später sauberer:* einen Credential-Helper einrichten (→ offener Punkt unten).
### 5. Auf Unraid neu ziehen und starten
### Auf Unraid neu ziehen und starten
Im **Compose Manager Plus**-Plugin (Unraid-Weboberfläche):
@@ -150,17 +159,17 @@ networks:
## Rollback
`:latest` allein macht Rollback unmöglich — deshalb sobald wir auf Gitea Actions
umstellen (Schritt 10) **immer auch mit dem Commit-SHA taggen** und pushen:
`scripts/release.sh` taggt jeden Release zusätzlich mit dem Commit-Kurz-SHA, der
sich — anders als das wandernde `:latest` — nie verschiebt. Zum Zurückrollen in
der Unraid-`compose.yaml` `:latest` durch `:<sha>` des letzten funktionierenden
Stands ersetzen und neu hochfahren:
```bash
docker build -t gitea.anticarnist.de/tom/elternbeirat:latest \
-t gitea.anticarnist.de/tom/elternbeirat:<sha> .
docker push gitea.anticarnist.de/tom/elternbeirat --all-tags
```yaml
image: gitea.anticarnist.de/tom/elternbeirat:99dd052 # statt :latest
```
Zum Zurückrollen in der Unraid-`compose.yaml` `:latest` durch `:<sha>` des
letzten funktionierenden Stands ersetzen und neu hochfahren.
Welche SHA-Tags in der Registry liegen, zeigt Gitea unter
`Tom/-/packages` → `elternbeirat`.
---
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Baut ein lokales Dev-Image zum Ausprobieren auf dem eigenen Rechner.
# Pusht NICHTS in die Registry -- der Stand bleibt privat auf dem Laptop.
#
# Der Tag enthaelt den aktuellen Branchnamen, damit Dev-Images nicht mit dem
# Produktiv-:latest verwechselt werden. Anschliessend z.B. lokal starten:
# docker run --rm -p 5000:8080 elternbeirat-web:dev-<branch>
# oder ueber die lokale compose.yaml.
set -euo pipefail
cd "$(dirname "$0")/.."
branch="$(git rev-parse --abbrev-ref HEAD | tr '/' '-')"
tag="elternbeirat-web:dev-$branch"
echo "Baue lokales Dev-Image: $tag (kein Push)"
docker build -t "$tag" .
echo
echo "Fertig. Lokal starten z.B. mit:"
echo " docker run --rm -p 5000:8080 $tag"
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Baut das Produktiv-Image aus dem aktuellen main-Stand und pusht es in die
# Gitea-Registry, getaggt mit :latest UND dem Commit-Kurz-SHA (Rollback).
#
# Nur fuer freigegebene Staende: das Skript verweigert den Push, wenn du nicht
# auf main bist oder uncommittete Aenderungen hast. Fuer Dev-Builds ohne Push
# stattdessen scripts/dev-build.sh nutzen.
#
# Voraussetzung: einmalig `docker login gitea.anticarnist.de` (siehe
# docs/deployment.md).
set -euo pipefail
IMAGE="gitea.anticarnist.de/tom/elternbeirat"
# Ins Repo-Root wechseln (Skript liegt in scripts/), damit der Docker-Build-
# Kontext stimmt, egal von wo aufgerufen.
cd "$(dirname "$0")/.."
branch="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$branch" != "main" ]]; then
echo "ABBRUCH: du bist auf '$branch', nicht auf 'main'." >&2
echo "Ein :latest-Release darf nur aus main gebaut werden." >&2
echo "Fuer einen Dev-Build ohne Push: scripts/dev-build.sh" >&2
exit 1
fi
if [[ -n "$(git status --porcelain)" ]]; then
echo "ABBRUCH: Arbeitsverzeichnis nicht sauber (uncommittete Aenderungen)." >&2
echo "Erst committen, damit der SHA-Tag den Image-Inhalt eindeutig benennt." >&2
exit 1
fi
# Warnung, wenn lokaler main dem Remote voraus ist (ungepushte Commits) — dann
# wuerde ein SHA getaggt, den es auf Gitea noch nicht gibt.
if git rev-parse --verify --quiet origin/main >/dev/null; then
ahead="$(git rev-list --count origin/main..HEAD)"
if [[ "$ahead" -gt 0 ]]; then
echo "WARNUNG: lokaler main ist origin/main um $ahead Commit(s) voraus." >&2
echo " Erst 'git push', damit der SHA auf Gitea existiert." >&2
read -r -p "Trotzdem fortfahren? [y/N] " answer
[[ "$answer" == "y" || "$answer" == "Y" ]] || exit 1
fi
fi
sha="$(git rev-parse --short HEAD)"
echo "Baue $IMAGE (Tags: latest, $sha)"
docker build -t "$IMAGE:latest" -t "$IMAGE:$sha" .
docker push "$IMAGE" --all-tags
echo
echo "Fertig. Gepusht: $IMAGE:latest und $IMAGE:$sha"
echo "Auf Unraid: Stack 'elternbeirat' -> Compose Down/Up (bzw. Pull), damit"
echo "das neue :latest gezogen wird."