Merge pull request 'Codeberg-Sync: offene Issues per Label der richtigen Board-Spalte zuordnen' (#16) from fds2610-bot/monitor:main into main
Reviewed-on: https://codeberg.org/fds2610/monitor/pulls/16
This commit is contained in:
commit
db92f5126e
5 changed files with 148 additions and 13 deletions
14
API.md
14
API.md
|
|
@ -104,9 +104,10 @@ Response 200: aktualisiertes Agent-Objekt.
|
|||
|
||||
Spalten (F3): `backlog`, `bereit`, `in_arbeit`, `qa`, `testing`, `fertig`, `done`.
|
||||
|
||||
- `backlog` und `done` werden zusaetzlich regelmaessig aus Codeberg-Issues befuellt
|
||||
(siehe [Codeberg-Sync](#codeberg-sync-backlogdone)), alle anderen Bewegungen ergeben
|
||||
sich ausschliesslich aus Agenten-Meldungen (F4).
|
||||
- `backlog`, `bereit`, `in_arbeit`, `testing` und `done` werden regelmaessig aus
|
||||
Codeberg-Issues befuellt (siehe [Codeberg-Sync](#codeberg-sync-backlogdone)); `qa` und
|
||||
`fertig` sind fuer zukuenftige Labels reserviert und ergeben sich ausschliesslich aus
|
||||
Agenten-Meldungen (F4).
|
||||
- Ticket-Typ und Farbe (statt Prioritaet) werden bei Codeberg-Tickets aus dem ersten
|
||||
Issue-Label uebernommen (F4).
|
||||
- Stapel-Darstellung ("aelteste 4 + Rest als Stapel", F4) ist reine Frontend-Logik;
|
||||
|
|
@ -239,9 +240,12 @@ schliessen.
|
|||
Codeberg-Repo (`CODEBERG_OWNER`/`CODEBERG_REPO`) und gleicht sie ueber `external_ref`
|
||||
(= Issue-Nummer) mit den Tickets ab:
|
||||
|
||||
- Offene Issues → Spalte `backlog` (nur falls das Ticket noch in `backlog`/`done` ist -
|
||||
manuell auf dem Board bewegte Tickets werden nicht zurueckgesetzt).
|
||||
- Offene Issues → Spalte je nach Label: `wip` → `in_arbeit`, sonst `testing` → `testing`,
|
||||
sonst `automate` → `bereit`, sonst `backlog`.
|
||||
- Geschlossene Issues → Spalte `done`.
|
||||
- Nur falls das Ticket noch in einer der Sync-Spalten (`backlog`, `bereit`, `in_arbeit`,
|
||||
`testing`, `done`) ist - auf `qa`/`fertig` manuell verschobene Tickets werden nicht
|
||||
zurueckgesetzt.
|
||||
- Ticket-Typ/-Farbe kommen vom ersten Issue-Label (Name/Farbe).
|
||||
|
||||
Ausgefuehrt wird das Skript vom Compose-Service `sync` (siehe
|
||||
|
|
|
|||
|
|
@ -13,6 +13,14 @@ final class TicketRepository
|
|||
/** @var string[] */
|
||||
public const COLUMNS = ['backlog', 'bereit', 'in_arbeit', 'qa', 'testing', 'fertig', 'done'];
|
||||
|
||||
/**
|
||||
* Spalten, die der Codeberg-Sync anhand von Issue-Status/Labels pflegt (F14). "qa" und
|
||||
* "fertig" sind fuer zukuenftige Labels reserviert und bleiben rein manuell.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private const SYNC_MANAGED_COLUMNS = ['backlog', 'bereit', 'in_arbeit', 'testing', 'done'];
|
||||
|
||||
public function __construct(private readonly Database $db)
|
||||
{
|
||||
}
|
||||
|
|
@ -140,9 +148,9 @@ final class TicketRepository
|
|||
return $this->create($data + ['source' => 'codeberg']);
|
||||
}
|
||||
|
||||
// Manuell auf dem Board bewegte Tickets (z. B. in_arbeit/qa/testing) werden vom
|
||||
// periodischen Codeberg-Sync nicht zurueckgesetzt - nur Backlog/Done werden gepflegt.
|
||||
if (!in_array($existing['column_name'], ['backlog', 'done'], true)) {
|
||||
// Manuell auf eine nicht vom Sync verwaltete Spalte bewegte Tickets (z. B. qa/fertig)
|
||||
// werden vom periodischen Codeberg-Sync nicht zurueckgesetzt.
|
||||
if (!in_array($existing['column_name'], self::SYNC_MANAGED_COLUMNS, true)) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,23 @@ use Monitor\Api\Repository\TicketRepository;
|
|||
|
||||
/**
|
||||
* F4: "Backlog und Done werden regelmaessig von Codeberg ausgelesen." Offene Issues landen
|
||||
* (falls noch nicht vorhanden bzw. noch nicht manuell bewegt) in Backlog, geschlossene in Done.
|
||||
* Ticket-Typ und Farbe werden aus dem ersten Codeberg-Label uebernommen (F4: "Ticket-Typen ...
|
||||
* koennen mit den Codeberg-Farben dargestellt werden").
|
||||
* (falls noch nicht vorhanden bzw. noch nicht manuell bewegt) je nach Label in Backlog,
|
||||
* Bereit, In Arbeit oder Testing, geschlossene in Done. Ticket-Typ und Farbe werden aus dem
|
||||
* ersten Codeberg-Label uebernommen (F4: "Ticket-Typen ... koennen mit den Codeberg-Farben
|
||||
* dargestellt werden").
|
||||
*/
|
||||
final class CodebergSyncService
|
||||
{
|
||||
/**
|
||||
* Reihenfolge = Prioritaet, falls ein Issue mehrere dieser Labels gleichzeitig traegt
|
||||
* (im normalen Workflow schliessen sie sich gegenseitig aus).
|
||||
*/
|
||||
private const LABEL_TO_COLUMN = [
|
||||
'wip' => 'in_arbeit',
|
||||
'testing' => 'testing',
|
||||
'automate' => 'bereit',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly CodebergClient $client,
|
||||
private readonly TicketRepository $tickets,
|
||||
|
|
@ -25,7 +36,7 @@ final class CodebergSyncService
|
|||
public function sync(): void
|
||||
{
|
||||
foreach ($this->client->listIssues($this->owner, $this->repo, 'open') as $issue) {
|
||||
$this->tickets->upsertFromCodeberg($this->mapIssue($issue, 'backlog'));
|
||||
$this->tickets->upsertFromCodeberg($this->mapIssue($issue, $this->columnForOpenIssue($issue)));
|
||||
}
|
||||
|
||||
foreach ($this->client->listIssues($this->owner, $this->repo, 'closed') as $issue) {
|
||||
|
|
@ -33,6 +44,25 @@ final class CodebergSyncService
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{labels: array<int, array{name: string, color: string}>} $issue
|
||||
*/
|
||||
private function columnForOpenIssue(array $issue): string
|
||||
{
|
||||
$labelNames = array_map(
|
||||
static fn (array $label): string => $label['name'],
|
||||
$issue['labels'] ?? []
|
||||
);
|
||||
|
||||
foreach (self::LABEL_TO_COLUMN as $label => $column) {
|
||||
if (in_array($label, $labelNames, true)) {
|
||||
return $column;
|
||||
}
|
||||
}
|
||||
|
||||
return 'backlog';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{number: int, title: string, body: ?string, labels: array<int, array{name: string, color: string}>} $issue
|
||||
* @return array<string, mixed>
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ final class TicketRepositoryTest extends TestCase
|
|||
'column' => 'backlog',
|
||||
]);
|
||||
$ticket = $this->repository->findByExternalRef('42');
|
||||
$this->repository->update((int) $ticket['id'], ['column' => 'in_arbeit']);
|
||||
$this->repository->update((int) $ticket['id'], ['column' => 'qa']);
|
||||
|
||||
$result = $this->repository->upsertFromCodeberg([
|
||||
'external_ref' => '42',
|
||||
|
|
@ -130,6 +130,23 @@ final class TicketRepositoryTest extends TestCase
|
|||
'column' => 'backlog',
|
||||
]);
|
||||
|
||||
self::assertSame('qa', $result['column_name']);
|
||||
}
|
||||
|
||||
public function testUpsertFromCodebergMovesTicketBetweenSyncManagedColumns(): void
|
||||
{
|
||||
$this->repository->upsertFromCodeberg([
|
||||
'external_ref' => '42',
|
||||
'title' => 'Issue 42',
|
||||
'column' => 'bereit',
|
||||
]);
|
||||
|
||||
$result = $this->repository->upsertFromCodeberg([
|
||||
'external_ref' => '42',
|
||||
'title' => 'Issue 42',
|
||||
'column' => 'in_arbeit',
|
||||
]);
|
||||
|
||||
self::assertSame('in_arbeit', $result['column_name']);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,4 +87,80 @@ final class CodebergSyncServiceTest extends TestCase
|
|||
$stillQa = $tickets->findByExternalRef('1');
|
||||
self::assertSame('qa', $stillQa['column_name']);
|
||||
}
|
||||
|
||||
/** @return array<int, array{0: string, 1: string}> */
|
||||
public static function labelColumnProvider(): array
|
||||
{
|
||||
return [
|
||||
'automate -> bereit' => ['automate', 'bereit'],
|
||||
'wip -> in_arbeit' => ['wip', 'in_arbeit'],
|
||||
'testing -> testing' => ['testing', 'testing'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider labelColumnProvider */
|
||||
public function testSyncMapsOpenIssueLabelToBoardColumn(string $label, string $expectedColumn): void
|
||||
{
|
||||
$tickets = new TicketRepository(Database::inMemory());
|
||||
$client = new FakeCodebergClient([
|
||||
'open' => [
|
||||
['number' => 1, 'title' => 'Issue', 'body' => null, 'labels' => [['name' => $label, 'color' => 'ff0000']]],
|
||||
],
|
||||
'closed' => [],
|
||||
]);
|
||||
|
||||
(new CodebergSyncService($client, $tickets, 'fds2610', 'monitor'))->sync();
|
||||
|
||||
$ticket = $tickets->findByExternalRef('1');
|
||||
self::assertSame($expectedColumn, $ticket['column_name']);
|
||||
}
|
||||
|
||||
public function testSyncMovesTicketBetweenAutomatedColumnsWhenLabelChanges(): void
|
||||
{
|
||||
$tickets = new TicketRepository(Database::inMemory());
|
||||
$client = new FakeCodebergClient([
|
||||
'open' => [
|
||||
['number' => 1, 'title' => 'Issue', 'body' => null, 'labels' => [['name' => 'automate', 'color' => 'ff0000']]],
|
||||
],
|
||||
'closed' => [],
|
||||
]);
|
||||
$service = new CodebergSyncService($client, $tickets, 'fds2610', 'monitor');
|
||||
|
||||
$service->sync();
|
||||
self::assertSame('bereit', $tickets->findByExternalRef('1')['column_name']);
|
||||
|
||||
$client = new FakeCodebergClient([
|
||||
'open' => [
|
||||
['number' => 1, 'title' => 'Issue', 'body' => null, 'labels' => [['name' => 'wip', 'color' => 'ff0000']]],
|
||||
],
|
||||
'closed' => [],
|
||||
]);
|
||||
(new CodebergSyncService($client, $tickets, 'fds2610', 'monitor'))->sync();
|
||||
|
||||
self::assertSame('in_arbeit', $tickets->findByExternalRef('1')['column_name']);
|
||||
}
|
||||
|
||||
public function testSyncClosesTicketFromAnyAutomatedColumnIntoDone(): void
|
||||
{
|
||||
$tickets = new TicketRepository(Database::inMemory());
|
||||
$client = new FakeCodebergClient([
|
||||
'open' => [
|
||||
['number' => 1, 'title' => 'Issue', 'body' => null, 'labels' => [['name' => 'testing', 'color' => 'ff0000']]],
|
||||
],
|
||||
'closed' => [],
|
||||
]);
|
||||
$service = new CodebergSyncService($client, $tickets, 'fds2610', 'monitor');
|
||||
$service->sync();
|
||||
self::assertSame('testing', $tickets->findByExternalRef('1')['column_name']);
|
||||
|
||||
$client = new FakeCodebergClient([
|
||||
'open' => [],
|
||||
'closed' => [
|
||||
['number' => 1, 'title' => 'Issue', 'body' => null, 'labels' => [['name' => 'testing', 'color' => 'ff0000']]],
|
||||
],
|
||||
]);
|
||||
(new CodebergSyncService($client, $tickets, 'fds2610', 'monitor'))->sync();
|
||||
|
||||
self::assertSame('done', $tickets->findByExternalRef('1')['column_name']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue