Files
Deploymentcenter/public/docs/release.md
T
Deploymentcenter BotandClaude Opus 5 1967b49ad7 fix(client): Schluessel raus aus argv, Wartezeit einstellbar, Packager sperrt
Fuenf von sechs Befunden einer externen Integration. Der sechste - unsignierte
Lizenzurteile - ist bestaetigt, aber bewusst nicht Teil dieses Commits.

1. Lizenzschluessel stand in der Kommandozeile
   Der Agent nahm --license-key nur als Argument und las keine
   Umgebungsvariablen. "ps" zeigt argv jedem Benutzer der Maschine - exakt die
   Begruendung, mit der UPGRADE.md §5 den Crontab-Weg verwirft. Damit nahm das
   SDK einen bereits geloesten Sicherheitsbefund zurueck.
   Der Agent liest jetzt DC_LICENSE_KEY, DC_DOWNLOAD_USER und
   DC_DOWNLOAD_PASSWORD, Umgebung vor Argument. LaunchUpdateAgent uebergibt
   den Schluessel nicht mehr als Argument, sondern setzt die Variable auf dem
   eigenen Prozess: das Kind erbt den Umgebungsblock, danach wird sie wieder
   entfernt. Das funktioniert auch mit UseShellExecute=true, wo sich
   ProcessStartInfo.Environment nicht setzen laesst.

2. --wait-timeout war nicht durchgereicht
   Der Agent kannte den Parameter, LaunchUpdateAgent hatte keinen dafuer - es
   galten fest 60 Sekunden. Eine Anwendung, die allein fuer host.StopAsync 30
   Sekunden braucht, kommt damit gefaehrlich nah an die Grenze. Neu:
   waitTimeoutSeconds. Ausserdem ist im Quelltext und in der Doku jetzt
   festgehalten, dass exitCurrentApp:true ueber Environment.Exit(0) laeuft und
   damit finally-Bloecke und IHostApplicationLifetime uebergeht - bei offenem
   Zustand die falsche Wahl.

3. ILicensePrompt war tot
   Der Konstruktor nahm es entgegen, legte es in _prompt ab und benutzte es
   nirgends. Wer darauf eine headless-Story aufbaute, baute auf Sand. Neu:
   EnsureLicensedAsync() - zwischengespeicherten Schluessel nehmen, sonst
   fragen, pruefen, bei Ablehnung erneut fragen. allowPrompt:false lehnt ohne
   Cache ab, statt auf eine Eingabe zu warten, die im Dienst nie kommt. Ein
   voruebergehender Netzfehler fuehrt nicht zur erneuten Abfrage - der
   Schluessel ist ja nicht falsch.

4. Der Packager warnte nur
   Er bricht jetzt ab. Anlass war ein echter API-Schluessel in einem
   oeffentlich abrufbaren Paket - und die Warnung war damals ausgerechnet
   unterdrueckt, weil die Datei auf der preserve-Liste stand.
   Zwei Stufen: Dateiname (appsettings.Local.json, master.key, *.pfx, *.db,
   server_settings.xml) und Inhalt (gefuelltes Password=, sk-, ghp_,
   dc_master_, AKIA, private Schluessel). Die Inhaltspruefung findet auch
   Dateien mit unverdaechtigem Namen.
   Platzhalter loesen bewusst nicht aus: "sk-DEIN-SCHLUESSEL-HIER" haette
   sonst jede ausgelieferte Vorlage blockiert, und --allow-secrets waere nach
   einer Woche Gewohnheit. Beim Erproben zuerst genau in diese Falle gelaufen.

5. BuildInfo.targets war nur per Pfad-Import zu haben
   Die Anleitung empfahl einen <Import> ins Nachbar-Repository - das setzt
   voraus, dass beide Arbeitskopien nebeneinander liegen und in derselben
   Fassung stehen. Das Client-Projekt ist jetzt packbar und legt das Target
   unter build/ ins Paket, wo NuGet es selbst importiert.

Ausserdem: Unauthorized wurde nur im statischen Zweig erkannt, im API-Zweig
kam ein 401 als gewoehnlicher HTTP-Fehler an.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 13:47:55 +02:00

8.4 KiB

Ein Projekt veröffentlichungsfähig machen

Für Coding-Agenten, die den UpdateService in ein Projekt integrieren. Ergebnis: ./scripts/release.ps1 -Version 1.4.3 baut, packt, lädt hoch und meldet das Release beim Deploymentcenter an — für alle Zielplattformen.

Es gibt bereits ein Werkzeug, das den schwierigen Teil erledigt: pack-and-deploy. Es berechnet Prüfsummen, erzeugt das Dateimanifest, schreibt die latest.json fort und meldet das Release über die API an. Baue das nicht nach. Ein zweiter Ort für dieselbe Logik ist ein zweiter Ort, an dem dieselben Fehler wieder entstehen — und dieses Werkzeug hat sie bereits hinter sich.

Was fehlt, ist nur die Orchestrierung: pro Zielplattform einmal dotnet publish, dann pack-and-deploy. Genau das ist die Vorlage.


1. Einrichten

Drei Dateien, einmalig:

mkdir -p scripts .dc-tools

# Windows
curl -fsSL https://dc.mhdf.de/docs/release-template/release.ps1 -o scripts/release.ps1

# Linux / CI
curl -fsSL https://dc.mhdf.de/docs/release-template/release.sh  -o scripts/release.sh
chmod +x scripts/release.sh

# in beiden Fällen
curl -fsSL https://dc.mhdf.de/docs/release-template/release.config.example.json \
     -o scripts/release.config.json

echo '.dc-tools/' >> .gitignore
echo 'artifacts/' >> .gitignore

Nur release.config.json wird angepasst. Die Skripte selbst bleiben unverändert — dann lassen sie sich bei einer neuen Fassung einfach ersetzen.

{
  "project": "myapp",
  "csproj": "src/MyApp/MyApp.csproj",
  "runtimes": ["win-x64", "linux-x64"],
  "selfContained": true,
  "setupJson": "setup.json"
}

release.config.json enthält keine Zugangsdaten und darf versioniert werden.

Zugangsdaten

Die kommen aus Umgebungsvariablen:

export DC_FTP_HOST=ftp.example.com
export DC_FTP_USER=...
export DC_FTP_PASS=...
export DC_TOKEN=dc_master_...        # braucht das Recht updateservice:publish

Alternativ eine packager.config.json neben dem Werkzeug — die steht dann in .gitignore. Ohne DC_TOKEN wird das Paket zwar gebaut und hochgeladen, aber nicht angemeldet und nicht signiert; der Rückgabewert ist dann 2.

Voraussetzungen

Windows Linux
Skript release.ps1 (PowerShell 5.1 genügt) release.sh
Nötig .NET SDK .NET SDK, curl, dazu jq oder Python

pack-and-deploy holt sich das Skript beim ersten Lauf selbst von /installer/, prüft die Prüfsumme und legt es unter .dc-tools/ ab. Das Verzeichnis gehört in die .gitignore.


2. Veröffentlichen

.\scripts\release.ps1 -Version 1.4.3 -Changelog "Behebt den Login-Fehler."
./scripts/release.sh --version 1.4.3 --changelog "Behebt den Login-Fehler."

Ohne -Version wird sie aus Directory.Build.props oder der .csproj gelesen. Weitere Schalter: -Channel beta, -Critical, -WhatIf beziehungsweise --channel, --critical, --dry-run.

Je Laufzeitkennung entsteht ein eigenes Release. Ohne Plattformangabe würden sie sich gegenseitig überschreiben — bis Version 2.2 war genau das der Fall, und ein Linux-System zog sich das Windows-Paket.


3. Was du im Projekt vorbereiten musst

<Version> in die Directory.Build.props

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

Nicht in einzelne .csproj-Dateien. pack-and-deploy liest die Version aus der Hauptassembly und bricht bei einer Abweichung ab. Das ist Absicht: 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.

Konfigurationsdateien schützen

Zwei verschiedene Dinge, und die Verwechslung hat schon einen echten API-Schlüssel öffentlich gemacht:

excludePatterns preservePatterns
Im Paket? nein ja
Erstinstallation fehlt wird geschrieben
Update vorhandene Datei bleibt unangetastet

Eine appsettings.json mit echten Zugangsdaten gehört in keine von beiden Listen — sie gehört gar nicht erst ins Publish-Verzeichnis. Was ausgeliefert wird, muss eine Vorlage mit Platzhaltern sein.

pack-and-deploy bricht ab, wenn es Zugangsdaten findet — geprüft werden Dateinamen (appsettings.Local.json, master.key, *.pfx, *.db, …) und der Inhalt (gefüllte Password=-Felder, sk-…, dc_master_…, private Schlüssel). Platzhalter wie sk-DEIN-SCHLUESSEL-HIER lösen nicht aus.

Häufigste Ursache: eine CopyToOutputDirectory-Regel in der .csproj, die eine lokale Konfigurationsdatei ins Ausgabeverzeichnis kopiert. Dann gehört sie in excludePatterns — und die Regel auf den Prüfstand.

Notausgang: --allow-secrets. Nur benutzen, wenn du weißt, warum. Release-Pakete liegen hinter einem Zugangsschutz, aber jeder lizenzierte Kunde kann sie auspacken.

setup.json anlegen

Beschreibt, was die Erstinstallation abfragen muss. Das Skript kopiert sie ins Publish-Verzeichnis, sodass sie im Paket landet.

{
  "schema": 1,
  "targets": [
    { "id": "app", "file": "myapp/Settings.json", "location": "config" }
  ],
  "fields": [
    { "key": "ConnectionStrings:Main", "label": "Datenbank", "type": "secret" },
    { "key": "Deploymentcenter:Token", "source": "provision",
      "scopes": ["watchdog:ping"] }
  ]
}

Vollständig beschrieben in docs/SETUP_INTEGRATION_GUIDE.md im Deploymentcenter-Repository. Ohne setup.json lässt sich die Anwendung installieren, aber nicht einrichten.

Das SDK einbinden

var check = await new UpdateClient().CheckForUpdateAsync(
    baseUrl: "https://dc.mhdf.de", projectId: "myapp",
    currentVersion: BuildInfo.Version, channel: "prod",
    credentials: ReleaseCredentials.FromLicenseKey(meineLizenz));

Der Lizenzschlüssel ist Pflicht. Die Release-Ablage liegt hinter HTTP-Basic-Auth; ohne ihn bekommt die Anwendung 401 und keine Updates mehr. Details in docs/UPDATESERVICE_INTEGRATION_GUIDE.md §5A (Zugangsschutz der Release-Verzeichnisse).

Für ein Produkt, das noch nie veröffentlicht hat, gilt eine besondere Reihenfolge. /releases/<slug>/ existiert noch nicht und ist deshalb auch nicht geschützt. Das Verzeichnis entsteht mit dem ersten Upload, und der nächste Abgleich schützt es. Es gibt also kein Zeitfenster, um ein ungeschütztes Release zu ziehen und danach das SDK nachzurüsten: der erste ausgelieferte Build muss die Zugangsdaten schon mitbringen.


4. Rückgabewerte

Wert Bedeutung
0 vollständig veröffentlicht
1 Konfigurationsfehler oder Versionskonflikt — nichts wurde ausgeführt
2 teilweise: Upload oder Registrierung fehlgeschlagen

Bei 1 bricht das Skript sofort ab, statt die übrigen Plattformen ins Leere laufen zu lassen. Bei 2 läuft es weiter und meldet am Ende, welche Plattformen betroffen sind — dort ist zu prüfen, ob die bereits hochgeladenen Dateien zusammenpassen.


5. Prüfen, ob es getragen hat

curl "https://dc.mhdf.de/api/updateservice/v1/check?product=myapp&version=0.0.0&channel=prod&platform=win-x64"

Erwartet: update_available: true mit der neuen Version und "signed": true in der Antwort des Publish-Aufrufs. Im WebUI erscheint das Release unter UpdateService → Releases mit Plattform-Spalte; steht dort UNSIGNIERT, fehlt auf dem Server der Signierschlüssel.

Und der Zugangsschutz:

curl -I https://dc.mhdf.de/releases/myapp/prod/win-x64/1.4.3/package.tar.gz   # 401 erwartet

Antwortet das mit 200, ist das Paket öffentlich abrufbar — dann im WebUI unter UpdateService → 🔒 Zugangsschutz den Selbsttest laufen lassen.


6. Häufige Stolperstellen

Symptom Ursache
Versionskonflikt beim Packen <Version> steht nur in einem von mehreren Projekten — gehört in die Directory.Build.props
Rückgabewert 2, „Registrierung fehlgeschlagen" DC_TOKEN fehlt oder hat nicht updateservice:publish
unknown_project Der Slug ist im Deploymentcenter nicht angelegt
Client bekommt 401 statt Updates Kein licenseKey übergeben, oder die Lizenz ist abgelaufen
Linux-Paket startet nicht Unter Windows gebaut — der Agent setzt das Ausführungsbit beim Anwenden, ein von Hand entpacktes Archiv nicht
Update lädt endlos erneut Veröffentlichte Version weicht von der einkompilierten ab