adasd
This commit is contained in:
@@ -26,13 +26,15 @@ final class App
|
||||
*/
|
||||
public function desktopPayload(): array
|
||||
{
|
||||
require_once AppPaths::systemAddonPath($this->projectRoot, 'gitea-deploy-status') . '/GiteaDeployStatusService.php';
|
||||
|
||||
$keycloakConfig = ConfigLoader::load($this->projectRoot, 'keycloak');
|
||||
$auth = new KeycloakAuth($keycloakConfig);
|
||||
$moduleRegistry = new ModuleRegistry(AppPaths::customAppsRoot($this->projectRoot));
|
||||
$registry = new AppRegistry($this->projectRoot . '/config/apps.php', $moduleRegistry->desktopApps());
|
||||
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php', $moduleRegistry->widgets());
|
||||
$shellRegistry = new ShellAppRegistry(AppPaths::systemShellRoot($this->projectRoot));
|
||||
$giteaStatus = new GiteaDeployStatusService(ConfigLoader::load($this->projectRoot, 'gitea'));
|
||||
$giteaStatus = new \SystemAddons\GiteaDeployStatus\GiteaDeployStatusService(ConfigLoader::load($this->projectRoot, 'gitea'));
|
||||
$skins = SkinResolver::all();
|
||||
$isAuthenticated = isset($_SESSION['desktop_auth']) && is_array($_SESSION['desktop_auth']);
|
||||
$authUser = $isAuthenticated && is_array($_SESSION['desktop_auth']['user'] ?? null)
|
||||
|
||||
@@ -35,4 +35,14 @@ final class AppPaths
|
||||
{
|
||||
return self::systemShellRoot($projectRoot) . '/' . trim($shellName, '/');
|
||||
}
|
||||
|
||||
public static function systemAddonsRoot(string $projectRoot): string
|
||||
{
|
||||
return rtrim($projectRoot, '/') . '/system/addons';
|
||||
}
|
||||
|
||||
public static function systemAddonPath(string $projectRoot, string $addonName): string
|
||||
{
|
||||
return self::systemAddonsRoot($projectRoot) . '/' . trim($addonName, '/');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,374 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class GiteaDeployStatusService
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $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<string, mixed>
|
||||
*/
|
||||
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<int, array{owner: string, repo: string, full_name: string}>
|
||||
*/
|
||||
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<int|string, mixed> $payload
|
||||
* @return array<int, array{owner: string, repo: string, full_name: string}>
|
||||
*/
|
||||
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<int, array<string, mixed>>
|
||||
*/
|
||||
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<string, mixed> $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<int|string, mixed>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user