From e2b4a968c9dc575c987b2d2776e86d9608ef6771 Mon Sep 17 00:00:00 2001 From: boban Date: Mon, 22 Jun 2026 07:03:24 +0200 Subject: [PATCH] =?UTF-8?q?docs(spec):=203-repo=20release=20promotion=20(G?= =?UTF-8?q?itea=20=E2=86=92=20GitHub=20private=20=E2=86=92=20public)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design for promoting Clusev dev → staging → public across three repos with GitHub Actions CI, a manual gate before public, and a clean public history. Core: no repository URL in tracked files — derived from the clone origin, kept in gitignored .env per environment, so the public repo never exposes the private/Gitea URLs and the URLs plug in later when the GitHub repos exist. Co-Authored-By: Claude Opus 4.8 --- ...6-06-22-3-repo-release-promotion-design.md | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-22-3-repo-release-promotion-design.md diff --git a/docs/superpowers/specs/2026-06-22-3-repo-release-promotion-design.md b/docs/superpowers/specs/2026-06-22-3-repo-release-promotion-design.md new file mode 100644 index 0000000..75edcef --- /dev/null +++ b/docs/superpowers/specs/2026-06-22-3-repo-release-promotion-design.md @@ -0,0 +1,82 @@ +# 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) + +Das ist der zentrale Mechanismus, der „URLs später" + „keine private URL im Public" erfüllt: + +1. **Kein hardcodierter Default mehr.** `config/clusev.php`: `'repository' => env('CLUSEV_REPOSITORY')` — **kein** privater Fallback (aktuell ist die Gitea-URL als Default eingetragen → das muss raus, sonst spiegelt es ins Public). +2. **Ableitung aus dem Klon-Ursprung.** `install.sh`/`update.sh` ermitteln die Quelle per `git -C remote get-url origin` und schreiben `CLUSEV_REPOSITORY=` in die `.env`. → Update-Quelle = von wo das Panel geklont wurde (Gitea bei dev, GitHub-privat auf Staging, GitHub-public bei Nutzern). +3. **`.env` ist gitignored.** Die Gitea- + GitHub-privat-URLs liegen nur in der jeweiligen Installation (`.env` / lokale `git`-Config), **nie** in einem versionierten Baum. Das Public-Repo enthält damit **null** private URLs. +4. **Graceful Degradation.** `ReleaseChecker` + Versions-Seite behandeln leeres `CLUSEV_REPOSITORY` sauber (kein Update-Check, kein Crash, kein Sidebar-Badge). +5. **Dev-Maschine:** `CLUSEV_REPOSITORY` in der Dev-`.env` setzen (oder von `install.sh` ableiten lassen). + +**Ergebnis:** Keine Repo-URL in versionierten Dateien. Beim Anlegen der Repos trägst du die URLs nur in (a) Giteas Mirror-Einstellung, (b) GitHub-Actions-Secrets/Variablen, (c) die Public-README (Clone-Befehl) 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`: Repository-Default entfernen (kein Gitea-Hardcode). +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.