Neue KPI: offene PRs ueber alle Projekte (Issue #59)
CodebergSyncService berechnet am Ende jedes Sync-Laufs (periodisch und webhook-getriggert, auch bei PR-Aenderungen) die Summe der offenen Pull Requests aller konfigurierten Projekte und speichert sie als KPI "open_prs" im bestehenden generischen KPI-Repository - sie erscheint damit automatisch im KPI-Monitor der Wand und im KPI-Modal. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
988a145e31
commit
ec8c74c0f8
11 changed files with 212 additions and 20 deletions
7
API.md
7
API.md
|
|
@ -293,6 +293,13 @@ siehe Issue #38.
|
||||||
der Wert ueber 0 liegt - eine dauerhaft rote Kachel mit einer 0 waere genau das Rauschen, das den
|
der Wert ueber 0 liegt - eine dauerhaft rote Kachel mit einer 0 waere genau das Rauschen, das den
|
||||||
Ernstfall untergehen liesse.
|
Ernstfall untergehen liesse.
|
||||||
|
|
||||||
|
Issue #59: die Kennzahl `open_prs` ("Offene PRs") wird nicht per `POST /api/v1/kpis/{key}`
|
||||||
|
gemeldet, sondern am Ende jedes Sync-Laufs (`CodebergSyncService::sync()`/`resync()`) automatisch
|
||||||
|
aus der Summe der offenen Pull Requests aller konfigurierten Projekte berechnet und ueber das
|
||||||
|
generische KPI-Repository gespeichert. Ein Sync-Lauf startet u.a. bei jedem eingehenden
|
||||||
|
Codeberg-Webhook (auch PR-Aenderungen) sowie periodisch, siehe
|
||||||
|
[`POST /api/v1/webhooks/codeberg`](#post-apiv1webhookscodeberg--sofort-sync-per-webhook).
|
||||||
|
|
||||||
### `POST /api/v1/kpis/{key}` — KPI-Datenpunkt melden
|
### `POST /api/v1/kpis/{key}` — KPI-Datenpunkt melden
|
||||||
|
|
||||||
Auth: `X-Api-Key`.
|
Auth: `X-Api-Key`.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
use Monitor\Api\Config;
|
use Monitor\Api\Config;
|
||||||
use Monitor\Api\Db\Database;
|
use Monitor\Api\Db\Database;
|
||||||
|
use Monitor\Api\Repository\KpiRepository;
|
||||||
use Monitor\Api\Repository\RepoConfigRepository;
|
use Monitor\Api\Repository\RepoConfigRepository;
|
||||||
use Monitor\Api\Repository\TicketRepository;
|
use Monitor\Api\Repository\TicketRepository;
|
||||||
use Monitor\Api\Sync\CodebergSyncService;
|
use Monitor\Api\Sync\CodebergSyncService;
|
||||||
|
|
@ -21,6 +22,7 @@ $service = new CodebergSyncService(
|
||||||
new CurlCodebergClient($config->codebergApiBase(), $config->codebergToken()),
|
new CurlCodebergClient($config->codebergApiBase(), $config->codebergToken()),
|
||||||
new TicketRepository($db),
|
new TicketRepository($db),
|
||||||
new RepoConfigRepository($db),
|
new RepoConfigRepository($db),
|
||||||
|
new KpiRepository($db),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ohne --watch bleibt das Skript ein einmaliger Lauf (manueller Aufruf, siehe README).
|
// Ohne --watch bleibt das Skript ein einmaliger Lauf (manueller Aufruf, siehe README).
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ final class Application
|
||||||
// unabhaengig vom periodisch/webhook-getriebenen sync-Container (bin/sync-codeberg.php)
|
// unabhaengig vom periodisch/webhook-getriebenen sync-Container (bin/sync-codeberg.php)
|
||||||
// - beide teilen sich denselben CodebergSyncService, nur der Aufloeser ist verschieden.
|
// - beide teilen sich denselben CodebergSyncService, nur der Aufloeser ist verschieden.
|
||||||
$codebergClient = new CurlCodebergClient($config->codebergApiBase(), $config->codebergToken());
|
$codebergClient = new CurlCodebergClient($config->codebergApiBase(), $config->codebergToken());
|
||||||
$syncService = new CodebergSyncService($codebergClient, $tickets, $repoConfigs);
|
$syncService = new CodebergSyncService($codebergClient, $tickets, $repoConfigs, $kpis);
|
||||||
$ticketController = new TicketController($tickets, $syncService);
|
$ticketController = new TicketController($tickets, $syncService);
|
||||||
$eventController = new EventController($events);
|
$eventController = new EventController($events);
|
||||||
$systemController = new SystemController($systems, new LegacyStatusFileReader(), $config->statusFilePath());
|
$systemController = new SystemController($systems, new LegacyStatusFileReader(), $config->statusFilePath());
|
||||||
|
|
|
||||||
|
|
@ -32,4 +32,10 @@ interface CodebergClient
|
||||||
* @return array<int, array{active: bool, url: string, content_type: string}>
|
* @return array<int, array{active: bool, url: string, content_type: string}>
|
||||||
*/
|
*/
|
||||||
public function listWebhooks(string $owner, string $repo): array;
|
public function listWebhooks(string $owner, string $repo): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue #59: Anzahl offener Pull Requests eines Projekts, Grundlage fuer den
|
||||||
|
* projektuebergreifenden "Offene PRs"-KPI (CodebergSyncService).
|
||||||
|
*/
|
||||||
|
public function countOpenPullRequests(string $owner, string $repo): int;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Monitor\Api\Sync;
|
namespace Monitor\Api\Sync;
|
||||||
|
|
||||||
|
use Monitor\Api\Repository\KpiRepository;
|
||||||
use Monitor\Api\Repository\RepoConfigRepository;
|
use Monitor\Api\Repository\RepoConfigRepository;
|
||||||
use Monitor\Api\Repository\TicketRepository;
|
use Monitor\Api\Repository\TicketRepository;
|
||||||
|
|
||||||
|
|
@ -33,6 +34,7 @@ final class CodebergSyncService
|
||||||
private readonly CodebergClient $client,
|
private readonly CodebergClient $client,
|
||||||
private readonly TicketRepository $tickets,
|
private readonly TicketRepository $tickets,
|
||||||
private readonly RepoConfigRepository $repoConfigs,
|
private readonly RepoConfigRepository $repoConfigs,
|
||||||
|
private readonly KpiRepository $kpis,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,6 +55,8 @@ final class CodebergSyncService
|
||||||
// oben nicht mehr angefasst und wuerden sonst dauerhaft in der alten, nicht mehr
|
// oben nicht mehr angefasst und wuerden sonst dauerhaft in der alten, nicht mehr
|
||||||
// zutreffenden Projektfarbe angezeigt.
|
// zutreffenden Projektfarbe angezeigt.
|
||||||
$this->tickets->clearColorForReposNotIn($this->repoSlugs($repoConfigs));
|
$this->tickets->clearColorForReposNotIn($this->repoSlugs($repoConfigs));
|
||||||
|
|
||||||
|
$this->reportOpenPullRequestKpi($repoConfigs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -76,6 +80,26 @@ final class CodebergSyncService
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->tickets->clearColorForReposNotIn($this->repoSlugs($repoConfigs));
|
$this->tickets->clearColorForReposNotIn($this->repoSlugs($repoConfigs));
|
||||||
|
|
||||||
|
$this->reportOpenPullRequestKpi($repoConfigs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue #59: projektuebergreifende Summe offener PRs als KPI (F7). Laeuft am Ende von
|
||||||
|
* sync()/resync() statt direkt im Webhook-Handler, weil dieser Payloads bewusst nicht
|
||||||
|
* auswertet (siehe WebhookController) - ein PR-Webhook loest ohnehin denselben Sync-Lauf
|
||||||
|
* aus wie jeder andere, der neue Wert ist danach also aktuell.
|
||||||
|
*
|
||||||
|
* @param array<int, array{owner: string, repo: string}> $repoConfigs
|
||||||
|
*/
|
||||||
|
private function reportOpenPullRequestKpi(array $repoConfigs): void
|
||||||
|
{
|
||||||
|
$openPrCount = array_sum(array_map(
|
||||||
|
fn (array $repoConfig): int => $this->client->countOpenPullRequests($repoConfig['owner'], $repoConfig['repo']),
|
||||||
|
$repoConfigs
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->kpis->report('open_prs', (float) $openPrCount, 'Offene PRs', null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return string[] external_refs, die bei diesem Lauf tatsaechlich bei Codeberg gesehen wurden. */
|
/** @return string[] external_refs, die bei diesem Lauf tatsaechlich bei Codeberg gesehen wurden. */
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,31 @@ final class CurlCodebergClient implements CodebergClient
|
||||||
return $issues;
|
return $issues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countOpenPullRequests(string $owner, string $repo): int
|
||||||
|
{
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
for ($page = 1; $page <= self::MAX_PAGES; $page++) {
|
||||||
|
$url = sprintf(
|
||||||
|
'%s/repos/%s/%s/issues?state=open&type=pulls&page=%d&limit=%d',
|
||||||
|
$this->apiBase,
|
||||||
|
rawurlencode($owner),
|
||||||
|
rawurlencode($repo),
|
||||||
|
$page,
|
||||||
|
self::PER_PAGE
|
||||||
|
);
|
||||||
|
|
||||||
|
$pagePulls = $this->fetch($url);
|
||||||
|
$count += count($pagePulls);
|
||||||
|
|
||||||
|
if (count($pagePulls) < self::PER_PAGE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
public function repoPermissions(string $owner, string $repo): array
|
public function repoPermissions(string $owner, string $repo): array
|
||||||
{
|
{
|
||||||
$url = sprintf('%s/repos/%s/%s', $this->apiBase, rawurlencode($owner), rawurlencode($repo));
|
$url = sprintf('%s/repos/%s/%s', $this->apiBase, rawurlencode($owner), rawurlencode($repo));
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ final class RepoConfigControllerTest extends TestCase
|
||||||
{
|
{
|
||||||
return [['active' => true, 'url' => 'https://monitor.example.org/api/v1/webhooks/codeberg', 'content_type' => 'json']];
|
return [['active' => true, 'url' => 'https://monitor.example.org/api/v1/webhooks/codeberg', 'content_type' => 'json']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countOpenPullRequests(string $owner, string $repo): int
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
$this->controller = new RepoConfigController(new RepoConfigRepository($db), $this->tickets, $healthChecker);
|
$this->controller = new RepoConfigController(new RepoConfigRepository($db), $this->tickets, $healthChecker);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use Monitor\Api\Controller\TicketController;
|
||||||
use Monitor\Api\Db\Database;
|
use Monitor\Api\Db\Database;
|
||||||
use Monitor\Api\Http\NotFoundException;
|
use Monitor\Api\Http\NotFoundException;
|
||||||
use Monitor\Api\Http\Request;
|
use Monitor\Api\Http\Request;
|
||||||
|
use Monitor\Api\Repository\KpiRepository;
|
||||||
use Monitor\Api\Repository\RepoConfigRepository;
|
use Monitor\Api\Repository\RepoConfigRepository;
|
||||||
use Monitor\Api\Repository\TicketRepository;
|
use Monitor\Api\Repository\TicketRepository;
|
||||||
use Monitor\Api\Sync\CodebergClient;
|
use Monitor\Api\Sync\CodebergClient;
|
||||||
|
|
@ -30,6 +31,21 @@ final class StubCodebergClient implements CodebergClient
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function repoPermissions(string $owner, string $repo): array
|
||||||
|
{
|
||||||
|
return ['push' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listWebhooks(string $owner, string $repo): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countOpenPullRequests(string $owner, string $repo): int
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class TicketControllerTest extends TestCase
|
final class TicketControllerTest extends TestCase
|
||||||
|
|
@ -42,7 +58,12 @@ final class TicketControllerTest extends TestCase
|
||||||
$this->repository = new TicketRepository(Database::inMemory());
|
$this->repository = new TicketRepository(Database::inMemory());
|
||||||
$this->controller = new TicketController(
|
$this->controller = new TicketController(
|
||||||
$this->repository,
|
$this->repository,
|
||||||
new CodebergSyncService(new StubCodebergClient(), $this->repository, new RepoConfigRepository(Database::inMemory()))
|
new CodebergSyncService(
|
||||||
|
new StubCodebergClient(),
|
||||||
|
$this->repository,
|
||||||
|
new RepoConfigRepository(Database::inMemory()),
|
||||||
|
new KpiRepository(Database::inMemory())
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,7 +145,12 @@ final class TicketControllerTest extends TestCase
|
||||||
$repository = new TicketRepository(Database::inMemory());
|
$repository = new TicketRepository(Database::inMemory());
|
||||||
$controller = new TicketController(
|
$controller = new TicketController(
|
||||||
$repository,
|
$repository,
|
||||||
new CodebergSyncService(new StubCodebergClient(shouldFail: true), $repository, new RepoConfigRepository(Database::inMemory()))
|
new CodebergSyncService(
|
||||||
|
new StubCodebergClient(shouldFail: true),
|
||||||
|
$repository,
|
||||||
|
new RepoConfigRepository(Database::inMemory()),
|
||||||
|
new KpiRepository(Database::inMemory())
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$response = $controller->resync(Request::create('POST', '/api/v1/tickets/resync'));
|
$response = $controller->resync(Request::create('POST', '/api/v1/tickets/resync'));
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
namespace Monitor\Api\Tests\Sync;
|
namespace Monitor\Api\Tests\Sync;
|
||||||
|
|
||||||
use Monitor\Api\Db\Database;
|
use Monitor\Api\Db\Database;
|
||||||
|
use Monitor\Api\Repository\KpiRepository;
|
||||||
use Monitor\Api\Repository\RepoConfigRepository;
|
use Monitor\Api\Repository\RepoConfigRepository;
|
||||||
use Monitor\Api\Repository\TicketRepository;
|
use Monitor\Api\Repository\TicketRepository;
|
||||||
use Monitor\Api\Sync\CodebergClient;
|
use Monitor\Api\Sync\CodebergClient;
|
||||||
|
|
@ -13,15 +14,35 @@ use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
final class FakeCodebergClient implements CodebergClient
|
final class FakeCodebergClient implements CodebergClient
|
||||||
{
|
{
|
||||||
/** @param array<string, array<string, array<int, array{number:int,title:string,body:?string,labels:array}>>> $issuesByRepoAndState */
|
/**
|
||||||
public function __construct(private readonly array $issuesByRepoAndState)
|
* @param array<string, array<string, array<int, array{number:int,title:string,body:?string,labels:array}>>> $issuesByRepoAndState
|
||||||
{
|
* @param array<string, int> $openPullRequestsByRepo
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly array $issuesByRepoAndState,
|
||||||
|
private readonly array $openPullRequestsByRepo = [],
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function listIssues(string $owner, string $repo, string $state): array
|
public function listIssues(string $owner, string $repo, string $state): array
|
||||||
{
|
{
|
||||||
return $this->issuesByRepoAndState[$owner . '/' . $repo][$state] ?? [];
|
return $this->issuesByRepoAndState[$owner . '/' . $repo][$state] ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function repoPermissions(string $owner, string $repo): array
|
||||||
|
{
|
||||||
|
return ['push' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listWebhooks(string $owner, string $repo): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countOpenPullRequests(string $owner, string $repo): int
|
||||||
|
{
|
||||||
|
return $this->openPullRequestsByRepo[$owner . '/' . $repo] ?? 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class CodebergSyncServiceTest extends TestCase
|
final class CodebergSyncServiceTest extends TestCase
|
||||||
|
|
@ -55,6 +76,12 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
return $repoConfigs;
|
return $repoConfigs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Frisches, unabhaengiges KpiRepository - der KPI-Wert selbst ist nicht Gegenstand der meisten Tests hier. */
|
||||||
|
private static function kpis(): KpiRepository
|
||||||
|
{
|
||||||
|
return new KpiRepository(Database::inMemory());
|
||||||
|
}
|
||||||
|
|
||||||
public function testSyncCreatesBacklogAndDoneTicketsColoredByProject(): void
|
public function testSyncCreatesBacklogAndDoneTicketsColoredByProject(): void
|
||||||
{
|
{
|
||||||
$tickets = new TicketRepository(Database::inMemory());
|
$tickets = new TicketRepository(Database::inMemory());
|
||||||
|
|
@ -67,7 +94,7 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO])))->sync();
|
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO]), self::kpis()))->sync();
|
||||||
|
|
||||||
$backlog = $tickets->all('backlog');
|
$backlog = $tickets->all('backlog');
|
||||||
$done = $tickets->all('done');
|
$done = $tickets->all('done');
|
||||||
|
|
@ -102,7 +129,7 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([
|
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([
|
||||||
self::MONITOR_REPO,
|
self::MONITOR_REPO,
|
||||||
['owner' => 'fds2610', 'repo' => 'Steuerkram', 'color' => '#28a745'],
|
['owner' => 'fds2610', 'repo' => 'Steuerkram', 'color' => '#28a745'],
|
||||||
])))->sync();
|
]), self::kpis()))->sync();
|
||||||
|
|
||||||
$backlog = $tickets->all('backlog');
|
$backlog = $tickets->all('backlog');
|
||||||
self::assertCount(2, $backlog, 'gleiche Issue-Nummer aus verschiedenen Projekten darf nicht zu einem Ticket verschmelzen');
|
self::assertCount(2, $backlog, 'gleiche Issue-Nummer aus verschiedenen Projekten darf nicht zu einem Ticket verschmelzen');
|
||||||
|
|
@ -125,14 +152,14 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
'closed' => [],
|
'closed' => [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO])))->sync();
|
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO]), self::kpis()))->sync();
|
||||||
self::assertSame('#fbca04', $tickets->findByExternalRef('1', self::MONITOR_SLUG)['color']);
|
self::assertSame('#fbca04', $tickets->findByExternalRef('1', self::MONITOR_SLUG)['color']);
|
||||||
|
|
||||||
// Projekt wurde im Projekte-Modal entfernt (Issue #34): der naechste Sync-Lauf sieht
|
// Projekt wurde im Projekte-Modal entfernt (Issue #34): der naechste Sync-Lauf sieht
|
||||||
// es nicht mehr in repoConfigsFor() und darf das verwaiste Ticket nicht laenger blau
|
// es nicht mehr in repoConfigsFor() und darf das verwaiste Ticket nicht laenger blau
|
||||||
// faerben, obwohl es kein Projekt mit dieser Farbe mehr gibt.
|
// faerben, obwohl es kein Projekt mit dieser Farbe mehr gibt.
|
||||||
$client = new FakeCodebergClient([]);
|
$client = new FakeCodebergClient([]);
|
||||||
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([])))->sync();
|
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([]), self::kpis()))->sync();
|
||||||
|
|
||||||
self::assertNull($tickets->findByExternalRef('1', self::MONITOR_SLUG)['color']);
|
self::assertNull($tickets->findByExternalRef('1', self::MONITOR_SLUG)['color']);
|
||||||
}
|
}
|
||||||
|
|
@ -144,7 +171,7 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
'open' => [['number' => 1, 'title' => 'Issue', 'body' => null, 'labels' => []]],
|
'open' => [['number' => 1, 'title' => 'Issue', 'body' => null, 'labels' => []]],
|
||||||
'closed' => [],
|
'closed' => [],
|
||||||
]);
|
]);
|
||||||
$service = new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO]));
|
$service = new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO]), self::kpis());
|
||||||
|
|
||||||
$service->sync();
|
$service->sync();
|
||||||
$ticket = $tickets->findByExternalRef('1', self::MONITOR_SLUG);
|
$ticket = $tickets->findByExternalRef('1', self::MONITOR_SLUG);
|
||||||
|
|
@ -178,7 +205,7 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
'closed' => [],
|
'closed' => [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO])))->sync();
|
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO]), self::kpis()))->sync();
|
||||||
|
|
||||||
$ticket = $tickets->findByExternalRef('1', self::MONITOR_SLUG);
|
$ticket = $tickets->findByExternalRef('1', self::MONITOR_SLUG);
|
||||||
self::assertSame($expectedColumn, $ticket['column_name']);
|
self::assertSame($expectedColumn, $ticket['column_name']);
|
||||||
|
|
@ -193,7 +220,7 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
],
|
],
|
||||||
'closed' => [],
|
'closed' => [],
|
||||||
]);
|
]);
|
||||||
$service = new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO]));
|
$service = new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO]), self::kpis());
|
||||||
|
|
||||||
$service->sync();
|
$service->sync();
|
||||||
self::assertSame('bereit', $tickets->findByExternalRef('1', self::MONITOR_SLUG)['column_name']);
|
self::assertSame('bereit', $tickets->findByExternalRef('1', self::MONITOR_SLUG)['column_name']);
|
||||||
|
|
@ -204,7 +231,7 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
],
|
],
|
||||||
'closed' => [],
|
'closed' => [],
|
||||||
]);
|
]);
|
||||||
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO])))->sync();
|
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO]), self::kpis()))->sync();
|
||||||
|
|
||||||
self::assertSame('in_arbeit', $tickets->findByExternalRef('1', self::MONITOR_SLUG)['column_name']);
|
self::assertSame('in_arbeit', $tickets->findByExternalRef('1', self::MONITOR_SLUG)['column_name']);
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +244,7 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
'open' => [['number' => 1, 'title' => 'Issue', 'body' => null, 'labels' => []]],
|
'open' => [['number' => 1, 'title' => 'Issue', 'body' => null, 'labels' => []]],
|
||||||
'closed' => [],
|
'closed' => [],
|
||||||
]);
|
]);
|
||||||
(new CodebergSyncService($client, $tickets, $repoConfigs))->sync();
|
(new CodebergSyncService($client, $tickets, $repoConfigs, self::kpis()))->sync();
|
||||||
// Manuell auf "fertig" bewegt, damit sync() es normalerweise unangetastet liesse
|
// Manuell auf "fertig" bewegt, damit sync() es normalerweise unangetastet liesse
|
||||||
// (siehe testSyncDoesNotResetManuallyMovedTicket) - Issue #51 ist genau fuer solche
|
// (siehe testSyncDoesNotResetManuallyMovedTicket) - Issue #51 ist genau fuer solche
|
||||||
// liegen gebliebenen Karteileichen gedacht, die der normale Sync nicht mehr anfasst.
|
// liegen gebliebenen Karteileichen gedacht, die der normale Sync nicht mehr anfasst.
|
||||||
|
|
@ -225,7 +252,7 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
|
|
||||||
// Issue 1 wurde bei Codeberg inzwischen komplett geloescht (taucht weder offen noch
|
// Issue 1 wurde bei Codeberg inzwischen komplett geloescht (taucht weder offen noch
|
||||||
// geschlossen auf) - genau der Fall aus Issue #51, den der normale Sync nicht bereinigt.
|
// geschlossen auf) - genau der Fall aus Issue #51, den der normale Sync nicht bereinigt.
|
||||||
(new CodebergSyncService(new FakeCodebergClient([self::MONITOR_SLUG => ['open' => [], 'closed' => []]]), $tickets, $repoConfigs))->resync();
|
(new CodebergSyncService(new FakeCodebergClient([self::MONITOR_SLUG => ['open' => [], 'closed' => []]]), $tickets, $repoConfigs, self::kpis()))->resync();
|
||||||
|
|
||||||
self::assertNull($tickets->findByExternalRef('1', self::MONITOR_SLUG));
|
self::assertNull($tickets->findByExternalRef('1', self::MONITOR_SLUG));
|
||||||
self::assertNull($tickets->find((int) $orphanTicket['id']));
|
self::assertNull($tickets->find((int) $orphanTicket['id']));
|
||||||
|
|
@ -239,13 +266,13 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
'open' => [['number' => 1, 'title' => 'Issue', 'body' => null, 'labels' => []]],
|
'open' => [['number' => 1, 'title' => 'Issue', 'body' => null, 'labels' => []]],
|
||||||
'closed' => [],
|
'closed' => [],
|
||||||
]);
|
]);
|
||||||
(new CodebergSyncService($client, $tickets, $repoConfigs))->sync();
|
(new CodebergSyncService($client, $tickets, $repoConfigs, self::kpis()))->sync();
|
||||||
$ticket = $tickets->findByExternalRef('1', self::MONITOR_SLUG);
|
$ticket = $tickets->findByExternalRef('1', self::MONITOR_SLUG);
|
||||||
$tickets->update((int) $ticket['id'], ['column' => 'fertig']);
|
$tickets->update((int) $ticket['id'], ['column' => 'fertig']);
|
||||||
|
|
||||||
// Dasselbe Issue existiert bei Codeberg weiterhin (offen) - resync() darf es trotz
|
// Dasselbe Issue existiert bei Codeberg weiterhin (offen) - resync() darf es trotz
|
||||||
// manuell gesetzter, nicht vom Sync verwalteter Spalte nicht als Karteileiche werfen.
|
// manuell gesetzter, nicht vom Sync verwalteter Spalte nicht als Karteileiche werfen.
|
||||||
(new CodebergSyncService($client, $tickets, $repoConfigs))->resync();
|
(new CodebergSyncService($client, $tickets, $repoConfigs, self::kpis()))->resync();
|
||||||
|
|
||||||
$stillFertig = $tickets->findByExternalRef('1', self::MONITOR_SLUG);
|
$stillFertig = $tickets->findByExternalRef('1', self::MONITOR_SLUG);
|
||||||
self::assertNotNull($stillFertig);
|
self::assertNotNull($stillFertig);
|
||||||
|
|
@ -261,7 +288,7 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
],
|
],
|
||||||
'closed' => [],
|
'closed' => [],
|
||||||
]);
|
]);
|
||||||
$service = new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO]));
|
$service = new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO]), self::kpis());
|
||||||
$service->sync();
|
$service->sync();
|
||||||
self::assertSame('testing', $tickets->findByExternalRef('1', self::MONITOR_SLUG)['column_name']);
|
self::assertSame('testing', $tickets->findByExternalRef('1', self::MONITOR_SLUG)['column_name']);
|
||||||
|
|
||||||
|
|
@ -271,8 +298,43 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
['number' => 1, 'title' => 'Issue', 'body' => null, 'labels' => [['name' => 'testing', 'color' => 'ff0000']]],
|
['number' => 1, 'title' => 'Issue', 'body' => null, 'labels' => [['name' => 'testing', 'color' => 'ff0000']]],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO])))->sync();
|
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([self::MONITOR_REPO]), self::kpis()))->sync();
|
||||||
|
|
||||||
self::assertSame('done', $tickets->findByExternalRef('1', self::MONITOR_SLUG)['column_name']);
|
self::assertSame('done', $tickets->findByExternalRef('1', self::MONITOR_SLUG)['column_name']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSyncReportsOpenPullRequestCountAcrossProjectsAsKpi(): void
|
||||||
|
{
|
||||||
|
$tickets = new TicketRepository(Database::inMemory());
|
||||||
|
$client = new FakeCodebergClient(
|
||||||
|
issuesByRepoAndState: [],
|
||||||
|
openPullRequestsByRepo: ['fds2610/monitor' => 2, 'fds2610/Steuerkram' => 3],
|
||||||
|
);
|
||||||
|
$kpis = self::kpis();
|
||||||
|
|
||||||
|
(new CodebergSyncService($client, $tickets, self::repoConfigsFor([
|
||||||
|
self::MONITOR_REPO,
|
||||||
|
['owner' => 'fds2610', 'repo' => 'Steuerkram', 'color' => '#28a745'],
|
||||||
|
]), $kpis))->sync();
|
||||||
|
|
||||||
|
$kpi = $kpis->find('open_prs');
|
||||||
|
self::assertNotNull($kpi);
|
||||||
|
self::assertSame(5.0, $kpi['current_value']);
|
||||||
|
self::assertSame('Offene PRs', $kpi['label']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResyncAlsoRefreshesOpenPullRequestKpi(): void
|
||||||
|
{
|
||||||
|
$tickets = new TicketRepository(Database::inMemory());
|
||||||
|
$repoConfigs = self::repoConfigsFor([self::MONITOR_REPO]);
|
||||||
|
$kpis = self::kpis();
|
||||||
|
$client = new FakeCodebergClient(
|
||||||
|
issuesByRepoAndState: [self::MONITOR_SLUG => ['open' => [], 'closed' => []]],
|
||||||
|
openPullRequestsByRepo: [self::MONITOR_SLUG => 4],
|
||||||
|
);
|
||||||
|
|
||||||
|
(new CodebergSyncService($client, $tickets, $repoConfigs, $kpis))->resync();
|
||||||
|
|
||||||
|
self::assertSame(4.0, $kpis->find('open_prs')['current_value']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -185,5 +185,35 @@ namespace Monitor\Api\Tests\Sync {
|
||||||
self::assertSame(403, $e->status);
|
self::assertSame(403, $e->status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCountOpenPullRequestsCountsSinglePage(): void
|
||||||
|
{
|
||||||
|
CurlStub::$queue = [
|
||||||
|
['status' => 200, 'body' => json_encode([
|
||||||
|
['number' => 1, 'title' => 'PR A'],
|
||||||
|
['number' => 2, 'title' => 'PR B'],
|
||||||
|
])],
|
||||||
|
];
|
||||||
|
|
||||||
|
$count = (new CurlCodebergClient('https://codeberg.org/api/v1'))->countOpenPullRequests('fds2610', 'monitor');
|
||||||
|
|
||||||
|
self::assertSame(2, $count);
|
||||||
|
self::assertStringContainsString('state=open', CurlStub::$requestedUrls[0]);
|
||||||
|
self::assertStringContainsString('type=pulls', CurlStub::$requestedUrls[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCountOpenPullRequestsSumsAcrossPages(): void
|
||||||
|
{
|
||||||
|
$fullPage = array_fill(0, 50, ['number' => 1, 'title' => 'PR']);
|
||||||
|
CurlStub::$queue = [
|
||||||
|
['status' => 200, 'body' => json_encode($fullPage)],
|
||||||
|
['status' => 200, 'body' => json_encode([['number' => 99, 'title' => 'Letzte PR']])],
|
||||||
|
];
|
||||||
|
|
||||||
|
$count = (new CurlCodebergClient('https://codeberg.org/api/v1'))->countOpenPullRequests('fds2610', 'monitor');
|
||||||
|
|
||||||
|
self::assertSame(51, $count);
|
||||||
|
self::assertCount(2, CurlStub::$requestedUrls);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,11 @@ final class RepoHealthCheckerTest extends TestCase
|
||||||
|
|
||||||
return $this->listWebhooks ?? [];
|
return $this->listWebhooks ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countOpenPullRequests(string $owner, string $repo): int
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue