$config */ public function __construct( private readonly array $config, ) { } public function isConfigured(): bool { return $this->baseUrl() !== '' && $this->token() !== ''; } public function visibility(): string { $visibility = strtolower(trim((string) ($this->config['visibility'] ?? 'admins'))); return match ($visibility) { 'all', 'admins', 'disabled' => $visibility, default => 'admins', }; } public function shouldShowInTray(bool $isAdmin): bool { return match ($this->visibility()) { 'all' => true, 'admins' => $isAdmin, default => false, }; } /** * @return array */ public function status(): array { $idleSeconds = max(1, (int) ($this->config['poll_idle_seconds'] ?? 5)); $runningSeconds = max(1, (int) ($this->config['poll_running_seconds'] ?? 1)); if (!$this->isConfigured()) { return [ 'state' => 'unconfigured', 'poll_seconds' => $idleSeconds, 'message' => 'Gitea-Status ist noch nicht konfiguriert.', 'running_jobs' => [], 'repositories_checked' => 0, ]; } try { $repositories = $this->resolveRepositories(); $runningJobs = []; foreach ($repositories as $repository) { $runs = $this->listActionRuns((string) $repository['owner'], (string) $repository['repo']); foreach ($runs as $run) { if (!$this->isRunActive($run)) { continue; } $runningJobs[] = [ 'repository' => (string) $repository['full_name'], 'workflow' => (string) ($run['workflow_name'] ?? $run['name'] ?? 'Workflow'), 'status' => (string) ($run['status'] ?? 'running'), 'branch' => (string) ($run['head_branch'] ?? $run['ref'] ?? ''), 'event' => (string) ($run['event'] ?? ''), 'started_at' => (string) ($run['created_at'] ?? $run['run_started_at'] ?? ''), 'updated_at' => (string) ($run['updated_at'] ?? ''), 'url' => (string) ($run['html_url'] ?? $run['url'] ?? ''), ]; } } if ($runningJobs !== []) { $first = $runningJobs[0]; $message = sprintf( 'Deploy laeuft: %s%s%s', $first['repository'], $first['branch'] !== '' ? ' · ' . $first['branch'] : '', count($runningJobs) > 1 ? ' · +' . (count($runningJobs) - 1) . ' weitere' : '' ); return [ 'state' => 'running', 'poll_seconds' => $runningSeconds, 'message' => $message, 'running_jobs' => $runningJobs, 'repositories_checked' => count($repositories), ]; } return [ 'state' => 'idle', 'poll_seconds' => $idleSeconds, 'message' => sprintf('Kein laufendes Gitea-Deployment in %d Repositories.', count($repositories)), 'running_jobs' => [], 'repositories_checked' => count($repositories), ]; } catch (\Throwable $exception) { return [ 'state' => 'error', 'poll_seconds' => $idleSeconds, 'message' => 'Gitea-Status konnte nicht geladen werden: ' . $exception->getMessage(), 'running_jobs' => [], 'repositories_checked' => 0, ]; } } private function baseUrl(): string { return rtrim(trim((string) ($this->config['base_url'] ?? '')), '/'); } private function token(): string { return trim((string) ($this->config['token'] ?? '')); } /** * @return array */ private function resolveRepositories(): array { $configured = []; foreach ((array) ($this->config['repositories'] ?? []) as $entry) { $normalized = $this->normalizeRepositoryEntry($entry); if ($normalized !== null) { $configured[$normalized['full_name']] = $normalized; } } if ($configured !== []) { return array_values($configured); } $owner = trim((string) ($this->config['owner'] ?? '')); if ($owner !== '') { foreach ([ '/orgs/' . rawurlencode($owner) . '/repos?limit=100&page=1', '/users/' . rawurlencode($owner) . '/repos?limit=100&page=1', ] as $path) { try { $payload = $this->requestJson($path); $repositories = $this->normalizeRepositoriesPayload($payload); if ($repositories !== []) { return $repositories; } } catch (\Throwable) { continue; } } } $payload = $this->requestJson('/user/repos?limit=100&page=1'); $repositories = $this->normalizeRepositoriesPayload($payload); if ($repositories === []) { throw new \RuntimeException('Keine Repositories fuer den Gitea-Status gefunden.'); } return $repositories; } /** * @param mixed $entry * @return array{owner: string, repo: string, full_name: string}|null */ private function normalizeRepositoryEntry(mixed $entry): ?array { if (is_string($entry)) { $trimmed = trim($entry); if ($trimmed === '' || !str_contains($trimmed, '/')) { return null; } [$owner, $repo] = array_map('trim', explode('/', $trimmed, 2)); if ($owner === '' || $repo === '') { return null; } return [ 'owner' => $owner, 'repo' => $repo, 'full_name' => $owner . '/' . $repo, ]; } if (!is_array($entry)) { return null; } $owner = trim((string) ($entry['owner'] ?? ($entry['owner_name'] ?? ''))); $repo = trim((string) ($entry['repo'] ?? ($entry['name'] ?? ''))); $fullName = trim((string) ($entry['full_name'] ?? '')); if ($fullName !== '' && str_contains($fullName, '/')) { [$ownerFromFull, $repoFromFull] = array_map('trim', explode('/', $fullName, 2)); $owner = $owner !== '' ? $owner : $ownerFromFull; $repo = $repo !== '' ? $repo : $repoFromFull; } if ($owner === '' || $repo === '') { return null; } return [ 'owner' => $owner, 'repo' => $repo, 'full_name' => $owner . '/' . $repo, ]; } /** * @param array $payload * @return array */ private function normalizeRepositoriesPayload(array $payload): array { $rows = array_is_list($payload) ? $payload : (array) ($payload['data'] ?? []); $repositories = []; foreach ($rows as $row) { if (!is_array($row)) { continue; } $owner = trim((string) (($row['owner']['login'] ?? $row['owner_name'] ?? ''))); $repo = trim((string) ($row['name'] ?? '')); $fullName = trim((string) ($row['full_name'] ?? '')); if ($fullName !== '' && str_contains($fullName, '/')) { [$ownerFromFull, $repoFromFull] = array_map('trim', explode('/', $fullName, 2)); $owner = $owner !== '' ? $owner : $ownerFromFull; $repo = $repo !== '' ? $repo : $repoFromFull; } if ($owner === '' || $repo === '') { continue; } $repositories[$owner . '/' . $repo] = [ 'owner' => $owner, 'repo' => $repo, 'full_name' => $owner . '/' . $repo, ]; } return array_values($repositories); } /** * @return array> */ private function listActionRuns(string $owner, string $repo): array { $payload = $this->requestJson('/repos/' . rawurlencode($owner) . '/' . rawurlencode($repo) . '/actions/runs?limit=10&page=1'); if (is_array($payload['workflow_runs'] ?? null)) { return array_values(array_filter($payload['workflow_runs'], 'is_array')); } if (is_array($payload['runs'] ?? null)) { return array_values(array_filter($payload['runs'], 'is_array')); } if (is_array($payload['data'] ?? null)) { return array_values(array_filter($payload['data'], 'is_array')); } if (array_is_list($payload)) { return array_values(array_filter($payload, 'is_array')); } return []; } /** * @param array $run */ private function isRunActive(array $run): bool { $status = strtolower(trim((string) ($run['status'] ?? ''))); $conclusion = strtolower(trim((string) ($run['conclusion'] ?? ''))); if (in_array($status, ['queued', 'waiting', 'pending', 'requested', 'running', 'in_progress'], true)) { return true; } if ($status === 'completed') { return false; } if ($status === '' && $conclusion === '') { return false; } return $conclusion === ''; } /** * @return array */ private function requestJson(string $path): array { $url = $this->baseUrl() . '/' . ltrim($path, '/'); $headers = [ 'Accept: application/json', 'Authorization: token ' . $this->token(), 'User-Agent: desktop-kusche-berlin/deploy-status', ]; if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_HTTPHEADER => $headers, ]); $raw = curl_exec($ch); $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $error = curl_error($ch); curl_close($ch); if (!is_string($raw)) { throw new \RuntimeException($error !== '' ? $error : 'Leere Gitea-Antwort.'); } if ($status >= 400) { throw new \RuntimeException('Gitea-API antwortet mit HTTP ' . $status . '.'); } $decoded = json_decode($raw, true); if (!is_array($decoded)) { throw new \RuntimeException('Gitea-API liefert kein JSON.'); } return $decoded; } $context = stream_context_create([ 'http' => [ 'method' => 'GET', 'timeout' => 10, 'header' => implode("\r\n", $headers) . "\r\n", ], ]); $raw = @file_get_contents($url, false, $context); if (!is_string($raw)) { throw new \RuntimeException('Gitea-API konnte nicht geladen werden.'); } $decoded = json_decode($raw, true); if (!is_array($decoded)) { throw new \RuntimeException('Gitea-API liefert kein JSON.'); } return $decoded; } }