asdasd
This commit is contained in:
13
config/gitea.php
Normal file
13
config/gitea.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'base_url' => 'https://gitea.int.kusche.berlin/api/v1',
|
||||||
|
'token' => '',
|
||||||
|
'owner' => 'private',
|
||||||
|
'repositories' => [],
|
||||||
|
'poll_idle_seconds' => 5,
|
||||||
|
'poll_running_seconds' => 1,
|
||||||
|
'visibility' => 'admins',
|
||||||
|
];
|
||||||
7
config/prod/gitea.php
Normal file
7
config/prod/gitea.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'token' => '',
|
||||||
|
];
|
||||||
7
config/staging/gitea.php
Normal file
7
config/staging/gitea.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'token' => 'cf22682c47cc40f820c47d6c1226dd48910b9add',
|
||||||
|
];
|
||||||
@@ -29,6 +29,7 @@ Zentraler Dokumentationsindex fuer das Projekt.
|
|||||||
- `system/apps/` Zielort fuer nicht installierbare System-Tools
|
- `system/apps/` Zielort fuer nicht installierbare System-Tools
|
||||||
- `system/shell/` Zielort fuer globale Shell-Werkzeuge und shell-nahe Desktop-Helfer
|
- `system/shell/` Zielort fuer globale Shell-Werkzeuge und shell-nahe Desktop-Helfer
|
||||||
- `temp/nexus-module-import/` Rohbasis fuer importierte Nexus-Module
|
- `temp/nexus-module-import/` Rohbasis fuer importierte Nexus-Module
|
||||||
|
- `config/gitea.php` Basis fuer serverseitige Gitea-API-Anbindung wie Deploy-Status im Tray
|
||||||
|
|
||||||
## Fachliche Hinweise
|
## Fachliche Hinweise
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ Aktuell wichtig:
|
|||||||
- die API bleibt vorerst auf demselben Host und wird nicht auf `api.desktop.kusche.berlin` ausgelagert
|
- die API bleibt vorerst auf demselben Host und wird nicht auf `api.desktop.kusche.berlin` ausgelagert
|
||||||
- das gemeinsame Admin-Debug-Widget neben der Uhr ist der Standard fuer Live-Debugging in Desktop und Native-Apps
|
- das gemeinsame Admin-Debug-Widget neben der Uhr ist der Standard fuer Live-Debugging in Desktop und Native-Apps
|
||||||
- der Waehrungs-Checker stellt zusaetzlich ein Desktop-Widget fuer manuelle Kursaktualisierung bereit
|
- der Waehrungs-Checker stellt zusaetzlich ein Desktop-Widget fuer manuelle Kursaktualisierung bereit
|
||||||
|
- der Gitea-Deploy-Status kann als Tray-Status unten rechts serverseitig ueber `config/gitea.php` angebunden werden
|
||||||
- das Oeffnen des Waehrungs-Checkers darf keinen externen Kursabruf ausloesen; Refreshes laufen nur nach Altersregel oder mit `force`
|
- das Oeffnen des Waehrungs-Checkers darf keinen externen Kursabruf ausloesen; Refreshes laufen nur nach Altersregel oder mit `force`
|
||||||
- `Systemtools` und installierbare `Module` werden getrennt behandelt; Systemtools sind nicht Teil der Benutzer-Installationsauswahl
|
- `Systemtools` und installierbare `Module` werden getrennt behandelt; Systemtools sind nicht Teil der Benutzer-Installationsauswahl
|
||||||
- das `Cron Tool` ist als globales Systemtool vorhanden und sammelt Modul-Cronjobs automatisch ueber Manifest-Metadaten
|
- das `Cron Tool` ist als globales Systemtool vorhanden und sammelt Modul-Cronjobs automatisch ueber Manifest-Metadaten
|
||||||
|
|||||||
@@ -161,10 +161,11 @@ $renderStartIcon = static function (array $profile): string {
|
|||||||
<div class="tray">
|
<div class="tray">
|
||||||
<?php foreach ($desktopPayload['tray'] as $trayItem): ?>
|
<?php foreach ($desktopPayload['tray'] as $trayItem): ?>
|
||||||
<button
|
<button
|
||||||
class="tray-pill tray-pill-button"
|
class="tray-pill tray-pill-button<?= (($trayItem['id'] ?? '') === 'gitea-deploy-status') ? ' tray-pill-status tray-pill-status--idle' : '' ?>"
|
||||||
type="button"
|
type="button"
|
||||||
data-tray-id="<?= htmlspecialchars((string) $trayItem['id'], ENT_QUOTES) ?>"
|
data-tray-id="<?= htmlspecialchars((string) $trayItem['id'], ENT_QUOTES) ?>"
|
||||||
<?php if (!empty($trayItem['app_id'])): ?>data-tray-app-id="<?= htmlspecialchars((string) $trayItem['app_id'], ENT_QUOTES) ?>" title="<?= htmlspecialchars((string) ($trayItem['title'] ?? $trayItem['label'] ?? ''), ENT_QUOTES) ?>"<?php endif; ?>
|
<?php if (!empty($trayItem['app_id'])): ?>data-tray-app-id="<?= htmlspecialchars((string) $trayItem['app_id'], ENT_QUOTES) ?>" title="<?= htmlspecialchars((string) ($trayItem['title'] ?? $trayItem['label'] ?? ''), ENT_QUOTES) ?>"<?php endif; ?>
|
||||||
|
<?php if (empty($trayItem['app_id']) && !empty($trayItem['title'])): ?>title="<?= htmlspecialchars((string) $trayItem['title'], ENT_QUOTES) ?>"<?php endif; ?>
|
||||||
><?= htmlspecialchars($trayItem['label'], ENT_QUOTES) ?></button>
|
><?= htmlspecialchars($trayItem['label'], ENT_QUOTES) ?></button>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (($desktopPayload['session']['is_admin'] ?? false) === true): ?>
|
<?php if (($desktopPayload['session']['is_admin'] ?? false) === true): ?>
|
||||||
|
|||||||
39
public/api/system/deploy-status/index.php
Normal file
39
public/api/system/deploy-status/index.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once dirname(__DIR__, 4) . '/src/App/bootstrap.php';
|
||||||
|
|
||||||
|
use App\ConfigLoader;
|
||||||
|
use App\GiteaDeployStatusService;
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$authUser = is_array($_SESSION['desktop_auth']['user'] ?? null) ? $_SESSION['desktop_auth']['user'] : [];
|
||||||
|
$groups = array_values(array_map('strval', (array) ($authUser['groups'] ?? [])));
|
||||||
|
$isAdmin = in_array('administrators', $groups, true);
|
||||||
|
|
||||||
|
$projectRoot = dirname(__DIR__, 4);
|
||||||
|
$service = new GiteaDeployStatusService(ConfigLoader::load($projectRoot, 'gitea'));
|
||||||
|
|
||||||
|
if (!$service->shouldShowInTray($isAdmin)) {
|
||||||
|
respond([
|
||||||
|
'error' => 'forbidden',
|
||||||
|
'message' => 'Kein Zugriff auf den Gitea-Deploy-Status.',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
respond([
|
||||||
|
'data' => $service->status(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
function respond(array $payload, int $statusCode = 200): never
|
||||||
|
{
|
||||||
|
http_response_code($statusCode);
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -1400,6 +1400,27 @@ h1 {
|
|||||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tray-pill-status {
|
||||||
|
min-width: 34px;
|
||||||
|
padding: 8px 9px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tray-pill-status--idle {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tray-pill-status--running {
|
||||||
|
color: #facc15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tray-pill-status--error,
|
||||||
|
.tray-pill-status--unconfigured {
|
||||||
|
color: #f97316;
|
||||||
|
}
|
||||||
|
|
||||||
.tray-menu {
|
.tray-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ if (payloadNode) {
|
|||||||
const accountMenu = document.getElementById('tray-account-menu');
|
const accountMenu = document.getElementById('tray-account-menu');
|
||||||
const fxTrayMenu = document.getElementById('tray-fx-menu');
|
const fxTrayMenu = document.getElementById('tray-fx-menu');
|
||||||
const fxTrayStatusNode = document.getElementById('tray-fx-status');
|
const fxTrayStatusNode = document.getElementById('tray-fx-status');
|
||||||
|
const giteaStatusNode = document.querySelector('.tray-pill[data-tray-id="gitea-deploy-status"]');
|
||||||
const clockNode = document.getElementById('clock');
|
const clockNode = document.getElementById('clock');
|
||||||
const clockTopNode = document.getElementById('clock-top');
|
const clockTopNode = document.getElementById('clock-top');
|
||||||
const debugToggleNode = document.getElementById('debug-toggle');
|
const debugToggleNode = document.getElementById('debug-toggle');
|
||||||
@@ -71,6 +72,7 @@ if (payloadNode) {
|
|||||||
let activeFunctionArea = 'programs';
|
let activeFunctionArea = 'programs';
|
||||||
const windowControlsMode = document.body.dataset.windowControls || 'windows';
|
const windowControlsMode = document.body.dataset.windowControls || 'windows';
|
||||||
let wallpaperImagePromise = null;
|
let wallpaperImagePromise = null;
|
||||||
|
let giteaStatusTimer = null;
|
||||||
|
|
||||||
const loadDebugState = () => {
|
const loadDebugState = () => {
|
||||||
try {
|
try {
|
||||||
@@ -469,6 +471,63 @@ if (payloadNode) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateGiteaTrayUi = (state, message) => {
|
||||||
|
if (!(giteaStatusNode instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
giteaStatusNode.classList.remove(
|
||||||
|
'tray-pill-status--idle',
|
||||||
|
'tray-pill-status--running',
|
||||||
|
'tray-pill-status--error',
|
||||||
|
'tray-pill-status--unconfigured',
|
||||||
|
);
|
||||||
|
|
||||||
|
const normalizedState = typeof state === 'string' && state !== '' ? state : 'idle';
|
||||||
|
giteaStatusNode.classList.add(`tray-pill-status--${normalizedState}`);
|
||||||
|
giteaStatusNode.setAttribute('title', typeof message === 'string' && message !== '' ? message : 'Gitea-Deploy-Status');
|
||||||
|
giteaStatusNode.textContent = '●';
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleGiteaStatusPoll = (seconds) => {
|
||||||
|
if (giteaStatusTimer) {
|
||||||
|
window.clearTimeout(giteaStatusTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const delayMs = Math.max(1, Number(seconds || 5)) * 1000;
|
||||||
|
giteaStatusTimer = window.setTimeout(() => {
|
||||||
|
void loadGiteaDeployStatus();
|
||||||
|
}, delayMs);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadGiteaDeployStatus = async () => {
|
||||||
|
if (!(giteaStatusNode instanceof HTMLElement)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/system/deploy-status/index.php', {
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
});
|
||||||
|
const responsePayload = await response.json().catch(() => ({}));
|
||||||
|
const data = responsePayload && typeof responsePayload === 'object' ? (responsePayload.data || {}) : {};
|
||||||
|
const state = typeof data.state === 'string' ? data.state : (response.ok ? 'idle' : 'error');
|
||||||
|
const message = typeof data.message === 'string' && data.message !== ''
|
||||||
|
? data.message
|
||||||
|
: 'Gitea-Deploy-Status ist unbekannt.';
|
||||||
|
const nextPoll = Number(data.poll_seconds || 5);
|
||||||
|
|
||||||
|
updateGiteaTrayUi(state, message);
|
||||||
|
scheduleGiteaStatusPoll(nextPoll);
|
||||||
|
return data;
|
||||||
|
} catch (_error) {
|
||||||
|
updateGiteaTrayUi('error', 'Gitea-Deploy-Status konnte nicht geladen werden.');
|
||||||
|
scheduleGiteaStatusPoll(5);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const runFxTrayRefresh = async (force) => {
|
const runFxTrayRefresh = async (force) => {
|
||||||
if (!(fxTrayStatusNode instanceof HTMLElement)) {
|
if (!(fxTrayStatusNode instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
@@ -2042,7 +2101,12 @@ if (payloadNode) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
giteaStatusNode?.addEventListener('click', () => {
|
||||||
|
void loadGiteaDeployStatus();
|
||||||
|
});
|
||||||
|
|
||||||
void loadFxTrayStatus();
|
void loadFxTrayStatus();
|
||||||
|
void loadGiteaDeployStatus();
|
||||||
|
|
||||||
debugToggleNode?.addEventListener('click', () => {
|
debugToggleNode?.addEventListener('click', () => {
|
||||||
toggleDebugWindow();
|
toggleDebugWindow();
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ final class App
|
|||||||
$registry = new AppRegistry($this->projectRoot . '/config/apps.php', $moduleRegistry->desktopApps());
|
$registry = new AppRegistry($this->projectRoot . '/config/apps.php', $moduleRegistry->desktopApps());
|
||||||
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php', $moduleRegistry->widgets());
|
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php', $moduleRegistry->widgets());
|
||||||
$shellRegistry = new ShellAppRegistry(AppPaths::systemShellRoot($this->projectRoot));
|
$shellRegistry = new ShellAppRegistry(AppPaths::systemShellRoot($this->projectRoot));
|
||||||
|
$giteaStatus = new GiteaDeployStatusService(ConfigLoader::load($this->projectRoot, 'gitea'));
|
||||||
$skins = SkinResolver::all();
|
$skins = SkinResolver::all();
|
||||||
$isAuthenticated = isset($_SESSION['desktop_auth']) && is_array($_SESSION['desktop_auth']);
|
$isAuthenticated = isset($_SESSION['desktop_auth']) && is_array($_SESSION['desktop_auth']);
|
||||||
$authUser = $isAuthenticated && is_array($_SESSION['desktop_auth']['user'] ?? null)
|
$authUser = $isAuthenticated && is_array($_SESSION['desktop_auth']['user'] ?? null)
|
||||||
@@ -76,6 +77,13 @@ final class App
|
|||||||
'title' => (string) ($app['title'] ?? ''),
|
'title' => (string) ($app['title'] ?? ''),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if ($giteaStatus->shouldShowInTray($isAdmin)) {
|
||||||
|
$trayItems[] = [
|
||||||
|
'id' => 'gitea-deploy-status',
|
||||||
|
'label' => '●',
|
||||||
|
'title' => 'Gitea-Deploy-Status',
|
||||||
|
];
|
||||||
|
}
|
||||||
$displayName = (string) ($authUser['name'] ?? $authUser['username'] ?? 'Gast');
|
$displayName = (string) ($authUser['name'] ?? $authUser['username'] ?? 'Gast');
|
||||||
$storageScope = $isAuthenticated
|
$storageScope = $isAuthenticated
|
||||||
? strtolower((string) ($authUser['username'] ?? $authUser['sub'] ?? 'user'))
|
? strtolower((string) ($authUser['username'] ?? $authUser['sub'] ?? 'user'))
|
||||||
|
|||||||
374
src/App/GiteaDeployStatusService.php
Normal file
374
src/App/GiteaDeployStatusService.php
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
<?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