Skip to content

Commit dbc418d

Browse files
committed
ci(release): conventional-commit changelog body, draft-first publish
Replace softprops/action-gh-release's generate_release_notes (which only emits the stock "Full Changelog: ..." line) with an inline build from git log $prev..$tag grouped by conventional-commit type, published via gh release create --notes-file. Draft-first with asset verification so a mid-upload failure leaves the draft visible instead of going live broken.
1 parent d204591 commit dbc418d

1 file changed

Lines changed: 149 additions & 11 deletions

File tree

.github/workflows/release.yml

Lines changed: 149 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,20 @@ on:
1313
permissions:
1414
contents: write
1515

16+
concurrency:
17+
group: release-${{ github.ref }}
18+
cancel-in-progress: false
19+
1620
jobs:
1721
build:
1822
runs-on: windows-latest
23+
timeout-minutes: 30
1924
steps:
2025
- uses: actions/checkout@v4
26+
with:
27+
# Full history + all tags so the release-body step can diff
28+
# commits against the previous version tag for the changelog.
29+
fetch-depth: 0
2130

2231
- uses: actions/setup-go@v5
2332
with:
@@ -41,9 +50,11 @@ jobs:
4150
if ($version -notmatch '^\d{4}\.\d+\.\d+\.\d+(-[A-Fa-f0-9]{4})?$') {
4251
throw "version '$version' does not match the YYYY.M.D.N(-XXXX) shape"
4352
}
53+
$isPre = $version -like '*-*'
4454
"tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
4555
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
46-
Write-Host "tag=$tag version=$version"
56+
"is_pre=$($isPre.ToString().ToLower())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
57+
Write-Host "tag=$tag version=$version is_pre=$isPre"
4758
4859
- name: Fetch picotool
4960
shell: pwsh
@@ -77,14 +88,141 @@ jobs:
7788
}
7889
$manifest | ConvertTo-Json -Compress | Out-File -FilePath handoff-version.json -Encoding ASCII
7990
Get-Content handoff-version.json
91+
"sha256=$sha" >> $env:GITHUB_OUTPUT
8092
81-
- name: Upload release assets
82-
uses: softprops/action-gh-release@v2
83-
with:
84-
tag_name: ${{ steps.ver.outputs.tag }}
85-
files: |
86-
handoff.exe
87-
handoff-version.json
88-
generate_release_notes: true
89-
draft: false
90-
prerelease: ${{ contains(steps.ver.outputs.version, '-') }}
93+
- name: Publish GitHub release
94+
env:
95+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
96+
TAG_NAME: ${{ steps.ver.outputs.tag }}
97+
EXE_SHA256: ${{ steps.manifest.outputs.sha256 }}
98+
shell: pwsh
99+
run: |
100+
# Build the release body. Top section is a conventional-commit
101+
# changelog grouped by type, extracted from `git log $prev..$tag`.
102+
# Bottom section is the static install snippet.
103+
104+
$prevTag = git tag --list 'v*' --sort=-version:refname |
105+
Where-Object { $_ -ne $env:TAG_NAME } |
106+
Select-Object -First 1
107+
108+
$sections = [ordered]@{
109+
feat = 'Features'
110+
fix = 'Bug Fixes'
111+
perf = 'Performance'
112+
refactor = 'Refactors'
113+
docs = 'Documentation'
114+
test = 'Tests'
115+
ci = 'CI'
116+
build = 'Build'
117+
chore = 'Chores'
118+
style = 'Style'
119+
revert = 'Reverts'
120+
other = 'Other'
121+
}
122+
$groups = @{}
123+
$breaking = @()
124+
125+
if ($prevTag) {
126+
$range = "$prevTag..$env:TAG_NAME"
127+
$logLines = git log $range --pretty=format:"%h%x09%s" --no-merges
128+
foreach ($line in $logLines) {
129+
if ([string]::IsNullOrWhiteSpace($line)) { continue }
130+
$parts = $line -split "`t", 2
131+
$sha = $parts[0]
132+
$subject = $parts[1]
133+
if ($subject -match '^(?<type>\w+)(\((?<scope>[^)]+)\))?(?<bang>!)?:\s*(?<desc>.+)$') {
134+
$type = $matches['type'].ToLowerInvariant()
135+
$scope = $matches['scope']
136+
$isBreaking = -not [string]::IsNullOrEmpty($matches['bang'])
137+
$desc = $matches['desc']
138+
if (-not $sections.Contains($type)) { $type = 'other' }
139+
if ($scope) {
140+
$entry = "- ``$sha`` **${scope}**: $desc"
141+
} else {
142+
$entry = "- ``$sha`` $desc"
143+
}
144+
if ($isBreaking) {
145+
if ($scope) {
146+
$breaking += "- ``$sha`` **${scope}**: $desc"
147+
} else {
148+
$breaking += "- ``$sha`` $desc"
149+
}
150+
}
151+
if (-not $groups.ContainsKey($type)) { $groups[$type] = @() }
152+
$groups[$type] += $entry
153+
} else {
154+
if (-not $groups.ContainsKey('other')) { $groups['other'] = @() }
155+
$groups['other'] += "- ``$sha`` $subject"
156+
}
157+
}
158+
}
159+
160+
$out = New-Object System.Collections.Generic.List[string]
161+
if ($prevTag) {
162+
$out.Add("## Changes since $prevTag")
163+
$out.Add("")
164+
if ($breaking.Count -gt 0) {
165+
$out.Add("### Breaking Changes")
166+
$out.Add("")
167+
foreach ($e in $breaking) { $out.Add($e) }
168+
$out.Add("")
169+
}
170+
$emitted = $false
171+
foreach ($key in $sections.Keys) {
172+
if ($groups.ContainsKey($key) -and $groups[$key].Count -gt 0) {
173+
$emitted = $true
174+
$out.Add("### $($sections[$key])")
175+
$out.Add("")
176+
foreach ($e in $groups[$key]) { $out.Add($e) }
177+
$out.Add("")
178+
}
179+
}
180+
if (-not $emitted) {
181+
$out.Add("_No conventional-commit changes since $prevTag._")
182+
$out.Add("")
183+
}
184+
$out.Add("**Full diff:** https://github.com/$env:GITHUB_REPOSITORY/compare/$prevTag...$env:TAG_NAME")
185+
$out.Add("")
186+
} else {
187+
$out.Add("Initial release.")
188+
$out.Add("")
189+
}
190+
191+
$out.Add("## Install")
192+
$out.Add("")
193+
$out.Add("Download ``handoff.exe`` from the assets below and run it from anywhere on PATH (or wherever you keep portable tools).")
194+
$out.Add("")
195+
$out.Add("SHA256: ``$env:EXE_SHA256``")
196+
$out.Add("")
197+
$out.Add("Auto-update clients can poll ``handoff-version.json`` next to the binary in the assets list.")
198+
$out.Add("")
199+
$out.Add("Getting started: https://github.com/$env:GITHUB_REPOSITORY/wiki/Getting-Started")
200+
201+
$notes = $out -join "`n"
202+
$notesPath = Join-Path $env:RUNNER_TEMP "release-notes.md"
203+
Set-Content -LiteralPath $notesPath -Value $notes -Encoding UTF8
204+
205+
# Draft-first: create as draft, upload assets, only then promote
206+
# to published. If anything between create and promote fails the
207+
# release stays as a draft, which is more visible than a live
208+
# release missing one of its assets.
209+
$extra = @('--draft')
210+
if ('${{ steps.ver.outputs.is_pre }}' -eq 'true') { $extra += @('--prerelease','--latest=false') }
211+
gh release create $env:TAG_NAME handoff.exe handoff-version.json --title $env:TAG_NAME --notes-file $notesPath @extra
212+
if ($LASTEXITCODE -ne 0) { throw "gh release create failed (exit $LASTEXITCODE)" }
213+
214+
# Verify the assets actually attached -- gh release create can
215+
# succeed with the metadata but fail mid-upload on the asset
216+
# stage; the draft would then be live with missing files.
217+
$json = gh release view $env:TAG_NAME --json assets,isDraft
218+
if ($LASTEXITCODE -ne 0) { throw "gh release view failed (exit $LASTEXITCODE)" }
219+
$info = $json | ConvertFrom-Json
220+
$expected = @('handoff.exe', 'handoff-version.json')
221+
foreach ($e in $expected) {
222+
if (-not ($info.assets.name -contains $e)) {
223+
throw "Asset '$e' did not attach to draft release $env:TAG_NAME -- leaving as draft for manual inspection."
224+
}
225+
}
226+
Write-Host "Draft release $env:TAG_NAME has all expected assets, promoting to published."
227+
gh release edit $env:TAG_NAME --draft=false
228+
if ($LASTEXITCODE -ne 0) { throw "gh release edit --draft=false failed (exit $LASTEXITCODE)" }

0 commit comments

Comments
 (0)