Update-Event des Monitorings und PR-Flag bestehender Tickets

update.sh meldet nach dem Neustart ein Event (POST /api/v1/events/system, Admin-Key).
upsertPullRequest markiert bereits vorhandene Tickets als Pull Request, damit sie den
/pulls/-Link bekommen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
fds2610-bot 2026-10-01 12:48:10 +02:00
parent fb0baff3df
commit 65f6bc6c2e
7 changed files with 86 additions and 1 deletions

6
API.md
View file

@ -354,6 +354,12 @@ Auth: `X-Api-Key`.
Pflichtfelder: `name`, `message`, `severity`. `occurred_at` optional (Default: jetzt). Pflichtfelder: `name`, `message`, `severity`. `occurred_at` optional (Default: jetzt).
Response `201` mit dem gespeicherten Event. 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 (<Commit>)"`,
`severity: "warning"` meldet.
### `GET /api/v1/events` — Event-Liste ### `GET /api/v1/events` — Event-Liste
Query-Parameter: `limit` (Default `5`, entspricht der im Plan festgelegten Zeilenzahl des Query-Parameter: `limit` (Default `5`, entspricht der im Plan festgelegten Zeilenzahl des

View file

@ -14,6 +14,8 @@ aktualisiert.
fuehrt (per Default) die Testsuiten aus. Kann jederzeit auch manuell aufgerufen werden, siehe 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`, Kommentar am Kopf der Datei fuer die Umgebungsvariablen `UPDATE_REMOTE`, `UPDATE_BRANCH`,
`UPDATE_RUN_TESTS`. `UPDATE_RUN_TESTS`.
Nach dem Neustart meldet das Skript ein Event "Monitor: Update eingespielt (<Commit>)" 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, - **`deploy-webhook.py`** (Projekt-Wurzel) - kleiner HTTP-Server (nur Python-Standardbibliothek,
keine Abhaengigkeiten) auf Basis von `http.server`. Prueft eingehende Codeberg-Webhooks per keine Abhaengigkeiten) auf Basis von `http.server`. Prueft eingehende Codeberg-Webhooks per
HMAC-Signatur (`X-Forgejo-Signature`/`X-Gitea-Signature`, analog zum bestehenden HMAC-Signatur (`X-Forgejo-Signature`/`X-Gitea-Signature`, analog zum bestehenden

View file

@ -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('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'); $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('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'); $this->router->add('POST', '/api/v1/systems/{id}/status', fn (Request $r, array $p) => $systemController->reportStatus($r, $p), 'agent');

View file

@ -185,11 +185,21 @@ final class TicketRepository
return $this->create($data + ['column' => 'fertig', 'source' => 'codeberg', 'is_pull_request' => 1]); 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'], 'title' => $data['title'],
'description' => $data['description'] ?? null, 'description' => $data['description'] ?? null,
'color' => $data['color'] ?? 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;
} }
/** /**

View file

@ -50,6 +50,23 @@ final class ApplicationTest extends TestCase
self::assertSame(401, $response->status); 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 public function testFullFlowRegisterAgentThenCreateTicket(): void
{ {
$registerResponse = $this->app->handle(Request::create( $registerResponse = $this->app->handle(Request::create(

View file

@ -27,6 +27,16 @@ final class TicketRepositoryTest extends TestCase
self::assertSame('Neues Ticket', $ticket['title']); 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 public function testCreateRejectsUnknownColumn(): void
{ {
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);

View file

@ -97,4 +97,41 @@ fi
log "Starte Dienste neu (api, web, sync)..." log "Starte Dienste neu (api, web, sync)..."
$DOCKER_COMPOSE up -d 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))." log "Update abgeschlossen (${UPDATE_BRANCH} @ $(git rev-parse --short HEAD))."