diff --git a/.env.example b/.env.example index 6047b19..4931a6f 100644 --- a/.env.example +++ b/.env.example @@ -90,6 +90,11 @@ CLUSEV_BUILD_BRANCH= # default in config/clusev.php — an empty value here would instead disable the check, so leave the # key absent rather than blank. Never put a private repo URL in a tracked file — only in the .env. # CLUSEV_REPOSITORY=https://github.com/clusev/clusev +# Dev-only release controls (dashboard Release page + host git-bridge + live pipeline). Leave unset on +# any non-dev install. Both values below are PRIVATE — never commit them, set only on the dev box: +# CLUSEV_RELEASE_CONTROLS=true # enables the /release page + watcher install +# GIT_STAGING_ACCESS_TOKEN= # GitHub PAT, READ only (Actions:Read + Contents:Read) +# CLUSEV_STAGING_SLUG=owner/private-mirror # the GitHub-private mirror repo (for live CI status) # Display timezone for dashboard times (DB stays UTC). install.sh sets this from the host; UTC default. APP_TIMEZONE=UTC # External reverse-proxy TLS: set to the upstream proxy's address/CIDR so Caddy trusts its diff --git a/app/Livewire/Release/Index.php b/app/Livewire/Release/Index.php index 5447fd6..6ac1c5a 100644 --- a/app/Livewire/Release/Index.php +++ b/app/Livewire/Release/Index.php @@ -168,6 +168,7 @@ class Index extends Component 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; @@ -179,6 +180,7 @@ class Index extends Component } 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')); } diff --git a/resources/views/livewire/release/index.blade.php b/resources/views/livewire/release/index.blade.php index b0a5160..8a8748b 100644 --- a/resources/views/livewire/release/index.blade.php +++ b/resources/views/livewire/release/index.blade.php @@ -139,6 +139,12 @@ 'unset' => __('release.state_unset'), default => __('release.state_unknown'), }; + $detailClass = match ($s['state']) { + 'done' => 'text-online', + 'running' => 'text-warning', + 'failed' => 'text-offline', + default => 'text-ink-4', + }; @endphp
  • @@ -161,10 +167,11 @@
    ! $loop->last])>

    {{ $names[$s['key']] }}

    +

    {{ $sub[$s['key']] }}

    @if ($s['key'] === 'ci' && $s['url']) - {{ $detail }} + {{ $detail }} @else -

    {{ $detail }}

    +

    {{ $detail }}

    @endif @if ($s['key'] === 'test') diff --git a/tests/Feature/PipelineStatusTest.php b/tests/Feature/PipelineStatusTest.php index e5062df..3223d07 100644 --- a/tests/Feature/PipelineStatusTest.php +++ b/tests/Feature/PipelineStatusTest.php @@ -14,7 +14,7 @@ class PipelineStatusTest extends TestCase { use RefreshDatabase; - private const SLUG = 'boksbc/clusev-staging'; + private const SLUG = 'acme/staging'; protected function setUp(): void { @@ -64,8 +64,8 @@ class PipelineStatusTest extends TestCase public function test_mirror_done_when_the_tag_exists_on_github(): void { Http::fake([ - 'api.github.com/repos/boksbc/clusev-staging/git/refs/tags/v0.9.61-beta1' => Http::response(['ref' => 'refs/tags/v0.9.61-beta1']), - 'api.github.com/repos/boksbc/clusev-staging/actions/runs*' => Http::response(['workflow_runs' => []]), + 'api.github.com/repos/acme/staging/git/refs/tags/v0.9.61-beta1' => Http::response(['ref' => 'refs/tags/v0.9.61-beta1']), + 'api.github.com/repos/acme/staging/actions/runs*' => Http::response(['workflow_runs' => []]), ]); $p = app(PipelineStatus::class)->forTrackedBeta(); $this->assertSame('done', $this->step($p, 'mirror')['state']); @@ -74,8 +74,8 @@ class PipelineStatusTest extends TestCase public function test_mirror_pending_on_404(): void { Http::fake([ - 'api.github.com/repos/boksbc/clusev-staging/git/refs/tags/*' => Http::response([], 404), - 'api.github.com/repos/boksbc/clusev-staging/actions/runs*' => Http::response(['workflow_runs' => []]), + 'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response([], 404), + 'api.github.com/repos/acme/staging/actions/runs*' => Http::response(['workflow_runs' => []]), ]); $p = app(PipelineStatus::class)->forTrackedBeta(); $this->assertSame('pending', $this->step($p, 'mirror')['state']); @@ -85,12 +85,12 @@ class PipelineStatusTest extends TestCase { $run = fn (string $status, ?string $conclusion) => Http::response(['workflow_runs' => [[ 'head_branch' => 'v0.9.61-beta1', 'status' => $status, 'conclusion' => $conclusion, - 'html_url' => 'https://github.com/boksbc/clusev-staging/actions/runs/1', + 'html_url' => 'https://github.com/acme/staging/actions/runs/1', ]]]); Http::fake([ - 'api.github.com/repos/boksbc/clusev-staging/git/refs/tags/*' => Http::response(['ref' => 'x']), - 'api.github.com/repos/boksbc/clusev-staging/actions/runs*' => $run('in_progress', null), + 'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response(['ref' => 'x']), + 'api.github.com/repos/acme/staging/actions/runs*' => $run('in_progress', null), ]); $p = app(PipelineStatus::class)->forTrackedBeta(); $this->assertSame('running', $this->step($p, 'ci')['state']); @@ -99,15 +99,15 @@ class PipelineStatusTest extends TestCase $this->resetForNextPhase(); Http::fake([ - 'api.github.com/repos/boksbc/clusev-staging/git/refs/tags/*' => Http::response(['ref' => 'x']), - 'api.github.com/repos/boksbc/clusev-staging/actions/runs*' => $run('completed', 'success'), + 'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response(['ref' => 'x']), + 'api.github.com/repos/acme/staging/actions/runs*' => $run('completed', 'success'), ]); $this->assertSame('done', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'ci')['state']); $this->resetForNextPhase(); Http::fake([ - 'api.github.com/repos/boksbc/clusev-staging/git/refs/tags/*' => Http::response(['ref' => 'x']), - 'api.github.com/repos/boksbc/clusev-staging/actions/runs*' => $run('completed', 'failure'), + 'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response(['ref' => 'x']), + 'api.github.com/repos/acme/staging/actions/runs*' => $run('completed', 'failure'), ]); $this->assertSame('failed', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'ci')['state']); } @@ -115,8 +115,8 @@ class PipelineStatusTest extends TestCase public function test_ci_pending_when_no_run_matches_the_tag(): void { Http::fake([ - 'api.github.com/repos/boksbc/clusev-staging/git/refs/tags/*' => Http::response(['ref' => 'x']), - 'api.github.com/repos/boksbc/clusev-staging/actions/runs*' => Http::response(['workflow_runs' => [['head_branch' => 'v9.9.9', 'status' => 'completed', 'conclusion' => 'success']]]), + 'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response(['ref' => 'x']), + 'api.github.com/repos/acme/staging/actions/runs*' => Http::response(['workflow_runs' => [['head_branch' => 'v9.9.9', 'status' => 'completed', 'conclusion' => 'success']]]), ]); $this->assertSame('pending', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'ci')['state']); } @@ -161,4 +161,30 @@ class PipelineStatusTest extends TestCase Http::assertSent(fn ($req) => $req->hasHeader('Authorization') && ! str_contains($req->url(), 'tok_secret')); } + + public function test_github_5xx_degrades_to_unknown(): void + { + Http::fake(['api.github.com/*' => Http::response('boom', 500)]); + $p = app(PipelineStatus::class)->forTrackedBeta(); + $this->assertSame('unknown', $this->step($p, 'mirror')['state']); + $this->assertSame('unknown', $this->step($p, 'ci')['state']); + } + + public function test_github_throwing_degrades_to_unknown_never_throws(): void + { + Http::fake(['api.github.com/*' => fn () => throw new \RuntimeException('net down')]); + $p = app(PipelineStatus::class)->forTrackedBeta(); + $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']); + } }