feat(release): remove the no-op channel selector + dead beta→stable promotion (single stable channel)
parent
10486672a5
commit
383f2fe74a
|
|
@ -34,6 +34,7 @@
|
||||||
# …and their tests (which reference the dev-only classes above). NOTE: ReleaseChecker.php,
|
# …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.
|
# ReleaseCheckerTest.php and VersionUpdateCheckTest.php are PUBLIC (the update-available check) — do NOT ignore them.
|
||||||
/tests/Feature/PipelineStatusTest.php export-ignore
|
/tests/Feature/PipelineStatusTest.php export-ignore
|
||||||
|
/tests/Feature/PromotionServiceTest.php export-ignore
|
||||||
/tests/Feature/ReleaseBridgeTest.php export-ignore
|
/tests/Feature/ReleaseBridgeTest.php export-ignore
|
||||||
/tests/Feature/ReleaseGatingTest.php export-ignore
|
/tests/Feature/ReleaseGatingTest.php export-ignore
|
||||||
/tests/Feature/ReleasePageTest.php export-ignore
|
/tests/Feature/ReleasePageTest.php export-ignore
|
||||||
|
|
|
||||||
|
|
@ -220,38 +220,6 @@ class Index extends Component
|
||||||
level: $ok ? 'info' : 'error');
|
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
|
public function confirmYank(string $tag, PromotionService $promotion): void
|
||||||
{
|
{
|
||||||
if (! in_array($tag, $promotion->publicTags(), true)) {
|
if (! in_array($tag, $promotion->publicTags(), true)) {
|
||||||
|
|
@ -314,7 +282,6 @@ class Index extends Component
|
||||||
'targets' => app(ReleasePlanner::class)->proposedTargets($current),
|
'targets' => app(ReleasePlanner::class)->proposedTargets($current),
|
||||||
'pipeline' => app(PipelineStatus::class)->forTrackedBeta(),
|
'pipeline' => app(PipelineStatus::class)->forTrackedBeta(),
|
||||||
'publicTags' => app(PromotionService::class)->publicTags(),
|
'publicTags' => app(PromotionService::class)->publicTags(),
|
||||||
'stableTarget' => 'v'.preg_replace('/-.*$/', '', ltrim($current, 'vV')),
|
|
||||||
])->title(__('release.title'));
|
])->title(__('release.title'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
namespace App\Livewire\System;
|
namespace App\Livewire\System;
|
||||||
|
|
||||||
use App\Models\AuditEvent;
|
use App\Models\AuditEvent;
|
||||||
use App\Models\Setting;
|
|
||||||
use App\Services\DeploymentService;
|
use App\Services\DeploymentService;
|
||||||
use App\Support\Confirm\ConfirmToken;
|
use App\Support\Confirm\ConfirmToken;
|
||||||
use App\Support\Confirm\InvalidConfirmToken;
|
use App\Support\Confirm\InvalidConfirmToken;
|
||||||
|
|
@ -14,23 +13,17 @@ use Livewire\Attributes\On;
|
||||||
use Livewire\Component;
|
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
|
* 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
|
* `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
|
* 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
|
* 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.
|
* 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')]
|
#[Layout('layouts.app')]
|
||||||
class Index extends Component
|
class Index extends Component
|
||||||
{
|
{
|
||||||
/** Valid user-facing release channels (descriptions are localized at render time). */
|
|
||||||
public const CHANNELS = ['stable', 'beta'];
|
|
||||||
|
|
||||||
/** Valid TLS termination modes. */
|
/** Valid TLS termination modes. */
|
||||||
public const TLS_MODES = ['caddy', 'external'];
|
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. */
|
/** True once the operator clicked "restart now" — the host watcher is applying it. */
|
||||||
public bool $restartRequested = false;
|
public bool $restartRequested = false;
|
||||||
|
|
||||||
public string $channel = 'stable';
|
|
||||||
|
|
||||||
public string $tlsMode = 'caddy';
|
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
|
public function mount(DeploymentService $deployment): void
|
||||||
{
|
{
|
||||||
// Active = what the stack currently serves (snapshot); the form edits the PENDING
|
// 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->domainInput = (string) ($deployment->configuredDomain() ?? '');
|
||||||
$this->restartPending = $deployment->restartPending();
|
$this->restartPending = $deployment->restartPending();
|
||||||
$this->restartRequested = $deployment->restartRequested();
|
$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';
|
$this->tlsMode = $deployment->externalTls() ? 'external' : 'caddy';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,50 +180,6 @@ class Index extends Component
|
||||||
$this->dispatch('notify', message: $message, level: $level);
|
$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). */
|
/** TLS-mode changes are audited (confirmation via the shared modal). */
|
||||||
public function confirmTlsMode(string $mode): void
|
public function confirmTlsMode(string $mode): void
|
||||||
{
|
{
|
||||||
|
|
@ -306,7 +241,6 @@ class Index extends Component
|
||||||
'hasTls' => $this->domain !== '',
|
'hasTls' => $this->domain !== '',
|
||||||
'panelUrl' => $deployment->panelUrl(),
|
'panelUrl' => $deployment->panelUrl(),
|
||||||
'isOverridden' => $deployment->domainIsOverridden(),
|
'isOverridden' => $deployment->domainIsOverridden(),
|
||||||
'channels' => $this->channelDescriptions(),
|
|
||||||
// Cert request is meaningful only when Caddy serves TLS for an ACTIVE domain.
|
// Cert request is meaningful only when Caddy serves TLS for an ACTIVE domain.
|
||||||
'showCert' => $this->tlsMode === 'caddy' && $this->domain !== '',
|
'showCert' => $this->tlsMode === 'caddy' && $this->domain !== '',
|
||||||
'certStatus' => $deployment->certStatus(),
|
'certStatus' => $deployment->certStatus(),
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,6 @@ class PromotionService
|
||||||
return $this->dispatch('promote-public.yml', ['tag' => $betaTag]);
|
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
|
public function yank(string $tag): bool
|
||||||
{
|
{
|
||||||
return $this->dispatch('yank.yml', ['tag' => $tag]);
|
return $this->dispatch('yank.yml', ['tag' => $tag]);
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ class ConfirmToken
|
||||||
'userRemoved',
|
'userRemoved',
|
||||||
'userLoggedOut',
|
'userLoggedOut',
|
||||||
'domainChanged',
|
'domainChanged',
|
||||||
'channelChanged',
|
|
||||||
'tlsModeChanged',
|
'tlsModeChanged',
|
||||||
'fileConfirmed',
|
'fileConfirmed',
|
||||||
'keyRemoved',
|
'keyRemoved',
|
||||||
|
|
@ -63,7 +62,6 @@ class ConfirmToken
|
||||||
'wgSetSubnet',
|
'wgSetSubnet',
|
||||||
'releaseStaged',
|
'releaseStaged',
|
||||||
'releasePublic',
|
'releasePublic',
|
||||||
'releaseStable',
|
|
||||||
'releaseYank',
|
'releaseYank',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
// System settings (Domain/TLS + release channel) page strings (R16). Shared buttons live in common.php.
|
// System settings (Domain/TLS) page strings (R16). Shared buttons live in common.php.
|
||||||
return [
|
return [
|
||||||
// Header
|
// Header
|
||||||
'eyebrow' => 'System',
|
'eyebrow' => 'System',
|
||||||
'heading' => 'Domain, TLS & Release-Kanal',
|
'heading' => 'Domain & TLS',
|
||||||
'subheading' => 'Zugriffsadresse des Panels und Update-Quelle',
|
'subheading' => 'Zugriffsadresse des Panels',
|
||||||
'https_active' => 'HTTPS aktiv',
|
'https_active' => 'HTTPS aktiv',
|
||||||
'https_plain' => 'Klartext-HTTP',
|
'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_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://<Server-IP> als Rückfallweg erreichbar.',
|
'restart_lockout_hint' => 'Nach dem Neustart eventuell neu anmelden. Falls die neue Domain (DNS/Zertifikat) noch nicht erreichbar ist, bleibt das Panel über http://<Server-IP> 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 selector
|
||||||
'tls_mode_title' => 'TLS-Terminierung',
|
'tls_mode_title' => 'TLS-Terminierung',
|
||||||
'tls_mode_caddy' => 'Eingebautes TLS',
|
'tls_mode_caddy' => 'Eingebautes TLS',
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
// System settings (Domain/TLS + release channel) page strings (R16). Shared buttons live in common.php.
|
// System settings (Domain/TLS) page strings (R16). Shared buttons live in common.php.
|
||||||
return [
|
return [
|
||||||
// Header
|
// Header
|
||||||
'eyebrow' => 'System',
|
'eyebrow' => 'System',
|
||||||
'heading' => 'Domain, TLS & release channel',
|
'heading' => 'Domain & TLS',
|
||||||
'subheading' => 'Panel access address and update source',
|
'subheading' => 'Panel access address',
|
||||||
'https_active' => 'HTTPS active',
|
'https_active' => 'HTTPS active',
|
||||||
'https_plain' => 'Plaintext HTTP',
|
'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_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://<server-IP> as a fallback.',
|
'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://<server-IP> 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 selector
|
||||||
'tls_mode_title' => 'TLS termination',
|
'tls_mode_title' => 'TLS termination',
|
||||||
'tls_mode_caddy' => 'Built-in TLS',
|
'tls_mode_caddy' => 'Built-in TLS',
|
||||||
|
|
|
||||||
|
|
@ -119,13 +119,10 @@
|
||||||
{{-- Publish the current beta --}}
|
{{-- Publish the current beta --}}
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-2.5 font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('release.publish') }}</p>
|
<p class="mb-2.5 font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('release.publish') }}</p>
|
||||||
<div class="grid gap-3 sm:grid-cols-2">
|
<div class="grid gap-3">
|
||||||
<x-btn variant="primary" wire:click="confirmDeployPublic" wire:loading.attr="disabled">
|
<x-btn variant="primary" wire:click="confirmDeployPublic" wire:loading.attr="disabled">
|
||||||
{{ __('release.deploy_public', ['tag' => $current]) }}
|
{{ __('release.deploy_public', ['tag' => $current]) }}
|
||||||
</x-btn>
|
</x-btn>
|
||||||
<x-btn variant="accent" wire:click="confirmPromoteStable" wire:loading.attr="disabled">
|
|
||||||
{{ __('release.promote_stable', ['v' => $stableTarget]) }}
|
|
||||||
</x-btn>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 font-mono text-[11px] leading-relaxed text-ink-4">{{ __('release.publish_hint') }}</p>
|
<p class="mt-3 font-mono text-[11px] leading-relaxed text-ink-4">{{ __('release.publish_hint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@
|
||||||
<div class="space-y-3 border-t border-line pt-5">
|
<div class="space-y-3 border-t border-line pt-5">
|
||||||
<p class="text-sm font-medium text-ink-2">{{ __('system.tls_mode_title') }}</p>
|
<p class="text-sm font-medium text-ink-2">{{ __('system.tls_mode_title') }}</p>
|
||||||
|
|
||||||
{{-- Segmented control (mirrors the channel selector) --}}
|
{{-- Segmented control --}}
|
||||||
<div role="radiogroup" aria-label="{{ __('system.tls_mode_title') }}" class="flex w-full rounded-md border border-line bg-inset p-1">
|
<div role="radiogroup" aria-label="{{ __('system.tls_mode_title') }}" class="flex w-full rounded-md border border-line bg-inset p-1">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
role="radio"
|
role="radio"
|
||||||
|
|
@ -233,43 +233,4 @@
|
||||||
</div>
|
</div>
|
||||||
</x-panel>
|
</x-panel>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ── Release-Kanal (full-width band, below the grid) ──────────────────── --}}
|
|
||||||
<x-panel :title="__('system.channel_title')" :subtitle="__('system.channel_subtitle')">
|
|
||||||
<div class="space-y-4">
|
|
||||||
{{-- Segmented control --}}
|
|
||||||
<div role="radiogroup" aria-label="{{ __('system.channel_radiogroup_label') }}" class="inline-flex w-full max-w-md rounded-md border border-line bg-inset p-1">
|
|
||||||
@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". --}}
|
|
||||||
<button type="button"
|
|
||||||
wire:key="channel-{{ $key }}"
|
|
||||||
role="radio"
|
|
||||||
aria-checked="{{ $channel === $key ? 'true' : 'false' }}"
|
|
||||||
wire:click="confirmChannel('{{ $key }}')"
|
|
||||||
x-data="modalTrigger(null, {}, @js(__('shell.toast_modal_failed')), 3500)"
|
|
||||||
x-on:click="arm()"
|
|
||||||
x-bind:disabled="pending"
|
|
||||||
x-bind:aria-busy="pending"
|
|
||||||
@class([
|
|
||||||
'inline-flex h-9 flex-1 items-center justify-center gap-1.5 rounded font-mono text-xs uppercase tracking-wider transition-colors',
|
|
||||||
'bg-accent/15 text-accent-text shadow-[inset_0_0_0_1px_var(--color-accent)]' => $channel === $key,
|
|
||||||
'text-ink-3 hover:bg-raised hover:text-ink-2' => $channel !== $key,
|
|
||||||
])>
|
|
||||||
<svg x-show="pending" x-cloak class="h-3.5 w-3.5 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56" stroke-linecap="round" /></svg>
|
|
||||||
<span x-show="!pending" class="contents">@if ($channel === $key)<x-icon name="git-branch" class="h-3.5 w-3.5" />@endif{{ $key }}</span>
|
|
||||||
</button>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Per-channel description --}}
|
|
||||||
<div class="flex items-start gap-3 rounded-md border border-line bg-raised/40 p-3">
|
|
||||||
<x-icon name="git-branch" class="mt-0.5 h-4 w-4 shrink-0 text-accent-text" />
|
|
||||||
<div class="min-w-0">
|
|
||||||
<p class="text-sm text-ink">{{ __('system.channel_current') }} <span class="font-mono text-accent-text">{{ $channel }}</span></p>
|
|
||||||
<p class="mt-0.5 font-mono text-[11px] text-ink-3">{{ $channels[$channel] ?? '' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</x-panel>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -38,17 +38,6 @@ class PromotionServiceTest extends TestCase
|
||||||
&& ! str_contains($req->url(), 'tok_secret'));
|
&& ! 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
|
public function test_yank_dispatches_yank_with_the_tag(): void
|
||||||
{
|
{
|
||||||
Http::fake(['api.github.com/*' => Http::response('', 204)]);
|
Http::fake(['api.github.com/*' => Http::response('', 204)]);
|
||||||
|
|
|
||||||
|
|
@ -132,23 +132,6 @@ class ReleasePageTest extends TestCase
|
||||||
$this->assertTrue(AuditEvent::where('action', 'deploy.public')->exists());
|
$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
|
public function test_yank_only_dispatches_for_a_tag_that_exists_on_public(): void
|
||||||
{
|
{
|
||||||
config()->set('clusev.github_token', 'tok');
|
config()->set('clusev.github_token', 'tok');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue