Files
punktfunk/scripts/ci/gitea-release.ps1
T
enricobuehlerandClaude Opus 4.8 15d09a8c10 feat(ci/release): author release notes in-repo + announce stable releases to Discord
Release notes now live in the repo at docs/releases/vX.Y.Z.md, authored during the
version bump (before the tag). ensure_release (gitea-release.sh + .ps1 twin) seeds the
Gitea release body from that file in the create POST, so the release is born WITH its
notes instead of being created empty and PATCHed afterward. canary/rc have no file ->
empty body, unchanged.

New manual `announce` workflow (workflow_dispatch, tag input) re-syncs the notes file
over the live release and posts a violet embed (title, notes lead-in, release-page link)
to the Discord #releases channel via the DISCORD_RELEASE_WEBHOOK secret. Stable-only:
a -rc tag is refused unless allow_prerelease=true. Pressing "go" is the quality gate, so
a half-built or failed release is never announced.

docs/releases/README.md documents the ritual; TEMPLATE.md is the skeleton.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:43:18 +02:00

83 lines
4.1 KiB
PowerShell

# Shared Gitea Release helpers for the punktfunk Windows CI workflows (pwsh / PowerShell 7).
#
# Dot-source it, then call Ensure-GiteaRelease / Upsert-GiteaAsset:
# . scripts/ci/gitea-release.ps1
# Mirrors scripts/ci/gitea-release.sh; parses JSON with ConvertFrom-Json (the Windows runner
# has no python). Same idempotent semantics: Upsert-GiteaAsset deletes an existing asset of
# the same name before uploading, so re-runs / rolling canary uploads don't 409.
#
# Env (Gitea Actions sets the first two automatically):
# GITHUB_SERVER_URL e.g. https://git.unom.io
# GITHUB_REPOSITORY e.g. unom/punktfunk
# GITEA_TOKEN a PAT with repository (release) write scope — set from secrets.REGISTRY_TOKEN
# (must carry write:repository, not only write:package)
$ErrorActionPreference = 'Stop'
function _GiteaApi {
if (-not $env:GITHUB_SERVER_URL) { throw 'GITHUB_SERVER_URL unset' }
if (-not $env:GITHUB_REPOSITORY) { throw 'GITHUB_REPOSITORY unset' }
"$($env:GITHUB_SERVER_URL)/api/v1/repos/$($env:GITHUB_REPOSITORY)"
}
function _GiteaHeaders {
if (-not $env:GITEA_TOKEN) { throw 'GITEA_TOKEN unset' }
@{ Authorization = "token $($env:GITEA_TOKEN)" }
}
# Ensure-GiteaRelease TAG NAME PRERELEASE [TARGETCOMMITISH] -> release id
# Idempotently create or fetch the release for TAG. Prerelease is 'true', 'false', or 'auto'
# (auto marks it a prerelease iff TAG carries a `-` pre-release suffix, e.g. v0.2.0-rc1, so an
# rc never becomes "Latest"). TargetCommitish (optional) creates the tag if missing.
function Ensure-GiteaRelease {
param(
[Parameter(Mandatory)] [string]$Tag,
[Parameter(Mandatory)] [string]$Name,
[Parameter(Mandatory)] [string]$Prerelease,
[string]$TargetCommitish
)
$pre = if ($Prerelease -eq 'auto') { [bool]($Tag -match '-') } else { [System.Convert]::ToBoolean($Prerelease) }
$api = _GiteaApi; $h = _GiteaHeaders
$payload = @{ tag_name = $Tag; name = $Name; prerelease = $pre }
if ($TargetCommitish) { $payload.target_commitish = $TargetCommitish }
# Seed the body from docs/releases/<Tag>.md (the source of truth) so a Windows job winning the
# create race is born WITH its notes, exactly like the bash twin. Missing file (canary/rc) -> no
# body key. $PSScriptRoot is scripts/ci; walk up two levels to the repo root.
$notes = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) "docs/releases/$Tag.md"
if (Test-Path $notes) { $payload.body = Get-Content -Raw -Encoding utf8 $notes }
try {
$r = Invoke-RestMethod -Method Post -Uri "$api/releases" -Headers $h `
-ContentType 'application/json' -Body ($payload | ConvertTo-Json -Compress)
return $r.id
} catch {
# Almost always: the release already exists. Fetch it by tag; error if that fails too.
$r = Invoke-RestMethod -Method Get -Uri "$api/releases/tags/$Tag" -Headers $h
if (-not $r.id) { throw "gitea-release: could not create or find a release for tag '$Tag'" }
return $r.id
}
}
# Upsert-GiteaAsset RELEASEID FILE [NAME]
# Attach FILE, replacing any existing asset of the same name first (idempotent).
function Upsert-GiteaAsset {
param(
[Parameter(Mandatory)] [string]$ReleaseId,
[Parameter(Mandatory)] [string]$File,
[string]$Name
)
if (-not (Test-Path $File)) { throw "gitea-release: asset file not found: $File" }
if (-not $Name) { $Name = Split-Path $File -Leaf }
$api = _GiteaApi; $h = _GiteaHeaders
$assets = Invoke-RestMethod -Method Get -Uri "$api/releases/$ReleaseId/assets" -Headers $h
$existing = $assets | Where-Object { $_.name -eq $Name } | Select-Object -First 1
if ($existing) {
Invoke-RestMethod -Method Delete -Uri "$api/releases/$ReleaseId/assets/$($existing.id)" -Headers $h | Out-Null
}
$enc = [uri]::EscapeDataString($Name)
# curl.exe for the multipart upload — matches the rest of the windows workflows.
curl.exe -fsS -H "Authorization: token $($env:GITEA_TOKEN)" -o NUL `
-X POST "$api/releases/$ReleaseId/assets?name=$enc" -F "attachment=@$File"
if ($LASTEXITCODE -ne 0) { throw "gitea-release: asset upload failed ($LASTEXITCODE): $Name" }
Write-Output "gitea-release: uploaded '$Name' -> release $ReleaseId"
}