From 383f2fe74a695fa3df38f7af0325f0c5b90a803f Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 3 Jul 2026 20:43:15 +0200 Subject: [PATCH] =?UTF-8?q?feat(release):=20remove=20the=20no-op=20channel?= =?UTF-8?q?=20selector=20+=20dead=20beta=E2=86=92stable=20promotion=20(sin?= =?UTF-8?q?gle=20stable=20channel)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 1 + app/Livewire/Release/Index.php | 33 --------- app/Livewire/System/Index.php | 68 +------------------ app/Services/PromotionService.php | 5 -- app/Support/Confirm/ConfirmToken.php | 2 - lang/de/system.php | 22 +----- lang/en/system.php | 22 +----- .../views/livewire/release/index.blade.php | 5 +- .../views/livewire/system/index.blade.php | 41 +---------- tests/Feature/PromotionServiceTest.php | 11 --- tests/Feature/ReleasePageTest.php | 17 ----- 11 files changed, 10 insertions(+), 217 deletions(-) diff --git a/.gitattributes b/.gitattributes index e097f8f..f7c5689 100644 --- a/.gitattributes +++ b/.gitattributes @@ -34,6 +34,7 @@ # …and their tests (which reference the dev-only classes above). NOTE: ReleaseChecker.php, # ReleaseCheckerTest.php and VersionUpdateCheckTest.php are PUBLIC (the update-available check) — do NOT ignore them. /tests/Feature/PipelineStatusTest.php export-ignore +/tests/Feature/PromotionServiceTest.php export-ignore /tests/Feature/ReleaseBridgeTest.php export-ignore /tests/Feature/ReleaseGatingTest.php export-ignore /tests/Feature/ReleasePageTest.php export-ignore diff --git a/app/Livewire/Release/Index.php b/app/Livewire/Release/Index.php index a23463c..f882040 100644 --- a/app/Livewire/Release/Index.php +++ b/app/Livewire/Release/Index.php @@ -220,38 +220,6 @@ class Index extends Component level: $ok ? 'info' : 'error'); } - public function confirmPromoteStable(): void - { - $tag = $this->currentBetaTag(); - if ($tag === null) { - return; - } - $stable = 'v'.preg_replace('/-.*$/', '', ltrim((string) config('clusev.version'), 'vV')); - $this->openConfirm('releaseStable', ['betaTag' => $tag], - __('release.stable_confirm_title'), __('release.stable_confirm_body', ['v' => $stable]), - __('release.stable_action'), danger: false, icon: 'tag'); - } - - #[On('releaseStable')] - public function applyPromoteStable(string $confirmToken, PromotionService $promotion): void - { - try { - $payload = ConfirmToken::consume($confirmToken, 'releaseStable'); - } catch (InvalidConfirmToken) { - return; - } - $beta = (string) ($payload['params']['betaTag'] ?? ''); - if ($beta === '' || $this->currentBetaTag() !== $beta || ! $this->throttlePromote()) { - return; - } - $stable = 'v'.preg_replace('/-.*$/', '', ltrim($beta, 'vV')); - $ok = $promotion->promoteStable($beta, $stable); - $this->auditPromotion('deploy.stable', $stable, $ok); - $this->dispatch('notify', - message: $ok ? __('release.stable_dispatched', ['v' => $stable]) : __('release.promote_failed'), - level: $ok ? 'info' : 'error'); - } - public function confirmYank(string $tag, PromotionService $promotion): void { if (! in_array($tag, $promotion->publicTags(), true)) { @@ -314,7 +282,6 @@ class Index extends Component 'targets' => app(ReleasePlanner::class)->proposedTargets($current), 'pipeline' => app(PipelineStatus::class)->forTrackedBeta(), 'publicTags' => app(PromotionService::class)->publicTags(), - 'stableTarget' => 'v'.preg_replace('/-.*$/', '', ltrim($current, 'vV')), ])->title(__('release.title')); } } diff --git a/app/Livewire/System/Index.php b/app/Livewire/System/Index.php index 248b0b2..8fcb4ad 100644 --- a/app/Livewire/System/Index.php +++ b/app/Livewire/System/Index.php @@ -3,7 +3,6 @@ namespace App\Livewire\System; use App\Models\AuditEvent; -use App\Models\Setting; use App\Services\DeploymentService; use App\Support\Confirm\ConfirmToken; use App\Support\Confirm\InvalidConfirmToken; @@ -14,23 +13,17 @@ use Livewire\Attributes\On; use Livewire\Component; /** - * System settings — dashboard-configurable Domain + TLS and the release channel. + * System settings — dashboard-configurable Domain + TLS. * * Domain & TLS: the panel domain is EDITABLE here. Saving persists it (Setting * `panel_domain`, overriding the install-time APP_DOMAIN) and asks the operator to * restart the stack; on restart everything re-derives from it (app.url, the Reverb * client endpoint, Caddy's on-demand cert, the secure-cookie flag). Nothing switches * mid-session — that is what makes it safe. Bare-IP/HTTP access stays a recovery path. - * - * Release-Kanal: a stable|beta choice persisted as a Setting (default from - * config('clusev.channel')), audited on change. */ #[Layout('layouts.app')] class Index extends Component { - /** Valid user-facing release channels (descriptions are localized at render time). */ - public const CHANNELS = ['stable', 'beta']; - /** Valid TLS termination modes. */ public const TLS_MODES = ['caddy', 'external']; @@ -46,19 +39,8 @@ class Index extends Component /** True once the operator clicked "restart now" — the host watcher is applying it. */ public bool $restartRequested = false; - public string $channel = 'stable'; - public string $tlsMode = 'caddy'; - /** Channel key => localized description, built per-request for the view. */ - private function channelDescriptions(): array - { - return [ - 'stable' => __('system.channel_stable'), - 'beta' => __('system.channel_beta'), - ]; - } - public function mount(DeploymentService $deployment): void { // Active = what the stack currently serves (snapshot); the form edits the PENDING @@ -67,9 +49,6 @@ class Index extends Component $this->domainInput = (string) ($deployment->configuredDomain() ?? ''); $this->restartPending = $deployment->restartPending(); $this->restartRequested = $deployment->restartRequested(); - $channel = Setting::get('release_channel', config('clusev.channel')) ?? 'stable'; - // Clamp to a valid user channel — a stale/legacy value (e.g. 'dev') falls back to stable. - $this->channel = in_array($channel, self::CHANNELS, true) ? $channel : 'stable'; $this->tlsMode = $deployment->externalTls() ? 'external' : 'caddy'; } @@ -201,50 +180,6 @@ class Index extends Component $this->dispatch('notify', message: $message, level: $level); } - /** Release channel changes are audited (confirmation via the shared modal). */ - public function confirmChannel(string $channel): void - { - if (! in_array($channel, self::CHANNELS, true) || $channel === $this->channel) { - return; - } - - $this->dispatch('openModal', - component: 'modals.confirm-action', - arguments: [ - 'heading' => __('system.change_channel_heading'), - 'body' => __('system.change_channel_body', ['channel' => $channel]), - 'confirmLabel' => __('system.change_channel_confirm'), - 'danger' => false, - 'icon' => 'git-branch', - 'notify' => __('system.change_channel_notify', ['channel' => $channel]), - 'token' => ConfirmToken::issue( - 'channelChanged', - ['channel' => $channel], - 'system.settings_updated', - 'release_channel='.$channel, - ), - ], - ); - } - - #[On('channelChanged')] - public function applyChannel(string $confirmToken): void - { - try { - $payload = ConfirmToken::consume($confirmToken, 'channelChanged'); - } catch (InvalidConfirmToken) { - return; // forged / replayed / direct-bypass attempt — no-op - } - $channel = $payload['params']['channel']; - - if (! in_array($channel, self::CHANNELS, true)) { - return; - } - - Setting::put('release_channel', $channel); - $this->channel = $channel; - } - /** TLS-mode changes are audited (confirmation via the shared modal). */ public function confirmTlsMode(string $mode): void { @@ -306,7 +241,6 @@ class Index extends Component 'hasTls' => $this->domain !== '', 'panelUrl' => $deployment->panelUrl(), 'isOverridden' => $deployment->domainIsOverridden(), - 'channels' => $this->channelDescriptions(), // Cert request is meaningful only when Caddy serves TLS for an ACTIVE domain. 'showCert' => $this->tlsMode === 'caddy' && $this->domain !== '', 'certStatus' => $deployment->certStatus(), diff --git a/app/Services/PromotionService.php b/app/Services/PromotionService.php index d84db3b..f73ee9f 100644 --- a/app/Services/PromotionService.php +++ b/app/Services/PromotionService.php @@ -19,11 +19,6 @@ class PromotionService return $this->dispatch('promote-public.yml', ['tag' => $betaTag]); } - public function promoteStable(string $betaTag, string $stableTag): bool - { - return $this->dispatch('promote-stable.yml', ['betaTag' => $betaTag, 'stableTag' => $stableTag]); - } - public function yank(string $tag): bool { return $this->dispatch('yank.yml', ['tag' => $tag]); diff --git a/app/Support/Confirm/ConfirmToken.php b/app/Support/Confirm/ConfirmToken.php index bb0cc95..465c380 100644 --- a/app/Support/Confirm/ConfirmToken.php +++ b/app/Support/Confirm/ConfirmToken.php @@ -44,7 +44,6 @@ class ConfirmToken 'userRemoved', 'userLoggedOut', 'domainChanged', - 'channelChanged', 'tlsModeChanged', 'fileConfirmed', 'keyRemoved', @@ -63,7 +62,6 @@ class ConfirmToken 'wgSetSubnet', 'releaseStaged', 'releasePublic', - 'releaseStable', 'releaseYank', ]; diff --git a/lang/de/system.php b/lang/de/system.php index 16be80e..855c1bd 100644 --- a/lang/de/system.php +++ b/lang/de/system.php @@ -1,11 +1,11 @@ 'System', - 'heading' => 'Domain, TLS & Release-Kanal', - 'subheading' => 'Zugriffsadresse des Panels und Update-Quelle', + 'heading' => 'Domain & TLS', + 'subheading' => 'Zugriffsadresse des Panels', 'https_active' => 'HTTPS aktiv', 'https_plain' => 'Klartext-HTTP', @@ -49,22 +49,6 @@ return [ 'restart_watcher_hint' => 'Falls nach ~30 s nichts passiert, ist der Host-Watcher evtl. nicht installiert — siehe Doku (docker/restart-sentinel).', 'restart_lockout_hint' => 'Nach dem Neustart eventuell neu anmelden. Falls die neue Domain (DNS/Zertifikat) noch nicht erreichbar ist, bleibt das Panel über http:// als Rückfallweg erreichbar.', - // Release channel panel - 'channel_title' => 'Release-Kanal', - 'channel_subtitle' => 'Quelle für Updates', - 'channel_radiogroup_label' => 'Release-Kanal', - 'channel_current' => 'Kanal:', - - // Channel descriptions (CHANNELS array) - 'channel_stable' => 'Nur getestete, vom Maintainer freigegebene Releases (getaggte Versionen). Empfohlen für den Produktivbetrieb.', - 'channel_beta' => 'Vorabversionen kommender Releases zum Testen neuer Funktionen. Kann instabil sein. Entwicklungs-Builds werden Nutzern nie angeboten.', - - // Change-channel confirmation modal - 'change_channel_heading' => 'Release-Kanal wechseln', - 'change_channel_body' => 'Der Release-Kanal wird auf „:channel“ gesetzt. Updates werden dann aus dieser Quelle bezogen.', - 'change_channel_confirm' => 'Wechseln', - 'change_channel_notify' => 'Release-Kanal auf „:channel“ gesetzt.', - // TLS-mode selector 'tls_mode_title' => 'TLS-Terminierung', 'tls_mode_caddy' => 'Eingebautes TLS', diff --git a/lang/en/system.php b/lang/en/system.php index d7ff095..5a4e2e4 100644 --- a/lang/en/system.php +++ b/lang/en/system.php @@ -1,11 +1,11 @@ 'System', - 'heading' => 'Domain, TLS & release channel', - 'subheading' => 'Panel access address and update source', + 'heading' => 'Domain & TLS', + 'subheading' => 'Panel access address', 'https_active' => 'HTTPS active', 'https_plain' => 'Plaintext HTTP', @@ -49,22 +49,6 @@ return [ 'restart_watcher_hint' => 'If nothing happens after ~30 s, the host watcher may not be installed — see the docs (docker/restart-sentinel).', 'restart_lockout_hint' => 'You may need to sign in again after the restart. If the new domain (DNS/certificate) is not reachable yet, the panel stays available at http:// as a fallback.', - // Release channel panel - 'channel_title' => 'Release channel', - 'channel_subtitle' => 'Source for updates', - 'channel_radiogroup_label' => 'Release channel', - 'channel_current' => 'Channel:', - - // Channel descriptions (CHANNELS array) - 'channel_stable' => 'Only tested releases approved by the maintainer (tagged versions). Recommended for production use.', - 'channel_beta' => 'Pre-releases of upcoming versions for testing new features. May be unstable. Development builds are never offered to users.', - - // Change-channel confirmation modal - 'change_channel_heading' => 'Switch release channel', - 'change_channel_body' => 'The release channel will be set to “:channel”. Updates will then be pulled from this source.', - 'change_channel_confirm' => 'Switch', - 'change_channel_notify' => 'Release channel set to “:channel”.', - // TLS-mode selector 'tls_mode_title' => 'TLS termination', 'tls_mode_caddy' => 'Built-in TLS', diff --git a/resources/views/livewire/release/index.blade.php b/resources/views/livewire/release/index.blade.php index 505d441..98745cf 100644 --- a/resources/views/livewire/release/index.blade.php +++ b/resources/views/livewire/release/index.blade.php @@ -119,13 +119,10 @@ {{-- Publish the current beta --}}

{{ __('release.publish') }}

-
+
{{ __('release.deploy_public', ['tag' => $current]) }} - - {{ __('release.promote_stable', ['v' => $stableTarget]) }} -

{{ __('release.publish_hint') }}

diff --git a/resources/views/livewire/system/index.blade.php b/resources/views/livewire/system/index.blade.php index aa6d250..59042d8 100644 --- a/resources/views/livewire/system/index.blade.php +++ b/resources/views/livewire/system/index.blade.php @@ -93,7 +93,7 @@

{{ __('system.tls_mode_title') }}

- {{-- Segmented control (mirrors the channel selector) --}} + {{-- Segmented control --}}
- - {{-- ── Release-Kanal (full-width band, below the grid) ──────────────────── --}} - -
- {{-- Segmented control --}} -
- @foreach ($channels as $key => $desc) - {{-- wire:key keeps Livewire's morph from cloning by position, which would drop the - inline Alpine x-data (modalTrigger) scope → "pending is not defined". --}} - - @endforeach -
- - {{-- Per-channel description --}} -
- -
-

{{ __('system.channel_current') }} {{ $channel }}

-

{{ $channels[$channel] ?? '' }}

-
-
-
-
diff --git a/tests/Feature/PromotionServiceTest.php b/tests/Feature/PromotionServiceTest.php index efc9007..a077634 100644 --- a/tests/Feature/PromotionServiceTest.php +++ b/tests/Feature/PromotionServiceTest.php @@ -38,17 +38,6 @@ class PromotionServiceTest extends TestCase && ! str_contains($req->url(), 'tok_secret')); } - public function test_promote_stable_dispatches_with_both_tags(): void - { - Http::fake(['api.github.com/*' => Http::response('', 204)]); - - $this->assertTrue(app(PromotionService::class)->promoteStable('v0.10.0-beta3', 'v0.10.0')); - - Http::assertSent(fn ($req) => str_contains($req->url(), 'promote-stable.yml/dispatches') - && $req['inputs']['betaTag'] === 'v0.10.0-beta3' - && $req['inputs']['stableTag'] === 'v0.10.0'); - } - public function test_yank_dispatches_yank_with_the_tag(): void { Http::fake(['api.github.com/*' => Http::response('', 204)]); diff --git a/tests/Feature/ReleasePageTest.php b/tests/Feature/ReleasePageTest.php index ef3ccaf..93e52ef 100644 --- a/tests/Feature/ReleasePageTest.php +++ b/tests/Feature/ReleasePageTest.php @@ -132,23 +132,6 @@ class ReleasePageTest extends TestCase $this->assertTrue(AuditEvent::where('action', 'deploy.public')->exists()); } - public function test_promote_stable_uses_the_beta_base_as_the_stable_tag(): void - { - config()->set('clusev.version', '0.10.0-beta3'); - config()->set('clusev.github_token', 'tok'); - config()->set('clusev.staging_slug', 'acme/staging'); - config()->set('clusev.release_branch', 'feat/v1-foundation'); - Http::fake(['api.github.com/*' => Http::response('', 204)]); - - $token = ConfirmToken::issue('releaseStable', ['betaTag' => 'v0.10.0-beta3']); - ConfirmToken::confirm($token); // the modal confirmation step - Livewire::test(Index::class)->call('applyPromoteStable', $token); - - Http::assertSent(fn ($req) => str_contains($req->url(), 'promote-stable.yml/dispatches') - && $req['inputs']['stableTag'] === 'v0.10.0'); - $this->assertTrue(AuditEvent::where('action', 'deploy.stable')->exists()); - } - public function test_yank_only_dispatches_for_a_tag_that_exists_on_public(): void { config()->set('clusev.github_token', 'tok');