, 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); $test = $this->testStep($tag, $version); $steps = [ ['key' => 'tag', 'state' => 'done', 'detail' => $tag, 'url' => null], $mirror, $ci, $test, ]; $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', 'test'] 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]; } }); } /** @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]; } }); } }