diff --git a/API.md b/API.md index 9f07378..88f6b88 100644 --- a/API.md +++ b/API.md @@ -354,6 +354,12 @@ Auth: `X-Api-Key`. Pflichtfelder: `name`, `message`, `severity`. `occurred_at` optional (Default: jetzt). Response `201` mit dem gespeicherten Event. +### `POST /api/v1/events/system` — Event des Monitorings selbst melden + +Wie `POST /api/v1/events`, aber Auth per `X-Admin-Key`. Genutzt von `update.sh`, das nach einem +erfolgreichen Update `name: "Monitor"`, `message: "Update eingespielt ()"`, +`severity: "warning"` meldet. + ### `GET /api/v1/events` — Event-Liste Query-Parameter: `limit` (Default `5`, entspricht der im Plan festgelegten Zeilenzahl des diff --git a/README_UPDATE.md b/README_UPDATE.md index 71342de..ec6cb97 100644 --- a/README_UPDATE.md +++ b/README_UPDATE.md @@ -14,6 +14,8 @@ aktualisiert. fuehrt (per Default) die Testsuiten aus. Kann jederzeit auch manuell aufgerufen werden, siehe Kommentar am Kopf der Datei fuer die Umgebungsvariablen `UPDATE_REMOTE`, `UPDATE_BRANCH`, `UPDATE_RUN_TESTS`. + Nach dem Neustart meldet das Skript ein Event "Monitor: Update eingespielt ()" per + `POST /api/v1/events/system` (Admin-Key aus der `.env`); schlaegt das fehl, bleibt das Update gueltig. - **`deploy-webhook.py`** (Projekt-Wurzel) - kleiner HTTP-Server (nur Python-Standardbibliothek, keine Abhaengigkeiten) auf Basis von `http.server`. Prueft eingehende Codeberg-Webhooks per HMAC-Signatur (`X-Forgejo-Signature`/`X-Gitea-Signature`, analog zum bestehenden diff --git a/api/src/Application.php b/api/src/Application.php index b612a04..f3e3ce2 100644 --- a/api/src/Application.php +++ b/api/src/Application.php @@ -103,6 +103,9 @@ final class Application $this->router->add('GET', '/api/v1/events', fn (Request $r, array $p) => $eventController->index($r)); $this->router->add('POST', '/api/v1/events', fn (Request $r, array $p) => $eventController->create($r), 'agent'); + // update.sh meldet hier, dass sich das Monitoring selbst aktualisiert hat. Es laeuft auf dem + // Host und kennt nur den Admin-Key aus der .env, keinen Agenten-Key. + $this->router->add('POST', '/api/v1/events/system', fn (Request $r, array $p) => $eventController->create($r), 'admin'); $this->router->add('GET', '/api/v1/systems', fn (Request $r, array $p) => $systemController->index($r)); $this->router->add('POST', '/api/v1/systems/{id}/status', fn (Request $r, array $p) => $systemController->reportStatus($r, $p), 'agent'); diff --git a/api/src/Repository/TicketRepository.php b/api/src/Repository/TicketRepository.php index 7084c47..7112153 100644 --- a/api/src/Repository/TicketRepository.php +++ b/api/src/Repository/TicketRepository.php @@ -185,11 +185,21 @@ final class TicketRepository return $this->create($data + ['column' => 'fertig', 'source' => 'codeberg', 'is_pull_request' => 1]); } - return $this->update((int) $existing['id'], [ + $updated = $this->update((int) $existing['id'], [ 'title' => $data['title'], 'description' => $data['description'] ?? null, 'color' => $data['color'] ?? null, ]); + + // Ein Ticket, das vor der PR-Darstellung (z. B. durch einen aelteren Sync) als Issue + // angelegt wurde, behielte sonst is_pull_request = 0 und damit den /issues/-Link. + if (empty($existing['is_pull_request'])) { + $this->db->pdo()->prepare('UPDATE tickets SET is_pull_request = 1 WHERE id = :id') + ->execute(['id' => $existing['id']]); + $updated['is_pull_request'] = 1; + } + + return $updated; } /** diff --git a/api/tests/Unit/ApplicationTest.php b/api/tests/Unit/ApplicationTest.php index 724cfd4..5706fad 100644 --- a/api/tests/Unit/ApplicationTest.php +++ b/api/tests/Unit/ApplicationTest.php @@ -50,6 +50,23 @@ final class ApplicationTest extends TestCase self::assertSame(401, $response->status); } + public function testSystemEventNeedsAdminKey(): void + { + $body = ['name' => 'Monitor', 'message' => 'Update eingespielt (abc1234)', 'severity' => 'warning']; + + $denied = $this->app->handle(Request::create('POST', '/api/v1/events/system', body: $body)); + self::assertSame(401, $denied->status); + + $created = $this->app->handle(Request::create( + 'POST', + '/api/v1/events/system', + body: $body, + headers: ['X-Admin-Key' => 'admin-secret'] + )); + self::assertSame(201, $created->status); + self::assertSame('Monitor', $created->body()['event']['name']); + } + public function testFullFlowRegisterAgentThenCreateTicket(): void { $registerResponse = $this->app->handle(Request::create( diff --git a/api/tests/Unit/Repository/TicketRepositoryTest.php b/api/tests/Unit/Repository/TicketRepositoryTest.php index 6ab7486..0b6e011 100644 --- a/api/tests/Unit/Repository/TicketRepositoryTest.php +++ b/api/tests/Unit/Repository/TicketRepositoryTest.php @@ -27,6 +27,16 @@ final class TicketRepositoryTest extends TestCase self::assertSame('Neues Ticket', $ticket['title']); } + public function testUpsertPullRequestFlagsExistingIssueTicket(): void + { + $this->repository->create(['title' => 'PR', 'external_ref' => '107', 'repo' => 'a/b', 'source' => 'codeberg']); + + $ticket = $this->repository->upsertPullRequest(['external_ref' => '107', 'repo' => 'a/b', 'title' => 'PR neu']); + + self::assertSame(1, (int) $ticket['is_pull_request']); + self::assertSame(1, (int) $this->repository->findByExternalRef('107', 'a/b')['is_pull_request']); + } + public function testCreateRejectsUnknownColumn(): void { $this->expectException(InvalidArgumentException::class); diff --git a/update.sh b/update.sh index 5ec2ff4..3511fa7 100644 --- a/update.sh +++ b/update.sh @@ -97,4 +97,41 @@ fi log "Starte Dienste neu (api, web, sync)..." $DOCKER_COMPOSE up -d api web sync +# Meldet das Update als Event (POST /api/v1/events/system, Admin-Key aus der .env). Die API ist +# direkt nach "up -d" evtl. noch nicht bereit, daher mehrere Versuche; ein Fehlschlag bricht das +# (bereits erfolgreiche) Update nicht ab. +report_update_event() { + command -v curl >/dev/null 2>&1 || { log "Hinweis: curl fehlt, Update-Event wird nicht gemeldet."; return 0; } + local env_file="$REPO_DIR/.env" admin_key api_base + env_value() { + [ -f "$env_file" ] || return 0 + sed -n "s/^[[:space:]]*$1=//p" "$env_file" | tail -n1 \ + | sed -e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'\$/\1/" + } + admin_key="${MONITOR_ADMIN_API_KEY:-$(env_value MONITOR_ADMIN_API_KEY)}" + api_base="${MONITOR_API_BASE:-$(env_value MONITOR_API_BASE)}" + api_base="${api_base:-http://localhost:8091}" + api_base="${api_base%/}" + api_base="${api_base%/api/v1}" + if [ -z "$admin_key" ]; then + log "Hinweis: MONITOR_ADMIN_API_KEY fehlt, Update-Event wird nicht gemeldet." + return 0 + fi + + local payload i + payload="{\"name\":\"Monitor\",\"message\":\"Update eingespielt ($(git rev-parse --short HEAD))\",\"severity\":\"warning\"}" + for i in 1 2 3 4 5 6 7 8 9 10; do + if curl -sf --max-time 10 -X POST "$api_base/api/v1/events/system" \ + -H "Content-Type: application/json" -H "X-Admin-Key: $admin_key" \ + -d "$payload" >/dev/null 2>&1; then + log "Update-Event gemeldet." + return 0 + fi + sleep 3 + done + log "Warnung: Update-Event konnte nicht gemeldet werden (API unter $api_base nicht erreichbar oder Key ungueltig)." +} + +report_update_event + log "Update abgeschlossen (${UPDATE_BRANCH} @ $(git rev-parse --short HEAD))."