fix(release): neutralise private slug in tests + render step descriptions (review)
- PipelineStatusTest: the private mirror slug 'boksbc/clusev-staging' was hardcoded in this tracked test (tests/ ships to the public repo) — exactly the leak the design forbids. Neutralise to an example slug 'acme/staging'. Add the missing HTTP-error degrade tests (GitHub 5xx, GitHub throwing, test-server unreachable → 'unknown', never throws) — the core robustness guarantee was untested. - Live rail: render the per-step description (the built $sub array was dead code) and colour the live state by status (online/warning/offline). - saveTestServer: refresh() the pipeline cache so a changed test-server URL is reflected immediately, not after the 15s TTL. - .env.example: document the dev-only release/pipeline keys (commented, no values). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/v1-foundation
parent
787e48d8fd
commit
fca75bc0a3
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
<li class="flex gap-3" wire:key="pstep-{{ $s['key'] }}">
|
||||
<div class="flex flex-col items-center">
|
||||
|
|
@ -161,10 +167,11 @@
|
|||
</div>
|
||||
<div @class(['min-w-0', 'pb-5' => ! $loop->last])>
|
||||
<p class="font-mono text-xs text-ink">{{ $names[$s['key']] }}</p>
|
||||
<p class="mt-0.5 font-mono text-[11px] text-ink-4">{{ $sub[$s['key']] }}</p>
|
||||
@if ($s['key'] === 'ci' && $s['url'])
|
||||
<a href="{{ $s['url'] }}" target="_blank" rel="noopener" class="mt-0.5 block font-mono text-[11px] text-cyan hover:text-cyan-bright">{{ $detail }}</a>
|
||||
<a href="{{ $s['url'] }}" target="_blank" rel="noopener" class="mt-1 block font-mono text-[11px] text-cyan hover:text-cyan-bright">{{ $detail }}</a>
|
||||
@else
|
||||
<p class="mt-0.5 font-mono text-[11px] text-ink-4">{{ $detail }}</p>
|
||||
<p class="mt-1 font-mono text-[11px] {{ $detailClass }}">{{ $detail }}</p>
|
||||
@endif
|
||||
|
||||
@if ($s['key'] === 'test')
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue