Skip to content

Fix atrocity arg parsing #61

Fix atrocity arg parsing

Fix atrocity arg parsing #61

Workflow file for this run

name: Release Build
on:
pull_request:
types: [closed]
branches:
- main
workflow_dispatch:
inputs:
version:
description: "Release tag to build (e.g. v0.1.7)"
required: false
type: string
version_number:
description: "Version number for compilation (e.g. 0.1.7)"
required: false
type: string
notes:
description: "Release notes override"
required: false
type: string
permissions:
contents: write
actions: write
checks: write
jobs:
prepare:
name: Prepare release (check files, version, notes)
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.override.outputs.should_release || steps.check_files.outputs.should_release }}
version: ${{ steps.override.outputs.version || steps.next_version.outputs.version }}
version_number: ${{ steps.override.outputs.version_number || steps.next_version.outputs.version_number }}
notes: ${{ steps.override.outputs.notes || steps.parse_commit.outputs.notes }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Override outputs for manual runs
if: github.event_name == 'workflow_dispatch'
id: override
shell: pwsh
run: |
Write-Output "should_release=true" >> $env:GITHUB_OUTPUT
if (-not [string]::IsNullOrWhiteSpace("${{ inputs.version }}")) {
Write-Output "version=${{ inputs.version }}" >> $env:GITHUB_OUTPUT
}
if (-not [string]::IsNullOrWhiteSpace("${{ inputs.version_number }}")) {
Write-Output "version_number=${{ inputs.version_number }}" >> $env:GITHUB_OUTPUT
} elseif (-not [string]::IsNullOrWhiteSpace("${{ inputs.version }}")) {
$verNum = "${{ inputs.version }}"
if ($verNum.StartsWith("v")) { $verNum = $verNum.Substring(1) }
Write-Output "version_number=$verNum" >> $env:GITHUB_OUTPUT
}
if (-not [string]::IsNullOrWhiteSpace("${{ inputs.notes }}")) {
Write-Output "notes=${{ inputs.notes }}" >> $env:GITHUB_OUTPUT
}
- name: Resolve last merged PR for manual runs
if: github.event_name == 'workflow_dispatch'
id: resolve_pr
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$repo = "${{ github.repository }}"
$headers = @{"Authorization" = "token $env:GITHUB_TOKEN"; "Accept" = "application/vnd.github.v3+json"}
$prs = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/pulls?state=closed&base=main&sort=updated&direction=desc&per_page=50" -Headers $headers -UseBasicParsing
$merged = $prs | Where-Object { $_.merged_at -ne $null } | Select-Object -First 1
if (-not $merged) {
Write-Error "No merged PR found for base=main"
exit 1
}
Write-Output "base_sha=$($merged.base.sha)" >> $env:GITHUB_OUTPUT
Write-Output "head_sha=$($merged.head.sha)" >> $env:GITHUB_OUTPUT
# Get the merge commit SHA and fetch its message body
# This allows personalized release notes to be written in the merge commit message
$mergeCommitSha = $merged.merge_commit_sha
Write-Output "merge_commit_sha=$mergeCommitSha" >> $env:GITHUB_OUTPUT
# Fetch the merge commit details to get its message body
if (-not [string]::IsNullOrWhiteSpace($mergeCommitSha)) {
try {
$commitData = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/commits/$mergeCommitSha" -Headers $headers -UseBasicParsing
$commitMessageParts = $commitData.commit.message -split "`n", 2
if ($commitMessageParts.Count -gt 1) {
$body = $commitMessageParts[1].Trim()
} else {
$body = ""
}
} catch {
Write-Output "Warning: Could not fetch merge commit body, using empty string"
$body = ""
}
} else {
$body = ""
}
Write-Output "merge_body=$body" >> $env:GITHUB_OUTPUT
- name: Check if source files changed
if: github.event_name != 'workflow_dispatch'
id: check_files
shell: pwsh
run: |
Write-Output "DEBUG: Starting check_files"
Write-Output "DEBUG: PR Event Data - Base: ${{ github.event.pull_request.base.sha }} - Head: ${{ github.event.pull_request.head.sha }}"
$changedFiles = git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}
Write-Output "DEBUG: Changed files found:"
Write-Output $changedFiles
$sourceChanged = $false
foreach ($file in $changedFiles) {
if ($file -match '\.(cpp|h|hpp|c|cc|cxx)$') {
$sourceChanged = $true
Write-Output "DEBUG: Source file changed: $file"
break
}
}
Write-Output "DEBUG: sourceChanged is $sourceChanged"
if ($sourceChanged) {
Write-Output "should_release=true" >> $env:GITHUB_OUTPUT
Write-Output "DEBUG: Setting should_release=true"
} else {
Write-Output "should_release=false" >> $env:GITHUB_OUTPUT
Write-Output "DEBUG: Setting should_release=false"
}
- name: Get latest release tag
if: github.event_name != 'workflow_dispatch' && steps.check_files.outputs.should_release == 'true'
id: get_latest_tag
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
Write-Output "DEBUG: Starting get_latest_tag"
$repo = "${{ github.repository }}"
Write-Output "DEBUG: Repo is $repo"
$headers = @{"Authorization" = "token $env:GITHUB_TOKEN"; "Accept" = "application/vnd.github.v3+json"}
# Try GitHub Releases API first (includes prereleases)
try {
Write-Output "DEBUG: Fetching releases from API..."
$releases = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases?per_page=100" -Headers $headers -UseBasicParsing
Write-Output "DEBUG: Fetched $($releases.Count) releases"
} catch {
Write-Output "DEBUG: Failed to fetch releases from API: $_"
$releases = @()
}
$candidates = @()
foreach ($r in $releases) {
Write-Output "DEBUG: Checking release tag: $($r.tag_name)"
if ($r.tag_name -match 'v?(\d+)\.(\d+)\.(\d+)') {
$candidates += [PSCustomObject]@{ Tag = $r.tag_name; Major = [int]$Matches[1]; Minor = [int]$Matches[2]; Patch = [int]$Matches[3] }
}
}
if ($candidates.Count -gt 0) {
Write-Output "DEBUG: Found valid candidates in releases."
$sorted = $candidates | Sort-Object -Property @{Expression={$_.Major};Descending=$true}, @{Expression={$_.Minor};Descending=$true}, @{Expression={$_.Patch};Descending=$true}
$latestTag = $sorted[0].Tag
Write-Output "DEBUG: Selected latest tag from API: $latestTag"
Write-Output "latest_tag=$latestTag" >> $env:GITHUB_OUTPUT
exit 0
}
# Fallback: ensure tags are fetched and inspect local tags
Write-Output "DEBUG: Fallback to local git tags"
git fetch --tags --prune || true
$local = git tag -l "v[0-9]*.[0-9]*.[0-9]*"
Write-Output "DEBUG: Local tags found: $local"
if ($local) {
$sortedTags = $local | ForEach-Object {
if ($_ -match 'v?(\d+)\.(\d+)\.(\d+)') {
[PSCustomObject]@{ Tag = $_; Major = [int]$Matches[1]; Minor = [int]$Matches[2]; Patch = [int]$Matches[3] }
}
} | Sort-Object -Property @{Expression={$_.Major};Descending=$true}, @{Expression={$_.Minor};Descending=$true}, @{Expression={$_.Patch};Descending=$true}
if ($sortedTags.Count -gt 0) {
$latestTag = $sortedTags[0].Tag
Write-Output "DEBUG: Selected latest tag from local tags: $latestTag"
Write-Output "latest_tag=$latestTag" >> $env:GITHUB_OUTPUT
} else {
Write-Output "DEBUG: No valid semantic version tags found locally. Defaulting to v0.0.0"
Write-Output "latest_tag=v0.0.0" >> $env:GITHUB_OUTPUT
}
} else {
Write-Output "DEBUG: No local tags found. Defaulting to v0.0.0"
Write-Output "latest_tag=v0.0.0" >> $env:GITHUB_OUTPUT
}
- name: Determine next version
if: github.event_name != 'workflow_dispatch' && steps.check_files.outputs.should_release == 'true'
id: next_version
shell: pwsh
run: |
$latestTag = "${{ steps.get_latest_tag.outputs.latest_tag }}"
Write-Output "DEBUG: Starting next_version. Input latestTag: $latestTag"
if ($latestTag -match 'v?(\d+)\.(\d+)\.(\d+)') {
$major = [int]$Matches[1]
$minor = [int]$Matches[2]
$patch = [int]$Matches[3]
$patch = $patch + 1
$newVersion = "v$major.$minor.$patch"
Write-Output "DEBUG: Calculated new version: $newVersion"
Write-Output "version=$newVersion" >> $env:GITHUB_OUTPUT
Write-Output "version_number=$major.$minor.$patch" >> $env:GITHUB_OUTPUT
} else {
Write-Error "Could not parse version from tag: $latestTag"
exit 1
}
- name: Parse commit message for release notes
if: (github.event_name == 'workflow_dispatch' && steps.override.outputs.should_release == 'true') || (github.event_name != 'workflow_dispatch' && steps.check_files.outputs.should_release == 'true')
id: parse_commit
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
Write-Output "DEBUG: Starting parse_commit"
# Get all commits from the PR (base to head)
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
$baseSha = "${{ steps.resolve_pr.outputs.base_sha }}"
$headSha = "${{ steps.resolve_pr.outputs.head_sha }}"
} else {
$baseSha = "${{ github.event.pull_request.base.sha }}"
$headSha = "${{ github.event.pull_request.head.sha }}"
}
Write-Output "DEBUG: Base SHA: $baseSha"
Write-Output "DEBUG: Head SHA: $headSha"
# Get the merge commit message body for personalized release notes
# This extracts the extended description from the merge commit (not the PR description)
# The merge commit body allows writing custom release notes during PR merge
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
# For manual runs, use the merge commit body from resolve_pr step
$mergeCommitBody = "${{ steps.resolve_pr.outputs.merge_body }}"
} else {
# For PR events, get the merge commit SHA and fetch its body
$mergeCommitSha = "${{ github.event.pull_request.merge_commit_sha }}"
Write-Output "DEBUG: Merge commit SHA: $mergeCommitSha"
if (-not [string]::IsNullOrWhiteSpace($mergeCommitSha)) {
try {
$repo = "${{ github.repository }}"
$headers = @{"Authorization" = "token $env:GITHUB_TOKEN"; "Accept" = "application/vnd.github.v3+json"}
$commitData = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/commits/$mergeCommitSha" -Headers $headers -UseBasicParsing
$commitMessage = $commitData.commit.message
# Split the message to get just the body (everything after the first line)
$messageParts = $commitMessage -split "`n", 2
if ($messageParts.Count -gt 1) {
$mergeCommitBody = $messageParts[1].Trim()
} else {
$mergeCommitBody = ""
}
} catch {
Write-Output "DEBUG: Could not fetch merge commit body via API, falling back to git log"
# Fallback to git log if API fails
$mergeCommitBody = git log -1 --pretty=format:"%b" "$mergeCommitSha" 2>$null
if ([string]::IsNullOrWhiteSpace($mergeCommitBody)) {
Write-Output "DEBUG: Git log fallback returned empty result for merge commit"
$mergeCommitBody = ""
}
}
} else {
Write-Output "DEBUG: No merge commit SHA available, using empty body"
$mergeCommitBody = ""
}
}
Write-Output "DEBUG: Merge commit body: $mergeCommitBody"
# Get all commit messages in the PR
$commits = @(git log --pretty=format:"%h|%s|%b" "$baseSha..$headSha")
Write-Output "DEBUG: Found $($commits.Count) commits in PR"
# Initialize categorized lists
$features = @()
$fixes = @()
$performance = @()
$refactors = @()
$docs = @()
$tests = @()
$chores = @()
$builds = @()
$ci = @()
$style = @()
$reverts = @()
$others = @()
# Parse each commit
foreach ($line in $commits) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
$parts = $line -split '\|', 3
if ($parts.Count -lt 2) { continue }
$commitHash = $parts[0]
$subject = $parts[1]
$body = if ($parts.Count -eq 3) { $parts[2] } else { "" }
Write-Output "DEBUG: Processing commit $commitHash : $subject"
# Parse conventional commit format
if ($subject -match '^(fix|feat|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?:\s*(.+)$') {
$type = $Matches[1]
$scope = if ($Matches[2]) { $Matches[2] } else { "" }
$description = $Matches[3]
$entry = "- $description ($commitHash)"
switch ($type) {
'feat' { $features += $entry }
'fix' { $fixes += $entry }
'perf' { $performance += $entry }
'refactor' { $refactors += $entry }
'docs' { $docs += $entry }
'test' { $tests += $entry }
'chore' { $chores += $entry }
'build' { $builds += $entry }
'ci' { $ci += $entry }
'style' { $style += $entry }
'revert' { $reverts += $entry }
default { $others += "- $subject ($commitHash)" }
}
} else {
# Non-conventional commit
$others += "- $subject ($commitHash)"
}
}
# Build release notes with BOTH merge commit body AND PR commit history
# The merge commit body appears first (for personalized notes), followed by
# categorized commit history from the PR
$releaseNotes = ""
# Add merge commit body at the top if it exists (personalized release notes)
if (-not [string]::IsNullOrWhiteSpace($mergeCommitBody)) {
$releaseNotes += "$mergeCommitBody`n`n---`n`n"
}
$releaseNotes += "## Changes`n`n"
if ($features.Count -gt 0) {
$releaseNotes += "### ✨ Features`n"
$releaseNotes += ($features -join "`n") + "`n`n"
}
if ($fixes.Count -gt 0) {
$releaseNotes += "### 🐛 Fixes`n"
$releaseNotes += ($fixes -join "`n") + "`n`n"
}
if ($performance.Count -gt 0) {
$releaseNotes += "### ⚡ Performance`n"
$releaseNotes += ($performance -join "`n") + "`n`n"
}
if ($refactors.Count -gt 0) {
$releaseNotes += "### ♻️ Refactoring`n"
$releaseNotes += ($refactors -join "`n") + "`n`n"
}
if ($docs.Count -gt 0) {
$releaseNotes += "### 📝 Documentation`n"
$releaseNotes += ($docs -join "`n") + "`n`n"
}
if ($tests.Count -gt 0) {
$releaseNotes += "### ✅ Tests`n"
$releaseNotes += ($tests -join "`n") + "`n`n"
}
if ($builds.Count -gt 0) {
$releaseNotes += "### 📦 Build`n"
$releaseNotes += ($builds -join "`n") + "`n`n"
}
if ($chores.Count -gt 0) {
$releaseNotes += "### 🔧 Chores`n"
$releaseNotes += ($chores -join "`n") + "`n`n"
}
if ($ci.Count -gt 0) {
$releaseNotes += "### 🔄 CI/CD`n"
$releaseNotes += ($ci -join "`n") + "`n`n"
}
if ($style.Count -gt 0) {
$releaseNotes += "### 💄 Style`n"
$releaseNotes += ($style -join "`n") + "`n`n"
}
if ($reverts.Count -gt 0) {
$releaseNotes += "### ⏪ Reverts`n"
$releaseNotes += ($reverts -join "`n") + "`n`n"
}
if ($others.Count -gt 0) {
$releaseNotes += "### 📋 Other Changes`n"
$releaseNotes += ($others -join "`n") + "`n`n"
}
Write-Output "DEBUG: releaseNotes: $releaseNotes"
$releaseNotes += "---`n*Based on commits from $baseSha to $headSha*"
$delimiter = "EOF_$(Get-Random)"
Write-Output "DEBUG: Writing notes to output using delimiter $delimiter"
Write-Output "notes<<$delimiter" >> $env:GITHUB_OUTPUT
Write-Output $releaseNotes >> $env:GITHUB_OUTPUT
Write-Output $delimiter >> $env:GITHUB_OUTPUT
build:
name: Build matrix for multiple Windows architectures
needs: prepare
if: needs.prepare.outputs.should_release == 'true'
runs-on: windows-latest
strategy:
matrix:
arch: [x64, x86, arm64]
steps:
- uses: actions/checkout@v4
- name: Compile for ${{ matrix.arch }}
shell: pwsh
run: |
# Set version as environment variable
$env:VERSION_NUMBER = "${{ needs.prepare.outputs.version }}"
# Find and initialize MSVC
$vsPath = & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" `
-latest -products * `
-requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 `
-property installationPath
$vcvarsPath = Join-Path $vsPath "VC\Auxiliary\Build\vcvarsall.bat"
$targetArch = "${{ matrix.arch }}"
$vcvarsArch = $targetArch
if ($targetArch -eq "x86") { $vcvarsArch = "x64_x86" }
if ($targetArch -eq "arm64") { $vcvarsArch = "x64_arm64" }
# Use cmd to call vcvarsall and then cl
$outName = "win-witr-${{ matrix.arch }}.exe"
$verTag = "${{ needs.prepare.outputs.version }}"
cmd /c "`"$vcvarsPath`" $vcvarsArch && cl /O2 /Ot /GL /std:c++20 /EHsc main.cpp /DUNICODE /D_UNICODE /DVERSION_NUMBER=$verTag /Fe:$outName"
if ($LASTEXITCODE -ne 0) {
Write-Host "Build failed with exit code $LASTEXITCODE"
Exit $LASTEXITCODE
}
- name: Upload build artifact for ${{ matrix.arch }}
uses: actions/upload-artifact@v4
with:
name: win-witr-${{ matrix.arch }}
path: win-witr-${{ matrix.arch }}.exe
create-release:
name: Create GitHub Release with all artifacts
needs: [prepare, build]
if: needs.prepare.outputs.should_release == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository for tagging
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Download all build artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: List artifacts
run: ls -R artifacts
- name: Flatten artifact directory structure
run: |
set -e
echo "Flattening artifact directory structure..."
find artifacts -name "*.exe" -type f -exec mv -n {} artifacts/ \;
echo "Removing empty subdirectories..."
find artifacts -mindepth 1 -type d -delete || echo "Warning: Failed to delete some subdirectories in artifacts/ - this may indicate remaining files or permission issues"
echo "Flattened artifacts directory:"
ls -la artifacts/
- name: Ensure release tag exists and push
if: needs.prepare.outputs.version != ''
shell: bash
run: |
set -e
echo "DEBUG: Ensure Release Tag Exists"
echo "DEBUG: received version: '${{ needs.prepare.outputs.version }}'"
echo "DEBUG: received sha: $(git rev-parse HEAD)"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
TAG="${{ needs.prepare.outputs.version }}"
echo "Preparing tag: $TAG"
if [ -z "$TAG" ]; then
echo "No tag provided; cannot create release"
exit 1
fi
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "DEBUG: Tag $TAG already exists locally"
else
# Explicitly tag the HEAD sha to match the checked out state
HEAD_SHA=$(git rev-parse HEAD)
echo "DEBUG: Creating tag $TAG on $HEAD_SHA"
git tag -a "$TAG" -m "Release $TAG" "$HEAD_SHA"
echo "DEBUG: Created local tag $TAG"
fi
# Push tag (skip errors if it already exists on remote)
echo "DEBUG: Pushing tag $TAG"
git push origin "$TAG" || echo "Tag push failed or already exists on remote"
- name: Create Release using GH CLI
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.prepare.outputs.version }}
NOTES: ${{ needs.prepare.outputs.notes }}
run: |
TITLE="win-witr $TAG"
echo "Checking if release '$TAG' exists..."
if gh release view "$TAG" > /dev/null 2>&1; then
echo "Release '$TAG' found. Deleting to recreate..."
gh release delete "$TAG" -y
else
echo "Release '$TAG' does not exist."
fi
echo "Locating artifacts..."
# Find the files anywhere in the current directory, robust to structure changes
FILE_X64=$(find . -type f -name "win-witr-x64.exe" | head -n 1)
FILE_X86=$(find . -type f -name "win-witr-x86.exe" | head -n 1)
FILE_ARM64=$(find . -type f -name "win-witr-arm64.exe" | head -n 1)
echo "Found x64 artifact: $FILE_X64"
echo "Found x86 artifact: $FILE_X86"
echo "Found arm64 artifact: $FILE_ARM64"
if [[ -z "$FILE_X64" || -z "$FILE_X86" || -z "$FILE_ARM64" ]]; then
echo "Error: Could not locate all expected artifacts."
echo "Searching for any .exe files:"
find . -name "*.exe"
exit 1
fi
echo "Creating release '$TAG'..."
gh release create "$TAG" \
"$FILE_X64" \
"$FILE_X86" \
"$FILE_ARM64" \
--title "$TITLE" \
--notes "$NOTES" \
--prerelease