Bisher gab es nur den Update-Weg: eine Anwendung musste bereits installiert und eingerichtet sein, damit sich etwas aktualisieren liess. Die Erstinstallation auf einem neuen System war Handarbeit - Paket kopieren, Konfiguration abtippen, Token besorgen. Setup-API (neu) - POST /api/setup/v1/login tauscht Benutzername und Passwort gegen ein Token mit 30 Minuten Gueltigkeit und ausschliesslich setup:install. Es wird nicht mitgeschrieben und lebt im Installer nur im Speicher. - GET /api/setup/v1/catalog zeigt nur, was zur Laufzeitkennung des anfragenden Systems passt. Ein Projekt mit ausschliesslich Windows-Paket taucht auf einem Linux-Rechner gar nicht erst auf. - POST /api/setup/v1/token stellt das Dauertoken der Anwendung aus. Welche Rechte vergeben werden, entscheidet der Server; die Anfrage kann nur einschraenken. Sonst waere der Umweg ueber ein kurzlebiges Token wirkungslos. Rollentrennung (Migration 012) - dc_users bekommt role, disabled und last_login_at. Die Rolle "installer" darf sich ueber den Setup-Weg anmelden und nicht am WebUI. Die Zugangsdaten werden auf jedem Zielsystem eingetippt; mit einem Administratorkonto verteilte man damit den Zugang zu Tokens, Lizenzen und Monitoren auf jeden Rechner, auf dem je etwas installiert wurde. - Auth::verifyCredentials() prueft sessionfrei, damit Setup- und WebUI-Login nicht zwei verschiedene Haertungsgrade haben (Drosselung, Timing-Angleichung, Rehash gelten fuer beide). - Konten mit hinterlegtem TOTP-Geheimnis werden am Setup-Weg mit 501 abgewiesen. Eine TOTP-Pruefung gibt es im Deploymentcenter noch nicht; sie stillschweigend zu uebergehen waere ein Rueckschritt. - Benutzerverwaltung im WebUI - es gab bisher gar keine, nur den einen von install_db.php angelegten Admin. Das letzte aktive Administratorkonto laesst sich weder deaktivieren noch loeschen. Installer - update-agent --action install fuehrt durch Anmeldung, Auswahl, Zielverzeichnis, Installation und Einrichtung. Die Dateien kommen ueber denselben Pfad wie ein Update - mit Pruefsumme, Signatur, Staging und Rollback. Ein zweiter Download-Weg waere ein zweiter Ort fuer dieselben Fehler. - --action configure holt die Einrichtung nachtraeglich. - setup.json im Paket beschreibt die benoetigten Werte. Bewusst im Paket und nicht zentral: so ist sie mit der Anwendung versioniert. - Gefragt wird nur, was uebrig bleibt: bereits gesetzt -> detect:... -> provision -> fragen. Platzhalter wie changeme oder <dein-wert> gelten dabei nicht als eingerichtet, sonst liefe die Anwendung mit der Vorlage los. - SetupWriter erhaelt vorhandene Inhalte. Eine appsettings.json fuehrt neben den abgefragten Werten meist Logging und anderes; sie neu zu erzeugen waere bequemer und verloere das - bei einer Neuinstallation ohne Backup. int und bool landen als JSON-Typ, nicht als Zeichenkette. Downloads - scripts/build_installer.ps1 baut selbstenthaltende Einzeldateien fuer win-x64, linux-x64 und linux-arm64 (rund 34 MB, .NET-Laufzeit inbegriffen). Ohne NativeAOT und ohne Trimming: Spectre.Console loest ueber Reflexion auf und braeche sonst erst beim Anwender. - scripts/upload_installer.py laedt sie nach /installer/. Getrennt von deploy.py, das client-dotnet bewusst ausklammert. - Bereich "Installer" auf der UpdateService-Seite mit Groessen, Pruefsummen und den wget-Befehlen; die Angaben stammen aus installer.json statt aus fest eingetragenem Text. - install.sh und install.ps1 laden, pruefen die Pruefsumme und legen ab - sie richten bewusst nichts selbst ein. Das Manifest wird BOM-frei geschrieben, sonst scheitert json_decode() daran. Enthaelt ausserdem die bislang nicht committete Arbeit an den RocketChat-Benachrichtigungen (Migrationen 010 und 011) sowie die Loesch- und Editierfunktion des UpdateService; die betroffenen Dateien liessen sich nicht getrennt stagen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
681 lines
27 KiB
Markdown
681 lines
27 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.
|
|
|
|
---
|
|
|
|
## 7. Release-Verwaltung (Bearbeiten & Löschen)
|
|
|
|
Veröffentlichte Releases können sowohl über das Web-Interface als auch über die API nachträglich bearbeitet oder gelöscht werden.
|
|
|
|
### Web-Interface (`public/index.php`)
|
|
- Im Tab **UpdateService** → **📦 Veröffentlichte Software Releases** enthält jede Zeile die Aktionen `✏️ Bearbeiten` und `🗑️ Löschen`.
|
|
- **Bearbeiten**: Öffnet einen Modal-Dialog, in dem Release-Notes, Download-URL, SHA256-Hash, Git Commit, Plattform, Kanal, Version und IsCritical angepasst werden können. Beim Speichern wird die kanonische Signatur serverseitig neu berechnet.
|
|
- **Löschen**: Entfernt nach Bestätigung den Release-Datensatz aus der Datenbank.
|
|
|
|
### API-Endpunkte
|
|
|
|
#### Release Bearbeiten
|
|
- **POST** `/api/updateservice/v1/update` (oder `action=update` / `action=edit`)
|
|
- Scope: `updateservice:publish`
|
|
- Parameter: `release_id` (oder `id`), optional updated `product_slug`, `version`, `channel`, `platform`, `download_url`, `sha256_hash`, `git_commit`, `release_notes`, `size_bytes`, `is_critical`.
|
|
- Antwort (200 OK):
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"release_id": 42,
|
|
"updated": true,
|
|
"platform": "win-x64",
|
|
"signed": true,
|
|
"message": "Release 1.2.0 (prod, win-x64) fuer \"myapp\" erfolgreich aktualisiert."
|
|
}
|
|
```
|
|
|
|
#### Release Löschen
|
|
- **POST / DELETE** `/api/updateservice/v1/delete` (oder `action=delete`)
|
|
- Scope: `updateservice:publish`
|
|
- Parameter: `release_id` (oder `id`) ODER die Kombination aus `product_slug`, `version`, `channel`, `platform`.
|
|
- Antwort (200 OK):
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"release_id": 42,
|
|
"deleted": true,
|
|
"message": "Release v1.2.0 (prod, win-x64) fuer \"myapp\" geloescht."
|
|
}
|
|
```
|
|
|