feat(docs): Changelog mit "was ist seit meiner Fassung neu"

Bisher musste ein Agent, der eine Anbindung aktualisiert, die gesamte Historie
lesen - oder er las gar nichts und uebersah eine brechende Aenderung. Beides
schlecht.

- public/docs/changelog.json ist die einzige Quelle. Je Fassung eine
  Zusammenfassung, je Aenderung Bereich, ein "breaking"-Kennzeichen und vor
  allem ein Feld "action" mit dem, was konkret zu tun ist. Steht dort null,
  ist nichts zu tun - das ist die haeufigste und nuetzlichste Antwort.
- GET /api/updateservice/v1/changelog?since=2.2.0 liefert nur die neueren
  Fassungen, dazu die Anzahl der Punkte mit Handlungsbedarf und der
  brechenden Aenderungen. count:0 heisst "du bist auf Stand" - dann muss gar
  nichts gelesen werden. Optional nach Bereich filterbar (?area=packager).
- /docs/changelog.php rendert dieselbe Datei fuer Menschen, mit Eingabefeld
  fuer die eigene Fassung. Bewusst dieselbe Quelle: zwei Fassungen zu pflegen
  hiesse, sie auseinanderlaufen zu lassen.
- DeploymentcenterSdk.Version im SDK ist der Bezugspunkt. Damit muss die
  Fassung nicht abgetippt werden.
- AGENT_PROMPT_TEMPLATE.md verpflichtet dazu, sie in der AGENTS.md des
  Projekts festzuhalten und vor jeder Aenderung an der Anbindung den
  Unterschied abzufragen. Auch in der Kurzfassung fuer knappe Prompt-Budgets.

Die Historie ist rueckwirkend bis 2.0.0 gefuellt: 6 Fassungen, 25 Punkte mit
Handlungsbedarf, 13 brechende Aenderungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-08-14 16:45:50 +02:00
co-authored by Claude Opus 5
parent 1967b49ad7
commit a8b9f6f7c9
7 changed files with 657 additions and 1 deletions
+86 -1
View File
@@ -108,6 +108,90 @@ switch ($action) {
);
Http::ok(['count' => count($releases), 'releases' => $releases]);
case 'changelog':
// Was hat sich seit einer bestimmten Fassung geaendert?
//
// Damit muss ein Agent, der eine Anbindung aktualisiert, nicht die
// gesamte Historie lesen. Er merkt sich die Fassung, gegen die er
// integriert hat, und fragt spaeter nur nach dem Unterschied.
$changelogPath = dirname(__DIR__, 3) . '/docs/changelog.json';
if (!is_file($changelogPath)) {
Http::fail(404, 'no_changelog', 'Es ist kein Changelog hinterlegt.');
}
$raw = (string)file_get_contents($changelogPath);
// Windows-Werkzeuge stellen gern ein BOM voran; json_decode scheitert daran.
$raw = preg_replace('/^\xEF\xBB\xBF/', '', $raw) ?? $raw;
$changelog = json_decode($raw, true);
if (!is_array($changelog) || !isset($changelog['versions']) || !is_array($changelog['versions'])) {
Http::fail(500, 'invalid_changelog', 'Der hinterlegte Changelog ist nicht lesbar.');
}
$since = Http::str('since');
$area = Http::str('area');
$entries = [];
$actionItems = 0;
$breaking = 0;
foreach ($changelog['versions'] as $entry) {
if (!is_array($entry) || !isset($entry['version'])) {
continue;
}
// Nur echt neuere Fassungen. Wer auf 2.1.0 sitzt, will nicht
// wieder ueber 2.1.0 lesen.
if ($since !== null && $since !== ''
&& !Version::isNewer((string)$entry['version'], $since)) {
continue;
}
if ($area !== null && $area !== '') {
$entry['changes'] = array_values(array_filter(
$entry['changes'] ?? [],
static fn(array $c): bool => ($c['area'] ?? '') === $area
));
if ($entry['changes'] === []) {
continue;
}
}
foreach ($entry['changes'] ?? [] as $change) {
if (!empty($change['action'])) { $actionItems++; }
if (!empty($change['breaking'])) { $breaking++; }
}
$entries[] = $entry;
}
// Absteigend: das Neueste zuerst.
usort($entries, static fn(array $a, array $b): int
=> Version::compare((string)$b['version'], (string)$a['version']));
Http::ok([
'current' => $changelog['current'] ?? null,
'since' => $since,
'count' => count($entries),
'action_items' => $actionItems,
'breaking' => $breaking,
'versions' => $entries,
'message' => $entries === []
? ($since !== null && $since !== ''
? sprintf('Seit %s hat sich nichts geaendert.', $since)
: 'Es ist nichts hinterlegt.')
: sprintf(
'%d Fassung(en) neuer als %s, davon %d mit Handlungsbedarf und %d mit Bruch.',
count($entries),
$since !== null && $since !== '' ? $since : 'Anbeginn',
$actionItems,
$breaking
),
]);
case 'pubkey':
// Oeffentlicher Schluessel zum Pruefen der Release-Signaturen.
// Bewusst ohne Token: er ist oeffentlich, und der Agent braucht ihn,
@@ -393,7 +477,8 @@ function resolveUpdateAction(): string
$last = strtolower((string)end($segments));
return match ($last) {
'check', 'latest', 'releases', 'pubkey', 'publish', 'update', 'edit', 'delete' => $last === 'edit' ? 'update' : $last,
'check', 'latest', 'releases', 'pubkey', 'changelog', 'publish', 'update', 'edit', 'delete'
=> $last === 'edit' ? 'update' : $last,
'publish_release' => 'publish',
default => 'check',
};