288 lines
10 KiB
PowerShell
288 lines
10 KiB
PowerShell
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 { "<none>" }
|
|
|
|
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
|
|
|
|
$artifactsToUpload = New-Object System.Collections.Generic.List[string]
|
|
foreach ($artifact in $artifactPaths) {
|
|
$assetName = [System.IO.Path]::GetFileName($artifact)
|
|
$extension = [System.IO.Path]::GetExtension($artifact).ToLowerInvariant()
|
|
|
|
if ($extension -eq ".msi" -and $assetMap.ContainsKey($assetName)) {
|
|
$localSize = (Get-Item $artifact).Length
|
|
$remoteSize = [int64]$assetMap[$assetName].size
|
|
if ($localSize -eq $remoteSize) {
|
|
Write-Host "[OK] Skipping already uploaded MSI $assetName ($([math]::Round($localSize / 1MB, 2)) MB)" -ForegroundColor Cyan
|
|
continue
|
|
}
|
|
}
|
|
|
|
$artifactsToUpload.Add($artifact)
|
|
}
|
|
|
|
if ($artifactsToUpload.Count -eq 0) {
|
|
Write-Host "[OK] All release assets already uploaded." -ForegroundColor Green
|
|
Write-Host "View at: $ForgejoUrl/$Repo/releases/tag/v$Version" -ForegroundColor Cyan
|
|
exit 0
|
|
}
|
|
|
|
# Use Python requests library for more reliable large file uploads
|
|
$pythonUploadScript = @"
|
|
import sys
|
|
import requests
|
|
from requests.auth import HTTPBasicAuth
|
|
from pathlib import Path
|
|
import time
|
|
|
|
upload_url = sys.argv[1]
|
|
artifacts = sys.argv[2:]
|
|
username = '$ForgejoUser'
|
|
password = '$ForgejoPW'
|
|
delete_url_template = '${ForgejoUrl}/api/v1/repos/${Repo}/releases/$releaseId/assets/{}'
|
|
release_info_url = '${ForgejoUrl}/api/v1/repos/${Repo}/releases/$releaseId'
|
|
|
|
session = requests.Session()
|
|
session.auth = HTTPBasicAuth(username, password)
|
|
session.headers.update({'Connection': 'close'})
|
|
|
|
def upload_with_retry(artifact_path, max_retries=3):
|
|
asset_name = Path(artifact_path).name
|
|
|
|
# Check if asset already exists and delete it
|
|
try:
|
|
release_response = session.get(release_info_url, timeout=30)
|
|
release_response.raise_for_status()
|
|
for asset in release_response.json().get('assets', []):
|
|
if asset['name'] == asset_name:
|
|
delete_resp = session.delete(delete_url_template.format(asset['id']), timeout=30)
|
|
delete_resp.raise_for_status()
|
|
print(f'[OK] Replaced existing asset {asset_name}', file=sys.stderr)
|
|
break
|
|
except Exception as e:
|
|
print(f'Warning checking existing assets: {e}', file=sys.stderr)
|
|
|
|
# Upload file with streaming and retries
|
|
retryable_status_codes = {429, 502, 503, 504}
|
|
for attempt in range(max_retries):
|
|
try:
|
|
if attempt > 0:
|
|
print(f' Retry {attempt} of {max_retries}...', file=sys.stderr)
|
|
time.sleep(min(10, 2 * attempt))
|
|
|
|
with open(artifact_path, 'rb') as f:
|
|
files = {'attachment': (asset_name, f)}
|
|
response = session.post(
|
|
upload_url,
|
|
files=files,
|
|
timeout=900, # 15 minute timeout
|
|
stream=False
|
|
)
|
|
|
|
if response.status_code in [200, 201]:
|
|
print(f'[OK] Uploaded {asset_name}')
|
|
return True
|
|
|
|
if response.status_code in retryable_status_codes:
|
|
if attempt >= max_retries - 1:
|
|
print(f'ERROR uploading {asset_name} (HTTP {response.status_code} after {max_retries} retries)')
|
|
print(response.text)
|
|
sys.exit(1)
|
|
continue
|
|
|
|
print(f'ERROR uploading {asset_name} (HTTP {response.status_code})')
|
|
print(response.text)
|
|
sys.exit(1)
|
|
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
|
|
if attempt >= max_retries - 1:
|
|
print(f'ERROR uploading {asset_name}: Connection failed after {max_retries} retries')
|
|
print(str(e))
|
|
sys.exit(1)
|
|
time.sleep(min(10, 2 * (attempt + 1)))
|
|
except Exception as e:
|
|
print(f'ERROR uploading {asset_name}: {e}')
|
|
sys.exit(1)
|
|
|
|
for artifact_path in artifacts:
|
|
upload_with_retry(artifact_path)
|
|
|
|
print(f'[OK] All files uploaded successfully')
|
|
"@
|
|
|
|
$uploadScriptPath = ([System.IO.Path]::GetTempFileName() -replace 'tmp$', 'py')
|
|
Set-Content -Path $uploadScriptPath -Value $pythonUploadScript -Encoding UTF8
|
|
|
|
try {
|
|
$uploadUrl = "$ForgejoUrl/api/v1/repos/$Repo/releases/$releaseId/assets"
|
|
& $pythonExe $uploadScriptPath $uploadUrl @artifactsToUpload
|
|
if ($LASTEXITCODE -ne 0) {
|
|
exit 1
|
|
}
|
|
}
|
|
finally {
|
|
Remove-Item $uploadScriptPath -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Write-Host "`n[OK] Release complete!" -ForegroundColor Green
|
|
Write-Host "View at: $ForgejoUrl/$Repo/releases/tag/v$Version" -ForegroundColor Cyan
|