# 3-Repo Release-Promotion — Design Spec

**Datum:** 2026-06-22
**Status:** entworfen (Repos existieren noch nicht — URL-agnostisch gebaut)

## Ziel

Clusev über drei Repositories befördern — **Gitea (dev) → GitHub privat (staging) → GitHub public (release)** — mit CI/CD auf GitHub Actions, einem **manuellen Gate** vor Public und **sauberer Public-Historie** (ein Commit pro Release). **Keine Repository-URL steht in versionierten Dateien**, damit die noch nicht erstellten GitHub-Repos später eingehängt werden und das Public-Repo **niemals** die privaten/Gitea-URLs preisgibt.

## Nicht-Ziele

- Die GitHub-Repos jetzt anlegen (existieren noch nicht). Code + Workflows sind URL-agnostisch; die URLs werden beim Erstellen der Repos verdrahtet.
- Keine selbstgebaute Webhook-Infrastruktur (Giteas eingebauter Push-Mirror + GitHub Actions decken alles ab).

## Repos & Rollen

| Repo | Sichtbarkeit | Rolle |
|---|---|---|
| Gitea `git.bave.dev` | privat | **Einzige Dev-Quelle.** Du pushst + taggst hier. Nie direkt in die GitHub-Spiegel committen. |
| GitHub privat | privat | Exakter **Push-Mirror** von Gitea (Gitea-Built-in). Trägt CI + Staging-Deploy. |
| GitHub public | öffentlich | Erhält **nur freigegebene Versionen** — ein sauberer Commit pro Release + Tag. |

## Repository-URL-Handhabung (Sicherheits-Kern)

Klarstellung des Anforderers: **dev und staging dürfen alle URLs enthalten** (in ihrer gitignorierten `.env`); **nur das Public-Repo** darf in versionierten Dateien ausschließlich die Public-URL tragen — damit niemand über das Public-Repo die Dev-/Staging-Repos findet oder dorthin zu pushen versucht.

1. **Getrackter Default = die PUBLIC-URL** (sicher, weil ohnehin öffentlich), kein privater Fallback. `config/clusev.php`: `'repository' => env('CLUSEV_REPOSITORY', '<public-repo-url>')`. Aktuell ist die **Gitea-URL hart** eingetragen (`'repository' => 'https://git.bave.dev/boban/clusev'`) → ersetzen durch die Public-URL (Platzhalter, bis das Repo existiert). Das ist die **einzige** Repo-URL in versionierten Dateien — und sie ist die öffentliche.
2. **Private URLs nur in der `.env`.** Gitea- + GitHub-privat-URL stehen ausschließlich in der gitignorierten `.env` von dev/staging (`CLUSEV_REPOSITORY=…`), **nie** in einem versionierten Baum. → Das Public-Repo enthält **null** private URLs.
3. **Automatische Ableitung aus dem Klon-Ursprung.** `install.sh`/`update.sh` ermitteln die Quelle per `git -C <proj> remote get-url origin` und schreiben `CLUSEV_REPOSITORY=<das>` in die `.env`. → dev (von Gitea geklont) bekommt automatisch die Gitea-URL in seine `.env`, staging die GitHub-privat-URL, Public-Nutzer die Public-URL. Kein manuelles Eintragen, und die private URL landet nur in der gitignorierten `.env`.
4. **Graceful Degradation.** Ist `CLUSEV_REPOSITORY` leer **und** kein Default gesetzt (Phase bevor das Public-Repo existiert), behandeln `ReleaseChecker` + Versions-Seite das sauber (kein Update-Check, kein Crash, kein Sidebar-Badge).
5. **Dev-Maschine jetzt:** `CLUSEV_REPOSITORY=https://git.bave.dev/boban/clusev` in die Dev-`.env` (oder von `install.sh` ableiten lassen), damit der Update-Check schon heute auf Gitea zeigt.

**Ergebnis:** Versionierte Dateien tragen höchstens die Public-URL. Beim Anlegen der Repos trägst du die URLs nur in (a) Giteas Mirror-Einstellung, (b) GitHub-Actions-Secrets/Variablen, (c) die Dev-/Staging-`.env`, (d) den Public-Default in `config/clusev.php` ein.

## CI-Pipeline (GitHub Actions, im GitHub-privat-Repo)

- **Trigger:** Push eines Tags `v*.*.*` (gemäß Entscheidung „nur Versions-Tags").
- **Jobs (Container, wie lokal):** `composer install` → `php artisan test` (aktuell 374) → shellcheck (`koalaman/shellcheck:stable` auf `docker/wg/*.sh`, `install.sh`, `update.sh`) → `./vendor/bin/pint --test` → `npm ci` + `npm run build`.
- **Erfolg →** Staging-Deploy (siehe unten). **Fehler →** kein Deploy; GitHub-Actions-Status meldet rot.

## Staging-Deploy

Zwei Stufen, secret-frei startbar:

- **Default (kein Extra-Secret):** CI deployt **nicht** selbst — der Tag erreicht GitHub-privat, du klickst auf dem Staging-Server (.165) in der **Versions-Seite „Jetzt aktualisieren"** (vorhandener Self-Pull). Passt zu deinem heutigen Update-Weg.
- **Optional (wenn du einen SSH-Deploy-Key als Actions-Secret hinterlegst):** ein Deploy-Job per SSH auf Staging → `sudo ./update.sh`. Per `if`-Bedingung nur aktiv, wenn das Secret existiert.

## Public-Promotion (Gate, saubere Historie)

- **Workflow `promote-public.yml`:** `workflow_dispatch` mit Eingabe `tag` (z. B. `v0.9.56`), gebunden an ein GitHub-**Environment „production" mit Pflicht-Reviewer (du)** → läuft erst nach deiner Freigabe.
- **Schritte:** GitHub-privat am Tag auschecken → Baum **ohne `.git`** (und ohne `.github/workflows/` + staging-only Dateien) in einen Checkout von GitHub-public kopieren → Commit „Release vX.Y.Z" → Tag `vX.Y.Z` → Push (Push-Token als Secret).
- **Ergebnis:** Public-`main` = ein Commit pro Release; Tags vorhanden für den `ReleaseChecker`. Keine Dev-Churn, keine private Historie.
- **Hygiene:** Der Baum enthält keine `.env*` (gitignored, nie im Baum); das Token ist ein Actions-Secret, nie im Log.

## Konfig pro Umgebung (Rekapitulation)

- dev → `CLUSEV_REPOSITORY` = Gitea-URL · staging → GitHub-privat · public → GitHub-public — jeweils aus dem Klon-Ursprung abgeleitet.
- `ReleaseChecker`, Sidebar-Badge und „Was ist neu" lesen `CLUSEV_REPOSITORY` → korrekte Quelle pro Umgebung.

## Zu bauende Artefakte (im Implementierungsplan)

1. `config/clusev.php`: Gitea-Hardcode raus → `env('CLUSEV_REPOSITORY', '<public-url-platzhalter>')` (Default = Public-URL).
2. `install.sh`/`update.sh`: `CLUSEV_REPOSITORY` aus dem Klon-Ursprung ableiten + in `.env` schreiben.
3. `.github/workflows/ci-staging.yml` (Tests bei Tag; optional Staging-Deploy).
4. `.github/workflows/promote-public.yml` (Gated Clean-Push, Environment „production").
5. `scripts/promote.sh` (Baum-Kopie + Commit + Tag + Push) — vom Workflow aufgerufen, lokal shell-testbar.
6. README: 3-Repo-Flow dokumentieren + wie man die URLs beim Erstellen der Repos einträgt + Public-Clone-Befehl.
7. `ReleaseChecker`/Versions: „keine Repository-URL → kein Update, kein Crash" verifizieren (Test ergänzen).

## Platzhalter für später (wenn die GitHub-Repos existieren)

Nur **Einstellungen**, **kein Code**:
- Giteas Push-Mirror-Ziel = GitHub-privat-URL + GitHub-PAT.
- GitHub-Actions: Secrets `STAGING_SSH_KEY` (optional), `PUBLIC_REPO_TOKEN`; Variable/Secret `PUBLIC_REPO_URL`.
- Public-README: Clone-Befehl mit der Public-URL.

## Tests

- `install.sh`-URL-Ableitung: shell-testbar (`git remote get-url origin` → `.env`-Zeile).
- `config/clusev.php` repository: liest `CLUSEV_REPOSITORY`, degradiert auf `null` wenn ungesetzt.
- `ReleaseChecker`: „keine Repository-URL → `updateAvailable()` false, kein Netz, kein Crash" (Test ergänzen).
- Workflows: lokal mit `actionlint` linten; echte Verifikation erst, sobald die Repos existieren.
