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`.
|
Spalten (F3): `backlog`, `bereit`, `in_arbeit`, `qa`, `testing`, `fertig`, `done`.
|
||||||
|
|
||||||
- `backlog` und `done` werden zusaetzlich regelmaessig aus Codeberg-Issues befuellt
|
- `backlog`, `bereit`, `in_arbeit`, `testing` und `done` werden regelmaessig aus
|
||||||
(siehe [Codeberg-Sync](#codeberg-sync-backlogdone)), alle anderen Bewegungen ergeben
|
Codeberg-Issues befuellt (siehe [Codeberg-Sync](#codeberg-sync-backlogdone)); `qa` und
|
||||||
sich ausschliesslich aus Agenten-Meldungen (F4).
|
`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
|
- Ticket-Typ und Farbe (statt Prioritaet) werden bei Codeberg-Tickets aus dem ersten
|
||||||
Issue-Label uebernommen (F4).
|
Issue-Label uebernommen (F4).
|
||||||
- Stapel-Darstellung ("aelteste 4 + Rest als Stapel", F4) ist reine Frontend-Logik;
|
- 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`
|
Codeberg-Repo (`CODEBERG_OWNER`/`CODEBERG_REPO`) und gleicht sie ueber `external_ref`
|
||||||
(= Issue-Nummer) mit den Tickets ab:
|
(= Issue-Nummer) mit den Tickets ab:
|
||||||
|
|
||||||
- Offene Issues → Spalte `backlog` (nur falls das Ticket noch in `backlog`/`done` ist -
|
- Offene Issues → Spalte je nach Label: `wip` → `in_arbeit`, sonst `testing` → `testing`,
|
||||||
manuell auf dem Board bewegte Tickets werden nicht zurueckgesetzt).
|
sonst `automate` → `bereit`, sonst `backlog`.
|
||||||
- Geschlossene Issues → Spalte `done`.
|
- 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).
|
- Ticket-Typ/-Farbe kommen vom ersten Issue-Label (Name/Farbe).
|
||||||
|
|
||||||
Ausgefuehrt wird das Skript vom Compose-Service `sync` (siehe
|
Ausgefuehrt wird das Skript vom Compose-Service `sync` (siehe
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,14 @@ final class TicketRepository
|
||||||
/** @var string[] */
|
/** @var string[] */
|
||||||
public const COLUMNS = ['backlog', 'bereit', 'in_arbeit', 'qa', 'testing', 'fertig', 'done'];
|
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)
|
public function __construct(private readonly Database $db)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -140,9 +148,9 @@ final class TicketRepository
|
||||||
return $this->create($data + ['source' => 'codeberg']);
|
return $this->create($data + ['source' => 'codeberg']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manuell auf dem Board bewegte Tickets (z. B. in_arbeit/qa/testing) werden vom
|
// Manuell auf eine nicht vom Sync verwaltete Spalte bewegte Tickets (z. B. qa/fertig)
|
||||||
// periodischen Codeberg-Sync nicht zurueckgesetzt - nur Backlog/Done werden gepflegt.
|
// werden vom periodischen Codeberg-Sync nicht zurueckgesetzt.
|
||||||
if (!in_array($existing['column_name'], ['backlog', 'done'], true)) {
|
if (!in_array($existing['column_name'], self::SYNC_MANAGED_COLUMNS, true)) {
|
||||||
return $existing;
|
return $existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,23 @@ use Monitor\Api\Repository\TicketRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* F4: "Backlog und Done werden regelmaessig von Codeberg ausgelesen." Offene Issues landen
|
* 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.
|
* (falls noch nicht vorhanden bzw. noch nicht manuell bewegt) je nach Label in Backlog,
|
||||||
* Ticket-Typ und Farbe werden aus dem ersten Codeberg-Label uebernommen (F4: "Ticket-Typen ...
|
* Bereit, In Arbeit oder Testing, geschlossene in Done. Ticket-Typ und Farbe werden aus dem
|
||||||
* koennen mit den Codeberg-Farben dargestellt werden").
|
* ersten Codeberg-Label uebernommen (F4: "Ticket-Typen ... koennen mit den Codeberg-Farben
|
||||||
|
* dargestellt werden").
|
||||||
*/
|
*/
|
||||||
final class CodebergSyncService
|
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(
|
public function __construct(
|
||||||
private readonly CodebergClient $client,
|
private readonly CodebergClient $client,
|
||||||
private readonly TicketRepository $tickets,
|
private readonly TicketRepository $tickets,
|
||||||
|
|
@ -25,7 +36,7 @@ final class CodebergSyncService
|
||||||
public function sync(): void
|
public function sync(): void
|
||||||
{
|
{
|
||||||
foreach ($this->client->listIssues($this->owner, $this->repo, 'open') as $issue) {
|
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) {
|
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
|
* @param array{number: int, title: string, body: ?string, labels: array<int, array{name: string, color: string}>} $issue
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ final class TicketRepositoryTest extends TestCase
|
||||||
'column' => 'backlog',
|
'column' => 'backlog',
|
||||||
]);
|
]);
|
||||||
$ticket = $this->repository->findByExternalRef('42');
|
$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([
|
$result = $this->repository->upsertFromCodeberg([
|
||||||
'external_ref' => '42',
|
'external_ref' => '42',
|
||||||
|
|
@ -130,6 +130,23 @@ final class TicketRepositoryTest extends TestCase
|
||||||
'column' => 'backlog',
|
'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']);
|
self::assertSame('in_arbeit', $result['column_name']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,4 +87,80 @@ final class CodebergSyncServiceTest extends TestCase
|
||||||
$stillQa = $tickets->findByExternalRef('1');
|
$stillQa = $tickets->findByExternalRef('1');
|
||||||
self::assertSame('qa', $stillQa['column_name']);
|
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