Skip to content

Commit 66f57d6

Browse files
committed
feat(windows build): Add conventional commits release notes
Generate categorized release notes from conventional commit messages Add error handling and input validation for GitHub API calls Sanitize commit messages to prevent markdown injection attacks Add build metadata and commit links to release notes Implement fallback to auto-generated notes when API calls fail
1 parent 586c67e commit 66f57d6

File tree

1 file changed

+244
-3
lines changed

1 file changed

+244
-3
lines changed

.github/workflows/windows_build.yml

Lines changed: 244 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,246 @@ jobs:
171171
name: windows-installer
172172
path: release-artifacts
173173

174+
- name: Rename installer for pre-release
175+
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
176+
shell: pwsh
177+
run: |
178+
$files = Get-ChildItem release-artifacts\*.exe
179+
foreach ($file in $files) {
180+
$newName = $file.Name -replace '_setup_.*\.exe$', '_setup_latest_pre_release.exe'
181+
Rename-Item $file.FullName $newName
182+
Write-Host "Renamed $($file.Name) to $newName"
183+
}
184+
185+
- name: Generate categorized release notes
186+
id: release_notes
187+
shell: pwsh
188+
run: |
189+
# Determine if this is a pre-release or stable release
190+
$isPreRelease = "${{ github.ref }}" -eq "refs/heads/${{ github.event.repository.default_branch }}"
191+
$isStableRelease = "${{ github.ref }}" -like "refs/tags/v*"
192+
193+
try {
194+
if ($isPreRelease) {
195+
# For pre-releases, compare against latest stable release
196+
Write-Host "Fetching latest stable release..."
197+
$releases = gh release list --limit 50 --json tagName,isPrerelease 2>$null
198+
if (-not $releases) {
199+
Write-Host "Failed to fetch releases"
200+
echo "notes=" >> $env:GITHUB_OUTPUT
201+
return
202+
}
203+
204+
$releaseList = $releases | ConvertFrom-Json
205+
$latestStable = $releaseList | Where-Object { -not $_.isPrerelease -and $_.tagName } | Select-Object -First 1
206+
207+
if ($latestStable -and $latestStable.tagName) {
208+
$compareFrom = $latestStable.tagName
209+
$releaseType = "pre-release"
210+
$releaseTitle = "🚧 Development Build"
211+
$releaseWarning = "**⚠️ This is an unstable development build. Use at your own risk!**"
212+
Write-Host "Pre-release: Comparing against latest stable release: $compareFrom"
213+
} else {
214+
$compareFrom = ""
215+
Write-Host "No stable release found for comparison"
216+
}
217+
} elseif ($isStableRelease) {
218+
# For stable releases, compare against previous release (stable or pre-release)
219+
Write-Host "Fetching previous releases..."
220+
$releases = gh release list --limit 50 --json tagName 2>$null
221+
if (-not $releases) {
222+
Write-Host "Failed to fetch releases"
223+
echo "notes=" >> $env:GITHUB_OUTPUT
224+
return
225+
}
226+
227+
$allReleases = $releases | ConvertFrom-Json
228+
$currentTag = "${{ github.ref }}" -replace "refs/tags/", ""
229+
$previousRelease = $allReleases | Where-Object { $_.tagName -ne $currentTag -and $_.tagName } | Select-Object -First 1
230+
231+
if ($previousRelease -and $previousRelease.tagName) {
232+
$compareFrom = $previousRelease.tagName
233+
$releaseType = "stable"
234+
$releaseTitle = "🎉 Stable Release"
235+
$releaseWarning = ""
236+
Write-Host "Stable release: Comparing against previous release: $compareFrom"
237+
} else {
238+
$compareFrom = ""
239+
Write-Host "No previous release found for comparison"
240+
}
241+
} else {
242+
Write-Host "Neither pre-release nor stable release, skipping"
243+
echo "notes=" >> $env:GITHUB_OUTPUT
244+
return
245+
}
246+
}
247+
catch {
248+
Write-Host "Error during release detection: $($_.Exception.Message). Using auto-generated notes."
249+
echo "notes=" >> $env:GITHUB_OUTPUT
250+
return
251+
}
252+
253+
if ($compareFrom) {
254+
try {
255+
# Get commits since comparison point with error handling
256+
Write-Host "Fetching commits from $compareFrom to ${{ github.sha }}"
257+
$commits = gh api "repos/${{ github.repository }}/compare/$compareFrom...${{ github.sha }}" --jq '.commits[] | {message: .commit.message, sha: .sha, author: .commit.author.name, url: .html_url}' 2>$null
258+
259+
if (-not $commits) {
260+
Write-Host "No commits found or API call failed, falling back to auto-generated notes"
261+
echo "notes=" >> $env:GITHUB_OUTPUT
262+
return
263+
}
264+
265+
$commitList = $commits | ConvertFrom-Json
266+
if (-not $commitList -or $commitList.Count -eq 0) {
267+
Write-Host "No commits to process"
268+
echo "notes=" >> $env:GITHUB_OUTPUT
269+
return
270+
}
271+
}
272+
catch {
273+
Write-Host "Error fetching commits: $($_.Exception.Message). Using auto-generated notes."
274+
echo "notes=" >> $env:GITHUB_OUTPUT
275+
return
276+
}
277+
278+
# Initialize grouped commits using ArrayList for better performance
279+
$grouped = @{
280+
'feat' = New-Object System.Collections.ArrayList
281+
'fix' = New-Object System.Collections.ArrayList
282+
'docs' = New-Object System.Collections.ArrayList
283+
'style' = New-Object System.Collections.ArrayList
284+
'refactor' = New-Object System.Collections.ArrayList
285+
'perf' = New-Object System.Collections.ArrayList
286+
'test' = New-Object System.Collections.ArrayList
287+
'build' = New-Object System.Collections.ArrayList
288+
'ci' = New-Object System.Collections.ArrayList
289+
'chore' = New-Object System.Collections.ArrayList
290+
'revert' = New-Object System.Collections.ArrayList
291+
'other' = New-Object System.Collections.ArrayList
292+
}
293+
294+
foreach ($commit in $commitList) {
295+
# Input validation
296+
if (-not $commit.message -or -not $commit.sha -or -not $commit.url) {
297+
Write-Host "Skipping invalid commit object"
298+
continue
299+
}
300+
301+
$message = ($commit.message -split "`n" | Select-Object -First 1).Trim()
302+
303+
# Safe substring operation
304+
$shortSha = if ($commit.sha.Length -ge 7) {
305+
$commit.sha.Substring(0, 7)
306+
} else {
307+
$commit.sha
308+
}
309+
310+
$commitUrl = $commit.url
311+
312+
# Sanitize message to prevent markdown injection
313+
$sanitizedMessage = $message -replace '[<>&"]', '' -replace '\[', '\\[' -replace '\]', '\\]' -replace '`', '\\`'
314+
315+
# Parse conventional commit format
316+
if ($sanitizedMessage -match '^(\w+)(\(.+\))?\s*!?\s*:\s*(.+)$') {
317+
$type = $matches[1].ToLower()
318+
$description = $matches[3].Trim()
319+
320+
# Limit description length
321+
if ($description.Length -gt 100) {
322+
$description = $description.Substring(0, 97) + "..."
323+
}
324+
325+
if ($grouped.ContainsKey($type)) {
326+
$null = $grouped[$type].Add("- **$description** ([$shortSha]($commitUrl))")
327+
} else {
328+
$null = $grouped['other'].Add("- **$description** ([$shortSha]($commitUrl))")
329+
}
330+
} else {
331+
# Non-conventional commit
332+
$truncatedMessage = if ($sanitizedMessage.Length -gt 100) {
333+
$sanitizedMessage.Substring(0, 97) + "..."
334+
} else {
335+
$sanitizedMessage
336+
}
337+
$null = $grouped['other'].Add("- **$truncatedMessage** ([$shortSha]($commitUrl))")
338+
}
339+
}
340+
341+
# Build categorized release notes
342+
$sections = @()
343+
344+
# Define section mappings with emojis
345+
$sectionMap = @{
346+
'feat' = @{ title = '🚀 New Features'; items = $grouped['feat'] }
347+
'fix' = @{ title = '🐛 Bug Fixes'; items = $grouped['fix'] }
348+
'docs' = @{ title = '📚 Documentation'; items = $grouped['docs'] }
349+
'perf' = @{ title = '⚡ Performance'; items = $grouped['perf'] }
350+
'refactor' = @{ title = '♻️ Code Refactoring'; items = $grouped['refactor'] }
351+
'style' = @{ title = '💎 Style Changes'; items = $grouped['style'] }
352+
'test' = @{ title = '🧪 Tests'; items = $grouped['test'] }
353+
'build' = @{ title = '📦 Build System'; items = $grouped['build'] }
354+
'ci' = @{ title = '⚙️ CI/CD'; items = $grouped['ci'] }
355+
'chore' = @{ title = '🔧 Maintenance'; items = $grouped['chore'] }
356+
'revert' = @{ title = '⏪ Reverts'; items = $grouped['revert'] }
357+
'other' = @{ title = '📋 Other Changes'; items = $grouped['other'] }
358+
}
359+
360+
# Process sections in a defined order for consistency
361+
$orderedKeys = @('feat', 'fix', 'perf', 'refactor', 'docs', 'style', 'test', 'build', 'ci', 'chore', 'revert', 'other')
362+
foreach ($key in $orderedKeys) {
363+
if ($sectionMap.ContainsKey($key) -and $sectionMap[$key].items.Count -gt 0) {
364+
$sections += ""
365+
$sections += "### $($sectionMap[$key].title)"
366+
$sections += ""
367+
$sections += $sectionMap[$key].items.ToArray()
368+
}
369+
}
370+
371+
$changesSummary = if ($sections.Count -gt 0) { $sections -join "`n" } else { "No changes found." }
372+
373+
# Build release notes based on release type
374+
if ($isPreRelease) {
375+
$fullNotes = @"
376+
## $releaseTitle
377+
378+
$releaseWarning
379+
380+
### Changes since last stable release ($compareFrom):
381+
$changesSummary
382+
383+
---
384+
**Build Info:**
385+
- Commit: [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }})
386+
- Build: #${{ github.run_number }}
387+
- Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss UTC')
388+
"@
389+
} else {
390+
$currentVersion = "${{ github.ref }}" -replace "refs/tags/", ""
391+
$fullNotes = @"
392+
## $releaseTitle
393+
394+
### Changes since previous release ($compareFrom):
395+
$changesSummary
396+
397+
---
398+
**Release Info:**
399+
- Version: $currentVersion
400+
- Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss UTC')
401+
"@
402+
}
403+
404+
# Output the notes for next step (escape for multiline)
405+
$fullNotes = $fullNotes -replace "(\r\n|\r|\n)", "%0A"
406+
echo "notes=$fullNotes" >> $env:GITHUB_OUTPUT
407+
} else {
408+
Write-Host "No comparison point found, using auto-generated notes"
409+
echo "notes=" >> $env:GITHUB_OUTPUT
410+
}
411+
env:
412+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
413+
174414
- name: Pre Release
175415
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
176416
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
@@ -179,16 +419,17 @@ jobs:
179419
prerelease: true
180420
name: "Development Build"
181421
files: release-artifacts/*
182-
generate_release_notes: true
422+
body: ${{ steps.release_notes.outputs.notes || '' }}
423+
generate_release_notes: ${{ steps.release_notes.outputs.notes == '' }}
183424
token: ${{ secrets.GITHUB_TOKEN }}
184-
append_body: false
185425

186426
- name: Release
187427
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
188428
if: startsWith(github.ref, 'refs/tags/v')
189429
with:
190430
prerelease: false
191431
files: release-artifacts/*
192-
generate_release_notes: true
432+
body: ${{ steps.release_notes.outputs.notes || '' }}
433+
generate_release_notes: ${{ steps.release_notes.outputs.notes == '' }}
193434
token: ${{ secrets.GITHUB_TOKEN }}
194435
make_latest: true

0 commit comments

Comments
 (0)