Files
Deploymentcenter/docs/UPDATESERVICE_INTEGRATION_GUIDE.md
T
Deploymentcenter BotandClaude Opus 5 2388b5abe1 feat(updateservice): Plattform-Dimension, signierte Releases, Update mit Rollback
Behebt eine Reihe zusammenhaengender Fehler im Update-Weg, die zusammen
verhindert haben, fuer mehr als eine Plattform auszuliefern - und die im
Fehlerfall halb aktualisierte Installationen hinterliessen.

Server
- Migration 009: Spalte platform samt neuem Unique-Key. Zuvor verdraengte das
  zuletzt veroeffentlichte Paket alle anderen Plattformen derselben Version,
  weil ON DUPLICATE KEY auf (slug, version, channel) griff. Ein Linux-System
  zog sich damit das Windows-Paket.
- Aufloesungsregel: je Version das plattformgenaue Paket, sonst das
  plattformunabhaengige. Ein Client ohne Plattformangabe sieht ausschliesslich
  'any' - lieber kein Update als das falsche.
- manifest_json wird endlich befuellt; die Spalte blieb bisher immer leer,
  wodurch die API nie der Rueckfall sein konnte, als der sie gedacht war.
- Releases werden serverseitig mit RSA-SHA256 signiert, neuer Endpunkt
  /api/updateservice/v1/pubkey. Bewusst kein HMAC: der Pruefende laeuft auf
  fremden Systemen und darf den Signierschluessel nicht besitzen.

Packager
- Bricht ab, statt die Versionshistorie zu verlieren. Schlug das Lesen der
  bestehenden latest.json fehl, ersetzte ein leeres catch die komplette
  Historie durch einen einzigen Eintrag - ohne jede Meldung.
- Echte Glob-Muster. Zuvor trafen "logs/**" und "scratch/**" aus der
  mitgelieferten Beispielkonfiguration nie zu.
- preservePatterns: Konfigurationsvorlagen werden ausgeliefert, ersetzen am
  Ziel aber keine vorhandene Datei. Eine settings.json mit Zugangsdaten
  ueberschrieb bisher beim Update die Konfiguration jedes Zielsystems.
- Warnt vor Dateien, die nach Zugangsdaten aussehen und auf keiner Liste stehen.
- Prueft --version gegen die Hauptassembly. Eine Abweichung fuehrte zu einer
  Endlosschleife: Clients aktualisieren, melden weiter die alte Version,
  halten das Release erneut fuer neu.
- --platform mit Ableitung aus dem Publish-Pfad.

Agent
- Anwenden mit Plan, Backup und vollstaendigem Rollback. Die Stelle war als
  "Atomic Replace with Backup" kommentiert und war eine Kopierschleife.
- Verwaiste Dateien werden entfernt, aber nur solche aus dem Manifest der
  Vorversion. Was nicht aus einem Release stammt, bleibt liegen.
- Das laufende Agent-Binary wird zur Seite gelegt statt ueberschrieben.
- API-Rueckfall in FetchManifestAsync; bisher nur im SDK vorhanden, weshalb
  die Anwendung "Update verfuegbar" und der Agent "kein Release" sagen konnte.
- Installierte Version aus --current-version oder manifest.json statt des
  Textes "Unbekannt", der als 0 gelesen wurde und jede Version neuer erscheinen
  liess. Reparatur funktioniert damit auch ohne manifest.json.
- Setzt das Ausfuehrungsbit fuer Linux-Pakete, die unter Windows gebaut wurden.

SDK
- ResolveAgentPath() liefert den plattformrichtigen Namen; ein fest verdrahtetes
  "update-agent.exe" wird unter Linux nie gefunden.
- LaunchUpdateAgent uebergibt jetzt --restart (wurde nie uebergeben, die
  Anwendung blieb nach dem Update zu), --wait-for-pid (kein Wettlauf mehr mit
  dem Herunterfahren) und --platform.

Enthaelt ausserdem die bislang nicht committete Arbeit an Watchdog, Lizenz-
Client und cli/tick.php samt Migration 008; die betroffenen Dateien liessen
sich nicht getrennt stagen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 19:56:35 +02:00

25 KiB

Deploymentcenter — UpdateService Integration & Deployment Guide

⚠️ Geändert in Version 2.0 — Das Veröffentlichen eines Releases läuft jetzt über POST /api/updateservice/v1/publish und verlangt ein Token mit dem Scope updateservice:publish (zuvor völlig ungeschützt). Der Versionsvergleich folgt jetzt der semantischen Versionsordnung, 1.10.0 gilt also korrekt als neuer als 1.9.0. Umstellungsschritte: UPGRADE.md.

⚠️ Geändert in Version 2.1

  • Deploymentcenter.BuildInfo.targets erzeugt die Klasse jetzt im Namensraum des einbindenden Projekts. Die vorherige Fassung war nicht einbindbar (CS0433), siehe §2B.
  • Der API-Rückfall in CheckForUpdateAsync liest die Antwort jetzt korrekt. Fehlte die latest.json, kamen zuvor weder Download-Adresse noch Prüfsumme, Changelog oder Kritikalität an, siehe §2C.

⚠️ Geändert in Version 2.2 — bitte vollständig lesen, bevor das nächste Release gebaut wird. Umstellungsschritte: UPGRADE.md §15.

  • Releases tragen eine Plattform. Ohne sie überschrieben sich win-x64 und linux-x64 gegenseitig, siehe §1A.
  • Konfigurationsdateien überleben ein Update. Bisher überschrieb jedes Update die eingerichteten Werte des Zielsystems, siehe §3A.
  • Anwenden mit Rollback. Ein Abbruch hinterlässt keine halbe Installation mehr, und entfernte Dateien werden aufgeräumt, siehe §4B.
  • Releases sind signiert, siehe §6.
  • LaunchUpdateAgent übergibt jetzt Neustart, Prozesskennung und Plattform, siehe §2A.

Das UpdateService-Modul des Deploymentcenters bietet ein unternehmensweites, leichtgewichtiges Update-, Rollback- und Reparatur-Schema auf Basis eines LEMP-Stacks (Nginx Static Files + PHP API).


1. Übersicht & Architektur

  • Kein dauerhafter Background-Dienst: Hauptanwendungen prüfen beim Start einmalig schnell und netzwerktolerant auf verfügbare Updates und Dateiintegrität.
  • Entkoppelte Ausführung: Bei Handlungsbedarf beendet sich die Hauptanwendung sauber und übergibt die Kontrolle an den eigenständigen Console Agent (update-agent.exe / update-agent).
  • 3-Kanal-System: Kanäle prod (Produktiv), beta (Vorab-Test), dev (Entwicklung).
  • Plattform-Dimension: je Kanal getrennte Pakete für win-x64, linux-x64 usw.
  • Statische LEMP-Verteilung: Downloads und Versionen-Manifeste (latest.json, manifest.json, package.tar.gz) werden über Nginx extrem performant bereitgestellt.

1A. Plattformen

Ein Release wird durch vier Angaben bestimmt: Projekt, Kanal, Version und Plattform. Die Plattform ist eine .NET-Laufzeitkennung (win-x64, linux-x64, linux-musl-arm64, osx-arm64, …) oder any für Pakete, die überall laufen.

Zuvor gab es diese Dimension nicht. Wer für zwei Plattformen baute, veröffentlichte beide Pakete unter derselben Version im selben Kanal — das zweite überschrieb das erste, und ein Linux-System zog sich das Windows-Paket. Behelfe waren getrennte Projekt-Slugs (myapp-win, myapp-linux) oder zweckentfremdete Kanäle; beides trug nicht weit.

Auswahlregel

Client schickt bekommt
platform=win-x64 Pakete mit win-x64, ersatzweise any
platform=linux-x64 Pakete mit linux-x64, ersatzweise any
nichts ausschließlich any

Der letzte Fall ist Absicht. Ein Client, der seine Kennung nicht nennt, soll lieber kein Update bekommen als das Paket einer fremden Plattform. Alle vor Version 2.2 veröffentlichten Releases stehen auf any und bleiben damit für bereits ausgelieferte Anwendungen erreichbar.

Je Version gewinnt das plattformgenaue Paket; gibt es keines, wird das plattformunabhängige genommen.

Ablage

Plattformunabhängige Releases behalten den bisherigen Pfad, alle anderen bekommen eine Zwischenebene:

/releases/myapp/prod/1.4.3/package.tar.gz              ← platform = any
/releases/myapp/prod/win-x64/1.4.3/package.tar.gz      ← platform = win-x64
/releases/myapp/prod/linux-x64/1.4.3/package.tar.gz    ← platform = linux-x64

Ohne diese Rücksicht wären alle bestehenden Installationen von einem Tag auf den anderen von ihren Updates abgeschnitten gewesen.


2. Integration in .NET Client-Anwendungen

A. Referenz auf Deploymentcenter.Client

Binde das Projekt oder Paket Deploymentcenter.Client in deine Anwendung ein.

using Deploymentcenter.Client;
using Deploymentcenter.Client.Models;

// Early Start Hook in Program.cs (oder App.xaml.cs)
var updateClient = new UpdateClient();
var checkResult = await updateClient.CheckForUpdateAsync(
    baseUrl: "https://dc.mhdf.de",
    projectId: "myapp",
    currentVersion: BuildInfo.Version,
    channel: "prod"
    // platform: entfällt - ohne Angabe die Kennung des laufenden Systems
);

if (checkResult.UpdateAvailable)
{
    Console.WriteLine($"[UPDATE] Neues Release v{checkResult.LatestRelease.Version} verfügbar!");

    var agentPath = UpdateClient.ResolveAgentPath();
    if (agentPath == null)
    {
        Console.WriteLine("[UPDATE] Kein update-agent gefunden.");
        return;
    }

    UpdateClient.LaunchUpdateAgent(
        agentPath:      agentPath,
        projectId:      "myapp",
        channel:        "prod",
        action:         "update",
        version:        "latest",
        currentVersion: BuildInfo.Version,
        exitCurrentApp: true
    );
}

Nicht mehr "update-agent.exe" fest verdrahten. ResolveAgentPath() liefert den plattformrichtigen Namen — unter Linux und macOS trägt das Binary keine Endung, ein fester .exe-Name wird dort nie gefunden, und die Anwendung meldet stumm „kein Agent vorhanden".

Drei Dinge erledigt LaunchUpdateAgent seit 2.2 von selbst:

Was Warum
--restart mit dem Pfad der eigenen Anwendung Zuvor wurde der Parameter nie übergeben. Der Agent unterstützte ihn, bekam ihn aber nie zu sehen — die Anwendung schloss sich und blieb zu
--wait-for-pid mit der eigenen Prozesskennung Zuvor wurde der Agent gestartet und sofort Environment.Exit(0) gerufen. Bei langsamem Herunterfahren (Kestrel, EF, Log-Flush) kopierte er über noch gesperrte Dateien
--platform mit der Kennung des Systems Verhindert, dass ein Paket der falschen Plattform gezogen wird

Abschaltbar über restartPath: "" bzw. waitForCurrentProcess: false, wenn ein Dienst-Manager den Neustart übernimmt.

currentVersion ist wichtig für Installationen ohne manifest.json — siehe §4A.

B. MSBuild BuildInfo Generierung

Binde das Deploymentcenter.BuildInfo.targets Script in deine .csproj ein, damit Version, UTC-Build-Datum und Git Commit-Hash automatisch zur Übersetzungszeit generiert werden:

<PropertyGroup>
  <Version>1.4.3</Version>
</PropertyGroup>

<Import Project="..\Deploymentcenter.Client\Deploymentcenter.BuildInfo.targets" />

Die Klasse entsteht im Namensraum deines Projekts ($(RootNamespace)), nicht im SDK. Du erreichst sie also ohne using:

Console.WriteLine(BuildInfo.Version);   // "1.4.3" aus <Version>
Console.WriteLine(BuildInfo.Summary);   // v1.4.3 (5f9b0c5) built on ... [prod]

Verfügbare Werte: Version, GitCommit, GitCommitShort, BuildDateUtc, Channel, Summary. Ohne Git-Arbeitskopie stehen die Commit-Felder auf UNKNOWN, der Build läuft trotzdem durch.

Überschreibbare MSBuild-Eigenschaften:

Eigenschaft Wirkung
DeploymentcenterBuildInfoNamespace Zielnamensraum (Vorgabe: $(RootNamespace))
DeploymentcenterBuildInfoClass Klassenname (Vorgabe: BuildInfo)
BuildChannel prod, beta, dev (Vorgabe: prod)
GenerateDeploymentcenterBuildInfo auf false setzen, um die Erzeugung abzuschalten

Nicht auf Deploymentcenter.Client.Models zeigen lassen. Dort liefert das SDK bereits eine gleichnamige Klasse aus; partial verbindet Teilklassen nur innerhalb derselben Assembly. Genau daran scheiterte die vorherige Fassung des Targets: sie erzeugte fest in diesen Namensraum, wodurch im Consumer ein zweiter Typ mit demselben vollen Namen entstand (CS0433) und der generierte statische Konstruktor Eigenschaften setzte, die es dort nicht gab (CS0103). Ein Import war damit unmöglich.

C. Zwei Quellen, zwei Formate

CheckForUpdateAsync fragt zuerst die statische /releases/{projectId}/{channel}/latest.json ab und fällt auf /api/updateservice/v1/check zurück. Die beiden liefern unterschiedliche Feldnamen:

Bedeutung latest.json (Packager) API-Antwort (Datenbank)
Download-Adresse packageUrl download_url
Prüfsumme sha256 sha256_hash
Änderungshinweise changelog release_notes
Größe sizeBytes size_bytes
Kritisch isCritical is_critical auf oberster Ebene

Das SDK bildet beide ab: VersionInfo für die latest.json, ApiReleaseInfo für die API-Antwort. UpdateCheckResult.LatestRelease ist in beiden Fällen ein VersionInfo — für die API wird übersetzt.

Wer die API selbst anspricht, muss das berücksichtigen. Die vorherige SDK-Fassung deserialisierte die API-Antwort direkt nach VersionInfo; von beiden Formaten stimmt nur version überein. Über den API-Weg kam deshalb nichts weiter an — und IsCritical wurde aus dem Release-Objekt statt vom Wurzelfeld gelesen und war damit immer false. Da dieser Zweig genau der Rückfall ist, wenn die latest.json fehlt, degradierte die Update-Prüfung still.


3. Packaging & Deployment CLI (pack-and-deploy)

Das Packaging-Tool verpackt den dotnet publish-Output, berechnet Hashes, erzeugt das manifest.json und lädt alles per FTP auf den LEMP-Server.

Aufruf-Beispiel:

pack-and-deploy --project myapp --version 1.4.0 --channel prod \
                --platform win-x64 \
                --publish-dir ./bin/Release/net8.0/win-x64/publish \
                --changelog "Fehlerbehebungen und Performance-Optimierung"

Ohne --platform versucht der Packager, die Kennung aus dem Publish-Pfad zu lesen (.../net8.0/linux-x64/publishlinux-x64). Gelingt das nicht, gilt das Release als plattformunabhängig und es erscheint eine Warnung.

Konfiguration (packager.config.json)

Diese Datei enthält Zugangsdaten und ist per .gitignore von der Versionskontrolle ausgeschlossen. Vorlage: packager.config.example.json. In der vorherigen Fassung standen die echten FTP-Zugangsdaten sowohl hier in der Anleitung als auch als Standardwerte im Quelltext von Program.cs.

{
  "ftpHost": "ftp.example.com",
  "ftpPort": 21,
  "ftpUser": "ftp-user",
  "ftpPass": "ftp-password",
  "ftpRemoteBaseDir": "/public_html/releases",
  "apiBaseUrl": "https://dc.mhdf.de",
  "apiToken": "dc_sub_...",
  "excludePatterns": [
    "*.pdb",
    "*.xml",
    "appsettings.Development.json",
    "*.log",
    "logs/**"
  ],
  "preservePatterns": [
    "appsettings.json",
    "settings.json",
    ".env"
  ]
}

apiToken braucht das Recht updateservice:publish. Ohne Token baut und lädt der Packager das Paket zwar hoch, meldet es aber nicht beim Deploymentcenter an und beendet sich mit Rückgabewert 2. Ohne Registrierung entsteht auch keine Signatur.

Alternative: Umgebungsvariablen

Für CI-Läufe, in denen keine Datei abgelegt werden soll — sie haben Vorrang vor der Konfigurationsdatei:

export DC_FTP_HOST=ftp.example.com
export DC_FTP_USER=ftp-user
export DC_FTP_PASS='...'
export DC_TOKEN='dc_sub_...'

pack-and-deploy --project myapp --version 1.4.0 --channel prod \
                --platform linux-x64 \
                --publish-dir ./bin/Release/net8.0/linux-x64/publish

Rückgabewerte

Wert Bedeutung
0 Paket gebaut, hochgeladen und im Deploymentcenter registriert
1 Konfiguration unvollständig, Publish-Verzeichnis fehlt oder Versionskonflikt — nichts wurde ausgeführt
2 Teilweise fehlgeschlagen: FTP-Upload oder Registrierung ging schief

Zuvor lieferte das Werkzeug in allen Fällen 0 und meldete „successfully published", selbst wenn FTP-Upload und API-Aufruf beide fehlgeschlagen waren.


3A. Ausschließen oder schützen

Das sind zwei verschiedene Dinge, und die Unterscheidung ist der Grund, warum Updates bisher Konfigurationen zerstört haben.

excludePatterns preservePatterns
Im Paket? nein ja
Bei der Erstinstallation? fehlt wird geschrieben
Beim Update? vorhandene Datei bleibt unangetastet
Wofür Build-Artefakte, Logs, Entwicklungs-Einstellungen Konfigurationsvorlagen

Eine appsettings.json gehört ins Paket — sonst ist eine Erstinstallation unvollständig. Sie darf beim Update nur nicht über die eingerichteten Werte des Zielsystems geschrieben werden. Genau dafür ist preservePatterns da; die Liste wandert ins manifest.json und wird vom Agenten ausgewertet.

Vorher: IsExcluded verstand ausschließlich *.endung und exakte Namen. Die mitgelieferte Beispielkonfiguration enthielt logs/** und scratch/** — beides traf nie zu. Und eine settings.json mit Datenbankpasswort und DC-Token stand auf keiner der beiden Listen: sie wurde mitgeliefert und überschrieb beim Update die Konfiguration jedes Zielsystems.

Die Muster sind jetzt echte Globs:

Muster trifft
*.pdb jede .pdb in jedem Unterverzeichnis
logs/** alles unterhalb von logs/
wwwroot/*.css nur direkt in wwwroot/, nicht darunter
wwwroot/**/*.css auch in Unterverzeichnissen
appsettings*.json appsettings.json, appsettings.Production.json, …

Der Packager warnt zusätzlich von sich aus, wenn eine Datei nach Zugangsdaten aussieht und auf keiner der beiden Listen steht.

Versionsgegenprobe

Der Packager liest die Version aus der Hauptassembly und bricht bei einer Abweichung zu --version ab:

[FEHLER] Versionskonflikt:
          --version sagt   : 1.0.2
          MyApp.dll sagt   : 1.0.1

Der Grund dafür ist unangenehm genug, um dafür abzubrechen: Wird 1.0.1 als 1.0.2 veröffentlicht, aktualisieren alle Clients, melden danach weiterhin 1.0.1, halten das Release erneut für neu — und aktualisieren bei jedem Start wieder. Eine Endlosschleife über die gesamte Installationsbasis.

Üblicher Auslöser: <Version> steht nur in einem der beteiligten Projekte. Der Wert gehört in die Directory.Build.props. Notausgang für bewusste Abweichungen: --ignore-version-mismatch. Lässt sich die Assembly nicht bestimmen, wird nur gewarnt — --main-assembly gibt sie gezielt an.


4. Standalone UpdateAgent (update-agent)

Der update-agent kann sowohl interaktiv (Spectre.Console Terminal UI) als auch im Headless CLI-Modus betrieben werden.

CLI Modus Befehle:

# Nach Updates suchen
update-agent --project myapp --channel prod --action check

# Auf neueste Version aktualisieren
update-agent --project myapp --channel prod --action update --version latest --target-dir /opt/myapp

# Rollback auf ältere Version
update-agent --project myapp --channel prod --action update --version 1.3.2

# Integritäts-Reparatur der aktuellen Installation
update-agent --project myapp --channel prod --action repair

# Alle verfügbaren Versionen auflisten
update-agent --project myapp --channel prod --action list

Zusätzliche Parameter seit 2.2:

Parameter Wirkung
--platform <rid> Laufzeitkennung; Vorgabe ist die des laufenden Systems
--current-version <ver> Installierte Version, wenn keine manifest.json vorliegt
--wait-for-pid <pid> Vor dem Anwenden auf das Ende dieses Prozesses warten
--wait-timeout <sek> Geduld dabei (Vorgabe 60). Läuft der Prozess danach noch, wird nichts verändert
--pubkey <datei> Öffentlicher Schlüssel zur Signaturprüfung
--require-signature Ohne gültige Signatur nicht installieren

4A. Welche Version ist installiert?

Es gab zwei Antworten darauf, und sie widersprachen sich: Die Anwendung verglich BuildInfo.Version (einkompiliert), der Agent las manifest.json im Zielverzeichnis. Fehlte diese Datei — etwa bei einer von Hand aufgesetzten Installation — meldete der Agent „Unbekannt" und hielt jede Version für neuer. Die Reparatur suchte dann auf dem Server nach einer Version namens „Unbekannt" und brach genau dann ab, wenn man sie braucht.

Die Reihenfolge ist jetzt:

  1. --current-version, falls übergeben — die Anwendung kennt ihre eigene Version am sichersten
  2. manifest.json im Zielverzeichnis
  3. sonst 0.0.0, und die Reparatur greift auf latest zurück

Deshalb sollte LaunchUpdateAgent immer currentVersion: BuildInfo.Version mitgeben.


4B. Wie ein Update angewendet wird

Die Stelle war als „Atomic Replace with Backup" kommentiert und war tatsächlich eine Kopierschleife: kein Backup, kein Rollback, kein Aufräumen. Brach sie in der Mitte ab — gesperrte Datei, volle Platte —, blieb eine halb aktualisierte Installation zurück, aus der kein Weg zurückführte.

Der Ablauf ist jetzt:

  1. Plan bilden. Welche Dateien werden geschrieben, welche sind geschützt, welche gehören nicht mehr zum Release?
  2. Sichern. Jede Datei, die überschrieben oder entfernt wird, wandert vorher nach .dc-update-backup/.
  3. Anwenden. Schreiben, dann verwaiste Dateien entfernen.
  4. Bei einem Fehler: vollständiger Rollback aus dem Backup, danach wird die Ursache gemeldet. Die Installation bleibt auf dem alten Stand lauffähig.
  5. Bei Erfolg: Backup löschen, leer gewordene Verzeichnisse entfernen.

Verwaiste Dateien

Eine DLL, die es im neuen Release nicht mehr gibt, blieb bisher für immer im Verzeichnis liegen — bei .NET ein realer Weg in kaputte Assembly-Auflösung. Sie wird jetzt entfernt, aber nur, wenn sie in der manifest.json der Vorversion stand. Ohne dieses Wissen wird nichts gelöscht; Dateien, die nicht aus einem Release stammen, bleiben in jedem Fall unangetastet.

Der Agent im Paket

--target-dir zeigt in der Vorgabe auf das Verzeichnis des Agenten selbst. Liegt der Agent im Paket, kopierte er sich also unter laufendem Betrieb über sich selbst — unter Windows eine Zugriffsverletzung mitten im Update.

Eine laufende ausführbare Datei lässt sich unter Windows nicht überschreiben, aber umbenennen. Der Agent legt sich deshalb als update-agent.exe.dc-old zur Seite, schreibt die neue Fassung und entfernt den Rest beim nächsten Start.

Ausführungsrechte

Wird unter Windows für linux-x64 gebaut, kennt das tar-Archiv keine Unix-Rechte und alles landet als 644 — die Anwendung ließe sich auf dem Zielsystem nicht starten. Der Agent setzt das Ausführungsbit beim Anwenden für Dateien ohne Endung (der .NET-Apphost) und für *.sh.


4C. Prüf-Endpunkte (für eigene Anbindungen)

Die Lese-Endpunkte sind bewusst ohne Token erreichbar, damit ausgelieferte Anwendungen ohne Anpassung weiter nach Updates suchen können. Sie liefern nur Release-Metadaten, die über die Download-URL ohnehin öffentlich sind.

GET /api/updateservice/v1/check?product=myapp&version=1.4.2&channel=prod&platform=win-x64
{
  "status": "success",
  "update_available": true,
  "current_version": "1.4.2",
  "latest_version": "1.4.3",
  "platform": "win-x64",
  "is_critical": false,
  "latest_release": {
    "version": "1.4.3",
    "platform": "win-x64",
    "download_url": "https://dc.mhdf.de/releases/myapp/prod/win-x64/1.4.3/package.tar.gz",
    "sha256_hash": "e3b0c442...",
    "git_commit": "a21536f",
    "size_bytes": 8412160,
    "release_notes": "Behebt den Login-Fehler.",
    "manifest_signature": "hsuQVhef...",
    "is_critical": 0
  }
}

Ohne platform werden ausschließlich Releases mit platform=any berücksichtigt. Wer die Endpunkte selbst anspricht und für mehrere Plattformen ausliefert, muss den Parameter mitschicken.

Weitere Endpunkte:

Aufruf Zweck
GET .../latest?product=myapp&channel=prod&platform=win-x64 Höchstes Release, unabhängig von der Client-Version
GET .../releases?product=myapp Alle Releases, nach Versionsordnung sortiert. platform filtert hier exakt — der Endpunkt listet den Bestand, er wählt kein Paket aus
GET .../pubkey Öffentlicher Schlüssel zur Signaturprüfung

Versionsvergleich

Der Vergleich folgt der semantischen Versionsordnung. Konkret bedeutet das:

Installiert Verfügbar Update?
1.9.0 1.10.0 ja — zweistellige Minor ist höher
1.10.0 1.9.0 nein
1.0.0-rc.1 1.0.0 ja — Release schlägt Vorabversion
1.0.0 1.0.0-rc.1 nein
v1.4.2 v1.4.3 ja — führendes v wird ignoriert

Zuvor verglich der Server lexikografisch. 1.9.0 galt dadurch als neuer als 1.10.0, und Clients bekamen ein Downgrade als Update angeboten. Derselbe Fehler steckte im .NET-Client bei v-präfigierten Versionen und ist dort ebenfalls behoben.

Verknüpfung mit dem Bugtracker

Beim Veröffentlichen schließen sich alle Bugtracker-Items, deren resolved_in_build der veröffentlichten Version entspricht, automatisch. Die Antwort nennt die Anzahl:

{ "status": "success", "release_id": 12, "created": true, "auto_resolved": 3,
  "message": "Release 1.4.3 (prod) für \"myapp\" veröffentlicht. 3 Bugtracker-Item(s) automatisch geschlossen." }

Damit schließt sich der Kreis: Ein Agent markiert einen Bug als „gelöst in v1.4.3", der Packager veröffentlicht v1.4.3, und das Item schließt sich selbst.


5. LEMP Verzeichnisstruktur auf dem Server

/var/www/releases/ (oder /public_html/releases/)
└── {ProjectId}/                        # z.B. myapp, polytrader
    ├── prod/
    │   ├── latest.json                 # nur platform = any
    │   ├── 1.4.0/                      # nur platform = any
    │   │   ├── package.tar.gz
    │   │   ├── package.tar.gz.sha256
    │   │   └── manifest.json           # Einzeldateien, Hashes, preserve-Liste
    │   ├── win-x64/
    │   │   ├── latest.json             # eigene Historie je Plattform
    │   │   └── 1.4.0/
    │   │       ├── package.tar.gz
    │   │       ├── package.tar.gz.sha256
    │   │       └── manifest.json
    │   └── linux-x64/
    │       ├── latest.json
    │       └── 1.4.0/
    ├── beta/
    └── dev/

Jede Plattform führt ihre eigene latest.json. Der Agent fragt zuerst den plattformspezifischen Pfad ab und fällt auf den plattformlosen zurück — Pakete einer fremden Plattform werden dabei verworfen.

Aufbewahrung

latest.json führt die letzten 15 Versionen. Ältere Versionsverzeichnisse bleiben auf dem Server liegen, sind über den Agenten aber nicht mehr auswählbar. Der Packager weist beim Herausfallen einer Version ausdrücklich darauf hin; wer weiter zurück muss, holt das Paket von Hand.


6. Signatur der Releases

Der SHA256 eines Pakets stammt aus derselben Quelle wie das Paket selbst. Wer den Webroot oder die FTP-Zugangsdaten kontrolliert, tauscht beide gemeinsam aus — der Hash schützt dann gegen Übertragungsfehler, nicht gegen Manipulation. Ausgerechnet auf dem Pfad, der fremden Code ausführt.

Warum kein HMAC

Beim Lizenzmodul wird mit HMAC signiert, und das geht dort auf, weil der Server prüft. Ein Update wird auf dem Zielsystem geprüft. Ein HMAC bräuchte dort denselben geheimen Schlüssel wie auf dem Server; wer ihn ausliest, kann beliebige Pakete signieren — die Signatur verlöre genau die Eigenschaft, wegen der es sie gibt.

Deshalb asymmetrisch: der Server signiert mit einem privaten RSA-Schlüssel, der Agent prüft mit dem öffentlichen.

Einrichten

openssl genrsa -out /etc/dc/release-signing.pem 2048
chmod 600 /etc/dc/release-signing.pem
// config/config.php
'release_private_key' => dc_env('DC_RELEASE_SIGNING_KEY', '/etc/dc/release-signing.pem'),

Signiert wird beim Veröffentlichen, serverseitig. Der Packager bekommt den Schlüssel nicht zu sehen — er läuft auf Entwicklerrechnern, und der Schlüssel wäre so gut geschützt wie das schwächste dieser Systeme.

Signiert wird eine kanonische Zeile, nicht das Manifest-JSON: JSON-Ausgabe ist nicht bytestabil (Schlüsselreihenfolge, Escaping, Zahlenformat), eine Signatur darüber wäre unzuverlässig prüfbar.

dc-release-v1\n{product_slug}\n{version}\n{channel}\n{platform}\n
{sha256_hash klein}\n{download_url}\n{size_bytes}

Prüfen

Der Agent holt den öffentlichen Schlüssel einmalig von /api/updateservice/v1/pubkey und legt ihn als dc-release-pubkey.pem neben sich ab. Meldet der Server später einen anderen Schlüssel, wird gewarnt und weiterhin der hinterlegte benutzt — ein untergeschobener Server fällt damit auf. War der Wechsel beabsichtigt, die Datei löschen.

Lage Verhalten
Signatur gültig Installation läuft
Signatur ungültig Abbruch, immer
Release unsigniert Hinweis, Installation läuft
Kein öffentlicher Schlüssel Hinweis, Installation läuft
--require-signature gesetzt Die letzten beiden Fälle brechen ebenfalls ab

Ohne hinterlegten Schlüssel bleibt also alles funktionsfähig — es fehlt nur die Vertrauenskette, und darauf wird bei jedem Update hingewiesen.