-
Notifications
You must be signed in to change notification settings - Fork 4
fix(release): preserve stable binary immutability and manual publish opt-out #373
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a375a3e
b44fe28
2d444c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -447,7 +447,7 @@ jobs: | |
| ArIED61850Tester/dist/ARSAS-Windows-x64-PROVENANCE.json | ||
|
|
||
| - name: Create or update GitHub Release | ||
| if: github.ref_type == 'tag' || github.ref == 'refs/heads/main' || inputs.publish_release == true | ||
| if: (github.event_name == 'push' && (github.ref_type == 'tag' || github.ref == 'refs/heads/main')) || (github.event_name == 'workflow_dispatch' && inputs.publish_release == true) | ||
| shell: powershell | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
|
|
@@ -464,10 +464,43 @@ jobs: | |
|
|
||
| $null = cmd /c "gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY >NUL 2>NUL" | ||
| if ($LASTEXITCODE -eq 0) { | ||
| gh release upload $env:RELEASE_TAG @assets --repo $env:GITHUB_REPOSITORY --clobber | ||
| if ($LASTEXITCODE -ne 0) { throw "Failed to update GitHub Release assets." } | ||
| gh release edit $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --title "ARSAS $env:RELEASE_VERSION" --latest | ||
| if ($LASTEXITCODE -ne 0) { throw "Failed to mark the GitHub Release as latest." } | ||
| # Existing stable tags/assets are immutable. A rerun with the same source and | ||
| # identical bytes is a no-op; a different source or binary is rejected. | ||
| $tagCommit = git -C .\ArIED61850Tester rev-list -n 1 $env:RELEASE_TAG | ||
| if ($LASTEXITCODE -ne 0 -or "$tagCommit".Trim() -ne $env:GITHUB_SHA) { | ||
| throw "Existing release $env:RELEASE_TAG belongs to a different source commit; use a new version/tag." | ||
| } | ||
|
|
||
| $published = gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --json assets | ConvertFrom-Json | ||
| if ($LASTEXITCODE -ne 0) { throw "Cannot inspect existing release assets." } | ||
| $expectedNames = @($assets | ForEach-Object { [IO.Path]::GetFileName($_) }) | ||
| $publishedNames = @($published.assets | ForEach-Object { $_.name }) | ||
| if ($publishedNames.Count -ne $expectedNames.Count) { | ||
| throw "Existing release asset count differs; refusing to mutate published binaries." | ||
| } | ||
| foreach ($name in $expectedNames) { | ||
| if ($publishedNames -notcontains $name) { | ||
| throw "Existing release is missing $name; refusing in-place repair of a published tag." | ||
| } | ||
| } | ||
|
|
||
| $downloadDir = Join-Path $env:RUNNER_TEMP "arsas-existing-release-verification" | ||
| New-Item -ItemType Directory -Path $downloadDir -Force | Out-Null | ||
| gh release download $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --dir $downloadDir | ||
| if ($LASTEXITCODE -ne 0) { throw "Cannot download existing release assets for integrity verification." } | ||
| foreach ($asset in $assets) { | ||
| $existingAsset = Join-Path $downloadDir ([IO.Path]::GetFileName($asset)) | ||
| if (-not (Test-Path -LiteralPath $existingAsset -PathType Leaf)) { | ||
| throw "Missing existing release asset $existingAsset." | ||
| } | ||
| $rebuiltHash = (Get-FileHash -LiteralPath $asset -Algorithm SHA256).Hash | ||
| $publishedHash = (Get-FileHash -LiteralPath $existingAsset -Algorithm SHA256).Hash | ||
| if ($rebuiltHash -ne $publishedHash) { | ||
| throw "Existing release asset differs: $([IO.Path]::GetFileName($asset)). Publish a new version/tag instead." | ||
| } | ||
| } | ||
| "RELEASE_ALREADY_PUBLISHED=true" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | ||
| Write-Host "Existing release matches the exact source and all published asset hashes; no mutation performed." | ||
| exit 0 | ||
| } | ||
|
|
||
|
|
@@ -488,7 +521,7 @@ jobs: | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to create GitHub Release." } | ||
|
|
||
| - name: Record verified release publication | ||
| if: github.ref_type == 'tag' || github.ref == 'refs/heads/main' || inputs.publish_release == true | ||
| if: ((github.event_name == 'push' && (github.ref_type == 'tag' || github.ref == 'refs/heads/main')) || (github.event_name == 'workflow_dispatch' && inputs.publish_release == true)) && env.RELEASE_ALREADY_PUBLISHED != 'true' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If AGENTS.md reference: AGENTS.md:L58-L66 Useful? React with 👍 / 👎. |
||
| shell: powershell | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| namespace ARSAS.Tests; | ||
|
|
||
| public sealed class StableReleaseImmutabilityRegressionTests | ||
| { | ||
| [Fact] | ||
| public void CanonicalRelease_RespectsManualPublishOptOut_AndDoesNotClobberExistingAssets() | ||
| { | ||
| var workflow = File.ReadAllText(FindRepoFile(".github/workflows/release-windows.yml")); | ||
|
|
||
| Assert.Contains("github.event_name == 'workflow_dispatch' && inputs.publish_release == true", workflow, StringComparison.Ordinal); | ||
| Assert.Contains("github.event_name == 'push'", workflow, StringComparison.Ordinal); | ||
| Assert.DoesNotContain("gh release upload", workflow, StringComparison.Ordinal); | ||
| Assert.DoesNotContain("--clobber", workflow, StringComparison.Ordinal); | ||
| Assert.Contains("rev-list -n 1 $env:RELEASE_TAG", workflow, StringComparison.Ordinal); | ||
| Assert.Contains("gh release download $env:RELEASE_TAG", workflow, StringComparison.Ordinal); | ||
| Assert.Contains("Get-FileHash -LiteralPath $existingAsset -Algorithm SHA256", workflow, StringComparison.Ordinal); | ||
| Assert.Contains("RELEASE_ALREADY_PUBLISHED=true", workflow, StringComparison.Ordinal); | ||
| Assert.Contains("env.RELEASE_ALREADY_PUBLISHED != 'true'", workflow, StringComparison.Ordinal); | ||
| Assert.Contains("use a new version/tag", workflow, StringComparison.OrdinalIgnoreCase); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AlternateVerifiedPublisher_RefusesToReplaceAnExistingStableTag() | ||
| { | ||
| var workflow = File.ReadAllText(FindRepoFile(".github/workflows/publish-verified-release.yml")); | ||
|
|
||
| Assert.DoesNotContain("--clobber", workflow, StringComparison.Ordinal); | ||
| Assert.DoesNotContain("gh release upload", workflow, StringComparison.Ordinal); | ||
| Assert.Contains("Refusing to overwrite existing stable release", workflow, StringComparison.Ordinal); | ||
| Assert.Contains("gh release create", workflow, StringComparison.Ordinal); | ||
| } | ||
|
|
||
| private static string FindRepoFile(string relativePath) | ||
| { | ||
| DirectoryInfo? directory = new(AppContext.BaseDirectory); | ||
| while (directory is not null) | ||
| { | ||
| var candidate = Path.Combine(directory.FullName, relativePath); | ||
| if (File.Exists(candidate)) | ||
| return candidate; | ||
|
|
||
| directory = directory.Parent; | ||
| } | ||
|
|
||
| throw new FileNotFoundException($"Cannot locate repository file: {relativePath}"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an existing release has matching assets and source but is still a draft or prerelease, this query requests only
assets, so the workflow declares success without producing the stable/latest release promised by a main or stable-tag run. This can occur after an interruptedgh release create, whose help documents a draft → asset upload → publish sequence, or after a manual prerelease;gh release view --helpexposesisDraft,isPrerelease, andpublishedAt, so inspect those fields and reject or explicitly handle non-stable state before settingRELEASE_ALREADY_PUBLISHED.AGENTS.md reference: AGENTS.md:L58-L66
Useful? React with 👍 / 👎.