Skip to content

Build and Release

Build and Release #11

Workflow file for this run

name: Build and Release
on:
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g. 3.3.7)'
default: "latest"
required: true
draft-release:
description: 'Create the GitHub Release as a draft'
required: true
type: boolean
default: false
skip-publish:
description: 'Skip publishing to GitHub Releases'
required: true
type: boolean
default: false
dry-run:
description: 'Dry run (simulate without publishing)'
required: true
type: boolean
default: true
jobs:
preflight:
name: Preflight
runs-on: ubuntu-latest
outputs:
package-env: ${{ steps.info.outputs.package-env }}
package-version: ${{ steps.info.outputs.package-version }}
draft-release: ${{ steps.info.outputs.draft-release }}
skip-publish: ${{ steps.info.outputs.skip-publish }}
dry-run: ${{ steps.info.outputs.dry-run }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Resolve build parameters
id: info
shell: pwsh
run: |
$IsProductionBranch = @('main', 'master') -contains '${{ github.ref_name }}'
try { $DraftRelease = [System.Boolean]::Parse('${{ inputs.draft-release }}') } catch { $DraftRelease = $false }
try { $SkipPublish = [System.Boolean]::Parse('${{ inputs.skip-publish }}') } catch { $SkipPublish = $false }
try { $DryRun = [System.Boolean]::Parse('${{ inputs.dry-run }}') } catch { $DryRun = $true }
$PackageEnv = if ($IsProductionBranch) {
"publish-prod"
} else {
"publish-test"
}
if (-Not $IsProductionBranch) {
$DryRun = $true # force dry run when not on main/master branch
}
if (-Not $SkipPublish -And $PackageEnv -ne 'publish-prod') {
$DryRun = $true # force dry run when publishing outside production environment
}
$PackageVersion = '${{ inputs.version }}'
if ([string]::IsNullOrEmpty($PackageVersion) -or $PackageVersion -eq 'latest') {
# Read version from SharedAssemblyInfo.cs
$match = Select-String -Path 'src/SharedAssemblyInfo.cs' -Pattern 'AssemblyInformationalVersion\("([^"]+)"\)'
if ($match) {
$PackageVersion = $match.Matches[0].Groups[1].Value
} else {
$PackageVersion = (Get-Date -Format "yyyy.M.d") + ".0"
}
}
echo "package-env=$PackageEnv" >> $Env:GITHUB_OUTPUT
echo "package-version=$PackageVersion" >> $Env:GITHUB_OUTPUT
echo "draft-release=$($DraftRelease.ToString().ToLower())" >> $Env:GITHUB_OUTPUT
echo "skip-publish=$($SkipPublish.ToString().ToLower())" >> $Env:GITHUB_OUTPUT
echo "dry-run=$($DryRun.ToString().ToLower())" >> $Env:GITHUB_OUTPUT
echo "::notice::Environment: $PackageEnv"
echo "::notice::Version: $PackageVersion"
echo "::notice::DraftRelease: $DraftRelease"
echo "::notice::DryRun: $DryRun"
build:
name: Build & Sign (${{ matrix.platform }})
runs-on: windows-latest
needs: [preflight]
environment: ${{ needs.preflight.outputs.package-env }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
platform: [x64, arm64]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Windows SDK UAP platform
shell: pwsh
run: |
# CsWinRT in WindowsPackageManager.Interop requires UAP 10.0.19041.0 platform metadata
$VsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$VsInstaller = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vs_installer.exe"
$InstallPath = & $VsWhere -latest -property installationPath
& $VsInstaller modify --installPath $InstallPath `
--add Microsoft.VisualStudio.Component.UWP.Support `
--add Microsoft.VisualStudio.Component.Windows10SDK.19041 `
--quiet --norestart --nocache | Out-Default
Write-Host "Windows SDK UAP platform installed"
- name: Install .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.0.x
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install Inno Setup
shell: pwsh
run: |
choco install innosetup -y --no-progress
echo "C:\Program Files (x86)\Inno Setup 6" >> $Env:GITHUB_PATH
- name: Install code-signing tools
shell: pwsh
run: |
dotnet tool install --global AzureSignTool
Install-Module -Name Devolutions.Authenticode -Force
# Trust test code-signing CA
$TestCertsUrl = "https://raw.githubusercontent.com/Devolutions/devolutions-authenticode/master/data/certs"
Invoke-WebRequest -Uri "$TestCertsUrl/authenticode-test-ca.crt" -OutFile ".\authenticode-test-ca.crt"
Import-Certificate -FilePath ".\authenticode-test-ca.crt" -CertStoreLocation "cert:\LocalMachine\Root"
Remove-Item ".\authenticode-test-ca.crt" -ErrorAction SilentlyContinue | Out-Null
- name: Set version
shell: pwsh
run: |
$PackageVersion = '${{ needs.preflight.outputs.package-version }}'
.\scripts\set-version.ps1 -Version $PackageVersion
- name: Restore WinGet CLI cache
id: winget-cache
uses: actions/cache/restore@v4
with:
path: src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_${{ matrix.platform }}
key: winget-cli-${{ runner.os }}-${{ matrix.platform }}-${{ hashFiles('scripts/fetch-winget-cli.ps1') }}
- name: Fetch WinGet CLI bundle
if: steps.winget-cache.outputs.cache-hit != 'true'
shell: pwsh
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
$Platform = '${{ matrix.platform }}'
.\scripts\fetch-winget-cli.ps1 -Architectures @($Platform) -Force
- name: Save WinGet CLI cache
if: steps.winget-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_${{ matrix.platform }}
key: ${{ steps.winget-cache.outputs.cache-primary-key }}
- name: Restore dependencies
working-directory: src
run: dotnet restore UniGetUI.sln
- name: Run tests
working-directory: src
shell: pwsh
run: |
# Retry once to handle flaky tests (e.g. TaskRecyclerTests uses Random)
dotnet test UniGetUI.sln --no-restore --verbosity q --nologo
if ($LASTEXITCODE -ne 0) {
Write-Host "::warning::First test run failed, retrying..."
dotnet test UniGetUI.sln --no-restore --verbosity q --nologo
if ($LASTEXITCODE -ne 0) { exit 1 }
}
- name: Publish
shell: pwsh
run: |
$Platform = '${{ matrix.platform }}'
dotnet publish src/UniGetUI/UniGetUI.csproj /noLogo /p:Configuration=Release /p:Platform=$Platform -p:RuntimeIdentifier=win-$Platform -v m
if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed" }
# Stage binaries
$PublishDir = "src/UniGetUI/bin/$Platform/Release/net8.0-windows10.0.26100.0/win-$Platform/publish"
if (Test-Path "unigetui_bin") { Remove-Item "unigetui_bin" -Recurse -Force }
New-Item "unigetui_bin" -ItemType Directory | Out-Null
Get-ChildItem $PublishDir | Move-Item -Destination "unigetui_bin" -Force
# Backward-compat alias
Copy-Item "unigetui_bin/UniGetUI.exe" "unigetui_bin/WingetUI.exe" -Force
- name: Code-sign binaries
if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }}
shell: pwsh
run: |
$ListPath = Join-Path $PWD "signing-files.txt"
$files = Get-ChildItem "unigetui_bin" -Recurse -Include "*.exe", "*.dll" | Where-Object {
(Get-AuthenticodeSignature $_.FullName).Status -eq "NotSigned"
}
$files.FullName | Set-Content $ListPath
Write-Host "Signing list contains $($files.Count) files."
.\scripts\sign.ps1 `
-FileListPath $ListPath `
-AzureTenantId '${{ secrets.AZURE_TENANT_ID }}' `
-KeyVaultUrl '${{ secrets.CODE_SIGNING_KEYVAULT_URL }}' `
-ClientId '${{ secrets.CODE_SIGNING_CLIENT_ID }}' `
-ClientSecret '${{ secrets.CODE_SIGNING_CLIENT_SECRET }}' `
-CertificateName '${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}' `
-TimestampServer '${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}'
- name: Generate integrity tree
shell: pwsh
run: .\scripts\generate-integrity-tree.ps1 -Path $PWD/unigetui_bin -MinOutput
- name: Build installer
shell: pwsh
run: |
$Platform = '${{ matrix.platform }}'
$OutputDir = Join-Path $PWD "output"
New-Item $OutputDir -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
# Configure Inno Setup to use AzureSignTool
$IssPath = "UniGetUI.iss"
# Build the installer (signing of the installer itself happens in the next step)
# Temporarily remove SignTool line so ISCC doesn't try to sign during build
$issContent = Get-Content $IssPath -Raw
$issContentNoSign = $issContent -Replace '(?m)^SignTool=.*$', '; SignTool=azsign (disabled for CI, signed separately)'
$issContentNoSign = $issContentNoSign -Replace '(?m)^SignedUninstaller=yes', 'SignedUninstaller=no'
Set-Content $IssPath $issContentNoSign -NoNewline
$InstallerBaseName = "UniGetUI.Installer.$Platform"
& ISCC.exe $IssPath /F"$InstallerBaseName" /O"$OutputDir"
if ($LASTEXITCODE -ne 0) { throw "Inno Setup failed with exit code $LASTEXITCODE" }
# Restore original ISS content
Set-Content $IssPath $issContent -NoNewline
- name: Stage output
shell: pwsh
run: |
$Platform = '${{ matrix.platform }}'
New-Item "output" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
# Zip
Compress-Archive -Path "unigetui_bin/*" -DestinationPath "output/UniGetUI.$Platform.zip" -CompressionLevel Optimal
# Installer is created in output during the previous step
- name: Code-sign installer
if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }}
shell: pwsh
run: |
$Platform = '${{ matrix.platform }}'
.\scripts\sign.ps1 `
-InstallerPath "output/UniGetUI.Installer.$Platform.exe" `
-AzureTenantId '${{ secrets.AZURE_TENANT_ID }}' `
-KeyVaultUrl '${{ secrets.CODE_SIGNING_KEYVAULT_URL }}' `
-ClientId '${{ secrets.CODE_SIGNING_CLIENT_ID }}' `
-ClientSecret '${{ secrets.CODE_SIGNING_CLIENT_SECRET }}' `
-CertificateName '${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}' `
-TimestampServer '${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}'
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: UniGetUI-release-${{ matrix.platform }}
path: output/*
- name: Cleanup
if: always()
shell: pwsh
run: |
Remove-Item "unigetui_bin" -Recurse -Force -ErrorAction SilentlyContinue
publish:
name: Publish GitHub Release
runs-on: ubuntu-latest
needs: [preflight, build]
if: ${{ fromJSON(needs.preflight.outputs.skip-publish) == false }}
environment: ${{ needs.preflight.outputs.package-env }}
permissions:
contents: write
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: output
- name: Add legacy installer filename
shell: pwsh
working-directory: output
run: |
$InstallerFiles = Get-ChildItem -Path . -Recurse -File -Filter "UniGetUI.Installer.x64.exe"
if (-not $InstallerFiles) {
throw "Could not find UniGetUI.Installer.x64.exe in downloaded artifacts"
}
$InstallerFiles | ForEach-Object {
$LegacyInstallerPath = Join-Path $_.DirectoryName "UniGetUI.Installer.exe"
Copy-Item -Path $_.FullName -Destination $LegacyInstallerPath -Force
Write-Host "Created legacy installer alias: $LegacyInstallerPath"
}
- name: Generate consolidated checksums
shell: pwsh
working-directory: output
run: |
$ChecksumFile = Join-Path $PWD "checksums.txt"
$ChecksumLines = Get-ChildItem -Path . -Recurse -File | Where-Object {
$_.Name -notmatch '^checksums(\..+)?\.txt$'
} | Sort-Object Name | ForEach-Object {
$hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash
"$hash $($_.Name)"
}
Set-Content -Path $ChecksumFile -Value $ChecksumLines -Encoding utf8NoBOM
echo "::group::checksums"
Get-Content $ChecksumFile
echo "::endgroup::"
- name: Create GitHub Release
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
working-directory: output
run: |
$PackageVersion = '${{ needs.preflight.outputs.package-version }}'
$DraftRelease = [System.Boolean]::Parse('${{ needs.preflight.outputs.draft-release }}')
$DryRun = [System.Boolean]::Parse('${{ needs.preflight.outputs.dry-run }}')
echo "::group::checksums"
Get-Content "./checksums.txt"
echo "::endgroup::"
$ReleaseTag = "v$PackageVersion"
$ReleaseTitle = "UniGetUI v${PackageVersion}"
$Repository = $Env:GITHUB_REPOSITORY
$DraftArg = if ($DraftRelease) { '--draft' } else { $null }
$Files = Get-ChildItem -Path . -Recurse -File | Where-Object {
$_.Name -eq 'checksums.txt' -or $_.Name -notmatch '^checksums\..+\.txt$'
}
if ($DryRun) {
Write-Host "Dry Run: skipping GitHub release creation!"
Write-Host "Would create release $ReleaseTag with title '$ReleaseTitle' (draft=$DraftRelease)"
$Files | ForEach-Object { Write-Host " - $($_.FullName)" }
} else {
if ($DraftArg) {
& gh release create $ReleaseTag --repo $Repository --title $ReleaseTitle $DraftArg $Files.FullName
} else {
& gh release create $ReleaseTag --repo $Repository --title $ReleaseTitle $Files.FullName
}
}