Skip to content

chore(release): prepare v1.1.2 release alignment #8

chore(release): prepare v1.1.2 release alignment

chore(release): prepare v1.1.2 release alignment #8

Workflow file for this run

name: Release
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
permissions:
contents: write
jobs:
build:
runs-on: windows-latest
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Resolve version
id: version
shell: pwsh
run: |
$refName = "${{ github.ref_name }}"
if ($refName -match '^v\d+\.\d+\.\d+$') {
$version = $refName.TrimStart('v')
$tag = $refName
}
else {
[xml]$project = Get-Content "ThreadPilot.csproj"
$version = $project.Project.PropertyGroup.Version
if ([string]::IsNullOrWhiteSpace($version)) {
throw "Unable to resolve version from tag or ThreadPilot.csproj"
}
$tag = "v$version"
}
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
- name: Restore
run: dotnet restore "ThreadPilot_1.sln"
- name: Build
run: dotnet build "ThreadPilot_1.sln" --configuration Release --no-restore
- name: Test
run: dotnet test "ThreadPilot_1.sln" --configuration Release --no-build --collect:"XPlat Code Coverage" --results-directory TestResults
- name: Publish self-contained single-file
run: dotnet publish "ThreadPilot.csproj" --configuration Release -p:PublishProfile=WinX64-SingleFile
- name: Ensure bundled power plans in single-file output
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
New-Item -ItemType Directory -Force -Path "artifacts/release/singlefile/Powerplans" | Out-Null
Copy-Item "assets/Powerplans/*" -Destination "artifacts/release/singlefile/Powerplans" -Recurse -Force
- name: Publish ReadyToRun
run: dotnet publish "ThreadPilot.csproj" --configuration Release -p:PublishProfile=WinX64-ReadyToRun
- name: Install Inno Setup
shell: pwsh
run: choco install innosetup --no-progress -y
- name: Build Inno Setup installer
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$version = "${{ steps.version.outputs.version }}"
$sourceDir = (Resolve-Path "artifacts/release/singlefile").Path
& iscc.exe "/DMyAppVersion=$version" "/DMyAppSourceDir=$sourceDir" "Installer/setup.iss"
- name: Prepare signing certificate (optional)
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($env:WINDOWS_SIGNING_CERT_BASE64) -or [string]::IsNullOrWhiteSpace($env:WINDOWS_SIGNING_CERT_PASSWORD))
{
Write-Host "Signing secrets not provided via environment variables. Skipping certificate preparation."
exit 0
}
$certPath = Join-Path $env:RUNNER_TEMP "threadpilot-signing.pfx"
[System.IO.File]::WriteAllBytes($certPath, [System.Convert]::FromBase64String($env:WINDOWS_SIGNING_CERT_BASE64))
"THREADPILOT_SIGN_CERT_PATH=$certPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Sign binaries and packages (optional)
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($env:THREADPILOT_SIGN_CERT_PATH) -or -not (Test-Path $env:THREADPILOT_SIGN_CERT_PATH))
{
Write-Host "No signing certificate prepared. Skipping signing step."
exit 0
}
if (-not (Get-Command signtool.exe -ErrorAction SilentlyContinue))
{
throw "signtool.exe not found on runner; install Windows SDK or disable signing."
}
$timestampUrl = "http://timestamp.digicert.com"
$password = $env:WINDOWS_SIGNING_CERT_PASSWORD
$targets = @()
$targets += Get-ChildItem "artifacts/release/singlefile" -Recurse -File -Include *.exe -ErrorAction SilentlyContinue
$targets += Get-ChildItem "artifacts/release/readytorun" -Recurse -File -Include *.exe -ErrorAction SilentlyContinue
$targets += Get-ChildItem "artifacts/release/installer" -Recurse -File -Include *.exe -ErrorAction SilentlyContinue
foreach ($target in $targets)
{
signtool.exe sign /fd SHA256 /tr $timestampUrl /td SHA256 /f "$env:THREADPILOT_SIGN_CERT_PATH" /p $password "$($target.FullName)"
}
- name: Create release packages
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$version = "${{ steps.version.outputs.tag }}"
New-Item -ItemType Directory -Force -Path "artifacts/release/packages" | Out-Null
New-Item -ItemType Directory -Force -Path "artifacts/release/package-staging" | Out-Null
$sharedUninstall = "artifacts/release/package-staging/uninstall.bat"
$sharedLicense = "artifacts/release/package-staging/LICENSE.md"
$uninstallContent = @'
@echo off
setlocal EnableExtensions
title ThreadPilot Uninstaller
set "APP_DIR=%~dp0"
if "%APP_DIR:~-1%"=="\" set "APP_DIR=%APP_DIR:~0,-1%"
echo ======================================================
echo ThreadPilot Uninstaller
echo ======================================================
echo.
echo [1/4] Closing running ThreadPilot processes...
taskkill /IM "ThreadPilot.exe" /F >nul 2>&1
echo [2/4] Removing startup task and startup registry entry...
schtasks /Delete /TN "ThreadPilot_Startup" /F >nul 2>&1
reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "ThreadPilot" /f >nul 2>&1
echo [3/4] Optional user data cleanup...
set "REMOVE_DATA=N"
set /p REMOVE_DATA=Do you want to remove user settings at "%APPDATA%\ThreadPilot"? [y/N]:
if /I "%REMOVE_DATA%"=="Y" (
if exist "%APPDATA%\ThreadPilot" (
rd /s /q "%APPDATA%\ThreadPilot"
echo User settings removed.
) else (
echo No user settings folder found.
)
) else (
echo User settings were kept.
)
echo [4/4] Scheduling app folder removal...
start "" /min cmd /c "timeout /t 5 /nobreak >nul & rd /s /q \"%APP_DIR%\""
echo.
echo Uninstall completed. Remaining files will be removed in a few seconds.
endlocal
exit /b 0
'@
Set-Content -LiteralPath $sharedUninstall -Value $uninstallContent -Encoding Ascii
Copy-Item "LICENSE" -Destination $sharedLicense -Force
$singleStage = "artifacts/release/package-staging/singlefile"
$readyToRunStage = "artifacts/release/package-staging/readytorun"
New-Item -ItemType Directory -Force -Path $singleStage | Out-Null
New-Item -ItemType Directory -Force -Path $readyToRunStage | Out-Null
Copy-Item "artifacts/release/singlefile/*" -Destination $singleStage -Recurse -Force
Copy-Item "artifacts/release/readytorun/*" -Destination $readyToRunStage -Recurse -Force
Copy-Item $sharedUninstall -Destination (Join-Path $singleStage "uninstall.bat") -Force
Copy-Item $sharedLicense -Destination (Join-Path $singleStage "LICENSE.md") -Force
Copy-Item $sharedUninstall -Destination (Join-Path $readyToRunStage "uninstall.bat") -Force
Copy-Item $sharedLicense -Destination (Join-Path $readyToRunStage "LICENSE.md") -Force
$singleFileZip = "artifacts/release/packages/ThreadPilot_$version`_singlefile_win-x64.zip"
$readyToRunZip = "artifacts/release/packages/ThreadPilot_$version`_readytorun_win-x64.zip"
Compress-Archive -Path "$singleStage/*" -DestinationPath $singleFileZip -Force
Compress-Archive -Path "$readyToRunStage/*" -DestinationPath $readyToRunZip -Force
- name: Generate checksums
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$hashFile = "artifacts/release/SHA256SUMS.txt"
$releaseFiles = @()
$releaseFiles += Get-ChildItem "artifacts/release/packages" -File -ErrorAction SilentlyContinue
$releaseFiles += Get-ChildItem "artifacts/release/installer/*.exe" -File -ErrorAction SilentlyContinue
$releaseFiles | ForEach-Object {
$hash = Get-FileHash $_.FullName -Algorithm SHA256
"$($hash.Hash) $($_.Name)" | Out-File -FilePath $hashFile -Append -Encoding utf8
}
- name: Update winget installer manifest
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$version = "${{ steps.version.outputs.version }}"
$tag = "${{ steps.version.outputs.tag }}"
$installer = Get-ChildItem "artifacts/release/installer/*.exe" -File | Select-Object -First 1
if (-not $installer)
{
throw "Installer executable not found."
}
$sha = (Get-FileHash $installer.FullName -Algorithm SHA256).Hash.ToUpperInvariant()
$url = "https://github.com/${{ github.repository }}/releases/download/$tag/$($installer.Name)"
$manifestPath = "winget/manifests/p/PrimeBuild/ThreadPilot/$version/PrimeBuild.ThreadPilot.installer.yaml"
if (-not (Test-Path $manifestPath))
{
throw "Winget installer manifest not found at $manifestPath"
}
$content = Get-Content $manifestPath -Raw
$content = $content.Replace("{{INSTALLER_URL}}", $url).Replace("{{INSTALLER_SHA256}}", $sha)
Set-Content -Path $manifestPath -Value $content -Encoding utf8
- name: Generate SBOM
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
dotnet tool install --global Microsoft.Sbom.DotNetTool
$env:PATH += ";$env:USERPROFILE\.dotnet\tools"
sbom-tool generate -b ./artifacts -bc . -pn ThreadPilot -pv "${{ steps.version.outputs.version }}" -ps PrimeBuild -nsb https://github.com/PrimeBuild-pc/ThreadPilot
$sbomReleaseDir = "artifacts/release/sbom"
New-Item -ItemType Directory -Force -Path $sbomReleaseDir | Out-Null
$sbomManifest = Get-ChildItem "artifacts" -Recurse -File -Filter "manifest.spdx.json" -ErrorAction SilentlyContinue |
Sort-Object LastWriteTimeUtc -Descending |
Select-Object -First 1
if (-not $sbomManifest)
{
throw "SBOM manifest not found under artifacts after generation."
}
Copy-Item -LiteralPath $sbomManifest.FullName -Destination (Join-Path $sbomReleaseDir "manifest.spdx.json") -Force
- name: Upload portable artifact
uses: actions/upload-artifact@v4
with:
name: release-portable
path: artifacts/release/singlefile
- name: Upload installer artifact
uses: actions/upload-artifact@v4
with:
name: release-installer
path: artifacts/release/installer
- name: Upload packages artifact
uses: actions/upload-artifact@v4
with:
name: release-packages
path: artifacts/release/packages
- name: Upload checksums artifact
uses: actions/upload-artifact@v4
with:
name: release-checksums
path: artifacts/release/SHA256SUMS.txt
- name: Upload winget manifests
uses: actions/upload-artifact@v4
with:
name: winget-manifests
path: winget/manifests/p/PrimeBuild/ThreadPilot/${{ steps.version.outputs.version }}
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: artifacts/release/sbom/manifest.spdx.json
if-no-files-found: error
smoke-test:
runs-on: windows-latest
needs: build
steps:
- uses: actions/download-artifact@v4
with:
name: release-portable
path: smoke
- name: Smoke test
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$exe = Get-ChildItem "smoke" -Recurse -Filter "ThreadPilot.exe" -File | Select-Object -First 1
if (-not $exe)
{
throw "ThreadPilot.exe not found in smoke-test artifact."
}
$process = Start-Process -FilePath $exe.FullName -ArgumentList "--smoke-test" -Wait -PassThru
if ($process.ExitCode -ne 0)
{
throw "Smoke test failed with exit code $($process.ExitCode)."
}
release:
runs-on: ubuntu-latest
needs:
- build
- smoke-test
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download installer artifact
uses: actions/download-artifact@v4
with:
name: release-installer
path: release-assets/installer
- name: Download packages artifact
uses: actions/download-artifact@v4
with:
name: release-packages
path: release-assets/packages
- name: Download checksums artifact
uses: actions/download-artifact@v4
with:
name: release-checksums
path: release-assets
- name: Download winget manifests
uses: actions/download-artifact@v4
with:
name: winget-manifests
path: winget-manifests
- name: Download SBOM artifact
uses: actions/download-artifact@v4
with:
name: sbom
path: sbom
- name: Validate release assets
shell: bash
run: |
set -euo pipefail
compgen -G "release-assets/installer/*.exe" > /dev/null || { echo "Missing installer executable in release-assets/installer"; exit 1; }
compgen -G "release-assets/packages/*.zip" > /dev/null || { echo "Missing release package zip in release-assets/packages"; exit 1; }
test -f "release-assets/SHA256SUMS.txt" || { echo "Missing release checksums file"; exit 1; }
find "winget-manifests" -type f -name "*.yaml" | grep -q . || { echo "Missing winget manifest yaml files"; exit 1; }
test -f "sbom/manifest.spdx.json" || { echo "Missing SBOM manifest"; exit 1; }
- name: Generate changelog
id: git-cliff
uses: orhun/git-cliff-action@v3
with:
config: cliff.toml
args: --latest
- name: Contributors file check (manual maintenance mode)
shell: pwsh
run: |
if (-not (Test-Path "CONTRIBUTORS.md")) {
throw "CONTRIBUTORS.md not found."
}
Write-Host "CONTRIBUTORS.md is maintained manually in this workflow."
- name: Publish GitHub release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.build.outputs.tag }}
name: ThreadPilot ${{ needs.build.outputs.tag }}
body: ${{ steps.git-cliff.outputs.content }}
files: |
release-assets/installer/*.exe
release-assets/packages/*.zip
release-assets/SHA256SUMS.txt
winget-manifests/**/*.yaml
sbom/manifest.spdx.json
generate_release_notes: true