From e01a608c08f865138931895da94d4400fe19c165 Mon Sep 17 00:00:00 2001 From: Deploymentcenter Bot Date: Fri, 14 Aug 2026 13:23:38 +0200 Subject: [PATCH] feat(release): Veroeffentlichungsvorlage fuer fremde Projekte Die Anleitung benutzte pack-and-deploy, als laege es im PATH - beziehbar war es nirgends. Ein Projekt, das den UpdateService einbindet, konnte also nicht veroeffentlichen, ohne dieses Repository auszuchecken und selbst zu uebersetzen. Das Werkzeug existierte, nur kam niemand daran. - build_installer.ps1 baut pack-and-deploy fuer dieselben Laufzeitkennungen mit und fuehrt es in installer.json unter "tools". Damit steht es neben dem Agenten unter /installer/ bereit. - Neue Vorlage unter public/docs/release-template/: release.ps1, release.sh und release.config.example.json. Kopieren, Konfiguration ausfuellen, fertig - die Skripte selbst bleiben unveraendert und lassen sich bei einer neuen Fassung einfach ersetzen. - Sie orchestrieren nur: je Laufzeitkennung einmal dotnet publish, dann pack-and-deploy. Pruefsummen, Dateimanifest, latest.json und die Anmeldung bleiben im Werkzeug - ein zweiter Ort fuer dieselbe Logik waere ein zweiter Ort fuer dieselben Fehler. - Das Werkzeug wird beim ersten Lauf selbst geholt, gegen die .sha256 geprueft und unter .dc-tools/ abgelegt. Die Vorlage ist damit wirklich eine Datei. - setup.json wird ins Publish-Verzeichnis kopiert, sonst faende der Installer sie nicht. - Rueckgabewert 1 (Konfigurations- oder Versionsfehler) bricht sofort ab; die weiteren Plattformen wuerden genauso scheitern. Bei 2 laeuft es weiter und meldet am Ende, welche betroffen sind. Anleitung: public/docs/release.md, oeffentlich unter /docs/release.md - dort, wo auch das Bugtracker-Handbuch liegt. Das Entwickler-docs/ wird nicht ausgeliefert; ein erster Anlauf legte die Vorlage dort ab und war deshalb nicht abrufbar. Beim Erproben in einem leeren Projekt aufgefallen und behoben: - Die Vorlage verlangte jq. Das ist auf den wenigsten Systemen vorinstalliert; sie kommt jetzt auch mit Python aus. - Windows legt unter WindowsApps einen python3-Platzhalter ab, der gefunden wird, beim Aufruf aber nur auf den Store verweist. Die Erkennung erprobt den Interpreter deshalb, statt nur seine Existenz zu pruefen. - Der Ternary-Operator in release.ps1 gibt es erst ab PowerShell 7; die Vorlage laeuft jetzt auch mit dem mitgelieferten 5.1. Co-Authored-By: Claude Opus 5 --- docs/README.md | 1 + docs/UPDATESERVICE_INTEGRATION_GUIDE.md | 14 + .../release.config.example.json | 27 ++ public/docs/release-template/release.ps1 | 233 +++++++++++++++ public/docs/release-template/release.sh | 280 ++++++++++++++++++ public/docs/release.md | 235 +++++++++++++++ scripts/build_installer.ps1 | 53 ++++ 7 files changed, 843 insertions(+) create mode 100644 public/docs/release-template/release.config.example.json create mode 100644 public/docs/release-template/release.ps1 create mode 100644 public/docs/release-template/release.sh create mode 100644 public/docs/release.md diff --git a/docs/README.md b/docs/README.md index 0c43da6..268c62e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ Monitoring und einen Bugtracker, den Coding-Agenten selbständig bedienen. | Dokument | Wofür | |---|---| | **[UPGRADE.md](./UPGRADE.md)** | **Ablaufplan für die Umstellung auf 2.0.** Enthält Pflichtschritte: Zugangsdaten wechseln, Migration, Evaluator-Cron. | +| **[Release-Anleitung für Agenten](../public/docs/release.md)** | Ein Projekt veröffentlichungsfähig machen: Vorlage kopieren, konfigurieren, ausliefern | | [Agent-Prompt-Vorlage](./AGENT_PROMPT_TEMPLATE.md) | Textbaustein für `CLAUDE.md` / `AGENTS.md` eines Projekts | | [Agenten-Handbuch](../public/docs/bugtracker.md) | Vollständige Beschreibung des Bugtracker-Workflows, öffentlich unter `/docs/` | diff --git a/docs/UPDATESERVICE_INTEGRATION_GUIDE.md b/docs/UPDATESERVICE_INTEGRATION_GUIDE.md index 2daead4..40c7281 100644 --- a/docs/UPDATESERVICE_INTEGRATION_GUIDE.md +++ b/docs/UPDATESERVICE_INTEGRATION_GUIDE.md @@ -233,6 +233,20 @@ für die API-Antwort. `UpdateCheckResult.LatestRelease` ist in beiden Fällen ei ## 3. Packaging & Deployment CLI (`pack-and-deploy`) +> **Woher das Werkzeug kommt.** Frühere Fassungen dieser Anleitung benutzten +> `pack-and-deploy`, als läge es im PATH — beziehbar war es nirgends. Es steht +> jetzt unter `/installer/` bereit: +> +> ```bash +> wget https://dc.mhdf.de/installer/pack-and-deploy-linux-x64 -O pack-and-deploy +> chmod +x pack-and-deploy +> ``` +> +> Wer nicht von Hand aufrufen will, nimmt die **Release-Vorlage**: ein Skript +> zum Kopieren ins eigene Projekt, das je Plattform `dotnet publish` und +> `pack-and-deploy` verkettet und sich das Werkzeug selbst holt. Siehe +> **[Release-Anleitung für Agenten](../public/docs/release.md)**. + 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: diff --git a/public/docs/release-template/release.config.example.json b/public/docs/release-template/release.config.example.json new file mode 100644 index 0000000..2d1b04e --- /dev/null +++ b/public/docs/release-template/release.config.example.json @@ -0,0 +1,27 @@ +{ + "_comment": "Kopie als scripts/release.config.json anlegen und ausfuellen. Diese Datei enthaelt KEINE Zugangsdaten - die kommen aus Umgebungsvariablen (DC_FTP_HOST, DC_FTP_USER, DC_FTP_PASS, DC_TOKEN) oder aus einer packager.config.json neben dem Werkzeug. release.config.json darf deshalb versioniert werden.", + + "_project_comment": "Projekt-Slug im Deploymentcenter. Muss dort unter Projekte angelegt sein, sonst schlaegt die Registrierung fehl.", + "project": "myapp", + + "_csproj_comment": "Pfad zur Startprojektdatei, relativ zur Repository-Wurzel.", + "csproj": "src/MyApp/MyApp.csproj", + + "_runtimes_comment": "Fuer welche Laufzeitkennungen gebaut wird. Je Eintrag entsteht ein eigenes Release - ohne Plattformangabe wuerden sie sich gegenseitig ueberschreiben.", + "runtimes": ["win-x64", "linux-x64"], + + "_selfContained_comment": "true nimmt die .NET-Laufzeit ins Paket. Fuer Zielsysteme ohne vorinstalliertes .NET die richtige Wahl - das Paket wird dadurch deutlich groesser.", + "selfContained": true, + + "_publishSingleFile_comment": "Alles in eine ausfuehrbare Datei. Bequem, erschwert aber das gezielte Ersetzen einzelner Dateien beim Update.", + "publishSingleFile": false, + + "_setupJson_comment": "Beschreibung der einzurichtenden Werte, relativ zur Repository-Wurzel. Wird ins Paket kopiert, damit der Installer sie findet. Fehlt die Datei, wird ohne Einrichtungsschritt ausgeliefert.", + "setupJson": "setup.json", + + "_mainAssembly_comment": "Optional. Datei, gegen die pack-and-deploy die Version gegenprueft. Ohne Angabe wird sie aus dem Projekt-Slug bzw. der runtimeconfig.json abgeleitet.", + "mainAssembly": "", + + "_baseUrl_comment": "Adresse des Deploymentcenters. Von hier wird auch pack-and-deploy geholt.", + "baseUrl": "https://dc.mhdf.de" +} diff --git a/public/docs/release-template/release.ps1 b/public/docs/release-template/release.ps1 new file mode 100644 index 0000000..1be4f08 --- /dev/null +++ b/public/docs/release-template/release.ps1 @@ -0,0 +1,233 @@ +<# +.SYNOPSIS + Veroeffentlicht dieses Projekt im Deploymentcenter. + +.DESCRIPTION + Vorlage zum Kopieren nach scripts/release.ps1 des eigenen Projekts. + Anzupassen ist nur der Kopf von release.config.json - dieses Skript + selbst bleibt unveraendert. + + Der Ablauf je Zielplattform: + dotnet publish -r -> pack-and-deploy --platform + + pack-and-deploy uebernimmt dabei Pruefsummen, Dateimanifest, das + Fortschreiben der latest.json und die Anmeldung beim Deploymentcenter. + Das hier nachzubauen waere ein zweiter Ort fuer dieselben Fehler; das + Skript orchestriert nur. + + Fehlt das Werkzeug, wird es geholt und die Pruefsumme geprueft. + +.EXAMPLE + .\scripts\release.ps1 -Version 1.4.3 -Changelog "Behebt den Login-Fehler." + +.EXAMPLE + .\scripts\release.ps1 -Version 1.5.0 -Channel beta -WhatIf +#> + +[CmdletBinding(SupportsShouldProcess = $true)] +param( + # Ohne Angabe wird die Version aus Directory.Build.props bzw. der csproj gelesen. + [string] $Version, + + [ValidateSet('prod', 'beta', 'dev')] + [string] $Channel = 'prod', + + [string] $Changelog, + + # Als kritisches Update kennzeichnen (Rollout priorisieren). + [switch] $Critical, + + [string] $ConfigFile = (Join-Path $PSScriptRoot 'release.config.json') +) + +$ErrorActionPreference = 'Stop' + +# ---------------------------------------------------------------- Konfiguration +if (-not (Test-Path $ConfigFile)) { + throw "Konfiguration fehlt: $ConfigFile`nVorlage kopieren: release.config.example.json -> release.config.json" +} + +$config = Get-Content $ConfigFile -Raw | ConvertFrom-Json + +foreach ($required in @('project', 'csproj', 'runtimes')) { + if (-not $config.$required) { + throw "In $ConfigFile fehlt der Eintrag '$required'." + } +} + +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..') +$csprojRel = $config.csproj +$csproj = Join-Path $repoRoot $csprojRel + +if (-not (Test-Path $csproj)) { + throw "Projektdatei nicht gefunden: $csproj" +} + +$baseUrl = if ($config.baseUrl) { $config.baseUrl.TrimEnd('/') } else { 'https://dc.mhdf.de' } +$toolDir = Join-Path $repoRoot '.dc-tools' + +# ---------------------------------------------------------------------- Version +function Get-ProjectVersion { + # Directory.Build.props zuerst: Steht nur in einem von mehreren + # Projekten, laufen die Angaben frueher oder spaeter auseinander - und + # pack-and-deploy bricht dann zu Recht mit einem Versionskonflikt ab. + foreach ($candidate in @( + (Join-Path $repoRoot 'Directory.Build.props'), + $csproj + )) { + if (-not (Test-Path $candidate)) { continue } + + $match = [regex]::Match((Get-Content $candidate -Raw), '\s*([^<]+?)\s*') + if ($match.Success) { + return $match.Groups[1].Value.Trim() + } + } + + return $null +} + +if (-not $Version) { + $Version = Get-ProjectVersion + if (-not $Version) { + throw "Keine in Directory.Build.props oder $csprojRel gefunden. Bitte -Version angeben." + } + Write-Host "Version aus dem Projekt gelesen: $Version" -ForegroundColor DarkGray +} + +if (-not $Changelog) { + $Changelog = "Release v$Version" +} + +# ------------------------------------------------------------------- Werkzeug +function Get-PackAndDeploy { + $exe = Join-Path $toolDir 'pack-and-deploy.exe' + if (Test-Path $exe) { return $exe } + + Write-Host "pack-and-deploy wird geholt ..." -ForegroundColor Cyan + New-Item -ItemType Directory -Force -Path $toolDir | Out-Null + + $name = 'pack-and-deploy-win-x64.exe' + $temp = Join-Path $toolDir 'download.tmp' + + Invoke-WebRequest -Uri "$baseUrl/installer/$name" -OutFile $temp -UseBasicParsing + + $expectedRaw = (Invoke-WebRequest -Uri "$baseUrl/installer/$name.sha256" -UseBasicParsing).Content + $expected = if ($expectedRaw -is [byte[]]) { + [System.Text.Encoding]::ASCII.GetString($expectedRaw) + } else { [string]$expectedRaw } + $expected = $expected.Trim().ToLower() + + $actual = (Get-FileHash $temp -Algorithm SHA256).Hash.ToLower() + + if ($actual -ne $expected) { + Remove-Item $temp -Force + throw "Pruefsumme von $name stimmt nicht.`n erwartet: $expected`n erhalten: $actual" + } + + Move-Item $temp $exe -Force + try { Unblock-File $exe -ErrorAction SilentlyContinue } catch { } + + Write-Host " Pruefsumme in Ordnung." -ForegroundColor DarkGray + return $exe +} + +$packAndDeploy = Get-PackAndDeploy + +# --------------------------------------------------------------------- Ablauf +Write-Host '' +Write-Host "Projekt : $($config.project)" -ForegroundColor White +Write-Host "Version : $Version" +Write-Host "Kanal : $Channel" +Write-Host "Plattform : $($config.runtimes -join ', ')" +Write-Host '' + +$results = @() + +foreach ($rid in $config.runtimes) { + Write-Host "=== $rid ===" -ForegroundColor Cyan + + $publishDir = Join-Path $repoRoot "artifacts/publish/$rid" + + if ($PSCmdlet.ShouldProcess("$($config.project) $Version ($rid)", 'dotnet publish')) { + # Sauber neu bauen: Reste einer vorherigen Laufzeitkennung wuerden + # sonst mit ins Paket wandern. + if (Test-Path $publishDir) { Remove-Item $publishDir -Recurse -Force } + + $publishArgs = @( + 'publish', $csproj, + '-c', 'Release', + '-r', $rid, + '-o', $publishDir, + '--nologo' + ) + + if ($config.selfContained) { $publishArgs += '--self-contained', 'true' } + else { $publishArgs += '--self-contained', 'false' } + + if ($config.publishSingleFile) { $publishArgs += '-p:PublishSingleFile=true' } + + & dotnet @publishArgs + if ($LASTEXITCODE -ne 0) { throw "dotnet publish fuer $rid ist fehlgeschlagen." } + } + + # setup.json mitliefern, damit der Installer weiss, was einzurichten ist. + # Bewusst kein Ternary-Operator: den gibt es erst ab PowerShell 7, und + # diese Vorlage soll auch mit dem mitgelieferten 5.1 laufen. + $setupRel = if ($config.setupJson) { $config.setupJson } else { 'setup.json' } + $setupJson = Join-Path $repoRoot $setupRel + if (Test-Path $setupJson) { + Copy-Item $setupJson (Join-Path $publishDir 'setup.json') -Force + Write-Host " setup.json mitgenommen" -ForegroundColor DarkGray + } + + if ($PSCmdlet.ShouldProcess("$($config.project) $Version ($rid)", 'pack-and-deploy')) { + $packArgs = @( + '--project', $config.project, + '--version', $Version, + '--channel', $Channel, + '--platform', $rid, + '--publish-dir', $publishDir, + '--changelog', $Changelog + ) + + if ($Critical) { $packArgs += '--critical' } + if ($config.mainAssembly) { $packArgs += '--main-assembly', $config.mainAssembly } + + & $packAndDeploy @packArgs + $code = $LASTEXITCODE + + $results += [pscustomobject]@{ Runtime = $rid; ExitCode = $code } + + # 1 = Konfigurationsfehler oder Versionskonflikt: dann stimmt etwas + # Grundsaetzliches, und die weiteren Plattformen wuerden genauso + # scheitern. + if ($code -eq 1) { throw "pack-and-deploy meldet einen Konfigurations- oder Versionsfehler." } + } + + Write-Host '' +} + +# ------------------------------------------------------------------ Ergebnis +Write-Host '=== Ergebnis ===' -ForegroundColor White + +foreach ($r in $results) { + $text = switch ($r.ExitCode) { + 0 { 'vollstaendig veroeffentlicht' } + 2 { 'TEILWEISE - Upload oder Registrierung fehlgeschlagen' } + default { "unerwarteter Rueckgabewert $($r.ExitCode)" } + } + $color = if ($r.ExitCode -eq 0) { 'Green' } else { 'Yellow' } + Write-Host (" {0,-16} {1}" -f $r.Runtime, $text) -ForegroundColor $color +} + +$failed = @($results | Where-Object { $_.ExitCode -ne 0 }) + +if ($failed.Count -gt 0) { + Write-Host '' + Write-Host 'Nicht alle Plattformen sind durchgelaufen. Vor einem erneuten Versuch pruefen,' -ForegroundColor Yellow + Write-Host 'ob die bereits hochgeladenen Dateien konsistent sind.' -ForegroundColor Yellow + exit 2 +} + +Write-Host '' +Write-Host "Fertig. $($config.project) $Version ist im Kanal $Channel verfuegbar." -ForegroundColor Green diff --git a/public/docs/release-template/release.sh b/public/docs/release-template/release.sh new file mode 100644 index 0000000..106cb74 --- /dev/null +++ b/public/docs/release-template/release.sh @@ -0,0 +1,280 @@ +#!/usr/bin/env bash +# +# Veroeffentlicht dieses Projekt im Deploymentcenter. +# +# Vorlage zum Kopieren nach scripts/release.sh des eigenen Projekts. +# Anzupassen ist nur scripts/release.config.json - dieses Skript selbst +# bleibt unveraendert. +# +# Der Ablauf je Zielplattform: +# dotnet publish -r -> pack-and-deploy --platform +# +# pack-and-deploy uebernimmt Pruefsummen, Dateimanifest, das Fortschreiben der +# latest.json und die Anmeldung beim Deploymentcenter. Das hier nachzubauen +# waere ein zweiter Ort fuer dieselben Fehler; das Skript orchestriert nur. +# +# ./scripts/release.sh --version 1.4.3 --changelog "Behebt den Login-Fehler." +# ./scripts/release.sh --version 1.5.0 --channel beta --dry-run + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG_FILE="${DC_RELEASE_CONFIG:-$SCRIPT_DIR/release.config.json}" + +VERSION="" +CHANNEL="prod" +CHANGELOG="" +CRITICAL=0 +DRY_RUN=0 + +usage() { + sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//' + exit 0 +} + +while [ $# -gt 0 ]; do + case "$1" in + --version) VERSION="$2"; shift 2 ;; + --channel) CHANNEL="$2"; shift 2 ;; + --changelog) CHANGELOG="$2"; shift 2 ;; + --critical) CRITICAL=1; shift ;; + --dry-run|-n) DRY_RUN=1; shift ;; + --help|-h) usage ;; + *) echo "Unbekannte Option: $1" >&2; exit 1 ;; + esac +done + +# ------------------------------------------------------------- Voraussetzungen +command -v dotnet >/dev/null 2>&1 || { + echo "FEHLER: dotnet wird gebraucht, ist aber nicht installiert." >&2 + exit 1 +} + +[ -f "$CONFIG_FILE" ] || { + echo "FEHLER: Konfiguration fehlt: $CONFIG_FILE" >&2 + echo " Vorlage kopieren: release.config.example.json -> release.config.json" >&2 + exit 1 +} + +# JSON lesen - mit jq, sonst mit Python. +# +# Bewusst nicht nur jq: Es ist auf den wenigsten Systemen vorinstalliert, und +# an einer fehlenden Abhaengigkeit soll die Vorlage nicht scheitern. Python +# liegt auf den meisten Entwickler- und CI-Systemen ohnehin bereit. +JSON_READER="" + +if command -v jq >/dev/null 2>&1; then + JSON_READER="jq" +else + # Nicht nur pruefen, ob der Befehl existiert, sondern ob er laeuft: + # Windows legt unter WindowsApps einen python3-Platzhalter ab, der + # gefunden wird, beim Aufruf aber nur auf den Store verweist. + for candidate in python3 python; do + if command -v "$candidate" >/dev/null 2>&1 \ + && "$candidate" -c "import json" >/dev/null 2>&1; then + JSON_READER="$candidate" + break + fi + done +fi + +if [ -z "$JSON_READER" ]; then + echo "FEHLER: Zum Lesen von $CONFIG_FILE wird jq oder ein lauffaehiges Python gebraucht." >&2 + exit 1 +fi + +# Liest einen Skalar. Pfad in jq-Schreibweise, z. B. .project +cfg() { + if [ "$JSON_READER" = "jq" ]; then + jq -r "$1 // empty" "$CONFIG_FILE" + else + "$JSON_READER" -c " +import json,sys +d=json.load(open(sys.argv[1], encoding='utf-8')) +for part in sys.argv[2].lstrip('.').split('.'): + if not isinstance(d, dict): d=None; break + d=d.get(part) +if d is None: print('') +elif isinstance(d, bool): print('true' if d else 'false') +else: print(d) +" "$CONFIG_FILE" "$1" + fi +} + +# Liest ein Feld mit Zeichenketten, eine je Zeile. +cfg_list() { + if [ "$JSON_READER" = "jq" ]; then + jq -r "$1[]?" "$CONFIG_FILE" + else + "$JSON_READER" -c " +import json,sys +d=json.load(open(sys.argv[1], encoding='utf-8')) +for part in sys.argv[2].lstrip('.').split('.'): + d = d.get(part) if isinstance(d, dict) else None +for item in (d or []): print(item) +" "$CONFIG_FILE" "$1" + fi +} + +PROJECT="$(cfg .project)" +CSPROJ_REL="$(cfg .csproj)" +BASE_URL="$(cfg .baseUrl)" +BASE_URL="${BASE_URL:-https://dc.mhdf.de}" +BASE_URL="${BASE_URL%/}" +SETUP_REL="$(cfg .setupJson)" +SETUP_REL="${SETUP_REL:-setup.json}" +MAIN_ASSEMBLY="$(cfg .mainAssembly)" + +[ -n "$PROJECT" ] || { echo "FEHLER: 'project' fehlt in $CONFIG_FILE" >&2; exit 1; } +[ -n "$CSPROJ_REL" ] || { echo "FEHLER: 'csproj' fehlt in $CONFIG_FILE" >&2; exit 1; } + +CSPROJ="$REPO_ROOT/$CSPROJ_REL" +[ -f "$CSPROJ" ] || { echo "FEHLER: Projektdatei nicht gefunden: $CSPROJ" >&2; exit 1; } + +mapfile -t RUNTIMES < <(cfg_list .runtimes) +[ "${#RUNTIMES[@]}" -gt 0 ] || { echo "FEHLER: 'runtimes' ist leer." >&2; exit 1; } + +SELF_CONTAINED="$(cfg .selfContained)" +SINGLE_FILE="$(cfg .publishSingleFile)" + +# -------------------------------------------------------------------- Version +if [ -z "$VERSION" ]; then + # Directory.Build.props zuerst: Steht nur in einem von mehreren + # Projekten, laufen die Angaben auseinander - und pack-and-deploy bricht + # dann zu Recht mit einem Versionskonflikt ab. + for candidate in "$REPO_ROOT/Directory.Build.props" "$CSPROJ"; do + [ -f "$candidate" ] || continue + VERSION="$(sed -n 's:.*\s*\([^<]*\)\s*.*:\1:p' "$candidate" | head -1 | tr -d '[:space:]')" + [ -n "$VERSION" ] && break + done + + [ -n "$VERSION" ] || { + echo "FEHLER: Keine gefunden. Bitte --version angeben." >&2 + exit 1 + } + echo "Version aus dem Projekt gelesen: $VERSION" +fi + +CHANGELOG="${CHANGELOG:-Release v$VERSION}" + +# ------------------------------------------------------------------- Werkzeug +TOOL_DIR="$REPO_ROOT/.dc-tools" +PACK="$TOOL_DIR/pack-and-deploy" + +fetch_tool() { + [ -x "$PACK" ] && return 0 + + case "$(uname -m)" in + x86_64|amd64) rid="linux-x64" ;; + aarch64|arm64) rid="linux-arm64" ;; + *) echo "FEHLER: Nicht unterstuetzte Architektur $(uname -m)" >&2; exit 1 ;; + esac + + echo "pack-and-deploy wird geholt ($rid) ..." + mkdir -p "$TOOL_DIR" + + tmp="$(mktemp)" + curl -fsSL "$BASE_URL/installer/pack-and-deploy-$rid" -o "$tmp" + expected="$(curl -fsSL "$BASE_URL/installer/pack-and-deploy-$rid.sha256" | tr -d ' \t\r\n')" + + if command -v sha256sum >/dev/null 2>&1; then + actual="$(sha256sum "$tmp" | cut -d' ' -f1)" + else + actual="$(shasum -a 256 "$tmp" | cut -d' ' -f1)" + fi + + if [ "$actual" != "$expected" ]; then + rm -f "$tmp" + echo "FEHLER: Pruefsumme stimmt nicht." >&2 + echo " erwartet: $expected" >&2 + echo " erhalten: $actual" >&2 + exit 1 + fi + + chmod +x "$tmp" + mv "$tmp" "$PACK" + echo " Pruefsumme in Ordnung." +} + +fetch_tool + +# --------------------------------------------------------------------- Ablauf +echo +echo "Projekt : $PROJECT" +echo "Version : $VERSION" +echo "Kanal : $CHANNEL" +echo "Plattform : ${RUNTIMES[*]}" +[ "$DRY_RUN" -eq 1 ] && echo "(Probelauf - es wird nichts hochgeladen)" +echo + +FAILED=0 + +for rid in "${RUNTIMES[@]}"; do + echo "=== $rid ===" + + PUBLISH_DIR="$REPO_ROOT/artifacts/publish/$rid" + + if [ "$DRY_RUN" -eq 0 ]; then + # Sauber neu bauen: Reste einer vorherigen Laufzeitkennung wuerden + # sonst mit ins Paket wandern. + rm -rf "$PUBLISH_DIR" + + publish_args=(publish "$CSPROJ" -c Release -r "$rid" -o "$PUBLISH_DIR" --nologo) + + if [ "$SELF_CONTAINED" = "true" ]; then + publish_args+=(--self-contained true) + else + publish_args+=(--self-contained false) + fi + + [ "$SINGLE_FILE" = "true" ] && publish_args+=(-p:PublishSingleFile=true) + + dotnet "${publish_args[@]}" + + # setup.json mitliefern, damit der Installer weiss, was einzurichten ist. + if [ -f "$REPO_ROOT/$SETUP_REL" ]; then + cp "$REPO_ROOT/$SETUP_REL" "$PUBLISH_DIR/setup.json" + echo " setup.json mitgenommen" + fi + + pack_args=( + --project "$PROJECT" + --version "$VERSION" + --channel "$CHANNEL" + --platform "$rid" + --publish-dir "$PUBLISH_DIR" + --changelog "$CHANGELOG" + ) + + [ "$CRITICAL" -eq 1 ] && pack_args+=(--critical) + [ -n "$MAIN_ASSEMBLY" ] && pack_args+=(--main-assembly "$MAIN_ASSEMBLY") + + set +e + "$PACK" "${pack_args[@]}" + code=$? + set -e + + case "$code" in + 0) echo " -> vollstaendig veroeffentlicht" ;; + 1) + # Konfigurationsfehler oder Versionskonflikt: die weiteren + # Plattformen wuerden genauso scheitern. + echo "FEHLER: Konfigurations- oder Versionsfehler - Abbruch." >&2 + exit 1 + ;; + 2) echo " -> TEILWEISE: Upload oder Registrierung fehlgeschlagen"; FAILED=1 ;; + *) echo " -> unerwarteter Rueckgabewert $code"; FAILED=1 ;; + esac + fi + + echo +done + +if [ "$FAILED" -ne 0 ]; then + echo "Nicht alle Plattformen sind durchgelaufen. Vor einem erneuten Versuch pruefen," >&2 + echo "ob die bereits hochgeladenen Dateien konsistent sind." >&2 + exit 2 +fi + +echo "Fertig. $PROJECT $VERSION ist im Kanal $CHANNEL verfuegbar." diff --git a/public/docs/release.md b/public/docs/release.md new file mode 100644 index 0000000..ebf8f27 --- /dev/null +++ b/public/docs/release.md @@ -0,0 +1,235 @@ +# 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: + +```bash +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. + +```json +{ + "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: + +```bash +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 + +```powershell +.\scripts\release.ps1 -Version 1.4.3 -Changelog "Behebt den Login-Fehler." +``` + +```bash +./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 + +### `` in die `Directory.Build.props` + +```xml + + + 1.4.3 + + +``` + +**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` warnt bei Dateien, die nach Zugangsdaten aussehen. **Nimm die +Warnung ernst.** 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. + +```json +{ + "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 + +```csharp +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//` 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 + +```bash +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: + +```bash +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 | `` 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 | diff --git a/scripts/build_installer.ps1 b/scripts/build_installer.ps1 index 4295f12..e76c805 100644 --- a/scripts/build_installer.ps1 +++ b/scripts/build_installer.ps1 @@ -24,6 +24,7 @@ $ErrorActionPreference = 'Stop' $repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..') $project = Join-Path $repoRoot 'client-dotnet\Deploymentcenter.UpdateAgent\Deploymentcenter.UpdateAgent.csproj' +$packager = Join-Path $repoRoot 'client-dotnet\Deploymentcenter.Packager\Deploymentcenter.Packager.csproj' $staging = Join-Path ([System.IO.Path]::GetTempPath()) ("dc-installer-build-" + [guid]::NewGuid().ToString('N')) if (-not (Test-Path $project)) { @@ -83,12 +84,64 @@ try { Write-Host (" {0,-22} {1,6:N1} MB {2}" -f $targetName, ($size / 1MB), $hash.Substring(0, 16)) } + # ------------------------------------------------------------------ + # pack-and-deploy + # ------------------------------------------------------------------ + # Das Veroeffentlichungswerkzeug wurde in der Anleitung benutzt, als laege + # es im PATH - beziehbar war es nirgends. Fremde Projekte konnten also + # nicht veroeffentlichen, ohne dieses Repository auszuchecken und selbst zu + # uebersetzen. + $tools = @() + + if (Test-Path $packager) { + foreach ($rid in $Runtimes) { + Write-Host "Baue pack-and-deploy fuer $rid ..." -ForegroundColor Cyan + + $ridOut = Join-Path $staging "packager-$rid" + + dotnet publish $packager ` + -c Release -r $rid ` + --self-contained true ` + -p:PublishSingleFile=true ` + -p:EnableCompressionInSingleFile=true ` + -p:DebugType=None ` + -o $ridOut ` + -v q --nologo + + if ($LASTEXITCODE -ne 0) { + throw "dotnet publish (pack-and-deploy) fuer $rid ist fehlgeschlagen." + } + + $isWindows = $rid.StartsWith('win') + $sourceName = if ($isWindows) { 'pack-and-deploy.exe' } else { 'pack-and-deploy' } + $targetName = if ($isWindows) { "pack-and-deploy-$rid.exe" } else { "pack-and-deploy-$rid" } + + $target = Join-Path $OutputDir $targetName + Copy-Item (Join-Path $ridOut $sourceName) $target -Force + + $hash = (Get-FileHash $target -Algorithm SHA256).Hash.ToLower() + [System.IO.File]::WriteAllText("$target.sha256", $hash) + + $tools += [ordered]@{ + platform = $rid + file = $targetName + sha256 = $hash + sizeBytes = (Get-Item $target).Length + } + + Write-Host (" {0,-26} {1,6:N1} MB {2}" -f $targetName, ((Get-Item $target).Length / 1MB), $hash.Substring(0, 16)) + } + } else { + Write-Warning "Packager-Projekt nicht gefunden - pack-and-deploy wird nicht mit ausgeliefert." + } + $manifest = [ordered]@{ tool = 'update-agent' version = $Version gitCommit = $gitCommit buildDateUtc = (Get-Date).ToUniversalTime().ToString('o') binaries = $binaries + tools = $tools } # Bewusst ueber WriteAllText mit einer BOM-freien Kodierung: Out-File