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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
56b1d2631a
commit
e01a608c08
@@ -10,6 +10,7 @@ Monitoring und einen Bugtracker, den Coding-Agenten selbständig bedienen.
|
|||||||
| Dokument | Wofür |
|
| Dokument | Wofür |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **[UPGRADE.md](./UPGRADE.md)** | **Ablaufplan für die Umstellung auf 2.0.** Enthält Pflichtschritte: Zugangsdaten wechseln, Migration, Evaluator-Cron. |
|
| **[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 |
|
| [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/` |
|
| [Agenten-Handbuch](../public/docs/bugtracker.md) | Vollständige Beschreibung des Bugtracker-Workflows, öffentlich unter `/docs/` |
|
||||||
|
|
||||||
|
|||||||
@@ -233,6 +233,20 @@ für die API-Antwort. `UpdateCheckResult.LatestRelease` ist in beiden Fällen ei
|
|||||||
|
|
||||||
## 3. Packaging & Deployment CLI (`pack-and-deploy`)
|
## 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.
|
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:
|
### Aufruf-Beispiel:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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 <rid> -> pack-and-deploy --platform <rid>
|
||||||
|
|
||||||
|
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 <Version> 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), '<Version>\s*([^<]+?)\s*</Version>')
|
||||||
|
if ($match.Success) {
|
||||||
|
return $match.Groups[1].Value.Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $Version) {
|
||||||
|
$Version = Get-ProjectVersion
|
||||||
|
if (-not $Version) {
|
||||||
|
throw "Keine <Version> 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
|
||||||
@@ -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 <rid> -> pack-and-deploy --platform <rid>
|
||||||
|
#
|
||||||
|
# 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 <Version> 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:.*<Version>\s*\([^<]*\)\s*</Version>.*:\1:p' "$candidate" | head -1 | tr -d '[:space:]')"
|
||||||
|
[ -n "$VERSION" ] && break
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$VERSION" ] || {
|
||||||
|
echo "FEHLER: Keine <Version> 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."
|
||||||
@@ -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
|
||||||
|
|
||||||
|
### `<Version>` in die `Directory.Build.props`
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<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` 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/<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
|
||||||
|
|
||||||
|
```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 | `<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 |
|
||||||
@@ -24,6 +24,7 @@ $ErrorActionPreference = 'Stop'
|
|||||||
|
|
||||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..')
|
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..')
|
||||||
$project = Join-Path $repoRoot 'client-dotnet\Deploymentcenter.UpdateAgent\Deploymentcenter.UpdateAgent.csproj'
|
$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'))
|
$staging = Join-Path ([System.IO.Path]::GetTempPath()) ("dc-installer-build-" + [guid]::NewGuid().ToString('N'))
|
||||||
|
|
||||||
if (-not (Test-Path $project)) {
|
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))
|
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]@{
|
$manifest = [ordered]@{
|
||||||
tool = 'update-agent'
|
tool = 'update-agent'
|
||||||
version = $Version
|
version = $Version
|
||||||
gitCommit = $gitCommit
|
gitCommit = $gitCommit
|
||||||
buildDateUtc = (Get-Date).ToUniversalTime().ToString('o')
|
buildDateUtc = (Get-Date).ToUniversalTime().ToString('o')
|
||||||
binaries = $binaries
|
binaries = $binaries
|
||||||
|
tools = $tools
|
||||||
}
|
}
|
||||||
|
|
||||||
# Bewusst ueber WriteAllText mit einer BOM-freien Kodierung: Out-File
|
# Bewusst ueber WriteAllText mit einer BOM-freien Kodierung: Out-File
|
||||||
|
|||||||
Reference in New Issue
Block a user