param( [Parameter(Mandatory = $false)] [string]$Version, [Parameter(Mandatory = $false)] [string[]]$Brands, [Parameter(Mandatory = $false)] [string]$ForgejoUser, [Parameter(Mandatory = $false)] [string]$ForgejoPW, [switch]$ClearCredentials, [switch]$DryRun, [string]$ForgejoUrl = "https://git.him-tools.de", [string]$Repo = "HIM-public/webdrop-bridge" ) $ErrorActionPreference = "Stop" $projectRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..") $pythonExe = Join-Path $projectRoot ".venv\Scripts\python.exe" if (-not (Test-Path $pythonExe)) { $pythonExe = "python" } $brandHelper = Join-Path $projectRoot "build\scripts\brand_config.py" $manifestOutput = Join-Path $projectRoot "build\dist\release-manifest.json" $localManifestPath = Join-Path $projectRoot "build\dist\release-manifest.local.json" $existingManifestPath = Join-Path $projectRoot "build\dist\release-manifest.existing.json" function Get-CurrentVersion { return (& $pythonExe -c "from pathlib import Path; import sys; sys.path.insert(0, str(Path(r'$projectRoot/build/scripts').resolve())); from version_utils import get_current_version; print(get_current_version())").Trim() } function Get-LocalReleaseData { $arguments = @($brandHelper, "local-release-data", "--platform", "windows", "--version", $Version) if ($Brands) { $arguments += "--brands" $arguments += $Brands } return (& $pythonExe @arguments | ConvertFrom-Json) } function Get-AssetMap { param([object[]]$Assets) $map = @{} foreach ($asset in ($Assets | Where-Object { $_ })) { $map[$asset.name] = $asset } return $map } if ($ClearCredentials) { Remove-Item env:FORGEJO_USER -ErrorAction SilentlyContinue Remove-Item env:FORGEJO_PASS -ErrorAction SilentlyContinue Write-Host "[OK] Credentials cleared from this session" -ForegroundColor Green exit 0 } if (-not $Version) { $Version = Get-CurrentVersion } $localData = Get-LocalReleaseData $artifactPaths = New-Object System.Collections.Generic.List[string] foreach ($artifact in $localData.artifacts) { $artifactPaths.Add([string]$artifact) if ((Test-Path $artifact) -and ((Get-Item $artifact).Extension -eq ".msi")) { $msiSize = (Get-Item $artifact).Length / 1MB Write-Host "Windows artifact: $([System.IO.Path]::GetFileName($artifact)) ($([math]::Round($msiSize, 2)) MB)" } } if ($artifactPaths.Count -eq 0) { Write-Host "ERROR: No local Windows artifacts found" -ForegroundColor Red exit 1 } $localManifestJson = $localData.manifest | ConvertTo-Json -Depth 10 [System.IO.File]::WriteAllText($localManifestPath, $localManifestJson, (New-Object System.Text.UTF8Encoding($false))) if ($DryRun) { Copy-Item $localManifestPath $manifestOutput -Force $brandsText = if ($localData.brands.Count -gt 0) { $localData.brands -join ", " } else { "" } Write-Host "[DRY RUN] No network requests or uploads will be performed." -ForegroundColor Yellow Write-Host "[DRY RUN] Release tag: v$Version" Write-Host "[DRY RUN] Release URL: $ForgejoUrl/$Repo/releases/tag/v$Version" Write-Host "[DRY RUN] Discovered brands: $brandsText" Write-Host "[DRY RUN] Artifacts that would be uploaded:" foreach ($artifact in $artifactPaths) { Write-Host " - $artifact" } Write-Host "[DRY RUN] Local manifest preview: $manifestOutput" exit 0 } if (-not $ForgejoUser) { $ForgejoUser = $env:FORGEJO_USER } if (-not $ForgejoPW) { $ForgejoPW = $env:FORGEJO_PASS } if (-not $ForgejoUser -or -not $ForgejoPW) { Write-Host "Forgejo credentials not found. Enter your credentials:" -ForegroundColor Yellow if (-not $ForgejoUser) { $ForgejoUser = Read-Host "Username" } if (-not $ForgejoPW) { $securePass = Read-Host "Password" -AsSecureString $ForgejoPW = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($securePass)) } $env:FORGEJO_USER = $ForgejoUser $env:FORGEJO_PASS = $ForgejoPW } $auth = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("${ForgejoUser}:${ForgejoPW}")) $headers = @{ "Authorization" = "Basic $auth" "Content-Type" = "application/json" } $releaseLookupUrl = "$ForgejoUrl/api/v1/repos/$Repo/releases/tags/v$Version" $releaseUrl = "$ForgejoUrl/api/v1/repos/$Repo/releases" $releaseData = @{ tag_name = "v$Version" name = "WebDropBridge v$Version" body = "Shared branded release for WebDrop Bridge v$Version" draft = $false prerelease = $false } | ConvertTo-Json try { $releaseInfo = Invoke-RestMethod -Uri $releaseLookupUrl -Method GET -Headers $headers -TimeoutSec 30 -ErrorAction Stop $releaseId = $releaseInfo.id Write-Host "[OK] Using existing release (ID: $releaseId)" -ForegroundColor Green } catch { $releaseInfo = Invoke-RestMethod -Uri $releaseUrl -Method POST -Headers $headers -Body $releaseData -TimeoutSec 30 -ErrorAction Stop $releaseId = $releaseInfo.id Write-Host "[OK] Release created (ID: $releaseId)" -ForegroundColor Green } $assetMap = Get-AssetMap -Assets $releaseInfo.assets if ($assetMap.ContainsKey("release-manifest.json")) { Invoke-WebRequest -Uri $assetMap["release-manifest.json"].browser_download_url -Method GET -Headers $headers -TimeoutSec 30 -OutFile $existingManifestPath | Out-Null & $pythonExe $brandHelper merge-manifests --base $existingManifestPath --overlay $localManifestPath --output $manifestOutput | Out-Null } else { Copy-Item $localManifestPath $manifestOutput -Force } # Ensure uploaded manifest is UTF-8 without BOM (for strict JSON parsers) if (Test-Path $manifestOutput) { $manifestText = Get-Content -Raw -Path $manifestOutput [System.IO.File]::WriteAllText($manifestOutput, $manifestText, (New-Object System.Text.UTF8Encoding($false))) } $artifactPaths.Add($manifestOutput) $assetMap = Get-AssetMap -Assets $releaseInfo.assets $curlAuth = "$ForgejoUser`:$ForgejoPW" $uploadUrl = "$ForgejoUrl/api/v1/repos/$Repo/releases/$releaseId/assets" foreach ($artifact in $artifactPaths) { $assetName = [System.IO.Path]::GetFileName($artifact) if ($assetMap.ContainsKey($assetName)) { $existingAsset = $assetMap[$assetName] Invoke-RestMethod -Uri "$ForgejoUrl/api/v1/repos/$Repo/releases/$releaseId/assets/$($existingAsset.id)" -Method DELETE -Headers $headers -TimeoutSec 30 | Out-Null Write-Host "[OK] Replaced existing asset $assetName" -ForegroundColor Yellow } $response = curl.exe -s -X POST -u $curlAuth -F "attachment=@$artifact" $uploadUrl if ($response -like "*error*" -or $response -like "*404*") { Write-Host "WARNING: Could not upload $artifact : $response" -ForegroundColor Yellow } else { Write-Host "[OK] Uploaded $assetName" -ForegroundColor Green } } Write-Host "`n[OK] Release complete!" -ForegroundColor Green Write-Host "View at: $ForgejoUrl/$Repo/releases/tag/v$Version" -ForegroundColor Cyan