Skip to content

Build and Draft Release #48

Build and Draft Release

Build and Draft Release #48

Workflow file for this run

name: Build and Draft Release
on:
workflow_dispatch:
inputs:
version:
description: "Release version (e.g. 1.2.3.4, v1.2.3.4, 1.0.0.1-beta, v2.0.0.0-rc1)"
required: true
default: "1.0.0.0"
type: string
jobs:
prepare:
name: Prepare Release Version
runs-on: ubuntu-latest
outputs:
release_version: ${{ steps.version.outputs.release_version }}
numeric_version: ${{ steps.version.outputs.numeric_version }}
release_tag: ${{ steps.version.outputs.release_tag }}
steps:
- name: Normalize and Validate Version
id: version
shell: pwsh
env:
VERSION_INPUT: ${{ github.event.inputs.version }}
run: |
$raw = "$env:VERSION_INPUT".Trim()
if ([string]::IsNullOrWhiteSpace($raw)) {
throw "Version input cannot be empty."
}
# Strip optional 'v' prefix
$normalized = if ($raw.StartsWith('v', [System.StringComparison]::OrdinalIgnoreCase)) {
$raw.Substring(1)
} else {
$raw
}
# Split into numeric part and optional pre-release suffix (e.g. "1.0.0.1-beta" -> "1.0.0.1" + "-beta")
if ($normalized -match '^([\d\.]+)(-[A-Za-z0-9\.\-]+)?$') {
$numericPart = $Matches[1]
# $Matches[2] is the suffix including the dash, or $null
} else {
throw "Invalid version '$raw'. Expected format: Major.Minor.Build.Revision[-suffix] (e.g. 1.0.0.1, 1.0.0.1-beta, 1.0.0.1-rc1)."
}
$parsed = $null
if (-not [System.Version]::TryParse($numericPart, [ref]$parsed)) {
throw "Invalid numeric version '$numericPart' from input '$raw'. The numeric portion must be a valid version like 1.2.3.4."
}
# release_version = full version with suffix (e.g. "1.0.0.1-beta")
# numeric_version = just the numeric part (e.g. "1.0.0.1") for AssemblyVersion/FileVersion
"release_version=$normalized" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"numeric_version=$numericPart" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"release_tag=v$normalized" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
build:
name: Build ${{ matrix.arch }}
needs: prepare
runs-on: windows-latest
strategy:
matrix:
arch: [x64, x86, arm64]
env:
RELEASE_VERSION: ${{ needs.prepare.outputs.release_version }}
NUMERIC_VERSION: ${{ needs.prepare.outputs.numeric_version }}
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
ARCH: ${{ matrix.arch }}
steps:
- name: Checkout Code
uses: actions/checkout@v7
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.0.x"
- name: Ensure VC++ ARM64 Tools
if: matrix.arch == 'arm64'
shell: pwsh
run: |
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$vsPath = & $vswhere -latest -property installationPath
function Test-Arm64Tools {
$msvcRoot = Join-Path $vsPath 'VC\Tools\MSVC'
if (-not (Test-Path $msvcRoot)) { return $false }
return [bool](Get-ChildItem $msvcRoot -Directory |
Where-Object { Test-Path (Join-Path $_.FullName 'bin\Hostx64\arm64\link.exe') } |
Select-Object -First 1)
}
if (Test-Arm64Tools) {
Write-Host "VC++ ARM64 tools already present."
} else {
Write-Host "Installing VC++ ARM64 tools into '$vsPath'..."
$installer = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vs_installer.exe"
$proc = Start-Process -FilePath $installer -Wait -PassThru -ArgumentList @(
'modify', '--installPath', "`"$vsPath`"",
'--add', 'Microsoft.VisualStudio.Component.VC.Tools.ARM64',
'--quiet', '--norestart', '--force', '--nocache'
)
Write-Host "vs_installer exit code: $($proc.ExitCode)"
# 0 = success, 3010 = success/reboot-required.
if ($proc.ExitCode -notin 0, 3010) {
throw "VS installer failed with exit code $($proc.ExitCode)."
}
if (-not (Test-Arm64Tools)) {
throw "VC++ ARM64 tools still missing after install."
}
Write-Host "VC++ ARM64 tools installed."
}
- name: Setup MSVC Toolchain (for NativeAOT)
# NativeAOT links a native binary, so it needs the MSVC linker and the
# target-architecture libraries on PATH/LIB. The runner is x64, so x86 and
# arm64 are cross-compiled (amd64_x86 / amd64_arm64). The AOT publish below
# consumes this environment via -p:IlcUseEnvironmentalTools=true.
uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{ matrix.arch == 'arm64' && 'amd64_arm64' || matrix.arch == 'x86' && 'amd64_x86' || 'amd64' }}
- name: Cache NuGet Packages
uses: actions/cache@v6
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', '**/packages.lock.json') }}
restore-keys: nuget-${{ runner.os }}-
- name: Publish WinUI 3 App (Framework-Dependent)
shell: pwsh
run: >
dotnet publish Raven/Raven.csproj
-c Release
-r win-$env:ARCH
--self-contained false
-o ./publish_output
-p:Version=$env:NUMERIC_VERSION
-p:AssemblyVersion=$env:NUMERIC_VERSION
-p:FileVersion=$env:NUMERIC_VERSION
-p:InformationalVersion=$env:RELEASE_TAG
- name: Clean Intermediates Before Self-Contained Publish
shell: pwsh
run: |
# The framework-dependent publish leaves a thin resources.pri in obj/.
# MSBuild's incremental PRI step does not treat WindowsAppSDKSelfContained
# as a changed input, so without this clean the self-contained publish
# reuses that thin PRI and ships without the bundled WinUI framework
# resources, crashing at startup with a missing themeresources.xaml.
Remove-Item -Path Raven/obj, Raven/bin -Recurse -Force -ErrorAction SilentlyContinue
- name: Publish WinUI 3 App (Self-Contained)
shell: pwsh
run: >
dotnet publish Raven/Raven.csproj
-c Release
-r win-$env:ARCH
--self-contained true
-p:WindowsAppSDKSelfContained=true
-o ./publish_output_full
-p:Version=$env:NUMERIC_VERSION
-p:AssemblyVersion=$env:NUMERIC_VERSION
-p:FileVersion=$env:NUMERIC_VERSION
-p:InformationalVersion=$env:RELEASE_TAG
- name: Publish Updater (NativeAOT)
shell: pwsh
run: >
dotnet publish Raven.Updater/Raven.Updater.csproj
-c Release
-r win-$env:ARCH
-p:PublishAot=true
-p:IlcUseEnvironmentalTools=true
-o ./updater_aot
-p:Version=$env:NUMERIC_VERSION
-p:AssemblyVersion=$env:NUMERIC_VERSION
-p:FileVersion=$env:NUMERIC_VERSION
- name: Replace Updater With NativeAOT Build
shell: pwsh
run: |
$aotUpdater = "./updater_aot/Raven.Updater.exe"
if (-not (Test-Path $aotUpdater)) {
throw "NativeAOT updater was not produced at $aotUpdater."
}
foreach ($dir in @('./publish_output', './publish_output_full')) {
# Drop the framework-dependent updater payload copied in via ProjectReference
# (.exe/.dll/.deps.json/.runtimeconfig.json/.pdb), then drop in the self-sufficient AOT exe.
Get-ChildItem -Path $dir -Filter 'Raven.Updater.*' -File | Remove-Item -Force
Copy-Item -Path $aotUpdater -Destination (Join-Path $dir 'Raven.Updater.exe') -Force
}
- name: Decode Certificate
shell: pwsh
env:
CERT_BASE64: ${{ secrets.CERT_BASE64 }}
run: |
$pfxCertBytes = [System.Convert]::FromBase64String($env:CERT_BASE64)
[System.IO.File]::WriteAllBytes("RavenCert.pfx", $pfxCertBytes)
- name: Sign App Files and Export Public Certificate
shell: pwsh
env:
CERT_PASSWORD: ${{ secrets.CERT_PASSWORD }}
run: |
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
"RavenCert.pfx",
$env:CERT_PASSWORD
)
Get-ChildItem -Path ./publish_output, ./publish_output_full -Include *.exe,*.dll -Recurse | ForEach-Object {
Set-AuthenticodeSignature -FilePath $_.FullName -Certificate $cert -TimestampServer "http://timestamp.digicert.com" | Out-Null
}
$certBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
[System.IO.File]::WriteAllBytes("RavenCert.cer", $certBytes)
- name: Create Zip Archives
shell: pwsh
run: |
Get-ChildItem -Path ./publish_output, ./publish_output_full -Include *.cer,*.pfx -Recurse -File -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
Compress-Archive -Path ./publish_output/* -DestinationPath "Raven-$($env:RELEASE_TAG)-win-$($env:ARCH).zip"
Compress-Archive -Path ./publish_output_full/* -DestinationPath "Raven-$($env:RELEASE_TAG)-win-$($env:ARCH)-self-contained.zip"
- name: Create Auto Install Certificate Zip
if: matrix.arch == 'x64'
shell: pwsh
run: |
$bundleDir = Join-Path $PWD "auto_install_cert"
New-Item -ItemType Directory -Path $bundleDir -Force | Out-Null
Copy-Item -Path "RavenCert.cer" -Destination (Join-Path $bundleDir "raven.cer") -Force
Copy-Item -Path ".github/scripts/Install-RavenCert.bat" -Destination (Join-Path $bundleDir "install_raven_cert.bat") -Force
Compress-Archive -Path (Join-Path $bundleDir "*") -DestinationPath "raven_cert.zip" -Force
- name: Upload App Artifact
uses: actions/upload-artifact@v7
with:
name: Raven-${{ matrix.arch }}
path: |
Raven-${{ needs.prepare.outputs.release_tag }}-win-${{ matrix.arch }}.zip
Raven-${{ needs.prepare.outputs.release_tag }}-win-${{ matrix.arch }}-self-contained.zip
- name: Upload Auto Install Certificate Artifact
if: matrix.arch == 'x64'
uses: actions/upload-artifact@v7
with:
name: Auto-Install-Cert
path: raven_cert.zip
release:
name: Create Draft Release
needs: [prepare, build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download Artifacts
uses: actions/download-artifact@v8
with:
path: ./artifacts
merge-multiple: true
- name: Create Draft GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ needs.prepare.outputs.release_tag }}
name: Raven ${{ needs.prepare.outputs.release_tag }}
draft: true
prerelease: ${{ needs.prepare.outputs.release_version != needs.prepare.outputs.numeric_version }}
generate_release_notes: true
files: |
./artifacts/Raven-*-win-x64.zip
./artifacts/Raven-*-win-x86.zip
./artifacts/Raven-*-win-arm64.zip
./artifacts/*-self-contained.zip
./artifacts/raven_cert.zip