param( [switch]$NoVersionBump, [switch]$SkipPublish, [switch]$SkipGit, [string]$TargetVersion = "" ) $ErrorActionPreference = "Stop" $Root = Split-Path -Parent $MyInvocation.MyCommand.Path Set-Location -LiteralPath $Root function Find-Python { foreach ($candidate in @("python", "py")) { try { & $candidate --version 2>$null | Out-Null if ($LASTEXITCODE -eq 0) { return $candidate } } catch { } } throw "Python wurde nicht gefunden." } function Find-InnoCompiler { $candidates = @( "$env:LOCALAPPDATA\Programs\Inno Setup 7\ISCC.exe", "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe", "$env:ProgramFiles\Inno Setup 7\ISCC.exe", "${env:ProgramFiles(x86)}\Inno Setup 7\ISCC.exe", "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", "$env:ProgramFiles\Inno Setup 6\ISCC.exe" ) foreach ($candidate in $candidates) { if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate } } throw "Inno Setup 6 oder 7 wurde nicht gefunden." } function Write-Utf8NoBom { param([string]$LiteralPath, [string]$Content) [System.IO.File]::WriteAllText($LiteralPath, $Content, [System.Text.UTF8Encoding]::new($false)) } function Get-Sha256Hex { param([string]$LiteralPath) return (Get-FileHash -LiteralPath $LiteralPath -Algorithm SHA256).Hash.ToLowerInvariant() } $VersionPath = Join-Path $Root "version.txt" $PyprojectPath = Join-Path $Root "pyproject.toml" $ChangelogPath = Join-Path $Root "CHANGELOG.md" $originalVersion = [System.IO.File]::ReadAllText($VersionPath, [System.Text.Encoding]::UTF8) $originalPyproject = [System.IO.File]::ReadAllText($PyprojectPath, [System.Text.Encoding]::UTF8) $currentVersion = $originalVersion.Trim() if ($currentVersion -notmatch '^\d+\.\d+\.\d+$') { throw "Ungültige Version in version.txt: $currentVersion" } $appVersion = $currentVersion if (-not [string]::IsNullOrWhiteSpace($TargetVersion)) { if ($NoVersionBump) { throw "TargetVersion und NoVersionBump dürfen nicht gemeinsam verwendet werden." } if ($TargetVersion -notmatch '^\d+\.\d+\.\d+$') { throw "Ungültige Zielversion: $TargetVersion" } if ([version]$TargetVersion -le [version]$currentVersion) { throw "Die Zielversion $TargetVersion muss neuer als $currentVersion sein." } $appVersion = $TargetVersion } elseif (-not $NoVersionBump) { $parts = @($currentVersion.Split('.') | ForEach-Object { [int]$_ }) $parts[2] += 1 $appVersion = "$($parts[0]).$($parts[1]).$($parts[2])" } $changelog = [System.IO.File]::ReadAllText($ChangelogPath, [System.Text.Encoding]::UTF8) $sectionPattern = '(?ms)^##\s+' + [regex]::Escape($appVersion) + '\s+.*?(?=^##\s+\d+\.\d+\.\d+\s+|\z)' $sectionMatch = [regex]::Match($changelog, $sectionPattern) if (-not $sectionMatch.Success) { throw "CHANGELOG.md enthält keinen Abschnitt für Version $appVersion." } $releaseNotes = "# SyncVito – Änderungsprotokoll`r`n`r`n$($sectionMatch.Value.Trim())`r`n" $versionParts = @($appVersion.Split('.') | ForEach-Object { [int]$_ }) $fileVersion = "$($versionParts[0]).$($versionParts[1]).$($versionParts[2]).0" $python = Find-Python $iscc = Find-InnoCompiler $sourceCommitted = $false try { Write-Utf8NoBom -LiteralPath $VersionPath -Content "$appVersion`n" $versionRegex = [regex]::new('(?m)^(version\s*=\s*")[^"]+("\s*)$') $updatedPyproject = $versionRegex.Replace($originalPyproject, "`${1}$appVersion`${2}", 1) if ($updatedPyproject -eq $originalPyproject -and $appVersion -ne $currentVersion) { throw "Die Projektversion in pyproject.toml konnte nicht aktualisiert werden." } Write-Utf8NoBom -LiteralPath $PyprojectPath -Content $updatedPyproject $buildRoot = Join-Path $Root "build" $appDist = Join-Path $buildRoot "app" $workPath = Join-Path $buildRoot "pyinstaller" New-Item -ItemType Directory -Force -Path $buildRoot, $appDist | Out-Null $versionInfoPath = Join-Path $buildRoot "version_info.txt" $versionInfo = @" VSVersionInfo( ffi=FixedFileInfo(filevers=($($versionParts[0]), $($versionParts[1]), $($versionParts[2]), 0), prodvers=($($versionParts[0]), $($versionParts[1]), $($versionParts[2]), 0), mask=0x3f, flags=0x0, OS=0x40004, fileType=0x1, subtype=0x0, date=(0, 0)), kids=[StringFileInfo([StringTable('040704B0', [StringStruct('CompanyName', 'Davito'), StringStruct('FileDescription', 'SyncVito'), StringStruct('FileVersion', '$fileVersion'), StringStruct('InternalName', 'SyncVito'), StringStruct('OriginalFilename', 'SyncVito.exe'), StringStruct('ProductName', 'SyncVito'), StringStruct('ProductVersion', '$appVersion')])]), VarFileInfo([VarStruct('Translation', [1031, 1200])])] ) "@ Write-Utf8NoBom -LiteralPath $versionInfoPath -Content $versionInfo Write-Host "Tests werden ausgeführt …" & $python -m pytest if ($LASTEXITCODE -ne 0) { throw "Die Tests sind fehlgeschlagen." } & $python -m ruff check . if ($LASTEXITCODE -ne 0) { throw "Ruff hat Fehler gefunden." } Write-Host "Programmsymbol wird erzeugt …" & $python tools\generate_icon.py if ($LASTEXITCODE -ne 0) { throw "Das Programmsymbol konnte nicht erzeugt werden." } Write-Host "Baue SyncVito.exe mit PyInstaller …" & $python -m PyInstaller --noconfirm --clean --distpath $appDist --workpath $workPath SyncVito.spec if ($LASTEXITCODE -ne 0) { throw "Der PyInstaller-Build ist fehlgeschlagen." } $appExe = Join-Path $appDist "SyncVito.exe" if (-not (Test-Path -LiteralPath $appExe -PathType Leaf)) { throw "SyncVito.exe wurde nicht erzeugt." } Write-Host "Prüfe den gebauten Programmstart …" $smoke = Start-Process -FilePath $appExe -ArgumentList "--smoke-test" -PassThru -Wait -WindowStyle Hidden if ($smoke.ExitCode -ne 0) { throw "Der EXE-Smoke-Test ist fehlgeschlagen (Exitcode $($smoke.ExitCode))." } New-Item -ItemType Directory -Force -Path (Join-Path $Root "release") | Out-Null Write-Host "Baue Windows-Installer …" & $iscc "/DMyAppVersion=$appVersion" "/DMyAppFileVersion=$fileVersion" "/DMyAppExeSource=$appExe" (Join-Path $Root "installer\SyncVito.iss") if ($LASTEXITCODE -ne 0) { throw "Der Inno-Setup-Build ist fehlgeschlagen." } $installerName = "SyncVitoSetup-$appVersion.exe" $installerPath = Join-Path $Root "release\$installerName" if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf)) { throw "Installer wurde nicht gefunden: $installerPath" } $hash = Get-Sha256Hex -LiteralPath $installerPath $checksumPath = "$installerPath.sha256" Write-Utf8NoBom -LiteralPath $checksumPath -Content "$hash *$installerName`n" $releaseChangelogPath = Join-Path $Root "release\CHANGELOG-$appVersion.md" Write-Utf8NoBom -LiteralPath $releaseChangelogPath -Content $releaseNotes if (-not $SkipGit) { git add --all if ($LASTEXITCODE -ne 0) { throw "Git konnte die Release-Änderungen nicht vormerken." } git diff --cached --quiet if ($LASTEXITCODE -ne 0) { git commit -m "Release SyncVito $appVersion" if ($LASTEXITCODE -ne 0) { throw "Der Release-Commit ist fehlgeschlagen." } } $sourceCommitted = $true git push origin main if ($LASTEXITCODE -ne 0) { throw "Der Quellstand konnte nicht nach Gitea gepusht werden." } } if (-not $SkipPublish) { & (Join-Path $Root "publish_gitea_release.ps1") -Version $appVersion if ($LASTEXITCODE -ne 0) { throw "Der Gitea-Release ist fehlgeschlagen." } } Write-Host "" Write-Host "Release-Workflow abgeschlossen:" Write-Host " Installer: $installerPath" Write-Host " Prüfsumme: $checksumPath" Write-Host " SHA256: $hash" } catch { if (-not $sourceCommitted) { Write-Utf8NoBom -LiteralPath $VersionPath -Content $originalVersion Write-Utf8NoBom -LiteralPath $PyprojectPath -Content $originalPyproject Write-Warning "Build fehlgeschlagen; Versionsdateien wurden zurückgesetzt." } throw }