diff --git a/.gitignore b/.gitignore index 2ac8ff8..04312b8 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,10 @@ settings-password.json # Packager-Zugangsdaten (FTP, updateservice:publish-Token) – Vorlage in deploy/packager.config.example.json deploy/packager.config.json +# Release (scripts/release.ps1): geholte Werkzeuge samt Packager-Konfiguration und Publish-Ausgabe +.dc-tools/ +artifacts/ + # ── Logs & temporäre Dateien ───────────────────── *.log *.tmp diff --git a/Directory.Build.props b/Directory.Build.props index 070b4ad..9266498 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -17,7 +17,7 @@ docs/archiv/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md (D-4). --> - 0.1.0 + 0.2.0 diff --git a/scripts/publish-release.ps1 b/scripts/publish-release.ps1 new file mode 100644 index 0000000..5ab3580 --- /dev/null +++ b/scripts/publish-release.ps1 @@ -0,0 +1,91 @@ +<# +.SYNOPSIS + Veroeffentlicht PolyTrader im Deploymentcenter - der Aufruf fuer ein Release. + +.DESCRIPTION + Vorschalt-Skript fuer scripts/release.ps1. Die Vorlage des Deploymentcenters bleibt + unveraendert (so laesst sie sich bei einer neuen Fassung einfach ersetzen); was PolyTrader + zusaetzlich braucht, passiert hier: + + 1. update-agent je Zielplattform von /installer/ holen und gegen installer.json pruefen. + Das Publish-Ziel BundleUpdateAgent (PolyTrader.App.Avalonia.csproj) legt ihn neben die + Anwendung - ohne ihn kann sich eine Installation nie selbst aktualisieren. + 2. deploy/packager.config.json neben pack-and-deploy legen. Dort sucht das Werkzeug seine + Zugangsdaten, und dort stehen die excludePatterns, die appsettings.Local.json, master.key + und server_settings.xml aus dem Paket halten. Beides bleibt in .dc-tools/ (gitignored). + 3. DcRequireUpdateAgent=true setzen: fehlt der Agent, bricht das Publish ab statt ein Paket + ohne Update-Weg zu veroeffentlichen. + +.EXAMPLE + .\scripts\publish-release.ps1 -Version 0.2.0 -Changelog "WebUI-Paritaet, Passwortsperre" + +.EXAMPLE + .\scripts\publish-release.ps1 -WhatIf +#> + +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [string] $Version, + + [ValidateSet('prod', 'beta', 'dev')] + [string] $Channel = 'prod', + + [string] $Changelog +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' # PowerShell 5.1 laedt mit Fortschrittsbalken um ein Vielfaches langsamer + +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..') +$toolDir = Join-Path $repoRoot '.dc-tools' +$config = Get-Content (Join-Path $PSScriptRoot 'release.config.json') -Raw | ConvertFrom-Json +$baseUrl = if ($config.baseUrl) { $config.baseUrl.TrimEnd('/') } else { 'https://dc.mhdf.de' } + +New-Item -ItemType Directory -Force -Path $toolDir | Out-Null + +# ------------------------------------------------------------ 1. update-agent +$installer = Invoke-RestMethod -Uri "$baseUrl/installer/installer.json" -UseBasicParsing +Write-Host "update-agent $($installer.version) laut installer.json" -ForegroundColor DarkGray + +foreach ($rid in $config.runtimes) { + $name = if ($rid.StartsWith('win')) { "update-agent-$rid.exe" } else { "update-agent-$rid" } + $entry = $installer.binaries | Where-Object { $_.file -eq $name } + if (-not $entry) { throw "installer.json fuehrt keinen update-agent fuer $rid." } + + $expected = $entry.sha256.ToLower() + $target = Join-Path $toolDir $name + + if ((Test-Path $target) -and (Get-FileHash $target -Algorithm SHA256).Hash.ToLower() -eq $expected) { + Write-Host " $name vorhanden, Pruefsumme in Ordnung" -ForegroundColor DarkGray + continue + } + + Write-Host " $name wird geholt ..." -ForegroundColor Cyan + $temp = "$target.download" + Invoke-WebRequest -Uri "$baseUrl/installer/$name" -OutFile $temp -UseBasicParsing + $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 $target -Force + Write-Host " Pruefsumme in Ordnung" -ForegroundColor DarkGray +} + +# ---------------------------------------------------- 2. Packager-Konfiguration +$packagerConfig = Join-Path $repoRoot 'deploy/packager.config.json' +if (-not (Test-Path $packagerConfig)) { + throw "deploy/packager.config.json fehlt. Vorlage: deploy/packager.config.example.json" +} +Copy-Item $packagerConfig (Join-Path $toolDir 'packager.config.json') -Force + +# ----------------------------------------------------------- 3. Release-Vorlage +$env:DcRequireUpdateAgent = 'true' +try { + & (Join-Path $PSScriptRoot 'release.ps1') @PSBoundParameters + $code = $LASTEXITCODE +} +finally { + Remove-Item Env:DcRequireUpdateAgent -ErrorAction SilentlyContinue +} +exit $code diff --git a/scripts/release.config.json b/scripts/release.config.json new file mode 100644 index 0000000..2f0a42f --- /dev/null +++ b/scripts/release.config.json @@ -0,0 +1,11 @@ +{ + "_comment": "Konfiguration fuer scripts/release.ps1 (unveraenderte Vorlage des Deploymentcenters). Enthaelt keine Zugangsdaten. Aufgerufen wird nicht release.ps1 direkt, sondern scripts/publish-release.ps1 - das holt vorher den update-agent und legt die Packager-Konfiguration neben das Werkzeug.", + "project": "polytrader", + "csproj": "src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj", + "runtimes": ["win-x64", "linux-x64"], + "selfContained": true, + "publishSingleFile": false, + "setupJson": "src/PolyTrader.App.Avalonia/setup.json", + "mainAssembly": "PolyTrader.App.Avalonia.dll", + "baseUrl": "https://dc.mhdf.de" +} diff --git a/scripts/release.ps1 b/scripts/release.ps1 new file mode 100644 index 0000000..1be4f08 --- /dev/null +++ b/scripts/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/src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj b/src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj index 39a44f3..a6e1b32 100644 --- a/src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj +++ b/src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj @@ -83,4 +83,24 @@ + + + + <_DcAgentExt Condition="$(RuntimeIdentifier.StartsWith('win'))">.exe + <_DcAgentSource>$(MSBuildThisFileDirectory)..\..\.dc-tools\update-agent-$(RuntimeIdentifier)$(_DcAgentExt) + + + + + + +