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
@@ -0,0 +1,58 @@
using System;
namespace Deploymentcenter.Client
{
/// <summary>
/// Auskunft ueber die eingebundene SDK-Fassung.
///
/// Wozu das gut ist: Wer eine Anbindung aktualisiert, will nicht die
/// gesamte Historie lesen, sondern nur wissen, was sich seit der eigenen
/// Fassung geaendert hat. Dafuer muss die Anwendung festhalten, gegen
/// welche Fassung sie gebaut wurde - und genau das ist
/// <see cref="Version"/>.
///
/// Der Wert wandert automatisch in die Update-Pruefung und den Heartbeat,
/// sodass im Deploymentcenter sichtbar wird, welche Installation auf
/// welchem Stand haengt. Ohne diese Angabe bliebe nur Nachfragen.
///
/// Abfragen laesst sich der Unterschied so:
///
/// GET /api/updateservice/v1/changelog?since=2.2.0
///
/// Die Antwort nennt nur die neueren Fassungen, dazu die Anzahl der
/// Punkte mit Handlungsbedarf und der brechenden Aenderungen.
/// </summary>
public static class DeploymentcenterSdk
{
/// <summary>
/// Fassung dieses SDK. Bei jeder Auslieferung mitzufuehren - sie ist
/// der Bezugspunkt fuer den Changelog.
/// </summary>
public const string Version = "2.5.0";
/// <summary>
/// Adresse, unter der sich der Unterschied zur eigenen Fassung
/// abfragen laesst.
/// </summary>
public static string ChangelogUrl(string baseUrl, string? since = null)
{
string clean = (baseUrl ?? string.Empty).TrimEnd('/');
string url = clean + "/api/updateservice/v1/changelog";
string from = string.IsNullOrWhiteSpace(since) ? Version : since!;
return url + "?since=" + Uri.EscapeDataString(from);
}
/// <summary>
/// Kurzer Vermerk fuer Protokolle und Fehlerberichte. Taucht ein
/// Problem auf, ist damit sofort ersichtlich, gegen welche Fassung die
/// Anbindung gebaut wurde.
/// </summary>
public static string Describe(string? appVersion = null)
{
return string.IsNullOrWhiteSpace(appVersion)
? $"Deploymentcenter-SDK {Version}"
: $"{appVersion} (Deploymentcenter-SDK {Version})";
}
}
}
+34
View File
@@ -129,6 +129,35 @@ Reagiere auf `code`, nicht auf den Text:
- `401 unauthorized` — Token prüfen, nicht wiederholen
- `409 already_claimed` — nächstes Item nehmen
- `429 rate_limited` — Intervall verdoppeln, später erneut
### Halte fest, gegen welche Fassung du integriert hast
**Pflicht bei jeder Änderung an der Deploymentcenter-Anbindung.** Trage die
Fassung in dieser Datei ein, direkt hier:
```
Deploymentcenter-Anbindung: SDK 2.5.0, integriert am 2026-08-14
```
Der Wert steht in `DeploymentcenterSdk.Version`. Ohne ihn muss beim nächsten
Mal die gesamte Historie gelesen werden.
**Bevor du die Anbindung anfasst,** frag zuerst, was sich seitdem geändert hat:
```
GET /api/updateservice/v1/changelog?since=2.5.0
```
Die Antwort nennt nur die neueren Fassungen, dazu `action_items` (Punkte mit
Handlungsbedarf) und `breaking` (brechende Änderungen). Jeder Eintrag hat ein
Feld `action` mit dem, was konkret zu tun ist — steht dort `null`, ist nichts
zu tun.
Kommt `"count": 0` zurück, bist du auf Stand und musst gar nichts lesen.
Zum Nachschlagen für Menschen: `https://dc.mhdf.de/docs/changelog.php?since=2.5.0`
**Nach getaner Arbeit die Zeile oben auf die neue Fassung setzen.**
```
---
@@ -141,5 +170,10 @@ Melde Fehler und Ideen an https://dc.mhdf.de/api/bugtracker/v1/report
description, error_message, stack_trace, severity, file_path, line_no,
client_ref). Vor der Arbeit an einem Item: POST .../manage?action=next zum
Übernehmen. Danach ?action=resolve mit resolved_in_build.
Deploymentcenter-Anbindung: SDK 2.5.0
Bevor du sie anfasst: GET /api/updateservice/v1/changelog?since=2.5.0 —
zeigt nur, was seitdem dazukam. Danach die Zeile hochsetzen.
Vollständige Beschreibung: https://dc.mhdf.de/api/openapi.json
```
+1
View File
@@ -10,6 +10,7 @@ Monitoring und einen Bugtracker, den Coding-Agenten selbständig bedienen.
| Dokument | Wofür |
|---|---|
| **[UPGRADE.md](./UPGRADE.md)** | **Ablaufplan für die Umstellung auf 2.0.** Enthält Pflichtschritte: Zugangsdaten wechseln, Migration, Evaluator-Cron. |
| **[Änderungen](../public/docs/changelog.json)** | Was sich je Fassung geändert hat und was zu tun ist. Öffentlich unter `/docs/changelog.php`, maschinenlesbar über `GET /api/updateservice/v1/changelog?since=X` |
| **[Release-Anleitung für Agenten](../public/docs/release.md)** | Ein Projekt veröffentlichungsfähig machen: Vorlage kopieren, konfigurieren, ausliefern |
| [Agent-Prompt-Vorlage](./AGENT_PROMPT_TEMPLATE.md) | Textbaustein für `CLAUDE.md` / `AGENTS.md` eines Projekts |
| [Agenten-Handbuch](../public/docs/bugtracker.md) | Vollständige Beschreibung des Bugtracker-Workflows, öffentlich unter `/docs/` |
+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',
};
+253
View File
@@ -0,0 +1,253 @@
{
"_comment": "Einzige Quelle der Wahrheit fuer den Changelog. Wird von /docs/changelog.php gerendert und von GET /api/updateservice/v1/changelog?since=X ausgeliefert. Neue Eintraege oben einfuegen.",
"schema": 1,
"current": "2.5.0",
"versions": [
{
"version": "2.5.0",
"date": "2026-08-14",
"summary": "Geheimnisse raus aus der Kommandozeile, Packager sperrt statt zu warnen, ILicensePrompt lebt.",
"actionRequired": true,
"changes": [
{
"area": "sdk",
"breaking": false,
"title": "Lizenzschluessel wird nicht mehr als Argument uebergeben",
"text": "Der Agent nahm --license-key nur aus argv. 'ps' zeigt das jedem Benutzer der Maschine - exakt die Begruendung, mit der UPGRADE.md §5 den Crontab-Weg verwirft. Der Agent liest jetzt DC_LICENSE_KEY, DC_DOWNLOAD_USER und DC_DOWNLOAD_PASSWORD; Umgebung vor Argument. LaunchUpdateAgent setzt die Variable auf dem eigenen Prozess, das Kind erbt sie.",
"action": "Keine Aenderung noetig, wenn ihr licenseKey an LaunchUpdateAgent uebergebt. Wer den Agenten selbst aufruft, sollte von --license-key auf DC_LICENSE_KEY umstellen."
},
{
"area": "sdk",
"breaking": false,
"title": "waitTimeoutSeconds an LaunchUpdateAgent",
"text": "Der Agent kannte --wait-timeout, das SDK reichte es nicht durch - es galten fest 60 Sekunden. Laeuft die Zeit ab, bricht der Agent ab, ohne etwas zu veraendern.",
"action": "Wenn euer Herunterfahren laenger als etwa 40 Sekunden dauert: waitTimeoutSeconds heraufsetzen."
},
{
"area": "sdk",
"breaking": false,
"title": "Environment.Exit(0) ist dokumentiert, nicht geaendert",
"text": "exitCurrentApp:true beendet den Prozess hart - laufende finally-Bloecke, IHostApplicationLifetime und Destruktoren kommen nicht mehr zum Zug.",
"action": "Bei offenem Zustand (Positionen, Transaktionen, ungeschriebene Puffer) exitCurrentApp:false setzen und selbst geordnet herunterfahren. Der Agent wartet ohnehin auf das Prozessende."
},
{
"area": "sdk",
"breaking": false,
"title": "EnsureLicensedAsync - ILicensePrompt wird endlich benutzt",
"text": "Der Konstruktor nahm ILicensePrompt entgegen, legte es ab und rief es nie auf. Neu: EnsureLicensedAsync() nimmt den zwischengespeicherten Schluessel, fragt sonst nach, prueft, und fragt bei Ablehnung erneut. allowPrompt:false lehnt ohne Cache ab, statt im Dienst auf eine Eingabe zu warten, die nie kommt.",
"action": "Optional. Wer eine eigene Abfrage gebaut hat, kann sie behalten."
},
{
"area": "packager",
"breaking": true,
"title": "Bricht bei Zugangsdaten im Paket ab",
"text": "Zuvor nur eine Warnung - und die war ausgerechnet unterdrueckt, wenn die Datei auf der preserve-Liste stand. So geriet ein echter API-Schluessel in ein oeffentlich abrufbares Paket. Geprueft werden Dateiname (appsettings.Local.json, master.key, *.pfx, *.db, server_settings.xml) und Inhalt (gefuelltes Password=, sk-, ghp_, dc_master_, AKIA, private Schluessel). Platzhalter loesen nicht aus.",
"action": "Pruefen, was im Publish-Verzeichnis landet - haeufig eine CopyToOutputDirectory-Regel in der csproj. Notausgang: --allow-secrets."
},
{
"area": "sdk",
"breaking": false,
"title": "BuildInfo.targets liegt im NuGet-Paket",
"text": "Die Anleitung empfahl einen <Import> per relativem Pfad ins Nachbar-Repository - das setzt voraus, dass beide Arbeitskopien nebeneinander liegen und in derselben Fassung stehen. Das Target liegt jetzt unter build/ im Paket, NuGet importiert es selbst.",
"action": "Den <Import Project=\"..\\Deploymentcenter.Client\\Deploymentcenter.BuildInfo.targets\" /> aus der csproj entfernen und stattdessen PackageReference auf Deploymentcenter.Client 2.5.0 setzen."
},
{
"area": "sdk",
"breaking": false,
"title": "Unauthorized auch im API-Zweig",
"text": "Ein 401 wurde nur auf dem statischen Weg als Lizenzproblem erkannt; ueber die API kam er als gewoehnlicher HTTP-Fehler an.",
"action": null
},
{
"area": "tooling",
"breaking": false,
"title": "pack-and-deploy ist beziehbar, Release-Vorlage vorhanden",
"text": "Das Werkzeug wurde in der Anleitung benutzt, als laege es im PATH - beziehbar war es nirgends. Es steht jetzt unter /installer/ neben dem Agenten. Dazu eine Vorlage zum Kopieren ins eigene Projekt, die je Plattform dotnet publish und pack-and-deploy verkettet und sich das Werkzeug selbst holt.",
"action": "Empfohlen: Vorlage von /docs/release-template/ holen. Anleitung unter /docs/release.md."
}
]
},
{
"version": "2.4.0",
"date": "2026-08-13",
"summary": "Release-Ablage liegt hinter Zugangsschutz. Ohne Lizenzschluessel keine Updates mehr.",
"actionRequired": true,
"changes": [
{
"area": "server",
"breaking": true,
"title": "/releases/ verlangt Zugangsdaten",
"text": "Zuvor konnte jeder im Internet die vollstaendigen Pakete herunterladen. Zugang haben jetzt gueltige Lizenzschluessel des jeweiligen Produkts sowie die Installationskonten. Je Produkt eine eigene .htpasswd - eine gemeinsame wuerde bedeuten, dass eine Lizenz fuer Produkt A auch Produkt B oeffnet.",
"action": "PFLICHT: credentials: ReleaseCredentials.FromLicenseKey(schluessel) an CheckForUpdateAsync, und licenseKey an LaunchUpdateAgent. Ohne das bekommt die Anwendung 401 und keine Updates mehr."
},
{
"area": "server",
"breaking": true,
"title": "Benutzername ist eine Ableitung, nicht der Schluessel",
"text": "Das htpasswd-Format hasht nur die Passwortspalte. Stuende der Lizenzschluessel auch als Benutzername darin, waere die Datei eine Klartext-Kundenliste. Der Benutzername ist deshalb lic_<sha256(schluessel), 16 Hexzeichen>.",
"action": "Nur relevant, wenn ihr Basic Auth selbst baut statt ReleaseCredentials zu benutzen."
},
{
"area": "sdk",
"breaking": false,
"title": "UpdateCheckResult.Unauthorized",
"text": "Trennt 'Lizenz traegt nicht mehr' von einem Netzwerkfehler. Ohne diese Unterscheidung sieht ein abgelaufener Vertrag aus wie eine Stoerung, und man sucht an der falschen Stelle.",
"action": "Empfohlen: Unauthorized abfragen und dem Benutzer als Lizenzhinweis zeigen."
},
{
"area": "server",
"breaking": false,
"title": "Reihenfolge bei Neuprodukten",
"text": "Fuer ein Produkt ohne Release existiert /releases/<slug>/ nicht und wird uebersprungen. Das Verzeichnis entsteht mit dem ersten Upload, der naechste Abgleich schuetzt es. Es gibt also kein Fenster, um ein ungeschuetztes Release zu ziehen und danach das SDK nachzuruesten.",
"action": "Der erste ausgelieferte Build eines neuen Produkts muss die Zugangsdaten schon mitbringen."
}
]
},
{
"version": "2.3.0",
"date": "2026-08-13",
"summary": "Erstinstallation ueber den Update-Agent, setup.json, Installationskonto.",
"actionRequired": false,
"changes": [
{
"area": "agent",
"breaking": false,
"title": "update-agent --action install",
"text": "Fuehrt durch Anmeldung, Auswahl aus dem Katalog, Zielverzeichnis, Installation und Einrichtung. Die Dateien kommen ueber denselben Pfad wie ein Update - mit Pruefsumme, Signatur, Staging und Rollback.",
"action": "Optional: setup.json ins Publish-Verzeichnis legen, damit die Erstinstallation die noetigen Werte abfragen kann."
},
{
"area": "agent",
"breaking": false,
"title": "setup.json beschreibt die einzurichtenden Werte",
"text": "Gefragt wird nur, was uebrig bleibt: bereits gesetzt -> detect:... -> provision -> fragen. Ziele haben ein 'location' (install, config, data, home) und optional fileWindows/fileLinux/fileMacOS. Dateien mit geheimen Werten werden auf den eigenen Benutzer beschraenkt.",
"action": "Optional. Ohne setup.json laesst sich die Anwendung installieren, aber nicht einrichten."
},
{
"area": "server",
"breaking": false,
"title": "Rolle 'installer' und Benutzerverwaltung",
"text": "Konten dieser Rolle koennen sich ausschliesslich ueber /api/setup/v1/login anmelden und Anwendungen einrichten - nicht am WebUI. Die Zugangsdaten werden auf jedem Zielsystem eingetippt; mit einem Administratorkonto verteilte man den Zugang zur gesamten Verwaltung.",
"action": "Migration 012 einspielen. Fuer Erstinstallationen ein installer-Konto anlegen."
}
]
},
{
"version": "2.2.0",
"date": "2026-08-09",
"summary": "Plattform-Dimension, signierte Releases, Update mit Rollback, geschuetzte Konfigurationsdateien.",
"actionRequired": true,
"changes": [
{
"area": "server",
"breaking": true,
"title": "Releases tragen eine Plattform",
"text": "Zuvor gab es die Dimension nicht: win-x64 und linux-x64 landeten unter derselben Version im selben Kanal und ueberschrieben sich: ein Linux-System zog sich das Windows-Paket. Ein Client, der keine Plattform mitschickt, sieht ausschliesslich Releases mit platform=any.",
"action": "PFLICHT beim Veroeffentlichen: --platform win-x64 (o. ae.) an pack-and-deploy. Clientseitig passiert es von selbst - CheckForUpdateAsync schickt ohne Angabe die Kennung des laufenden Systems."
},
{
"area": "packager",
"breaking": true,
"title": "Version wird gegen die Hauptassembly geprueft",
"text": "Weicht --version von der einkompilierten ab, bricht der Vorgang ab. Wird 1.0.1 als 1.0.2 veroeffentlicht, aktualisieren alle Clients, melden danach weiter 1.0.1, halten das Release erneut fuer neu - eine Endlosschleife ueber die gesamte Installationsbasis.",
"action": "<Version> in die Directory.Build.props, nicht in einzelne csproj-Dateien. Notausgang: --ignore-version-mismatch."
},
{
"area": "packager",
"breaking": true,
"title": "preservePatterns schuetzt Konfigurationsdateien",
"text": "Zuvor ueberschrieb jedes Update die eingerichteten Werte des Zielsystems. Ausschluss und Schutz sind zwei verschiedene Dinge: excludePatterns haelt eine Datei aus dem Paket, preservePatterns liefert sie aus, laesst am Ziel aber die vorhandene Fassung in Ruhe. Die Muster sind jetzt echte Globs - 'logs/**' traf zuvor nie zu.",
"action": "preservePatterns in die packager.config.json aufnehmen. Pruefen, ob eure Konfigurationsdateien bisher ueberschrieben wurden."
},
{
"area": "agent",
"breaking": false,
"title": "Anwenden mit Backup und Rollback",
"text": "Die Stelle war als 'Atomic Replace with Backup' kommentiert und war eine Kopierschleife. Bricht sie ab, blieb eine halb aktualisierte Installation zurueck. Jetzt: Plan, Backup, Anwenden, bei Fehler vollstaendiger Rollback. Verwaiste Dateien werden entfernt - aber nur solche aus dem Manifest der Vorversion.",
"action": null
},
{
"area": "sdk",
"breaking": true,
"title": "LaunchUpdateAgent uebergibt Neustart und Prozesskennung",
"text": "--restart wurde nie uebergeben - die Anwendung schloss sich nach 'Jetzt installieren' und blieb zu. --wait-for-pid gab es nicht: der Agent kopierte bei langsamem Herunterfahren ueber gesperrte Dateien. ResolveAgentPath() liefert den plattformrichtigen Namen; ein fest verdrahtetes update-agent.exe wird unter Linux nie gefunden.",
"action": "UpdateClient.ResolveAgentPath() statt eines festen Pfads benutzen. currentVersion: BuildInfo.Version mitgeben - fuer Installationen ohne manifest.json."
},
{
"area": "server",
"breaking": false,
"title": "Releases werden signiert",
"text": "Der SHA256 stammt aus derselben Quelle wie das Paket. Wer den Webroot kontrolliert, tauscht beide gemeinsam aus. Der Server signiert jetzt mit RSA-SHA256, der Agent prueft gegen /api/updateservice/v1/pubkey. Bewusst asymmetrisch: bei HMAC braeuchte der pruefende Agent denselben geheimen Schluessel.",
"action": "Serverseitig security.release_private_key hinterlegen. Ohne Schluessel bleiben Releases unsigniert und installierbar."
},
{
"area": "sdk",
"breaking": true,
"title": "CheckForUpdateAsync hat einen Parameter mehr",
"text": "platform steht vor dem CancellationToken. Wer den Token positionell uebergeben hat, bekommt einen Uebersetzungsfehler - kein stilles Fehlverhalten.",
"action": "Benannte Argumente benutzen."
}
]
},
{
"version": "2.1.0",
"date": "2026-08-08",
"summary": "Korrekturen im .NET-SDK, app_version am Heartbeat.",
"actionRequired": true,
"changes": [
{
"area": "sdk",
"breaking": true,
"title": "BuildInfo.targets erzeugt im Namensraum des Projekts",
"text": "Die vorherige Fassung erzeugte fest in Deploymentcenter.Client.Models und war damit nicht einbindbar (CS0433).",
"action": "Ein etwaiges 'using Deploymentcenter.Client.Models' fuer BuildInfo entfernen."
},
{
"area": "sdk",
"breaking": false,
"title": "API-Rueckfall der Update-Pruefung liefert vollstaendige Daten",
"text": "Fehlte die latest.json, kamen ueber die API weder Download-Adresse noch Pruefsumme, Changelog oder Kritikalitaet an - nur 'version' stimmte in beiden Formaten ueberein.",
"action": "Eigene Umgehungen koennen entfallen."
},
{
"area": "sdk",
"breaking": true,
"title": "unknown_error entfaellt, cache_ttl_hours wird ausgewertet",
"text": "Der Cache-Rueckfall greift jetzt bei jedem HTTP-Fehler, nicht nur bei bestimmten. app_version ist Parameter statt fest.",
"action": "Wer unknown_error abfaengt, prueft stattdessen IsTransient. LicenseClient.DefaultAppVersion beim Start setzen."
}
]
},
{
"version": "2.0.0",
"date": "2026-08-07",
"summary": "Sicherheitsumstellung: Veroeffentlichen braucht ein Token, Versionsvergleich nach Semver.",
"actionRequired": true,
"changes": [
{
"area": "server",
"breaking": true,
"title": "Veroeffentlichen verlangt updateservice:publish",
"text": "Zuvor voellig ungeschuetzt - jeder konnte download_url und sha256_hash eines bestehenden Releases ueberschreiben und allen Clients ein beliebiges Paket unterschieben.",
"action": "Token mit dem Recht updateservice:publish erzeugen und als DC_TOKEN hinterlegen."
},
{
"area": "server",
"breaking": true,
"title": "Versionsvergleich folgt der semantischen Ordnung",
"text": "Zuvor verglich SQL lexikografisch: 1.9.0 galt als neuer als 1.10.0, und Clients bekamen ein Downgrade als Update angeboten.",
"action": null
},
{
"area": "server",
"breaking": true,
"title": "Einheitliches Antwortformat",
"text": "Alle JSON-Endpunkte antworten mit {\"status\":\"success\",...} bzw. {\"status\":\"error\",\"error\":{\"code\":...}}. Der code ist stabil und fuer Programme gedacht, die message richtet sich an Menschen.",
"action": "Auswertung der Antworten auf das neue Format umstellen."
}
]
}
]
}
+212
View File
@@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
/**
* Lesbare Ansicht des Changelogs.
*
* Quelle ist changelog.json - dieselbe Datei, die
* GET /api/updateservice/v1/changelog ausliefert. Zwei Fassungen zu pflegen
* hiesse, sie frueher oder spaeter auseinanderlaufen zu lassen.
*
* /docs/changelog.php alles
* /docs/changelog.php?since=2.2.0 nur was seitdem kam
* /docs/changelog.php?raw die rohe JSON-Datei
*/
$path = __DIR__ . '/changelog.json';
$raw = is_file($path) ? (string)file_get_contents($path) : '';
$raw = preg_replace('/^\xEF\xBB\xBF/', '', $raw) ?? $raw;
if (isset($_GET['raw'])) {
header('Content-Type: application/json; charset=utf-8');
echo $raw;
exit;
}
$decoded = json_decode($raw, true);
// Auch bei kaputter Datei ein Array - sonst greift die Ausgabe unten auf null zu.
$data = is_array($decoded) ? $decoded : [];
$versions = isset($data['versions']) && is_array($data['versions'])
? $data['versions']
: [];
$since = isset($_GET['since']) ? trim((string)$_GET['since']) : '';
/** Semantischer Vergleich - dieselbe Ordnung wie serverseitig. */
function cl_core(string $v): array
{
$v = ltrim(trim($v), 'vV');
$plus = strpos($v, '+');
if ($plus !== false) { $v = substr($v, 0, $plus); }
$dash = strpos($v, '-');
if ($dash !== false) { $v = substr($v, 0, $dash); }
$out = [];
foreach (explode('.', $v) as $part) {
$out[] = (int)preg_replace('/\D/', '', $part);
}
return $out;
}
function cl_newer(string $candidate, string $than): bool
{
$a = cl_core($candidate);
$b = cl_core($than);
for ($i = 0; $i < max(count($a), count($b)); $i++) {
$x = $a[$i] ?? 0;
$y = $b[$i] ?? 0;
if ($x !== $y) { return $x > $y; }
}
return false;
}
if ($since !== '') {
$versions = array_values(array_filter(
$versions,
static fn(array $v): bool => cl_newer((string)($v['version'] ?? '0'), $since)
));
}
$actionItems = 0;
$breaking = 0;
foreach ($versions as $v) {
foreach ($v['changes'] ?? [] as $c) {
if (!empty($c['action'])) { $actionItems++; }
if (!empty($c['breaking'])) { $breaking++; }
}
}
function e(?string $s): string
{
return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
}
$areaLabels = [
'server' => 'Server',
'sdk' => 'SDK',
'agent' => 'Update-Agent',
'packager' => 'Packager',
'tooling' => 'Werkzeuge',
'docs' => 'Dokumentation',
];
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Deploymentcenter — Änderungen</title>
<style>
:root {
--bg: #0b0f19; --card: #141b2d; --accent: #5b9dff;
--text: #e2e8f0; --muted: #94a3b8; --border: #1f2937;
--warn: #fbbf24; --break: #f87171; --ok: #4ade80;
}
* { box-sizing: border-box; }
body { margin: 0; padding: 2rem 1rem; background: var(--bg); color: var(--text);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif; line-height: 1.6; }
.wrap { max-width: 60rem; margin: 0 auto; }
h1 { margin: 0 0 .25rem; font-size: 1.75rem; }
.lead { color: var(--muted); margin: 0 0 1.5rem; }
code { font-family: "Fira Code", ui-monospace, monospace; font-size: .85em;
background: rgba(91,157,255,.12); padding: .1rem .35rem; border-radius: 3px; }
pre code { display: block; padding: .75rem; overflow-x: auto; }
.box { background: var(--card); border: 1px solid var(--border);
border-radius: 8px; padding: 1rem 1.25rem; margin-bottom: 1.25rem; }
.filter { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
input[type=text] { background: var(--bg); border: 1px solid var(--border); color: var(--text);
padding: .4rem .6rem; border-radius: 5px; font-family: inherit; }
button { background: var(--accent); border: 0; color: #041225; font-weight: 600;
padding: .45rem .9rem; border-radius: 5px; cursor: pointer; font-family: inherit; }
.ver { border-left: 3px solid var(--accent); padding-left: 1rem; margin: 2rem 0; }
.ver h2 { margin: 0; font-size: 1.3rem; }
.date { color: var(--muted); font-size: .85rem; }
.summary { margin: .4rem 0 1rem; }
.chg { border-top: 1px solid var(--border); padding: .9rem 0; }
.chg:first-of-type { border-top: 0; }
.chg h3 { margin: 0 0 .35rem; font-size: 1rem; }
.tag { display: inline-block; font-size: .7rem; font-weight: 700; letter-spacing: .03em;
padding: .1rem .45rem; border-radius: 4px; margin-right: .4rem; vertical-align: middle; }
.t-area { background: rgba(148,163,184,.18); color: var(--muted); }
.t-break { background: rgba(248,113,113,.16); color: var(--break); }
.t-act { background: rgba(251,191,36,.16); color: var(--warn); }
.action { background: rgba(251,191,36,.08); border-left: 2px solid var(--warn);
padding: .5rem .75rem; margin-top: .5rem; font-size: .92rem; }
.action strong { color: var(--warn); }
.none { color: var(--muted); font-style: italic; }
a { color: var(--accent); }
</style>
</head>
<body>
<div class="wrap">
<h1>Änderungen am Deploymentcenter</h1>
<p class="lead">
Aktuelle Fassung: <code><?= e($data['current'] ?? '?') ?></code>.
Trage ein, gegen welche Fassung deine Anbindung gebaut wurde — dann steht hier
nur, was seitdem dazugekommen ist.
</p>
<div class="box">
<form method="get" class="filter">
<label for="since">Meine Fassung:</label>
<input type="text" id="since" name="since" value="<?= e($since) ?>" placeholder="2.2.0" size="10">
<button type="submit">Unterschied zeigen</button>
<?php if ($since !== ''): ?>
<a href="changelog.php" style="margin-left:.5rem;">alles zeigen</a>
<?php endif; ?>
</form>
<p style="margin:.75rem 0 0; color:var(--muted); font-size:.9rem;">
Maschinenlesbar: <code>GET /api/updateservice/v1/changelog?since=<?= e($since !== '' ? $since : '2.2.0') ?></code>
· <a href="changelog.php?raw">rohes JSON</a>
</p>
</div>
<?php if ($versions === []): ?>
<p class="none">
<?= $since !== ''
? 'Seit ' . e($since) . ' hat sich nichts geändert.'
: 'Es ist nichts hinterlegt.' ?>
</p>
<?php else: ?>
<div class="box">
<strong><?= count($versions) ?></strong> Fassung(en)<?= $since !== '' ? ' neuer als ' . e($since) : '' ?>,
davon <strong style="color:var(--warn);"><?= $actionItems ?></strong> Punkt(e) mit Handlungsbedarf
und <strong style="color:var(--break);"><?= $breaking ?></strong> mit Bruch.
</div>
<?php foreach ($versions as $v): ?>
<div class="ver">
<h2><?= e($v['version'] ?? '?') ?>
<span class="date">— <?= e($v['date'] ?? '') ?></span>
</h2>
<p class="summary"><?= e($v['summary'] ?? '') ?></p>
<?php foreach ($v['changes'] ?? [] as $c): ?>
<div class="chg">
<h3>
<span class="tag t-area"><?= e($areaLabels[$c['area'] ?? ''] ?? ($c['area'] ?? '?')) ?></span>
<?php if (!empty($c['breaking'])): ?><span class="tag t-break">BRUCH</span><?php endif; ?>
<?php if (!empty($c['action'])): ?><span class="tag t-act">TUN</span><?php endif; ?>
<?= e($c['title'] ?? '') ?>
</h3>
<div><?= e($c['text'] ?? '') ?></div>
<?php if (!empty($c['action'])): ?>
<div class="action"><strong>Zu tun:</strong> <?= e($c['action']) ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
<p style="margin-top:2.5rem; color:var(--muted); font-size:.9rem;">
<a href="/docs/">Agenten-Handbuch</a> ·
<a href="/docs/release.md">Veröffentlichen</a> ·
<a href="/api/openapi.json">OpenAPI</a>
</p>
</div>
</body>
</html>
+13
View File
@@ -4,6 +4,19 @@
> Ergebnis: `./scripts/release.ps1 -Version 1.4.3` baut, packt, lädt hoch und
> meldet das Release beim Deploymentcenter an — für alle Zielplattformen.
> **Aktualisierst du eine bestehende Anbindung?** Dann lies nicht alles neu.
> Frag zuerst, was seit deiner Fassung dazugekommen ist:
>
> ```
> GET /api/updateservice/v1/changelog?since=2.2.0
> ```
>
> `count: 0` heißt: du bist auf Stand. Sonst nennt jeder Eintrag unter
> `action`, was konkret zu tun ist. Für Menschen: [/docs/changelog.php](./changelog.php).
> Die eigene Fassung steht in `DeploymentcenterSdk.Version` — **und gehört in
> die `AGENTS.md` deines Projekts**, sonst fängst du beim nächsten Mal wieder
> von vorn an.
Es gibt bereits ein Werkzeug, das den schwierigen Teil erledigt:
**`pack-and-deploy`**. Es berechnet Prüfsummen, erzeugt das Dateimanifest,
schreibt die `latest.json` fort und meldet das Release über die API an. **Baue