docs(spec): Phase B1 — Deploy to Staging (dashboard release control)
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 <noreply@anthropic.com>feat/v1-foundation
parent
628c9fdbb0
commit
366d237cb8
|
|
@ -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/<branch>` (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 <gitea> <branch>` + `git push <gitea> 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/<branch>`), 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`.
|
||||||
Loading…
Reference in New Issue