<# .SYNOPSIS Upload pre-built NSIS artifacts to a Gitea release. .DESCRIPTION Uploads installer + blockmap + latest.yml to the release identified by -Tag. If the release does not exist it is created (only for semver-looking tags; for non-semver tags like 'update-channel' the release must exist already). Same-named existing assets are replaced. .PARAMETER Tag Release tag to upload INTO. May be a version tag (v0.5.1) or a channel tag (update-channel). Defaults to v. .PARAMETER AssetVersion Version of the artifacts being uploaded (e.g. 0.5.1). Defaults to the numeric part of -Tag. Specify explicitly when uploading version-X.Y.Z artifacts into a non-version tag (channel or bridge). .EXAMPLE pwsh scripts/upload-release-assets.ps1 pwsh scripts/upload-release-assets.ps1 -Tag v0.5.0 pwsh scripts/upload-release-assets.ps1 -Tag update-channel -AssetVersion 0.5.1 pwsh scripts/upload-release-assets.ps1 -Tag v0.4.0 -AssetVersion 0.5.1 #> param( [string]$Tag, [string]$AssetVersion ) $ErrorActionPreference = 'Stop' $repoOwner = 'AnRil' $repoName = 'laude' $giteaHost = 'xn--90adajar8af4h.xn--p1ai/git' $apiBase = "https://$giteaHost/api/v1" if (-not $env:GITEA_TOKEN) { Write-Error "GITEA_TOKEN not set. Set it via [Environment]::SetEnvironmentVariable('GITEA_TOKEN', '', 'User') and open a new PowerShell session." exit 1 } $root = Resolve-Path (Join-Path $PSScriptRoot '..') Set-Location $root if (-not $Tag) { $pkgVersion = (Get-Content package.json | ConvertFrom-Json).version $Tag = "v$pkgVersion" } if (-not $AssetVersion) { # Derive from tag when possible (vX.Y.Z -> X.Y.Z); otherwise read package.json. if ($Tag -match '^v\d+\.\d+\.\d+') { $AssetVersion = $Tag.TrimStart('v') } else { $AssetVersion = (Get-Content package.json | ConvertFrom-Json).version } } $version = $AssetVersion $installer = Join-Path 'release' "Exercise-Reminder-Setup-$version.exe" $blockmap = "$installer.blockmap" $manifest = Join-Path 'release' 'latest.yml' foreach ($f in @($installer, $blockmap, $manifest)) { if (-not (Test-Path $f)) { Write-Error "Artifact not found: $f. Build first with: npm run dist" exit 1 } } $headers = @{ Authorization = "token $env:GITEA_TOKEN" Accept = 'application/json' } # --- Find or create release ---------------------------------------------- Write-Host "Looking for existing release $Tag..." -ForegroundColor Cyan $release = $null try { $release = Invoke-RestMethod ` -Uri "$apiBase/repos/$repoOwner/$repoName/releases/tags/$Tag" ` -Method Get ` -Headers $headers Write-Host " Found release id=$($release.id)" -ForegroundColor DarkGray } catch { if ($_.Exception.Response.StatusCode.value__ -eq 404) { if ($Tag -notmatch '^v\d+\.\d+\.\d+') { Write-Error "Release '$Tag' not found and tag is not semver. Create it manually on Gitea (e.g. 'update-channel' is a one-time setup)." exit 1 } Write-Host " Not found, creating new release..." -ForegroundColor DarkGray $prev = $null try { $prevTagOutput = & git describe --tags --abbrev=0 "$Tag^" 2>$null if ($LASTEXITCODE -eq 0 -and $prevTagOutput) { $prev = $prevTagOutput.Trim() } } catch { $prev = $null } if ($prev) { $log = (& git log --pretty=format:"- %s" "$prev..$Tag") -join "`n" } else { # No prior tag - list last 10 commits up to this tag. $log = (& git log --pretty=format:"- %s" -n 10 "$Tag") -join "`n" } $body = "### Changes`n`n$log`n`n---`n`nInstaller below: run it; if app is already installed, it updates in place and keeps your settings." $payload = @{ tag_name = $Tag name = "Exercise Reminder $Tag" body = $body draft = $false prerelease = $false } | ConvertTo-Json -Depth 5 $release = Invoke-RestMethod ` -Uri "$apiBase/repos/$repoOwner/$repoName/releases" ` -Method Post ` -Headers $headers ` -Body $payload ` -ContentType 'application/json' Write-Host " Created release id=$($release.id)" -ForegroundColor Green } else { throw } } # --- Delete existing assets with same names (to allow re-upload) --------- $existing = Invoke-RestMethod ` -Uri "$apiBase/repos/$repoOwner/$repoName/releases/$($release.id)/assets" ` -Method Get ` -Headers $headers foreach ($asset in @($installer, $blockmap, $manifest)) { $name = Split-Path $asset -Leaf $found = $existing | Where-Object { $_.name -eq $name } if ($found) { Write-Host "Removing existing asset $name (id=$($found.id))..." -ForegroundColor Yellow Invoke-RestMethod ` -Uri "$apiBase/repos/$repoOwner/$repoName/releases/$($release.id)/assets/$($found.id)" ` -Method Delete ` -Headers $headers | Out-Null } } # --- Upload assets ------------------------------------------------------- # Use curl.exe (bundled with Win10+) because Invoke-RestMethod in PS 5.1 # chokes on large multipart uploads (>50MB) over slower connections. $curlCmd = Get-Command curl.exe -ErrorAction SilentlyContinue if ($curlCmd) { $curl = $curlCmd.Source } else { $curl = "$env:SystemRoot\System32\curl.exe" if (-not (Test-Path $curl)) { Write-Error "curl.exe not found. Install via 'winget install curl' or add to PATH." exit 1 } } $maxRetries = 4 $backoffs = @(15, 45, 120, 300) # seconds between attempts foreach ($asset in @($installer, $blockmap, $manifest)) { $name = Split-Path $asset -Leaf $size = (Get-Item $asset).Length $uri = "$apiBase/repos/$repoOwner/$repoName/releases/$($release.id)/assets?name=$([uri]::EscapeDataString($name))" $attempt = 0 $uploaded = $false while (-not $uploaded -and $attempt -le $maxRetries) { if ($attempt -gt 0) { $wait = $backoffs[[Math]::Min($attempt - 1, $backoffs.Length - 1)] Write-Host (" Retrying in {0}s (attempt {1}/{2})..." -f $wait, ($attempt + 1), ($maxRetries + 1)) -ForegroundColor Yellow Start-Sleep -Seconds $wait # Re-check whether prior attempt actually succeeded server-side before # 504-ing the client. If asset is already there, treat as success. try { $check = Invoke-RestMethod ` -Uri "$apiBase/repos/$repoOwner/$repoName/releases/$($release.id)/assets" ` -Method Get -Headers $headers $existing = $check | Where-Object { $_.name -eq $name } if ($existing -and $existing.size -eq $size) { Write-Host " Asset already present server-side ($($existing.size) bytes) - skipping retry." -ForegroundColor DarkGray $uploaded = $true break } # If asset is present but with wrong size (half-uploaded), delete first. if ($existing) { Write-Host " Removing partial asset id=$($existing.id) ($($existing.size) bytes) before retry..." -ForegroundColor DarkGray Invoke-RestMethod ` -Uri "$apiBase/repos/$repoOwner/$repoName/releases/$($release.id)/assets/$($existing.id)" ` -Method Delete -Headers $headers | Out-Null } } catch { # If the list call itself fails, just proceed with the retry. } } Write-Host ("Uploading {0} ({1:N1} MB)..." -f $name, ($size / 1MB)) -ForegroundColor Cyan & $curl ` --fail-with-body ` --silent --show-error ` --connect-timeout 30 ` --max-time 900 ` -H "Authorization: token $env:GITEA_TOKEN" ` -H "Content-Type: application/octet-stream" ` --data-binary "@$asset" ` $uri if ($LASTEXITCODE -eq 0) { $uploaded = $true } else { Write-Host " curl exit $LASTEXITCODE - will retry." -ForegroundColor Yellow $attempt++ } } if (-not $uploaded) { Write-Error "Upload failed for $name after $($maxRetries + 1) attempts." exit 1 } } $releaseUrl = "https://$giteaHost/$repoOwner/$repoName/releases/tag/$Tag" Write-Host "" Write-Host "Release assets uploaded" -ForegroundColor Green Write-Host " $releaseUrl"