Skip to content

Commit 1311ad6

Browse files
committed
fix: harden split release flow and Chocolatey packaging checks
Stabilize the split release model by moving Chocolatey into its own validated workflow, fixing the PowerShell zip-name interpolation bug, and adding preflight/smoke checks so packaging failures are caught before publish. Also filter release-pipeline churn from generated changelogs to keep user-facing notes concise. Made-with: Cursor
1 parent 7b9a3dd commit 1311ad6

5 files changed

Lines changed: 281 additions & 134 deletions

File tree

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
name: Chocolatey Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: "Release version (e.g. 2.3.6 or v2.3.6)"
8+
required: true
9+
type: string
10+
publish:
11+
description: "Push package to Chocolatey (false = dry run)"
12+
required: true
13+
default: false
14+
type: boolean
15+
workflow_run:
16+
workflows: ["Release"]
17+
types: [completed]
18+
19+
jobs:
20+
chocolatey:
21+
name: Build and Push Chocolatey
22+
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
23+
runs-on: windows-latest
24+
permissions:
25+
contents: read
26+
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@v4
30+
with:
31+
fetch-depth: 0
32+
33+
- name: Set up Go
34+
uses: actions/setup-go@v5
35+
with:
36+
go-version: "1.24.4"
37+
38+
- name: Install .NET (for NuGet push)
39+
uses: actions/setup-dotnet@v4
40+
with:
41+
dotnet-version: "8.0.x"
42+
43+
# Ensure Chocolatey is available (windows-latest usually has it; install if missing).
44+
- name: Install Chocolatey
45+
shell: pwsh
46+
run: |
47+
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
48+
Set-ExecutionPolicy Bypass -Scope Process -Force
49+
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
50+
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
51+
}
52+
53+
- name: Add Chocolatey to PATH
54+
shell: pwsh
55+
run: $env:Path += ";$env:ProgramData\chocolatey\bin"
56+
57+
- name: Verify Chocolatey installation
58+
shell: pwsh
59+
run: choco --version
60+
61+
- name: Resolve version and tag
62+
id: version
63+
shell: pwsh
64+
run: |
65+
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
66+
$raw = "${{ inputs.version }}"
67+
if ([string]::IsNullOrWhiteSpace($raw)) { Write-Error "version input is required"; exit 1 }
68+
$version = $raw -replace '^v', ''
69+
$tag = "v$version"
70+
} else {
71+
$sha = "${{ github.event.workflow_run.head_sha }}"
72+
if ([string]::IsNullOrWhiteSpace($sha)) { Write-Error "workflow_run.head_sha is empty"; exit 1 }
73+
git fetch --tags --force
74+
$tag = (git tag --points-at $sha | Select-Object -First 1)
75+
if ([string]::IsNullOrWhiteSpace($tag)) {
76+
Write-Error "No tag points at Release workflow SHA: $sha"
77+
exit 1
78+
}
79+
$version = $tag -replace '^v', ''
80+
}
81+
82+
if (-not ($version -match '^\d+\.\d+\.\d+([-.].+)?$')) {
83+
Write-Error "Invalid version format: $version"
84+
exit 1
85+
}
86+
87+
"VERSION=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
88+
"TAG=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
89+
Write-Host "Using tag=$tag version=$version"
90+
91+
- name: Validate release and required assets
92+
id: assets
93+
shell: pwsh
94+
env:
95+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
96+
run: |
97+
$version = "${{ steps.version.outputs.VERSION }}"
98+
$tag = "${{ steps.version.outputs.TAG }}"
99+
$zipName = "fontget_${version}_windows_amd64.zip"
100+
$checksumsName = "checksums.txt"
101+
102+
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/${{ github.repository }}/releases/tags/$tag" -Headers @{ Authorization = "token $env:GH_TOKEN"; Accept = "application/vnd.github.v3+json" }
103+
if ($release.draft -eq $true) {
104+
Write-Error "Release $tag is draft. Publish it before Chocolatey push."
105+
exit 1
106+
}
107+
108+
$zipAsset = $release.assets | Where-Object { $_.name -eq $zipName } | Select-Object -First 1
109+
if (-not $zipAsset) {
110+
Write-Error "Missing required asset: $zipName"
111+
exit 1
112+
}
113+
114+
$checksumsAsset = $release.assets | Where-Object { $_.name -eq $checksumsName } | Select-Object -First 1
115+
if (-not $checksumsAsset) {
116+
Write-Error "Missing required asset: $checksumsName"
117+
exit 1
118+
}
119+
120+
# Ensure public download URL is reachable (unauthenticated verification behavior).
121+
$zipPublicUrl = "https://github.com/${{ github.repository }}/releases/download/$tag/$zipName"
122+
$publicResponse = Invoke-WebRequest -Uri $zipPublicUrl -Method Head -MaximumRedirection 10 -ErrorAction Stop
123+
if ($publicResponse.StatusCode -lt 200 -or $publicResponse.StatusCode -ge 400) {
124+
Write-Error "Public release asset URL is not reachable: $zipPublicUrl"
125+
exit 1
126+
}
127+
128+
$zipPath = Join-Path $env:TEMP $zipName
129+
$checksumsPath = Join-Path $env:TEMP $checksumsName
130+
Invoke-WebRequest -Uri $zipAsset.url -Headers @{ Authorization = "token $env:GH_TOKEN"; Accept = "application/octet-stream" } -OutFile $zipPath -UseBasicParsing
131+
Invoke-WebRequest -Uri $checksumsAsset.url -Headers @{ Authorization = "token $env:GH_TOKEN"; Accept = "application/octet-stream" } -OutFile $checksumsPath -UseBasicParsing
132+
133+
$hash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
134+
if (-not ($hash -match '^[a-f0-9]{64}$')) {
135+
Write-Error "Computed SHA256 is invalid: $hash"
136+
exit 1
137+
}
138+
139+
$expectedHash = $null
140+
Get-Content $checksumsPath | ForEach-Object {
141+
if ($_ -match '^(?<sum>[a-fA-F0-9]{64})\s+\*?(?<file>.+)$') {
142+
$fileName = $matches['file'].Trim()
143+
if ($fileName -eq $zipName) {
144+
$expectedHash = $matches['sum'].ToLowerInvariant()
145+
}
146+
}
147+
}
148+
if ([string]::IsNullOrWhiteSpace($expectedHash)) {
149+
Write-Error "checksums.txt does not contain a valid SHA256 entry for $zipName"
150+
exit 1
151+
}
152+
if ($expectedHash -ne $hash) {
153+
Write-Error "SHA256 mismatch for $zipName. checksums.txt=$expectedHash computed=$hash"
154+
exit 1
155+
}
156+
157+
"ZIP_SHA256=$hash" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
158+
"ZIP_NAME=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
159+
Write-Host "Validated release assets and computed SHA256 for $zipName"
160+
161+
- name: Inject checksum into Chocolatey install script
162+
shell: pwsh
163+
run: |
164+
$scriptPath = "build/chocolatey/tools/chocolateyInstall.ps1"
165+
$content = Get-Content $scriptPath -Raw
166+
if (-not $content.Contains('CHECKSUM_PLACEHOLDER')) {
167+
Write-Error "CHECKSUM_PLACEHOLDER not found in $scriptPath"
168+
exit 1
169+
}
170+
171+
$updated = $content -replace 'CHECKSUM_PLACEHOLDER', "${{ steps.assets.outputs.ZIP_SHA256 }}"
172+
Set-Content $scriptPath $updated -NoNewline
173+
174+
$verify = Get-Content $scriptPath -Raw
175+
if ($verify.Contains('CHECKSUM_PLACEHOLDER')) {
176+
Write-Error "Checksum placeholder was not fully replaced"
177+
exit 1
178+
}
179+
180+
- name: Pack Chocolatey package
181+
shell: pwsh
182+
run: |
183+
choco pack build/chocolatey/fontget.nuspec --version ${{ steps.version.outputs.VERSION }} --output-directory nupkg
184+
$nupkg = Get-ChildItem nupkg/*.nupkg | Select-Object -First 1
185+
if (-not $nupkg) { Write-Error "No .nupkg generated"; exit 1 }
186+
Write-Host "Packed $($nupkg.Name)"
187+
188+
- name: Validate nupkg contents
189+
shell: pwsh
190+
run: |
191+
Add-Type -AssemblyName System.IO.Compression.FileSystem
192+
$nupkg = Get-ChildItem nupkg/*.nupkg | Select-Object -First 1
193+
if (-not $nupkg) { Write-Error "No .nupkg found"; exit 1 }
194+
195+
$zip = [System.IO.Compression.ZipFile]::OpenRead($nupkg.FullName)
196+
try {
197+
$entries = $zip.Entries | ForEach-Object { $_.FullName }
198+
if (-not ($entries -contains 'tools/chocolateyInstall.ps1')) {
199+
Write-Error "nupkg missing tools/chocolateyInstall.ps1"
200+
exit 1
201+
}
202+
if (-not ($entries -contains 'tools/chocolateyUninstall.ps1')) {
203+
Write-Error "nupkg missing tools/chocolateyUninstall.ps1"
204+
exit 1
205+
}
206+
} finally {
207+
$zip.Dispose()
208+
}
209+
210+
- name: Smoke test package install (no publish)
211+
shell: pwsh
212+
run: |
213+
$nupkg = Get-ChildItem nupkg/*.nupkg | Select-Object -First 1
214+
if (-not $nupkg) { Write-Error "No .nupkg found"; exit 1 }
215+
216+
$localSource = $nupkg.Directory.FullName
217+
$version = "${{ steps.version.outputs.VERSION }}"
218+
choco install fontget --source "$localSource" --version "$version" --yes --force --no-progress
219+
if ($LASTEXITCODE -ne 0) { Write-Error "Local install smoke test failed"; exit 1 }
220+
221+
# Basic sanity check that the executable is discoverable after install.
222+
$fontgetCmd = Get-Command fontget -ErrorAction SilentlyContinue
223+
if (-not $fontgetCmd) {
224+
Write-Error "fontget command not found after local install"
225+
exit 1
226+
}
227+
fontget --version
228+
229+
choco uninstall fontget --yes --force --no-progress
230+
if ($LASTEXITCODE -ne 0) { Write-Error "Local uninstall smoke test failed"; exit 1 }
231+
232+
- name: Push to Chocolatey
233+
if: ${{ github.event_name == 'workflow_run' || (github.event_name == 'workflow_dispatch' && inputs.publish == true) }}
234+
shell: pwsh
235+
env:
236+
CHOCOLATEY_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }}
237+
run: |
238+
$nupkg = Get-ChildItem nupkg/*.nupkg | Select-Object -First 1
239+
if (-not $nupkg) { Write-Error "No nupkg found"; exit 1 }
240+
dotnet nuget push $nupkg.FullName --api-key "$env:CHOCOLATEY_API_KEY" --source "https://push.chocolatey.org/" --skip-duplicate

.github/workflows/release.yml

Lines changed: 8 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -36,142 +36,17 @@ jobs:
3636
version: '~> v2'
3737
args: check
3838

39-
# Create draft release only. No push to any external repo until Chocolatey succeeds (publish-release job).
40-
- name: Run GoReleaser (draft only, no external pushes)
39+
# Main release flow: publish GitHub release + non-Chocolatey package managers.
40+
# Chocolatey is handled separately in chocolatey-release.yml with dedicated preflight checks.
41+
- name: Run GoReleaser
4142
uses: goreleaser/goreleaser-action@v6
4243
with:
4344
distribution: goreleaser
4445
version: '~> v2'
45-
args: release --clean --skip=chocolatey,winget,aur-source,homebrew,scoop --draft
46+
args: release --clean --skip=chocolatey
4647
env:
4748
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48-
49-
# Publish the draft release so the download URL is public when Chocolatey verification runs.
50-
publish-draft:
51-
name: Publish draft release
52-
runs-on: ubuntu-latest
53-
needs: release
54-
permissions:
55-
contents: write
56-
steps:
57-
- name: Checkout
58-
uses: actions/checkout@v4
59-
with:
60-
fetch-depth: 0
61-
- name: Install GitHub CLI
62-
run: |
63-
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
64-
curl -fsSL https://cli.github.com/packages/setup-apt-repo | sudo -E bash -
65-
sudo apt install gh -y
66-
- name: Publish draft release
67-
run: gh release edit "${{ github.ref_name }}" --draft=false
68-
env:
69-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
70-
71-
chocolatey:
72-
name: Build Chocolatey Package
73-
runs-on: windows-latest
74-
needs: publish-draft
75-
permissions:
76-
contents: write
77-
78-
steps:
79-
- name: Checkout
80-
uses: actions/checkout@v4
81-
with:
82-
fetch-depth: 0
83-
84-
- name: Set up Go
85-
uses: actions/setup-go@v5
86-
with:
87-
go-version: "1.24.4"
88-
89-
# Ensure Chocolatey is available (windows-latest usually has it; install if missing, per community pattern)
90-
- name: Install Chocolatey
91-
shell: pwsh
92-
run: |
93-
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
94-
Set-ExecutionPolicy Bypass -Scope Process -Force
95-
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
96-
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
97-
}
98-
99-
- name: Add Chocolatey to PATH
100-
shell: pwsh
101-
run: $env:Path += ";$env:ProgramData\chocolatey\bin"
102-
103-
- name: Verify Chocolatey installation
104-
shell: pwsh
105-
run: choco --version
106-
107-
- name: Get version from tag
108-
id: version
109-
shell: pwsh
110-
run: |
111-
$tag = $env:GITHUB_REF -replace 'refs/tags/', ''
112-
$version = $tag -replace '^v', ''
113-
"VERSION=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
114-
Write-Host "Packing Chocolatey for version $version"
115-
116-
# Download zip from release, compute SHA256, inject into install script (Chocolatey requires validated checksum).
117-
- name: Inject checksum into Chocolatey install script
118-
shell: pwsh
119-
env:
120-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
121-
run: |
122-
$version = "${{ steps.version.outputs.VERSION }}"
123-
$tag = "v$version"
124-
$zipName = "fontget_${version}_windows_amd64.zip"
125-
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/${{ github.repository }}/releases/tags/$tag" -Headers @{ Authorization = "token $env:GH_TOKEN"; Accept = "application/vnd.github.v3+json" }
126-
$asset = $release.assets | Where-Object { $_.name -eq $zipName } | Select-Object -First 1
127-
if (-not $asset) { Write-Error "Asset $zipName not found in release $tag"; exit 1 }
128-
$zipPath = Join-Path $env:TEMP $zipName
129-
Invoke-WebRequest -Uri $asset.url -Headers @{ Authorization = "token $env:GH_TOKEN"; Accept = "application/octet-stream" } -OutFile $zipPath -UseBasicParsing
130-
$hash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
131-
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
132-
$scriptPath = "build/chocolatey/tools/chocolateyInstall.ps1"
133-
(Get-Content $scriptPath -Raw) -replace 'CHECKSUM_PLACEHOLDER', $hash | Set-Content $scriptPath -NoNewline
134-
Write-Host "Injected SHA256 for $zipName"
135-
136-
- name: Pack Chocolatey package
137-
shell: pwsh
138-
run: |
139-
choco pack build/chocolatey/fontget.nuspec --version ${{ steps.version.outputs.VERSION }} --output-directory nupkg
140-
Get-ChildItem nupkg
141-
142-
- name: Install .NET (for NuGet push)
143-
uses: actions/setup-dotnet@v4
144-
with:
145-
dotnet-version: "8.0.x"
146-
147-
- name: Push to Chocolatey
148-
shell: pwsh
149-
run: |
150-
$nupkg = Get-ChildItem nupkg/*.nupkg | Select-Object -First 1
151-
if (-not $nupkg) { Write-Error "No nupkg found"; exit 1 }
152-
dotnet nuget push $nupkg.FullName --api-key "$env:CHOCOLATEY_API_KEY" --source "https://push.chocolatey.org/" --skip-duplicate
153-
env:
154-
CHOCOLATEY_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }}
155-
156-
# Only after Chocolatey succeeds: publish the release (make it public).
157-
publish-release:
158-
name: Publish release
159-
runs-on: ubuntu-latest
160-
needs: [release, chocolatey]
161-
permissions:
162-
contents: write
163-
164-
steps:
165-
- name: Checkout
166-
uses: actions/checkout@v4
167-
with:
168-
fetch-depth: 0
169-
- name: Install GitHub CLI
170-
run: |
171-
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
172-
curl -fsSL https://cli.github.com/packages/setup-apt-repo | sudo -E bash -
173-
sudo apt install gh -y
174-
- name: Publish draft release
175-
run: gh release edit "${{ github.ref_name }}" --draft=false
176-
env:
177-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49+
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
50+
SCOOP_BUCKET_TOKEN: ${{ secrets.SCOOP_BUCKET_TOKEN }}
51+
WINGET_PR_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
52+
AUR_PRIVATE_KEY: ${{ secrets.AUR_PRIVATE_KEY }}

0 commit comments

Comments
 (0)