1. AgentRepository::updateActivity() schrieb current_ticket_id ungeprueft. Eine
unbekannte Ticket-ID lief in die FOREIGN-KEY-Constraint und kam als
ungefangene PDOException samt Stacktrace und Dateipfaden beim Client an.
Jetzt wird die Existenz vorab geprueft (422). Zusaetzlich faengt
Application::handle() unerwartete Throwables ab und antwortet mit einem
generischen 500 - Details gehen nur noch ins Log.
2. Die Normalisierung der Codeberg-Label-Farbe ("#" davor) lag im
CurlCodebergClient statt im CodebergSyncService. Damit haetten andere
Client-Implementierungen unterschiedliche Farbformate ins Ticket geschrieben.
Verschoben in die Domaenenschicht, leere Farben ergeben jetzt null.
3. Config::get() griff selbst auf getenv() zurueck, wodurch ein in der Umgebung
gesetzter CODEBERG_TOKEN in eine explizit gebaute Config durchschlug - im
Testlauf landete der Token im Klartext in der PHPUnit-Ausgabe. Die Umgebung
wird jetzt ausschliesslich in fromEnvironment() eingesammelt.
Regressionstests fuer alle drei Punkte ergaenzt: 104 Tests, 181 Assertions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
93 lines
2.7 KiB
PHP
93 lines
2.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Monitor\Api\Sync;
|
|
|
|
use RuntimeException;
|
|
|
|
final class CurlCodebergClient implements CodebergClient
|
|
{
|
|
private const PER_PAGE = 50;
|
|
private const MAX_PAGES = 20;
|
|
|
|
public function __construct(
|
|
private readonly string $apiBase,
|
|
private readonly ?string $token = null,
|
|
) {
|
|
}
|
|
|
|
public function listIssues(string $owner, string $repo, string $state): array
|
|
{
|
|
$issues = [];
|
|
|
|
for ($page = 1; $page <= self::MAX_PAGES; $page++) {
|
|
$url = sprintf(
|
|
'%s/repos/%s/%s/issues?state=%s&type=issues&page=%d&limit=%d',
|
|
$this->apiBase,
|
|
rawurlencode($owner),
|
|
rawurlencode($repo),
|
|
rawurlencode($state),
|
|
$page,
|
|
self::PER_PAGE
|
|
);
|
|
|
|
$pageIssues = $this->fetch($url);
|
|
if ($pageIssues === []) {
|
|
break;
|
|
}
|
|
|
|
foreach ($pageIssues as $issue) {
|
|
$issues[] = [
|
|
'number' => (int) $issue['number'],
|
|
'title' => (string) $issue['title'],
|
|
'body' => isset($issue['body']) ? (string) $issue['body'] : null,
|
|
'labels' => array_map(
|
|
static fn (array $label): array => [
|
|
'name' => (string) $label['name'],
|
|
'color' => (string) $label['color'],
|
|
],
|
|
$issue['labels'] ?? []
|
|
),
|
|
];
|
|
}
|
|
|
|
if (count($pageIssues) < self::PER_PAGE) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $issues;
|
|
}
|
|
|
|
/** @return array<int, array<string, mixed>> */
|
|
private function fetch(string $url): array
|
|
{
|
|
$headers = ['Accept: application/json'];
|
|
if ($this->token !== null && $this->token !== '') {
|
|
$headers[] = 'Authorization: token ' . $this->token;
|
|
}
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_TIMEOUT => 15,
|
|
]);
|
|
$body = curl_exec($ch);
|
|
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($body === false) {
|
|
throw new RuntimeException('Codeberg-API nicht erreichbar: ' . $error);
|
|
}
|
|
if ($status >= 400) {
|
|
throw new RuntimeException('Codeberg-API-Fehler (HTTP ' . $status . '): ' . $body);
|
|
}
|
|
|
|
$decoded = json_decode((string) $body, true);
|
|
|
|
return is_array($decoded) ? $decoded : [];
|
|
}
|
|
}
|