#requires -Version 5.1 [CmdletBinding()] param( [Parameter(Mandatory)] [ValidatePattern('^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$')] [string]$Version, [string]$Notes = "Decky $Version", [string]$ForgejoBaseUrl = 'https://git.elijahkuntz.com', [string]$Repository = 'Elijah/Decky', [string]$Token = $(if ($env:FORGEJO_TOKEN) { $env:FORGEJO_TOKEN } else { $env:FORGEJO_PAT }) ) $ErrorActionPreference = 'Stop' if (-not $Token) { Write-Host 'A Forgejo personal access token is required to create the release.' -ForegroundColor Cyan Write-Host 'The token is used only by this process and is not saved by Decky.' -ForegroundColor DarkGray $secureToken = Read-Host 'Forgejo personal access token' -AsSecureString $Token = [Net.NetworkCredential]::new('', $secureToken).Password } if (-not $Token) { throw 'No Forgejo personal access token was entered.' } $repositoryRoot = Split-Path -Parent $PSScriptRoot $releaseDirectory = Join-Path $repositoryRoot "artifacts\release\v$Version" $latestJsonPath = Join-Path $releaseDirectory 'latest.json' if (-not (Test-Path -LiteralPath $latestJsonPath)) { throw "Run Prepare-Release.ps1 first: $latestJsonPath is missing." } $headers = @{ Authorization = "token $Token"; Accept = 'application/json' } $apiRoot = "$($ForgejoBaseUrl.TrimEnd('/'))/api/v1/repos/$Repository" $tag = "v$Version" $releaseBody = @{ tag_name = $tag target_commitish = 'main' name = "Decky $Version" body = $Notes draft = $false prerelease = $Version.Contains('-') } | ConvertTo-Json try { $release = Invoke-RestMethod -Method Get -Uri "$apiRoot/releases/tags/$tag" -Headers $headers } catch { if ($_.Exception.Response.StatusCode.value__ -ne 404) { throw } $release = Invoke-RestMethod -Method Post -Uri "$apiRoot/releases" -Headers $headers -ContentType 'application/json' -Body $releaseBody } $assets = Get-ChildItem -LiteralPath $releaseDirectory -File foreach ($asset in $assets) { $existing = @($release.assets | Where-Object name -eq $asset.Name) if ($existing.Count) { Write-Host "Release asset already exists, skipping: $($asset.Name)" continue } Add-Type -AssemblyName System.Net.Http $client = New-Object Net.Http.HttpClient try { $client.DefaultRequestHeaders.Authorization = New-Object Net.Http.Headers.AuthenticationHeaderValue('token', $Token) $form = New-Object Net.Http.MultipartFormDataContent $fileContent = New-Object Net.Http.ByteArrayContent(,[IO.File]::ReadAllBytes($asset.FullName)) $fileContent.Headers.ContentType = New-Object Net.Http.Headers.MediaTypeHeaderValue('application/octet-stream') $form.Add($fileContent, 'attachment', $asset.Name) $response = $client.PostAsync("$apiRoot/releases/$($release.id)/assets?name=$([Uri]::EscapeDataString($asset.Name))", $form).GetAwaiter().GetResult() if (-not $response.IsSuccessStatusCode) { throw "Asset upload failed ($([int]$response.StatusCode)): $($response.Content.ReadAsStringAsync().GetAwaiter().GetResult())" } } finally { if ($form) { $form.Dispose() } $client.Dispose() } Write-Host "Uploaded $($asset.Name)" } $encodedMetadata = [Convert]::ToBase64String([IO.File]::ReadAllBytes($latestJsonPath)) $contentsUri = "$apiRoot/contents/releases/latest.json?ref=main" $sha = $null try { $sha = (Invoke-RestMethod -Method Get -Uri $contentsUri -Headers $headers).sha } catch { if ($_.Exception.Response.StatusCode.value__ -ne 404) { throw } } $contentBody = @{ branch = 'main' message = "chore(release): publish Decky $Version updater metadata" content = $encodedMetadata } if ($sha) { $contentBody.sha = $sha } Invoke-RestMethod -Method Put -Uri "$apiRoot/contents/releases/latest.json" -Headers $headers -ContentType 'application/json' -Body ($contentBody | ConvertTo-Json) | Out-Null Write-Host "Published Decky $Version and updated releases/latest.json."