refactor(release): drop the test-server step — pipeline is build readiness only

'Staging' is not a single server: any beta-channel server pulls a release itself,
so a per-server 'deployed' step in the dev dashboard was arbitrary + contradicted
that model. The pipeline now tracks only the global build facts of a beta —
Tag (Gitea) → Mirror (GitHub-private) → CI. Removes PipelineStatus::testStep, the
test-server Setting/input/saveTestServer, and the now-orphaned lang keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-23 02:46:17 +02:00
parent f26aa66cf5
commit 44d0072c1a
7 changed files with 11 additions and 147 deletions

View File

@ -3,7 +3,6 @@
namespace App\Livewire\Release;
use App\Models\AuditEvent;
use App\Models\Setting;
use App\Services\PipelineStatus;
use App\Services\ReleaseBridge;
use App\Services\ReleasePlanner;
@ -40,14 +39,9 @@ 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. */
@ -155,35 +149,12 @@ class Index extends Component
}
/** 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. */
* render re-fetches the live GitHub 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 = '';
app(PipelineStatus::class)->refresh(); // drop the stale cached test-step status
$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;
app(PipelineStatus::class)->refresh(); // reflect the new test server immediately
$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.

View File

@ -2,16 +2,16 @@
namespace App\Services;
use App\Models\Setting;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
/**
* Live status of the build/deploy pipeline for the currently-cut beta, for the dev Release page.
* Queries GitHub (tag mirror + CI run) and the configured test server's /version.json. Each step is
* cached briefly; any failure degrades to 'unknown' (never throws). The read-only GitHub token and the
* private staging slug come from config (env) and are never rendered or put in a URL.
* Live status of the build pipeline for the currently-cut beta, for the dev Release page: tag (Gitea),
* mirror (GitHub-private), CI (GitHub Actions). These are global build facts of one beta there is no
* "deployed" step because any beta-channel server pulls a release itself (no single staging target).
* Each step is cached briefly; any failure degrades to 'unknown' (never throws). The read-only GitHub
* token and the private staging slug come from config (env) and are never rendered or put in a URL.
*/
class PipelineStatus
{
@ -30,13 +30,11 @@ class PipelineStatus
$mirror = $this->mirrorStep($tag);
$ci = $this->ciStep($tag);
$test = $this->testStep($tag, $version);
$steps = [
['key' => 'tag', 'state' => 'done', 'detail' => $tag, 'url' => null],
$mirror,
$ci,
$test,
];
$ciRunning = $mirror['state'] === 'pending'
@ -53,7 +51,7 @@ class PipelineStatus
return;
}
$tag = 'v'.ltrim($version, 'vV');
foreach (['mirror', 'ci', 'test'] as $step) {
foreach (['mirror', 'ci'] as $step) {
Cache::forget("clusev:pipeline:{$tag}:{$step}");
}
}
@ -129,30 +127,4 @@ class PipelineStatus
}
});
}
/** @return array{key:string,state:string,detail:?string,url:?string} */
private function testStep(string $tag, string $version): array
{
$url = trim((string) Setting::get('pipeline_test_server', ''));
if ($url === '') {
return ['key' => 'test', 'state' => 'unset', 'detail' => null, 'url' => null];
}
return Cache::remember("clusev:pipeline:{$tag}:test", self::TTL_SECONDS, function () use ($url, $version) {
try {
$res = Http::timeout(5)->acceptJson()->get(rtrim($url, '/').'/version.json');
$running = $res->successful() ? (string) ($res->json('version') ?? '') : '';
if ($running === '') {
return ['key' => 'test', 'state' => 'unknown', 'detail' => null, 'url' => null];
}
$state = version_compare(ltrim($running, 'vV'), ltrim($version, 'vV')) >= 0 ? 'done' : 'pending';
return ['key' => 'test', 'state' => $state, 'detail' => $running, 'url' => null];
} catch (\Throwable $e) {
report($e);
return ['key' => 'test', 'state' => 'unknown', 'detail' => null, 'url' => null];
}
});
}
}

View File

@ -29,25 +29,17 @@ return [
'staged_label' => 'Zuletzt gepusht:',
'failed' => 'Release fehlgeschlagen: :reason',
// pipeline rail
// pipeline rail (Tag → Mirror → CI; build readiness of a beta)
'pipeline' => 'Pipeline',
'pipe_sub_gitea' => 'Quelle · Tag wird hier erzeugt',
'pipe_sub_mirror' => 'Automatischer Push-Mirror',
'pipe_sub_ci' => 'Tests laufen',
'pipe_sub_staging' => 'Beta wird ausgerollt',
'pipe_sub_test' => 'Beta auf deinem Test-Server',
'no_beta_running' => 'Keine Beta in Arbeit.',
'step_test' => 'Test-Server',
'state_done' => 'fertig',
'state_running' => 'läuft …',
'state_failed' => 'fehlgeschlagen',
'state_pending' => 'ausstehend',
'state_unknown' => 'unbekannt',
'state_unset' => 'URL setzen',
'save' => 'Speichern',
'test_server_saved' => 'Test-Server gespeichert.',
'test_server_cleared' => 'Test-Server entfernt.',
'test_server_invalid' => 'Ungültige URL (http/https).',
// confirm modal (R5)
'confirm_title' => 'Beta auf Staging schneiden?',

View File

@ -29,25 +29,17 @@ return [
'staged_label' => 'Last pushed:',
'failed' => 'Release failed: :reason',
// pipeline rail
// pipeline rail (Tag → Mirror → CI; build readiness of a beta)
'pipeline' => 'Pipeline',
'pipe_sub_gitea' => 'Source · tag is cut here',
'pipe_sub_mirror' => 'Automatic push mirror',
'pipe_sub_ci' => 'Tests run',
'pipe_sub_staging' => 'Beta is deployed',
'pipe_sub_test' => 'Beta on your test server',
'no_beta_running' => 'No beta in flight.',
'step_test' => 'Test server',
'state_done' => 'done',
'state_running' => 'running …',
'state_failed' => 'failed',
'state_pending' => 'pending',
'state_unknown' => 'unknown',
'state_unset' => 'set a URL',
'save' => 'Save',
'test_server_saved' => 'Test server saved.',
'test_server_cleared' => 'Test server cleared.',
'test_server_invalid' => 'Invalid URL (http/https).',
// confirm modal (R5)
'confirm_title' => 'Cut a staging beta?',

View File

@ -125,18 +125,17 @@
@foreach ($pipeline['steps'] as $s)
@php
$names = [
'tag' => 'Gitea', 'mirror' => 'GitHub-privat', 'ci' => 'CI', 'test' => __('release.step_test'),
'tag' => 'Gitea', 'mirror' => 'GitHub-privat', 'ci' => 'CI',
];
$sub = [
'tag' => __('release.pipe_sub_gitea'), 'mirror' => __('release.pipe_sub_mirror'),
'ci' => __('release.pipe_sub_ci'), 'test' => __('release.pipe_sub_test'),
'ci' => __('release.pipe_sub_ci'),
];
$detail = match ($s['state']) {
'done' => $s['detail'] ?? __('release.state_done'),
'running' => __('release.state_running'),
'failed' => __('release.state_failed'),
'pending' => __('release.state_pending'),
'unset' => __('release.state_unset'),
default => __('release.state_unknown'),
};
$detailClass = match ($s['state']) {
@ -173,14 +172,6 @@
@else
<p class="mt-1 font-mono text-[11px] {{ $detailClass }}">{{ $detail }}</p>
@endif
@if ($s['key'] === 'test')
<div class="mt-2 flex items-center gap-2">
<input type="url" wire:model="testServer" placeholder="https://test-server"
class="h-8 w-full rounded-md border border-line bg-inset px-2.5 font-mono text-[11px] text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" />
<x-btn variant="secondary" wire:click="saveTestServer($wire.testServer)" wire:loading.attr="disabled">{{ __('release.save') }}</x-btn>
</div>
@endif
</div>
</li>
@endforeach

View File

@ -2,7 +2,6 @@
namespace Tests\Feature;
use App\Models\Setting;
use App\Services\PipelineStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\Factory as HttpFactory;
@ -131,29 +130,6 @@ class PipelineStatusTest extends TestCase
Http::assertNothingSent();
}
public function test_test_server_step_states(): void
{
Http::fake([
'api.github.com/*' => Http::response([], 404),
'staging.local/version.json' => Http::response(['version' => '0.9.61-beta1']),
]);
Setting::put('pipeline_test_server', 'http://staging.local');
$this->assertSame('done', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'test')['state']);
$this->resetForNextPhase();
Http::fake([
'api.github.com/*' => Http::response([], 404),
'staging.local/version.json' => Http::response(['version' => '0.9.60']),
]);
$this->assertSame('pending', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'test')['state']);
}
public function test_test_server_unset_when_no_url(): void
{
Http::fake(['api.github.com/*' => Http::response([], 404)]);
$this->assertSame('unset', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'test')['state']);
}
public function test_token_is_sent_in_the_header_never_in_the_url(): void
{
Http::fake(['api.github.com/*' => Http::response(['ref' => 'x'])]);
@ -177,14 +153,4 @@ class PipelineStatusTest extends TestCase
$this->assertSame('unknown', $this->step($p, 'mirror')['state']);
$this->assertSame('unknown', $this->step($p, 'ci')['state']);
}
public function test_test_server_unreachable_is_unknown(): void
{
Http::fake([
'api.github.com/*' => Http::response([], 404),
'*/version.json' => Http::response('nope', 503),
]);
Setting::put('pipeline_test_server', 'http://staging.local');
$this->assertSame('unknown', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'test')['state']);
}
}

View File

@ -4,7 +4,6 @@ namespace Tests\Feature;
use App\Livewire\Release\Index;
use App\Models\AuditEvent;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\RateLimiter;
@ -112,23 +111,4 @@ class ReleasePageTest extends TestCase
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', 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(Setting::get('pipeline_test_server'));
}
public function test_save_test_server_empty_clears_it(): void
{
Setting::put('pipeline_test_server', 'http://staging.local');
Livewire::test(Index::class)->call('saveTestServer', '');
$this->assertSame('', (string) Setting::get('pipeline_test_server', ''));
}
}