131 lines
5.3 KiB
PHP
131 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Http\Client\PendingRequest;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
private const TTL_SECONDS = 15;
|
|
|
|
/**
|
|
* @return array{tag:string, steps:array<int,array{key:string,state:string,detail:?string,url:?string}>, ciRunning:bool}|null
|
|
*/
|
|
public function forTrackedBeta(): ?array
|
|
{
|
|
$version = (string) config('clusev.version');
|
|
if (! str_contains($version, '-beta')) {
|
|
return null;
|
|
}
|
|
$tag = 'v'.ltrim($version, 'vV');
|
|
|
|
$mirror = $this->mirrorStep($tag);
|
|
$ci = $this->ciStep($tag);
|
|
|
|
$steps = [
|
|
['key' => 'tag', 'state' => 'done', 'detail' => $tag, 'url' => null],
|
|
$mirror,
|
|
$ci,
|
|
];
|
|
|
|
$ciRunning = $mirror['state'] === 'pending'
|
|
|| in_array($ci['state'], ['running', 'pending'], true);
|
|
|
|
return ['tag' => $tag, 'steps' => $steps, 'ciRunning' => $ciRunning];
|
|
}
|
|
|
|
/** Forget the cached step results so the next read re-fetches (driven by the page poll). */
|
|
public function refresh(): void
|
|
{
|
|
$version = (string) config('clusev.version');
|
|
if (! str_contains($version, '-beta')) {
|
|
return;
|
|
}
|
|
$tag = 'v'.ltrim($version, 'vV');
|
|
foreach (['mirror', 'ci'] as $step) {
|
|
Cache::forget("clusev:pipeline:{$tag}:{$step}");
|
|
}
|
|
}
|
|
|
|
private function github(): PendingRequest
|
|
{
|
|
return Http::timeout(5)
|
|
->withToken((string) config('clusev.github_token'))
|
|
->acceptJson()
|
|
->withHeaders(['User-Agent' => 'Clusev-Panel']);
|
|
}
|
|
|
|
/** @return array{key:string,state:string,detail:?string,url:?string} */
|
|
private function mirrorStep(string $tag): array
|
|
{
|
|
return Cache::remember("clusev:pipeline:{$tag}:mirror", self::TTL_SECONDS, function () use ($tag) {
|
|
$token = (string) config('clusev.github_token');
|
|
$slug = (string) config('clusev.staging_slug');
|
|
if ($token === '' || $slug === '') {
|
|
return ['key' => 'mirror', 'state' => 'unknown', 'detail' => null, 'url' => null];
|
|
}
|
|
try {
|
|
$res = $this->github()->get("https://api.github.com/repos/{$slug}/git/refs/tags/{$tag}");
|
|
$state = $res->status() === 404 ? 'pending' : ($res->successful() ? 'done' : 'unknown');
|
|
|
|
return ['key' => 'mirror', 'state' => $state, 'detail' => null, 'url' => null];
|
|
} catch (\Throwable $e) {
|
|
report($e);
|
|
|
|
return ['key' => 'mirror', 'state' => 'unknown', 'detail' => null, 'url' => null];
|
|
}
|
|
});
|
|
}
|
|
|
|
/** @return array{key:string,state:string,detail:?string,url:?string} */
|
|
private function ciStep(string $tag): array
|
|
{
|
|
return Cache::remember("clusev:pipeline:{$tag}:ci", self::TTL_SECONDS, function () use ($tag) {
|
|
$token = (string) config('clusev.github_token');
|
|
$slug = (string) config('clusev.staging_slug');
|
|
if ($token === '' || $slug === '') {
|
|
return ['key' => 'ci', 'state' => 'unknown', 'detail' => null, 'url' => null];
|
|
}
|
|
try {
|
|
$res = $this->github()->get("https://api.github.com/repos/{$slug}/actions/runs", ['per_page' => 30, 'event' => 'push']);
|
|
if (! $res->successful()) {
|
|
return ['key' => 'ci', 'state' => 'unknown', 'detail' => null, 'url' => null];
|
|
}
|
|
$run = null;
|
|
foreach ((array) ($res->json('workflow_runs') ?? []) as $r) {
|
|
if (is_array($r) && ($r['head_branch'] ?? null) === $tag) {
|
|
$run = $r; // workflow_runs are newest-first
|
|
break;
|
|
}
|
|
}
|
|
if ($run === null) {
|
|
return ['key' => 'ci', 'state' => 'pending', 'detail' => null, 'url' => null];
|
|
}
|
|
$status = (string) ($run['status'] ?? '');
|
|
$conclusion = (string) ($run['conclusion'] ?? '');
|
|
$url = is_string($run['html_url'] ?? null) ? $run['html_url'] : null;
|
|
$state = match (true) {
|
|
in_array($status, ['queued', 'in_progress', 'waiting', 'requested', 'pending'], true) => 'running',
|
|
$status === 'completed' => $conclusion === 'success' ? 'done' : 'failed',
|
|
default => 'unknown',
|
|
};
|
|
|
|
return ['key' => 'ci', 'state' => $state, 'detail' => null, 'url' => $url];
|
|
} catch (\Throwable $e) {
|
|
report($e);
|
|
|
|
return ['key' => 'ci', 'state' => 'unknown', 'detail' => null, 'url' => null];
|
|
}
|
|
});
|
|
}
|
|
}
|