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>
637 lines
25 KiB
Markdown
637 lines
25 KiB
Markdown
# 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](./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](#b-msbuild-buildinfo-generierung)**.
|
|
> - 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](#c-zwei-quellen-zwei-formate)**.
|
|
|
|
> **⚠️ Geändert in Version 2.2 — bitte vollständig lesen, bevor das nächste
|
|
> Release gebaut wird.** Umstellungsschritte: **[UPGRADE.md §15](./UPGRADE.md#15-umstellung-auf-22)**.
|
|
> - **Releases tragen eine Plattform.** Ohne sie überschrieben sich `win-x64`
|
|
> und `linux-x64` gegenseitig, siehe **[§1A](#1a-plattformen)**.
|
|
> - **Konfigurationsdateien überleben ein Update.** Bisher überschrieb jedes
|
|
> Update die eingerichteten Werte des Zielsystems, siehe **[§3A](#3a-ausschließen-oder-schützen)**.
|
|
> - **Anwenden mit Rollback.** Ein Abbruch hinterlässt keine halbe Installation
|
|
> mehr, und entfernte Dateien werden aufgeräumt, siehe **[§4B](#4b-wie-ein-update-angewendet-wird)**.
|
|
> - **Releases sind signiert**, siehe **[§6](#6-signatur-der-releases)**.
|
|
> - `LaunchUpdateAgent` übergibt jetzt Neustart, Prozesskennung und Plattform,
|
|
> siehe **[§2A](#a-referenz-auf-deploymentcenterclient)**.
|
|
|
|
|
|
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:
|
|
|
|
```text
|
|
/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.
|
|
|
|
```csharp
|
|
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](#4a-welche-version-ist-installiert)**.
|
|
|
|
### 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:
|
|
|
|
```xml
|
|
<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`:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```bash
|
|
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/publish` → `linux-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`.
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
# 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.
|
|
|
|
```bash
|
|
GET /api/updateservice/v1/check?product=myapp&version=1.4.2&channel=prod&platform=win-x64
|
|
```
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{ "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
|
|
|
|
```text
|
|
/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
|
|
|
|
```bash
|
|
openssl genrsa -out /etc/dc/release-signing.pem 2048
|
|
chmod 600 /etc/dc/release-signing.pem
|
|
```
|
|
|
|
```php
|
|
// 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.
|