119 lines
5.6 KiB
PowerShell
119 lines
5.6 KiB
PowerShell
#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'
|
|
$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." }
|
|
|
|
function Invoke-GitCommand {
|
|
param([Parameter(Mandatory)][string[]]$ArgumentList)
|
|
$output = @(& git -C $repositoryRoot @ArgumentList 2>&1)
|
|
if ($LASTEXITCODE) {
|
|
throw "git $($ArgumentList -join ' ') failed: $($output -join [Environment]::NewLine)"
|
|
}
|
|
return $output
|
|
}
|
|
|
|
$branch = ((Invoke-GitCommand @('rev-parse', '--abbrev-ref', 'HEAD')) | Select-Object -Last 1).Trim()
|
|
if ($branch -ne 'main') { throw "Releases must be published from main; the current branch is '$branch'." }
|
|
|
|
$workingChanges = @(Invoke-GitCommand @('status', '--porcelain'))
|
|
if ($workingChanges.Count) {
|
|
throw "Commit or discard all source changes before publishing:`n$($workingChanges -join [Environment]::NewLine)"
|
|
}
|
|
|
|
Invoke-GitCommand @('fetch', 'origin', 'main') | Out-Null
|
|
$localHead = ((Invoke-GitCommand @('rev-parse', 'HEAD')) | Select-Object -Last 1).Trim()
|
|
$remoteHead = ((Invoke-GitCommand @('rev-parse', 'origin/main')) | Select-Object -Last 1).Trim()
|
|
if ($localHead -ne $remoteHead) {
|
|
throw 'Local main does not exactly match origin/main. Pull/rebase or push the source before publishing.'
|
|
}
|
|
|
|
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.' }
|
|
|
|
$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."
|
|
|
|
Invoke-GitCommand @('fetch', 'origin', 'main') | Out-Null
|
|
$publishedHead = ((Invoke-GitCommand @('rev-parse', 'origin/main')) | Select-Object -Last 1).Trim()
|
|
if ($publishedHead -ne $localHead) {
|
|
$mergeBase = ((Invoke-GitCommand @('merge-base', $localHead, $publishedHead)) | Select-Object -Last 1).Trim()
|
|
if ($mergeBase -ne $localHead) {
|
|
throw 'The release was published, but origin/main also changed independently. Fetch and reconcile it manually.'
|
|
}
|
|
Invoke-GitCommand @('merge', '--ff-only', 'origin/main') | Out-Null
|
|
}
|
|
Write-Host 'Local main is synchronized with the Forgejo release metadata commit.'
|