From 366d237cb820de1bcbe3281b3df17c70ffbbeb6a Mon Sep 17 00:00:00 2001 From: boban Date: Mon, 22 Jun 2026 22:52:34 +0200 Subject: [PATCH] =?UTF-8?q?docs(spec):=20Phase=20B1=20=E2=80=94=20Deploy?= =?UTF-8?q?=20to=20Staging=20(dashboard=20release=20control)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First sub-project of Phase B: a dev-only Release page with a Deploy-to-Staging button that cuts a beta (bump config version, commit, tag vX.Y.Z-betaN, push to Gitea) via a host release bridge — same isolation as the WireGuard bridge, so the container never holds git/token credentials. Gated behind CLUSEV_RELEASE_CONTROLS. B2 (public promotion via GitHub API + vault token) is a separate later spec. Co-Authored-By: Claude Opus 4.8 --- ...06-22-phase-b1-deploy-to-staging-design.md | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-22-phase-b1-deploy-to-staging-design.md diff --git a/docs/superpowers/specs/2026-06-22-phase-b1-deploy-to-staging-design.md b/docs/superpowers/specs/2026-06-22-phase-b1-deploy-to-staging-design.md new file mode 100644 index 0000000..c910481 --- /dev/null +++ b/docs/superpowers/specs/2026-06-22-phase-b1-deploy-to-staging-design.md @@ -0,0 +1,141 @@ +# Phase B1 — Deploy to Staging (Dashboard Release control) — Design Spec + +**Datum:** 2026-06-22 +**Status:** entworfen +**Voraussetzung:** Phase A (Release-Pipeline-Infrastruktur, v0.9.58) ist gebaut. Dies ist das erste +Sub-Projekt von Phase B (Dashboard-Release-Steuerung). B2 (Deploy-to-Public / Promote-to-Stable über +die GitHub-API + Token) ist eine eigene, spätere Spec. + +## Ziel + +Einen **„Deploy to Staging"-Button** in einer neuen, dev-only **Release-Seite** des Dashboards, der +eine **Beta** schneidet: Version bumpen → committen → `vX.Y.Z-betaN` taggen → nach **Gitea** pushen. +Gitea spiegelt nach GitHub-privat, CI läuft, Staging fährt die Beta. Alle Git-/Token-Operationen +laufen **host-seitig** (Container bekommt nie Credentials) — gleiches Isolationsmodell wie die +WireGuard-Brücke. + +## Designentscheidungen (vom Nutzer bestätigt) + +1. **Zuschnitt:** B1 = nur Deploy-to-Staging. B2 (Public-Promotion) folgt als eigene Spec. +2. **Button bumpt + taggt:** Die Host-Brücke schreibt die neue Version in `config/clusev.php`, + committet, taggt, pusht. Folge-Klicks an derselben Ziel-Version zählen `-betaN` hoch. +3. **Brücke:** Eigene Host-Release-Brücke (Request-File → systemd `.path`-Watcher → `clusev-release.sh`), + analog zur WG-/Restart-Brücke. +4. **Gating:** Explizit per `CLUSEV_RELEASE_CONTROLS=true` (nur in der gitignorierten Dev-`.env`). + Standard aus → Seite/Route/Host-Units existieren auf Nicht-Dev-Installs gar nicht. + +## Token-Landschaft (Klarstellung — B1 braucht KEINEN GitHub-Token) + +| Token | Wer nutzt | Wohin | Zweck | +|---|---|---|---| +| **Gitea-Token** (`GIT_ACCESS_TOKEN`) | Host-Brücke | `/home/nexxo/.env.gitea` (host-seitig) | **B1**: Push Branch+Tag → Gitea | +| **Mirror-PAT** | Gitea | Gitea → Mirror → Authorization | Gitea → push GitHub-privat (boksbc/clusev-staging) | +| `GIT_STAGING_ACCESS_TOKEN` | Dashboard | `.env` jetzt / Vault geplant | **B2**: GitHub-Actions triggern (`workflow_dispatch`) | +| `PUBLIC_REPO_TOKEN` | GitHub Actions | Secret auf clusev-staging | Actions → push clusev/clusev | + +B1 pusht nur nach Gitea (vorhandener Gitea-Token, host-seitig). `GIT_STAGING_ACCESS_TOKEN` ist für B2. + +## Architektur & Datenfluss + +**Bausteine:** +- `config/clusev.php`: `'release_controls' => env('CLUSEV_RELEASE_CONTROLS', false)`. +- Route `/release` + `app/Livewire/Release/Index.php` + `resources/views/livewire/release/index.blade.php` + — nur registriert/erreichbar wenn der Flag an ist. +- `app/Services/ReleasePlanner.php` — reine Semver-Arithmetik (vorgeschlagene Ziel-Versionen). +- `app/Services/ReleaseBridge.php` (Container-Seite) — schreibt Request, liest Result (Sentinel-Muster). +- `docker/release/clusev-release.sh` (Host-Seite) — `serve-request`: validiert, bumpt, committet, taggt, pusht. +- systemd `clusev-release.path` + `clusev-release.service` (Host-Watcher) — von `install.sh` **nur bei + gesetztem Flag** installiert (host-seitig, wie WG/Restart). +- `lang/{de,en}/release.php`; Audit-Event `deploy.staging_release` (+ Label in `lang/{de,en}/audit.php`). + +**Datenfluss „Deploy to Staging":** +1. Operator auf `/release` wählt ein Ziel (patch/minor/major; bei laufender Beta zusätzlich „Nächste + Beta") → Button → **R5-Confirm-Modal** mit dem exakten Tag, der entsteht. +2. `Release\Index::deployStaging($target)`: Flag-Guard · Ziel **server-seitig neu ableiten** und prüfen + `target ∈ erlaubte` · per-User-Throttle (auto-expiring) · `ReleaseBridge->requestStaging($target)` + schreibt `release-request.json` `{action:"stage", target:"X.Y.Z"}` in den bind-gemounteten + Signal-Ordner · Redis-„läuft"-Marker + Audit (angefordert). +3. Host-`.path`-Watcher → `clusev-release.sh serve-request` (siehe unten) → Result-File. +4. Dashboard pollt (`wire:poll` während „läuft", Timeout wie Update-Flow) → Erfolgs-Toast + („Beta `vX.Y.Z-betaN` gepusht → Mirror → CI → Staging") oder lesbarer Fehler; Audit des Ergebnisses. + +**Sicherheits-Kern:** Container schreibt nur ein validiertes Ziel; Git + Token laufen host-seitig. + +## Versionslogik + +- Dashboard rechnet die Ziel-Version, Host nur die Beta-Nummer. Request trägt `target:"X.Y.Z"`. +- `cur` = aktuelle config-Version; `base` = `cur` ohne `-betaN`. +- `ReleasePlanner::proposedTargets(cur)`: + - **stabil** (`cur` = `X.Y.Z`): `{patch: bumpPatch(base), minor: bumpMinor(base), major: bumpMajor(base)}`. + - **Beta** (`cur` = `X.Y.Z-betaN`): zusätzlich `{continueBeta: base}` (weitere Beta derselben Ziel-Version). +- Host: `betaN` = höchster vorhandener `vTARGET-beta*` Tag + 1 (sonst 1) → neue Version `X.Y.Z-betaN`. + +## Host-Skript `clusev-release.sh serve-request` + +**Härtung (wie `clusev-wg.sh`):** `set -euo pipefail`; Feld-Extraktion mit `|| true`; Handler in +Subshell `( … )`; Whitelist `action=stage`; `target` muss `^[0-9]+\.[0-9]+\.[0-9]+$` sein **und ≥ base** +(kein Downgrade). + +**Preconditions (sonst Abbruch → Result-Error, keine Mutation):** +- Repo unter `PROJ` (`/home/nexxo/clusev`), auf dem erwarteten Branch. +- `git status --porcelain` leer (Arbeitsbaum sauber). +- `git fetch` + lokaler HEAD == `origin/` (nichts ungepusht/divergiert). +- Ziel-Tag `vX.Y.Z-betaN` existiert noch nicht. + +**Aktion:** +1. `config/clusev.php` Version → `X.Y.Z-betaN` (sed auf die `'version' => '…'`-Zeile). +2. `git commit -am "chore(release): X.Y.Z-betaN"`. +3. `git tag vX.Y.Z-betaN`. +4. `git push ` + `git push vX.Y.Z-betaN` (Token aus `/home/nexxo/.env.gitea`, + Ausgabe sanitisiert — nie der Token im Log/Result). +5. Result `{ok:true, tag:"vX.Y.Z-betaN"}`. + +**Rollback:** Schlägt Schritt 4 fehl (Netz/Token), wird der lokale Stand zurückgesetzt +(`git tag -d vX.Y.Z-betaN`; `git reset --hard origin/`), damit kein halber Commit/Tag bleibt; +Result `{ok:false, error:"…"}`. + +**Edge-Cases:** dirty → „erst committen/aufräumen" · ungepusht → „erst pushen" · Ziel-Tag existiert → +Beta hochzählen (kein Fehler) · Push scheitert → Rollback + lesbarer Fehler. + +## Gating (dreifach, safe-by-default) + +- `config/clusev.php` Flag `release_controls` (Default false). +- Route `/release` in `routes/web.php` nur registriert wenn `config('clusev.release_controls')`. +- `Release\Index::mount()` + Sidebar-Eintrag prüfen den Flag zusätzlich (`abort(404)` falls aus). +- `install.sh` installiert die systemd-Release-Units nur bei gesetztem Flag. + +## Fehlerbehandlung + +- Request nicht schreibbar (Bind-Mount nicht beschreibbar) → sofortiger lesbarer Fehler, kein „läuft". +- Host-Skript-Fehler → Result-Error, im Dashboard angezeigt + auditiert. +- Poll-Timeout (analog Update-Flow) → „Release hängt/fehlgeschlagen", Marker geräumt. +- Out-of-range/ungültiges Ziel → server-seitig abgelehnt (nie an die Brücke gereicht). +- Throttle erschöpft → lesbarer Hinweis (auto-expiring, nie Lockout). + +## Tests + +- `docker/release/serve-request.test.sh` (shell, host-Logik isoliert): temp-Repo + bare-Remote → + Bump+Commit+Tag+Push verifizieren · Beta-Inkrement (zweiter Lauf → `-beta2`) · Refusals (dirty / + ungepusht / Downgrade-Target / unbekannte action) · Rollback bei Push-Fehler (unerreichbares Remote). +- `ReleasePlannerTest` (pure): patch/minor/major aus stabil; `continueBeta` aus Beta; Strip von `-betaN`. +- `Release\Index`-Test (Livewire): Targets korrekt gerendert · `deployStaging` schreibt Request + + auditiert + setzt „läuft" · out-of-range-Target abgelehnt · Throttle blockt · Result-Handling + (Erfolg/Fehler) · Request-nicht-schreibbar → Fehler ohne „läuft". +- Gating-Test: `/release` 404 bei Flag aus, 200 bei an; Sidebar-Eintrag nur bei an. + +## Nicht-Ziele (B1) + +- Keine CI-/Staging-Status-Anzeige im Dashboard (Operator prüft GitHub Actions; evtl. späteres B3). +- Keine Public-Promotion / GitHub-API / Vault-Token (B2). +- Kein Changelog-Editieren (Betas akkumulieren; der Changelog zählt beim Stable-Release in B2). +- Kein Auto-Deploy auf Staging aus diesem Schritt (das macht die CI bzw. die Versions-Seite). + +## Artefakte + +1. `config/clusev.php` (Flag), `routes/web.php` (bedingte Route), Sidebar-Partial (bedingter Eintrag). +2. `app/Services/ReleasePlanner.php` + Test. +3. `app/Services/ReleaseBridge.php` (Request/Result) + Anteil im Component-Test. +4. `app/Livewire/Release/Index.php` + `resources/views/livewire/release/index.blade.php` + R5-Confirm-Modal. +5. `docker/release/clusev-release.sh` + `docker/release/serve-request.test.sh` + systemd + `clusev-release.path`/`.service`. +6. `install.sh` (bedingte Unit-Installation), `lang/{de,en}/release.php`, Audit-Label in `lang/{de,en}/audit.php`.