feat(release): wire the live pipeline + test-server setting into the page

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-23 01:49:01 +02:00
parent 070c6646f3
commit 0ac819e4ad
2 changed files with 71 additions and 0 deletions

View File

@ -3,6 +3,8 @@
namespace App\Livewire\Release;
use App\Models\AuditEvent;
use App\Models\Setting;
use App\Services\PipelineStatus;
use App\Services\ReleaseBridge;
use App\Services\ReleasePlanner;
use App\Support\Confirm\ConfirmToken;
@ -38,9 +40,14 @@ class Index extends Component
/** The tag of the last successfully-pushed staging beta (shown until the next deploy). */
public ?string $lastTag = null;
/** The configured test-server URL (bound to the inline input). */
public string $testServer = '';
public function mount(): void
{
abort_unless((bool) config('clusev.release_controls'), 404);
$this->testServer = (string) Setting::get('pipeline_test_server', '');
}
/** Opens the wire-elements/modal confirm dialog (R5) before cutting a staging beta of $target. */
@ -147,6 +154,34 @@ class Index extends Component
$this->reset(['pendingId', 'pendingSince', 'pendingTarget']);
}
/** wire:poll target while the pipeline is in flight drops the cached step results so the next
* render re-fetches the live GitHub/test-server status. */
public function pollPipeline(PipelineStatus $pipeline): void
{
$pipeline->refresh();
}
/** Save (or clear) the test-server URL whose /version.json the pipeline polls. */
public function saveTestServer(string $url): void
{
$url = trim($url);
if ($url === '') {
Setting::put('pipeline_test_server', '');
$this->testServer = '';
$this->dispatch('notify', message: __('release.test_server_cleared'));
return;
}
if (filter_var($url, FILTER_VALIDATE_URL) === false || ! preg_match('#^https?://#i', $url)) {
$this->dispatch('notify', message: __('release.test_server_invalid'), level: 'error');
return;
}
Setting::put('pipeline_test_server', $url);
$this->testServer = $url;
$this->dispatch('notify', message: __('release.test_server_saved'));
}
/**
* Open the shared ConfirmAction modal (R5) for the staging release. The issued token carries NO
* audit descriptor applyDeployStaging deployStaging audits exactly once itself.
@ -176,6 +211,7 @@ class Index extends Component
return view('livewire.release.index', [
'current' => $current,
'targets' => app(ReleasePlanner::class)->proposedTargets($current),
'pipeline' => app(PipelineStatus::class)->forTrackedBeta(),
])->title(__('release.title'));
}
}

View File

@ -96,4 +96,39 @@ class ReleasePageTest extends TestCase
config()->set('clusev.release_controls', false);
Livewire::test(Index::class)->assertStatus(404);
}
public function test_render_includes_the_live_pipeline_for_a_beta(): void
{
config()->set('clusev.version', '0.9.61-beta1');
config()->set('clusev.staging_slug', ''); // degrade: no GitHub calls
config()->set('clusev.github_token', '');
Livewire::test(Index::class)
->assertViewHas('pipeline', fn ($p) => is_array($p) && $p['tag'] === 'v0.9.61-beta1');
}
public function test_render_pipeline_is_null_for_a_stable_version(): void
{
config()->set('clusev.version', '0.9.60');
Livewire::test(Index::class)->assertViewHas('pipeline', null);
}
public function test_save_test_server_persists_a_valid_url(): void
{
Livewire::test(Index::class)->call('saveTestServer', 'http://staging.local');
$this->assertSame('http://staging.local', \App\Models\Setting::get('pipeline_test_server'));
}
public function test_save_test_server_rejects_a_bad_url(): void
{
Livewire::test(Index::class)->call('saveTestServer', 'not-a-url');
$this->assertNull(\App\Models\Setting::get('pipeline_test_server'));
}
public function test_save_test_server_empty_clears_it(): void
{
\App\Models\Setting::put('pipeline_test_server', 'http://staging.local');
Livewire::test(Index::class)->call('saveTestServer', '');
$this->assertSame('', (string) \App\Models\Setting::get('pipeline_test_server', ''));
}
}