Sieben Rueckmeldungen aus einer laufenden Integration. Der schwerwiegendste Punkt ist ein Fehler von mir. D2 - Predictalytics ist ausgesperrt. Bestaetigt: /releases/predictalytics/ antwortet mit 401, waehrend die API weiter "Update verfuegbar" meldet. Jede ausgelieferte Installation laeuft damit in die Wand. Ursache ist nicht der Schutz an sich, sondern dass ich ihn scharfgeschaltet habe, ohne zu pruefen, ob die Verbraucher nachgezogen sind - genau der Fall, vor dem UPGRADE §16.1 warnt. Behoben wird die Klasse des Problems, nicht nur dieser Fall: Produkte lassen sich unter UpdateService -> Zugangsschutz einzeln ausnehmen. Damit ist der gestaffelte Rollout moeglich, der bisher fehlte: ausnehmen, Build mit Schluessel ausliefern, wieder einschalten. Ausgenommene Produkte sind in der Uebersicht deutlich als AUSGENOMMEN markiert und faerben den Selbsttest nicht gruen. D5 - BuildInfo.targets verhinderte inkrementelle Builds. BuildDateUtc trug die volle Uhrzeit, aenderte sich also bei jedem Build; WriteOnlyWhenDifferent griff nie, und jedes einbindende Projekt wurde jedes Mal neu uebersetzt. Jetzt tagesgenau. Das Commit-Datum waere stabiler, laesst sich aber nicht verlaesslich holen - die Formatangabe von git log ueberlebt MSBuild und cmd.exe nicht, wie ein Fehlversuch gezeigt hat. D4 - LicenseConfig war uneinheitlich und fuer Dienste unbrauchbar. SetStorageDirectory benutzte den Pfad roh, waehrend der Weg ueber die Umgebungsvariable <slug>/license anhaengte: zwei Produkte im selben Prozess schrieben in dieselbe state.dat. Und ohne $HOME - systemd User= ohne Heimatverzeichnis - landete der Rueckfall im Installationsverzeichnis, unter /opt nicht beschreibbar. Neu: einheitliches Anhaengen und ein Rueckfall auf /var/lib/<slug>, der vorher prueft, ob dort ueberhaupt geschrieben werden kann. D1 - Woher die Anwendung den Lizenzschluessel fuer den Update-Zugang nimmt, stand nirgends zusammenhaengend. Jetzt ein Beispiel in UPDATESERVICE §5A, das TryGetCachedKey und CheckForUpdateAsync verbindet. D3 - Fuer einen laufenden systemd-Dienst gab es keinen Update-Weg. Neu: SETUP §4A mit einer oneshot-Unit, die stoppt, aktualisiert und wieder startet - ohne --restart, weil der Agent sonst an systemd vorbei einen zweiten Prozess startet. Inklusive EnvironmentFile fuer den Schluessel und dem Hinweis auf die Dateirechte nach einem Lauf als root. D6 - Die Empfehlung Environment.Exit(1) passt fuer handelnde Systeme nicht. Ein neuer Abschnitt im Lizenz-Leitfaden beschreibt den Sperrbetrieb: abschalten, was neue Verpflichtungen eingeht; weiterlaufen lassen, was bestehende abwickelt. D7 - Die Drosselungsgrenzen aller Endpunkte stehen jetzt in docs/README.md. /api/errors/v1/report erlaubt 300 pro Minute, nicht 60; die Einstellung bugtracker.error_rate fehlte in der Beispielkonfiguration. Der zweite Teil des Befunds war veraltet: docs/README.md fuehrt die Release-Anleitung bereits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
297 lines
18 KiB
JSON
297 lines
18 KiB
JSON
{
|
|
"_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.1",
|
|
|
|
"versions": [
|
|
{
|
|
"version": "2.5.1",
|
|
"date": "2026-08-14",
|
|
"summary": "Zugangsschutz je Produkt abschaltbar, Dienst-Betrieb dokumentiert, inkrementelle Builds repariert.",
|
|
"actionRequired": true,
|
|
"changes": [
|
|
{
|
|
"area": "server",
|
|
"breaking": false,
|
|
"title": "Produkte lassen sich vom Zugangsschutz ausnehmen",
|
|
"text": "Der Schutz entstand beim ersten Lauf nach dem Deployment von selbst - auch fuer Produkte, deren ausgelieferte Installationen noch ohne Lizenzschluessel bauen. Die bekamen ab dem Moment 401 statt Updates; bei Predictalytics ist genau das passiert. Unter UpdateService -> Zugangsschutz laesst sich je Produkt umschalten.",
|
|
"action": "Wer eine Installationsbasis ohne SDK 2.4 hat: Produkt ausnehmen, Build mit Schluessel ausliefern, danach wieder einschalten."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": false,
|
|
"title": "BuildInfo.targets verhindert keine inkrementellen Builds mehr",
|
|
"text": "BuildDateUtc trug die volle Uhrzeit und aenderte sich bei jedem Build; WriteOnlyWhenDifferent griff nie, und jedes einbindende Projekt wurde jedes Mal neu uebersetzt. Jetzt tagesgenau - hoechstens ein Neubau pro Tag.",
|
|
"action": "Keine. BuildInfo.BuildDateUtc enthaelt nur noch das Datum, nicht die Uhrzeit."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": true,
|
|
"title": "LicenseConfig.SetStorageDirectory haengt jetzt <slug>/license an",
|
|
"text": "Zuvor wurde der gesetzte Pfad roh benutzt, waehrend der Weg ueber DEPLOYMENTCENTER_STORAGE_DIR <slug>/license anhaengte. Zwei Produkte im selben Prozess mit gesetztem Override schrieben damit in dieselbe state.dat.",
|
|
"action": "Wer SetStorageDirectory benutzt: Der Cache liegt jetzt eine Ebene tiefer. Ein vorhandener Cache wird einmalig neu aufgebaut."
|
|
},
|
|
{
|
|
"area": "sdk",
|
|
"breaking": false,
|
|
"title": "Lizenz-Cache faellt auf /var/lib/<slug> zurueck",
|
|
"text": "Eine systemd-Unit mit User= und ohne eigenes Heimatverzeichnis hat weder XDG_CONFIG_HOME noch HOME. Der Rueckfall landete im Installationsverzeichnis, unter /opt fuer den Dienstbenutzer nicht beschreibbar - also kein Offline-Cache, ausgerechnet im kopflosen Betrieb.",
|
|
"action": "Wenn /var/lib nicht beschreibbar ist: DEPLOYMENTCENTER_STORAGE_DIR setzen."
|
|
},
|
|
{
|
|
"area": "docs",
|
|
"breaking": false,
|
|
"title": "Lueckenschluss in den Leitfaeden",
|
|
"text": "Woher die Anwendung den Lizenzschluessel fuer den Update-Zugang nimmt (TryGetCachedKey), wie ein laufender systemd-Dienst aktualisiert wird, was statt Environment.Exit(1) zu tun ist, wenn Beenden die gefaehrlichere Option waere, und die Drosselungsgrenzen aller Endpunkte.",
|
|
"action": null
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"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."
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|