diff --git a/AGENTS.md b/AGENTS.md index 9edf6bf..990f7bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,10 +40,11 @@ runner only validates the web client via `.forgejo/workflows/validate.yml`. `artifacts/release/vX.Y.Z/` and stay out of Git. - Publish by double-clicking `scripts/Publish-DeckyRelease.cmd` or by invoking `scripts/Publish-ForgejoRelease.ps1`. The publisher creates the tag/release, - uploads the installer/signature, and commits `releases/latest.json` to `main`. -- Publishing creates a new remote metadata commit. Fetch/rebase or pull before - the next push; a non-fast-forward rejection usually means this commit is - missing locally. LF-to-CRLF messages on Windows are warnings, not failures. + uploads the installer/signature, commits `releases/latest.json` to `main`, and + automatically fast-forwards the local checkout to that metadata commit. +- The publisher requires a clean `main` that exactly matches `origin/main`, so + commit and push source first. LF-to-CRLF messages on Windows are warnings, + not failures. - Forgejo: `https://git.elijahkuntz.com/Elijah/Decky`. A publishing PAT needs `write:repository`; never commit or print it. - The permanent Tauri updater key is outside the repository at diff --git a/app/UPDATES.md b/app/UPDATES.md index 4646a49..39a8ad3 100644 --- a/app/UPDATES.md +++ b/app/UPDATES.md @@ -73,7 +73,6 @@ current PowerShell process and run: $env:FORGEJO_PAT = 'paste-token-for-this-session' powershell -ExecutionPolicy Bypass -File .\scripts\Publish-ForgejoRelease.ps1 -Version 0.3.0 -Notes "Board and stack management" Remove-Item Env:FORGEJO_PAT -git pull --ff-only ``` For an interactive, double-clickable option, run: @@ -86,11 +85,12 @@ It bypasses PowerShell's script policy only for the publisher process, prompts for the version and a hidden personal access token, and leaves the window open so any error can be read. The token is not saved. -The publisher creates the `v0.3.0` Forgejo release/tag at `main`, uploads the -installer, signature, and metadata, then commits the tracked -`releases/latest.json` through the Forgejo API. The final pull synchronizes that -small metadata commit locally. Existing Decky installations then discover the -release through Settings. +The publisher first verifies that the working tree is clean and local `main` +exactly matches `origin/main`. It then creates the `v0.3.0` Forgejo release/tag, +uploads the installer, signature, and metadata, and commits the tracked +`releases/latest.json` through the Forgejo API. Finally, it automatically +fast-forwards local `main` to that metadata commit. Existing Decky installations +then discover the release through Settings. ## Forgejo Actions diff --git a/scripts/Publish-ForgejoRelease.ps1 b/scripts/Publish-ForgejoRelease.ps1 index 0d29d04..554c9f6 100644 --- a/scripts/Publish-ForgejoRelease.ps1 +++ b/scripts/Publish-ForgejoRelease.ps1 @@ -12,6 +12,35 @@ param( ) $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 @@ -20,11 +49,6 @@ if (-not $Token) { } 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" @@ -82,3 +106,14 @@ $contentBody = @{ 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.'