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
parent
f26aa66cf5
commit
44d0072c1a
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?',
|
||||
|
|
|
|||
|
|
@ -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?',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', ''));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue