From 30034a564b6836686c48c2f65dfd48b5ac066eb1 Mon Sep 17 00:00:00 2001 From: boban Date: Mon, 22 Jun 2026 19:34:21 +0200 Subject: [PATCH] =?UTF-8?q?docs(spec):=20full=20release=20model=20?= =?UTF-8?q?=E2=80=94=20channels,=20dashboard=20buttons,=20two=20phases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture the maintainer's release workflow: dev-dashboard buttons drive the pipeline (Deploy to Staging / Deploy to Public / Promote to Stable). Beta = vX.Y.Z-betaN on staging, Stable = vX.Y.Z on public; public serves both channels. Public promotion triggered via the GitHub API with a deploy token in the encrypted vault. Decomposed: Phase A = git/CI/CD + config-URL infra (buildable now, URL-agnostic); Phase B = the dashboard release-control UI (own spec, after A + the repos exist). Co-Authored-By: Claude Opus 4.8 --- ...6-06-22-3-repo-release-promotion-design.md | 117 ++++++++++-------- 1 file changed, 66 insertions(+), 51 deletions(-) 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 index 8c2ce3b..3404a8b 100644 --- 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 @@ -1,82 +1,97 @@ -# 3-Repo Release-Promotion — Design Spec +# 3-Repo Release-Promotion + Release-Steuerung — Design Spec **Datum:** 2026-06-22 -**Status:** entworfen (Repos existieren noch nicht — URL-agnostisch gebaut) +**Status:** entworfen (GitHub-Repos existieren noch nicht — alles URL-agnostisch) ## 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. +Clusev über drei Repositories befördern — **Gitea (dev) → GitHub privat (staging) → GitHub public (release)** — vollständig **vom Dev-Dashboard aus per Buttons** gesteuert, mit GitHub-Actions-CI, **manuellem Gate** (jeder Promotions-Klick = bewusste R5-Bestätigung) und **sauberer Public-Historie** (ein Commit pro Release). **Keine private Repo-URL** in versionierten Dateien. -## Nicht-Ziele +## Kanäle & Tags (Semver) -- 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). +- **Beta:** Tag `vX.Y.Z-betaN` (z. B. `v0.10.0-beta1`). +- **Stable:** Tag `vX.Y.Z` (z. B. `v0.10.0`). +- `ReleaseChecker` kann das bereits: **Stable-Kanal** sieht nur `vX.Y.Z`, **Beta-Kanal** auch `-betaN`. +- **Mapping:** Staging fährt Betas (interner Test). Das Public-Repo bedient **beide** Kanäle — Beta-Tags für Beta-Tester, Stable-Tags für alle. + +## Release-Fluss (alles vom Dev-Dashboard) + +1. **Dev:** du committest auf Gitea (Auto-Mirror nach GitHub-privat). Lokal testen. +2. **Deploy to Staging** → schneidet eine **Beta** `vX.Y.Z-betaN` → CI (GitHub-privat) testet → Staging (.165) fährt die Beta. Du + Staging prüfen. +3. **Deploy to Public** → befördert die Beta ins **GitHub-public** (Beta-Kanal) → Beta-Tester weltweit. +4. **Promote beta → stable** → finalisiert als `vX.Y.Z` auf public (Stable-Kanal) → alle. ## 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. | +| Gitea `git.bave.dev` | privat | **Einzige Dev-Quelle.** Du pushst/taggst (auch via Dashboard-Brücke). Nie direkt in die GitHub-Spiegel committen. | +| GitHub privat | privat | Exakter **Push-Mirror** von Gitea. Trägt CI + Staging-Deploy. | +| GitHub public | öffentlich | **Nur Releases** — ein sauberer Commit pro Release + Tag (Beta + Stable). | ## 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. +Anforderung: dev/staging dürfen alle URLs in ihrer **gitignorierten `.env`** halten; das **Public-Repo** trägt in versionierten Dateien **ausschließlich die Public-URL** — damit niemand über Public die Dev-/Staging-Repos findet oder dorthin pusht. -1. **Getrackter Default = die PUBLIC-URL** (sicher, weil ohnehin öffentlich), kein privater Fallback. `config/clusev.php`: `'repository' => env('CLUSEV_REPOSITORY', '')`. 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 remote get-url origin` und schreiben `CLUSEV_REPOSITORY=` 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. +1. **Getrackter Default = Public-URL** (sicher, weil öffentlich). `config/clusev.php`: `'repository' => env('CLUSEV_REPOSITORY', '')`. Aktuell ist die Gitea-URL hart eingetragen (`'repository' => 'https://git.bave.dev/boban/clusev'`) → durch die Public-URL ersetzen (Platzhalter, bis das Repo existiert). Einzige Repo-URL in versionierten Dateien — und die ist öffentlich. +2. **Private URLs nur in `.env`.** Gitea-/GitHub-privat-URL stehen ausschließlich in der gitignorierten `.env` von dev/staging. +3. **Auto-Ableitung aus dem Klon-Ursprung.** `install.sh`/`update.sh`: `git -C remote get-url origin` → `CLUSEV_REPOSITORY=` in `.env`. → dev bekommt Gitea, staging GitHub-privat, Public-Nutzer Public — automatisch, ohne Hardcode. +4. **Graceful Degradation.** Leeres `CLUSEV_REPOSITORY` + kein Default → kein Update-Check, kein Crash, kein Badge. -**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. +**Ergebnis:** versionierte Dateien tragen höchstens die Public-URL. -## CI-Pipeline (GitHub Actions, im GitHub-privat-Repo) +## CI-Pipeline (GitHub Actions, GitHub-privat) -- **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. +- **Trigger:** Push eines Tags `v*` (Beta + Stable). +- **Jobs (Container):** `composer install` → `php artisan test` → shellcheck (`docker/wg/*.sh`, `install.sh`, `update.sh`) → `./vendor/bin/pint --test` → `npm ci` + `npm run build`. +- **Bei `*-beta*`-Tag & grün:** Staging-Deploy auf .165 (per SSH-Deploy-Key als Actions-Secret → `sudo ./update.sh`). Rot → kein Deploy, Status meldet rot. -## Staging-Deploy +## Promotion-Workflows (per `workflow_dispatch`, vom Dashboard aufrufbar) -Zwei Stufen, secret-frei startbar: +- **`promote-public.yml`** (Eingabe: `tag`): checkt GitHub-privat am Tag aus → Baum **ohne `.git`/`.github`/staging-only Dateien** in einen Checkout von GitHub-public → Commit „Release ``" → Tag → Push (Push-Token-Secret). Beta-Tags landen so im Beta-Kanal. +- **`promote-stable.yml`** (Eingabe: `betaTag`): nimmt den Commit der Beta, legt den **Stable-Tag** `vX.Y.Z` an + pusht ins Public → Stable-Kanal. +- Beide an ein GitHub-**Environment „production" mit Pflicht-Reviewer** gebunden (zweite Sicherung zusätzlich zum Dashboard-Confirm). +- **Hygiene:** Baum ohne `.env*` (gitignored); Tokens nur als Actions-Secret, nie im Log. -- **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. +## Phase B — Dashboard-Release-Steuerung (eigene Spec, NACH Phase A + Repos) -## Public-Promotion (Gate, saubere Historie) +Neue **„Release"-Seite** im Dev-Dashboard: +- Zeigt installierte Version + letzte Tags (Beta/Stable) je Kanal. +- **Buttons (jeder R5-Confirm):** „Deploy to Staging" · „Deploy to Public" · „Promote to Stable". +- **Tag-Erzeugung** (Beta/Stable) über eine **Host-Git-Brücke** analog der WG-/Restart-Brücke (`./run`-Bind-Mount → Host-Skript `git tag` + `git push` zu Gitea; Container bekommt nie Git-Credentials). +- **Public-Promotion** (Deploy to Public / Promote to Stable): Dashboard ruft die **GitHub-API** (`workflow_dispatch`) auf; der **GitHub-Deploy-Token liegt im vorhandenen verschlüsselten Credential-Vault** (`App\Support\Ssh\CredentialVault`-Muster). Direktes Feedback im Dashboard. +- Jede Aktion ins Audit-Log (`deploy.*`). -- **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. +## Artefakte -## Konfig pro Umgebung (Rekapitulation) +**Phase A (jetzt baubar/testbar, URL-agnostisch):** +1. `config/clusev.php`: Gitea-Hardcode → `env('CLUSEV_REPOSITORY', '')`. +2. `install.sh`/`update.sh`: `CLUSEV_REPOSITORY` aus Klon-Ursprung ableiten → `.env`. +3. `.github/workflows/ci-staging.yml` (Tests bei `v*`; Staging-Deploy bei `*-beta*`). +4. `.github/workflows/promote-public.yml` + `promote-stable.yml` (`workflow_dispatch`, Environment „production"). +5. `scripts/promote.sh` (Baum-Kopie + Commit + Tag + Push) — vom Workflow genutzt, lokal shell-testbar. +6. README: 3-Repo-Flow + Kanäle + wo die URLs/Tokens beim Repo-Anlegen eingetragen werden. +7. `ReleaseChecker`/Versions: „keine Repository-URL → kein Update, kein Crash" verifizieren. -- 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. +**Phase B (eigene Spec, später):** Release-Seite (Livewire), Host-Git-Tag-Brücke, GitHub-Deploy-Token im Vault, GitHub-API-Client, Audit. -## Zu bauende Artefakte (im Implementierungsplan) +## Platzhalter für später (nur Einstellungen/Secrets, kein Code) -1. `config/clusev.php`: Gitea-Hardcode raus → `env('CLUSEV_REPOSITORY', '')` (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. +- Gitea Push-Mirror-Ziel = GitHub-privat-URL + GitHub-PAT. +- GitHub-Actions-Secrets: `STAGING_SSH_KEY`, `PUBLIC_REPO_TOKEN`; Variable `PUBLIC_REPO_URL`. +- Public-Default in `config/clusev.php`; Dev-/Staging-`.env`. +- (Phase B) GitHub-Deploy-Token im Vault. ## 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. +- `install.sh`-URL-Ableitung: shell-testbar. +- `config/clusev.php`: liest `CLUSEV_REPOSITORY`, Default = Public-URL, degradiert sauber. +- `ReleaseChecker`: „keine Repo-URL → false, kein Netz, kein Crash". +- `scripts/promote.sh`: shell-testbar (Baum-Kopie ohne `.git`/`.env`, Commit/Tag in einem temp-Repo). +- Workflows: `actionlint`; echte Verifikation erst mit existierenden Repos. + +## Nicht-Ziele + +- GitHub-Repos jetzt anlegen (existieren nicht). +- Keine selbstgebaute Webhook-Infra (Gitea-Mirror + GitHub Actions + GitHub-API genügen). +- Phase B nicht in diesem Plan umsetzen (eigene Spec, sobald A + Repos stehen).