897 lines
37 KiB
YAML
897 lines
37 KiB
YAML
name: Build and Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: 'Release version (required, e.g. 2026.1.0)'
|
|
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 }}
|
|
onedrive-version: ${{ steps.info.outputs.onedrive-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@v7
|
|
|
|
- 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]::IsNullOrWhiteSpace($PackageVersion)) {
|
|
throw "The workflow_dispatch version input is required."
|
|
}
|
|
|
|
echo "package-env=$PackageEnv" >> $Env:GITHUB_OUTPUT
|
|
$OneDriveVersion = "$PackageVersion.0"
|
|
|
|
echo "package-version=$PackageVersion" >> $Env:GITHUB_OUTPUT
|
|
echo "onedrive-version=$OneDriveVersion" >> $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
|
|
env:
|
|
UNIGETUI_GITHUB_CLIENT_ID: ${{ secrets.UNIGETUI_GITHUB_CLIENT_ID }}
|
|
UNIGETUI_GITHUB_CLIENT_SECRET: ${{ secrets.UNIGETUI_GITHUB_CLIENT_SECRET }}
|
|
UNIGETUI_OPENSEARCH_USERNAME: ${{ secrets.UNIGETUI_OPENSEARCH_USERNAME }}
|
|
UNIGETUI_OPENSEARCH_PASSWORD: ${{ secrets.UNIGETUI_OPENSEARCH_PASSWORD }}
|
|
NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
platform: [x64, arm64]
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Validate GitHub OAuth secrets
|
|
shell: pwsh
|
|
run: |
|
|
if ([string]::IsNullOrWhiteSpace($env:UNIGETUI_GITHUB_CLIENT_ID)) {
|
|
throw "UNIGETUI_GITHUB_CLIENT_ID is not configured for this build environment."
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($env:UNIGETUI_GITHUB_CLIENT_SECRET)) {
|
|
throw "UNIGETUI_GITHUB_CLIENT_SECRET is not configured for this build environment."
|
|
}
|
|
|
|
Write-Host "::notice::GitHub OAuth secrets are configured for this build."
|
|
|
|
- name: Install .NET
|
|
uses: actions/setup-dotnet@v5
|
|
with:
|
|
global-json-file: global.json
|
|
|
|
- name: Cache NuGet packages
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: ${{ env.NUGET_PACKAGES }}
|
|
key: ${{ runner.os }}-nuget-${{ hashFiles('global.json', 'src/**/*.csproj', 'src/**/*.props', 'src/**/*.targets', 'src/**/*.sln', 'src/**/*.slnx') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-nuget-
|
|
|
|
- name: Install Python
|
|
uses: actions/setup-python@v6
|
|
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 dependencies
|
|
working-directory: src
|
|
run: dotnet restore UniGetUI.Windows.slnx
|
|
|
|
- name: Run tests
|
|
working-directory: src
|
|
shell: pwsh
|
|
run: |
|
|
dotnet build-server shutdown
|
|
# Retry once to handle flaky tests (e.g. TaskRecyclerTests uses Random)
|
|
dotnet test UniGetUI.Windows.slnx --no-restore --verbosity q --nologo /p:Platform=x64 /p:UseSharedCompilation=false /m:1
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Host "::warning::First test run failed, retrying..."
|
|
dotnet build-server shutdown
|
|
dotnet test UniGetUI.Windows.slnx --no-restore --verbosity q --nologo /p:Platform=x64 /p:UseSharedCompilation=false /m:1
|
|
if ($LASTEXITCODE -ne 0) { exit 1 }
|
|
}
|
|
|
|
- name: Publish
|
|
shell: pwsh
|
|
run: |
|
|
$Platform = '${{ matrix.platform }}'
|
|
$TargetFramework = (
|
|
dotnet msbuild src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj `
|
|
/nologo `
|
|
-getProperty:TargetFramework `
|
|
/p:Configuration=Release `
|
|
/p:Platform=$Platform `
|
|
/p:RuntimeIdentifier=win-$Platform |
|
|
Select-Object -Last 1
|
|
).Trim()
|
|
|
|
if ([string]::IsNullOrWhiteSpace($TargetFramework)) {
|
|
throw "Could not resolve the Avalonia target framework from MSBuild"
|
|
}
|
|
|
|
dotnet publish src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj `
|
|
/noLogo `
|
|
/p:Configuration=Release `
|
|
/p:Platform=$Platform `
|
|
/p:RuntimeIdentifier=win-$Platform `
|
|
/p:PublishProfile=Win-$Platform-NativeAot `
|
|
/p:UseSharedCompilation=false `
|
|
/p:BuildInParallel=false `
|
|
/m:1 `
|
|
-v m
|
|
if ($LASTEXITCODE -ne 0) { throw "dotnet publish Avalonia failed" }
|
|
|
|
# Stage binaries
|
|
$PublishDir = "src/UniGetUI.Avalonia/bin/$Platform/Release/$TargetFramework/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
|
|
|
|
if (-not (Test-Path "unigetui_bin/UniGetUI.exe")) {
|
|
throw "Windows app host was not produced at unigetui_bin/UniGetUI.exe"
|
|
}
|
|
|
|
$MaxShippedPdbSizeBytes = 1MB
|
|
$PdbsToRemove = Get-ChildItem "unigetui_bin" -Filter "*.pdb" -File -Recurse | Where-Object {
|
|
$_.Length -gt $MaxShippedPdbSizeBytes
|
|
}
|
|
|
|
if ($PdbsToRemove.Count -gt 0) {
|
|
$RemovedPdbBytes = ($PdbsToRemove | Measure-Object -Property Length -Sum).Sum
|
|
$PdbsToRemove | Remove-Item -Force
|
|
Write-Host ("Removed {0} oversized PDBs above {1:N2} MiB ({2:N2} MiB total)." -f $PdbsToRemove.Count, ($MaxShippedPdbSizeBytes / 1MB), ($RemovedPdbBytes / 1MB))
|
|
}
|
|
|
|
- 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: Build installer
|
|
shell: pwsh
|
|
run: |
|
|
$Platform = '${{ matrix.platform }}'
|
|
$OutputDir = Join-Path $PWD "output"
|
|
New-Item $OutputDir -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
|
|
|
|
.\scripts\refresh-integrity-tree.ps1 -Path $PWD/unigetui_bin -FailOnUnexpectedFiles
|
|
|
|
# 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
|
|
|
|
try {
|
|
$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"
|
|
$IsccArgs = @($IssPath, "/F$InstallerBaseName", "/O$OutputDir")
|
|
if ([System.Boolean]::Parse('${{ needs.preflight.outputs.dry-run }}') -eq $false) {
|
|
Write-Host "Using lzma/ultra64 installer compression for release build."
|
|
$IsccArgs = @('/DInstallerCompression=lzma/ultra64') + $IsccArgs
|
|
}
|
|
|
|
& ISCC.exe @IsccArgs
|
|
if ($LASTEXITCODE -ne 0) { throw "Inno Setup failed with exit code $LASTEXITCODE" }
|
|
}
|
|
finally {
|
|
Set-Content $IssPath $issContent -NoNewline
|
|
}
|
|
|
|
- name: Stage output
|
|
shell: pwsh
|
|
run: |
|
|
$Platform = '${{ matrix.platform }}'
|
|
New-Item "output" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
|
|
|
|
.\scripts\refresh-integrity-tree.ps1 -Path $PWD/unigetui_bin -FailOnUnexpectedFiles
|
|
|
|
# Zip
|
|
Compress-Archive -Path "unigetui_bin/*" -DestinationPath "output/UniGetUI.$Platform.zip" -CompressionLevel Optimal
|
|
|
|
# Installer is created in output during the previous step
|
|
|
|
- name: Capture artifact sizes
|
|
shell: pwsh
|
|
run: |
|
|
$Platform = '${{ matrix.platform }}'
|
|
$SizesFile = "output/sizes.$Platform.json"
|
|
$UncompressedSize = (Get-ChildItem "unigetui_bin" -Recurse -File | Measure-Object -Property Length -Sum).Sum
|
|
$Artifacts = Get-ChildItem "output" -File | Where-Object { $_.Name -ne "sizes.$Platform.json" } | ForEach-Object {
|
|
@{ Name = $_.Name; SizeBytes = $_.Length }
|
|
}
|
|
$Report = @{
|
|
Platform = $Platform
|
|
RuntimeIdentifier = "win-$Platform"
|
|
UncompressedSizeBytes = $UncompressedSize
|
|
Artifacts = $Artifacts
|
|
Timestamp = [DateTimeOffset]::UtcNow.ToString("o")
|
|
}
|
|
$Report | ConvertTo-Json -Depth 4 | Set-Content $SizesFile -Encoding UTF8
|
|
Write-Host "Artifact sizes saved to $SizesFile"
|
|
Get-Content $SizesFile
|
|
|
|
- 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@v7
|
|
with:
|
|
name: UniGetUI-release-${{ matrix.platform }}
|
|
path: output/*
|
|
|
|
- name: Cleanup
|
|
if: always()
|
|
shell: pwsh
|
|
run: |
|
|
Remove-Item "unigetui_bin" -Recurse -Force -ErrorAction SilentlyContinue
|
|
|
|
build-avalonia:
|
|
name: Build (${{ matrix.name }})
|
|
runs-on: ${{ matrix.os }}
|
|
needs: [preflight]
|
|
environment: ${{ needs.preflight.outputs.package-env }}
|
|
permissions:
|
|
contents: read
|
|
env:
|
|
UNIGETUI_GITHUB_CLIENT_ID: ${{ secrets.UNIGETUI_GITHUB_CLIENT_ID }}
|
|
UNIGETUI_GITHUB_CLIENT_SECRET: ${{ secrets.UNIGETUI_GITHUB_CLIENT_SECRET }}
|
|
UNIGETUI_OPENSEARCH_USERNAME: ${{ secrets.UNIGETUI_OPENSEARCH_USERNAME }}
|
|
UNIGETUI_OPENSEARCH_PASSWORD: ${{ secrets.UNIGETUI_OPENSEARCH_PASSWORD }}
|
|
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
|
SIGNING_IDENTITY: 'Developer ID Application: Devolutions inc.'
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- os: macos-latest
|
|
name: macos-arm64
|
|
runtime: osx-arm64
|
|
publish_profile: osx-arm64-NativeAot
|
|
- os: macos-latest
|
|
name: macos-x64
|
|
runtime: osx-x64
|
|
publish_profile: osx-x64-NativeAot
|
|
- os: ubuntu-latest
|
|
name: linux-x64
|
|
runtime: linux-x64
|
|
publish_profile: linux-x64-NativeAot
|
|
- os: ubuntu-latest
|
|
name: linux-arm64
|
|
runtime: linux-arm64
|
|
publish_profile: linux-arm64-NativeAot
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Install .NET
|
|
uses: actions/setup-dotnet@v5
|
|
with:
|
|
global-json-file: global.json
|
|
|
|
- name: Cache NuGet packages
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: ${{ env.NUGET_PACKAGES }}
|
|
key: ${{ runner.os }}-nuget-${{ hashFiles('global.json', 'src/**/*.csproj', 'src/**/*.props', 'src/**/*.targets', 'src/**/*.sln', 'src/**/*.slnx') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-nuget-
|
|
|
|
- name: Install Linux arm64 NativeAOT toolchain
|
|
if: matrix.runtime == 'linux-arm64'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
. /etc/os-release
|
|
sudo dpkg --add-architecture arm64
|
|
|
|
# Keep the runner's default Ubuntu feeds bound to amd64, then add the
|
|
# Ubuntu Ports arm64 feed that provides the target CRT and zlib objects.
|
|
if compgen -G "/etc/apt/sources.list.d/*.sources" > /dev/null; then
|
|
for source in /etc/apt/sources.list.d/*.sources; do
|
|
if ! grep -q '^Architectures:' "$source"; then
|
|
sudo sed -i '/^Types: deb$/a Architectures: amd64' "$source"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ -f /etc/apt/sources.list ]; then
|
|
sudo sed -i -E '/^deb /s/^deb /deb [arch=amd64] /' /etc/apt/sources.list
|
|
fi
|
|
|
|
sudo tee /etc/apt/sources.list.d/ubuntu-arm64.sources >/dev/null <<EOF
|
|
Types: deb
|
|
URIs: http://ports.ubuntu.com/ubuntu-ports/
|
|
Suites: ${VERSION_CODENAME} ${VERSION_CODENAME}-updates ${VERSION_CODENAME}-backports
|
|
Components: main restricted universe multiverse
|
|
Architectures: arm64
|
|
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
|
|
|
Types: deb
|
|
URIs: http://ports.ubuntu.com/ubuntu-ports/
|
|
Suites: ${VERSION_CODENAME}-security
|
|
Components: main restricted universe multiverse
|
|
Architectures: arm64
|
|
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
|
EOF
|
|
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends \
|
|
clang \
|
|
llvm \
|
|
binutils-aarch64-linux-gnu \
|
|
gcc-aarch64-linux-gnu \
|
|
zlib1g-dev:arm64
|
|
|
|
- name: Set version
|
|
shell: pwsh
|
|
run: |
|
|
$PackageVersion = '${{ needs.preflight.outputs.package-version }}'
|
|
./scripts/set-version.ps1 -Version $PackageVersion
|
|
|
|
- name: Restore dependencies
|
|
working-directory: src
|
|
run: dotnet restore UniGetUI.Avalonia/UniGetUI.Avalonia.csproj --runtime ${{ matrix.runtime }}
|
|
|
|
- name: Publish
|
|
working-directory: src
|
|
shell: pwsh
|
|
run: |
|
|
$PublishProfile = '${{ matrix.publish_profile }}'
|
|
$PublishArgs = @(
|
|
'publish',
|
|
'UniGetUI.Avalonia/UniGetUI.Avalonia.csproj',
|
|
'--configuration', 'Release',
|
|
'--runtime', '${{ matrix.runtime }}',
|
|
'--self-contained', 'true',
|
|
'--output', '../bin/${{ matrix.name }}'
|
|
)
|
|
if ($PublishProfile) {
|
|
$PublishArgs += "/p:PublishProfile=$PublishProfile"
|
|
}
|
|
dotnet @PublishArgs
|
|
|
|
- name: Remove oversized Linux debug symbols
|
|
if: runner.os == 'Linux'
|
|
shell: pwsh
|
|
run: |
|
|
$PublishDir = "bin/${{ matrix.name }}"
|
|
$MaxDebugSymbolSizeBytes = 1MB
|
|
$DebugSymbolsToRemove = @(Get-ChildItem $PublishDir -Include "*.dbg", "*.pdb" -File -Recurse | Where-Object {
|
|
$_.Length -gt $MaxDebugSymbolSizeBytes
|
|
})
|
|
|
|
if ($DebugSymbolsToRemove.Count -gt 0) {
|
|
$RemovedDebugSymbolBytes = ($DebugSymbolsToRemove | Measure-Object -Property Length -Sum).Sum
|
|
$DebugSymbolsToRemove | Remove-Item -Force
|
|
Write-Host ("Removed {0} oversized Linux debug symbols above {1:N2} MiB ({2:N2} MiB total)." -f $DebugSymbolsToRemove.Count, ($MaxDebugSymbolSizeBytes / 1MB), ($RemovedDebugSymbolBytes / 1MB))
|
|
}
|
|
|
|
- name: Install macOS packaging tools
|
|
if: runner.os == 'macOS'
|
|
run: brew install create-dmg
|
|
|
|
- name: Check out Devolutions/actions
|
|
if: ${{ runner.os == 'macOS' && fromJSON(needs.preflight.outputs.dry-run) == false }}
|
|
uses: actions/checkout@v7
|
|
with:
|
|
repository: Devolutions/actions
|
|
ref: v1
|
|
token: ${{ secrets.DEVOLUTIONSBOT_TOKEN }}
|
|
path: ./.github/workflows
|
|
|
|
- name: Import Developer ID certificate
|
|
if: ${{ runner.os == 'macOS' && fromJSON(needs.preflight.outputs.dry-run) == false }}
|
|
uses: ./.github/workflows/macos-code-sign-setup
|
|
with:
|
|
base64_certificate: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE }}
|
|
certificate_password: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD }}
|
|
|
|
- name: Package (macOS)
|
|
if: ${{ runner.os == 'macOS' }}
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION='${{ needs.preflight.outputs.package-version }}'
|
|
APP_BUNDLE="UniGetUI.app"
|
|
APP_EXECUTABLE="UniGetUI"
|
|
DRY_RUN='${{ needs.preflight.outputs.dry-run }}'
|
|
|
|
rm -rf "${APP_BUNDLE}" output
|
|
mkdir -p output
|
|
mkdir -p "${APP_BUNDLE}/Contents/MacOS"
|
|
mkdir -p "${APP_BUNDLE}/Contents/Resources"
|
|
|
|
cp -R "bin/${{ matrix.name }}/." "${APP_BUNDLE}/Contents/MacOS/"
|
|
find "${APP_BUNDLE}/Contents/MacOS" -name "*.pdb" -type f -delete
|
|
chmod +x "${APP_BUNDLE}/Contents/MacOS/${APP_EXECUTABLE}"
|
|
sed "s|@VERSION@|${VERSION}|g" \
|
|
scripts/macos/Info.plist \
|
|
> "${APP_BUNDLE}/Contents/Info.plist"
|
|
# Compile the appearance-aware app icon (Default/Dark/Tinted/Clear) from the Icon Composer
|
|
# .icon into the bundle: produces Assets.car + AppIcon.icns. Info.plist already declares
|
|
# CFBundleIconName=AppIcon, so macOS renders the appearance styles itself (incl. when closed).
|
|
xcrun actool scripts/macos/AppIcon.icon \
|
|
--compile "${APP_BUNDLE}/Contents/Resources" \
|
|
--app-icon AppIcon \
|
|
--output-partial-info-plist "$(mktemp).plist" \
|
|
--platform macosx \
|
|
--minimum-deployment-target 12.0 \
|
|
--errors --warnings
|
|
# Static .icns fallback for macOS < 26 (CFBundleIconFile) and the dmg volume icon.
|
|
cp scripts/macos/UniGetUI.icns "${APP_BUNDLE}/Contents/Resources/"
|
|
|
|
if [ "$DRY_RUN" = "false" ]; then
|
|
ENTITLEMENTS="scripts/macos/Entitlements.plist"
|
|
find "${APP_BUNDLE}/Contents/MacOS" -type f \! -name "${APP_EXECUTABLE}" -print0 \
|
|
| while IFS= read -r -d '' f; do
|
|
codesign --force --timestamp --options runtime \
|
|
--entitlements "$ENTITLEMENTS" \
|
|
--sign "$SIGNING_IDENTITY" "$f"
|
|
done
|
|
codesign --force --timestamp --options runtime \
|
|
--entitlements "$ENTITLEMENTS" \
|
|
--sign "$SIGNING_IDENTITY" "${APP_BUNDLE}"
|
|
codesign --verify --verbose=2 "${APP_BUNDLE}"
|
|
else
|
|
find "${APP_BUNDLE}/Contents/MacOS" -type f | while IFS= read -r f; do
|
|
codesign --force --sign - "$f" 2>/dev/null || true
|
|
done
|
|
codesign --force --sign - "${APP_BUNDLE}"
|
|
fi
|
|
|
|
COPYFILE_DISABLE=1 tar -czf "output/UniGetUI.${{ matrix.name }}.tar.gz" "${APP_BUNDLE}"
|
|
|
|
create-dmg \
|
|
--volname "UniGetUI" \
|
|
--volicon "scripts/macos/UniGetUI.icns" \
|
|
--window-pos 200 120 \
|
|
--window-size 600 400 \
|
|
--icon-size 128 \
|
|
--icon "UniGetUI.app" 150 185 \
|
|
--hide-extension "UniGetUI.app" \
|
|
--app-drop-link 450 185 \
|
|
"output/UniGetUI.${{ matrix.name }}.dmg" \
|
|
"${APP_BUNDLE}"
|
|
|
|
test -f "output/UniGetUI.${{ matrix.name }}.tar.gz"
|
|
test -f "output/UniGetUI.${{ matrix.name }}.dmg"
|
|
|
|
- name: Sign DMG
|
|
if: ${{ runner.os == 'macOS' && fromJSON(needs.preflight.outputs.dry-run) == false }}
|
|
run: |
|
|
set -euo pipefail
|
|
DMG="output/UniGetUI.${{ matrix.name }}.dmg"
|
|
codesign --force --timestamp --sign "$SIGNING_IDENTITY" "$DMG"
|
|
codesign --verify --verbose=2 "$DMG"
|
|
|
|
- name: Notarize DMG
|
|
if: ${{ runner.os == 'macOS' && fromJSON(needs.preflight.outputs.dry-run) == false }}
|
|
uses: ./.github/workflows/macos-notarize
|
|
timeout-minutes: 15
|
|
with:
|
|
apple_password: ${{ secrets.APPLE_BOT_PASSWORD }}
|
|
package_path: output/UniGetUI.${{ matrix.name }}.dmg
|
|
|
|
- name: Install Linux packaging tools
|
|
if: runner.os == 'Linux'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y rpm
|
|
|
|
- name: Package (Linux)
|
|
if: runner.os == 'Linux'
|
|
shell: pwsh
|
|
run: |
|
|
$Version = '${{ needs.preflight.outputs.package-version }}'
|
|
$Parts = $Version -split '-', 2
|
|
$BaseVersion = $Parts[0]
|
|
$Prerelease = if ($Parts.Count -gt 1) { $Parts[1] } else { $null }
|
|
|
|
# deb: use tilde notation for pre-releases (Ubuntu 18.04+ compatible)
|
|
$DebVersion = if ($Prerelease) { "${BaseVersion}~${Prerelease}" } else { $Version }
|
|
# rpm: pre-release encoded in Release/Iteration field (RHEL 8+ / RPM 4.14+)
|
|
$RpmVersion = $BaseVersion
|
|
$RpmIteration = if ($Prerelease) { "0.${Prerelease}" } else { '1' }
|
|
|
|
# Map .NET RID -> package arch names
|
|
$DebArch, $RpmArch = switch ('${{ matrix.runtime }}') {
|
|
'linux-arm64' { 'arm64', 'aarch64' }
|
|
default { 'amd64', 'x86_64' }
|
|
}
|
|
|
|
New-Item -ItemType Directory -Force -Path output | Out-Null
|
|
|
|
# .tar.gz
|
|
& tar -czf "output/UniGetUI.${{ matrix.name }}.tar.gz" -C "bin/${{ matrix.name }}" .
|
|
if ($LASTEXITCODE -ne 0) { exit 1 }
|
|
|
|
# .deb — Ubuntu 18.04+ (glibc 2.27, Debian policy 3.9.6)
|
|
& ./scripts/package-linux.ps1 `
|
|
-PackageType deb `
|
|
-SourceDir "bin/${{ matrix.name }}" `
|
|
-OutputPath "output/UniGetUI.${{ matrix.name }}.deb" `
|
|
-Version $DebVersion `
|
|
-Architecture $DebArch
|
|
|
|
# .rpm — RHEL 8+ (RPM 4.14, glibc 2.28)
|
|
& ./scripts/package-linux.ps1 `
|
|
-PackageType rpm `
|
|
-SourceDir "bin/${{ matrix.name }}" `
|
|
-OutputPath "output/UniGetUI.${{ matrix.name }}.rpm" `
|
|
-Version $RpmVersion `
|
|
-Iteration $RpmIteration `
|
|
-Architecture $RpmArch
|
|
|
|
- name: Capture artifact sizes
|
|
shell: pwsh
|
|
run: |
|
|
$Name = '${{ matrix.name }}'
|
|
$Runtime = '${{ matrix.runtime }}'
|
|
$SizesFile = "output/sizes.$Name.json"
|
|
$SourceDir = if ('${{ runner.os }}' -eq 'macOS') { "UniGetUI.app/Contents/MacOS" } else { "bin/$Name" }
|
|
$UncompressedSize = (Get-ChildItem $SourceDir -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
|
|
$Artifacts = Get-ChildItem "output" -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "sizes.$Name.json" } | ForEach-Object {
|
|
@{ Name = $_.Name; SizeBytes = $_.Length }
|
|
}
|
|
$Report = @{
|
|
Platform = $Name
|
|
RuntimeIdentifier = $Runtime
|
|
UncompressedSizeBytes = $UncompressedSize
|
|
Artifacts = $Artifacts
|
|
Timestamp = [DateTimeOffset]::UtcNow.ToString("o")
|
|
}
|
|
$Report | ConvertTo-Json -Depth 4 | Set-Content $SizesFile -Encoding UTF8
|
|
Write-Host "Artifact sizes saved to $SizesFile"
|
|
Get-Content $SizesFile
|
|
|
|
- name: Upload artifacts
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: UniGetUI-${{ matrix.name }}
|
|
path: output/*
|
|
|
|
publish:
|
|
name: Publish GitHub Release
|
|
runs-on: ubuntu-latest
|
|
needs: [preflight, build, build-avalonia]
|
|
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@v8
|
|
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: Generate consolidated size report
|
|
shell: pwsh
|
|
working-directory: output
|
|
run: |
|
|
$SizeFiles = Get-ChildItem -Path . -Recurse -File -Filter "sizes.*.json" | Sort-Object Name
|
|
if (-not $SizeFiles) {
|
|
Write-Host "No size files found."
|
|
return
|
|
}
|
|
|
|
$Rows = foreach ($file in $SizeFiles) {
|
|
$data = Get-Content $file.FullName -Raw | ConvertFrom-Json
|
|
$uncompressedMiB = [math]::Round($data.UncompressedSizeBytes / 1MB, 2)
|
|
foreach ($artifact in $data.Artifacts) {
|
|
[PSCustomObject]@{
|
|
Platform = $data.Platform
|
|
RID = $data.RuntimeIdentifier
|
|
Artifact = $artifact.Name
|
|
CompressedMiB = [math]::Round($artifact.SizeBytes / 1MB, 2)
|
|
UncompressedMiB = $uncompressedMiB
|
|
}
|
|
}
|
|
}
|
|
|
|
$SizeReportFile = Join-Path $PWD "sizes.md"
|
|
$Lines = @("# UniGetUI Release Artifact Sizes", "")
|
|
$Lines += $Rows | Format-Table -AutoSize | Out-String -Stream | Where-Object { $_.Trim() -ne '' }
|
|
Set-Content -Path $SizeReportFile -Value $Lines -Encoding utf8NoBOM
|
|
|
|
echo "::group::sizes"
|
|
Get-Content $SizeReportFile
|
|
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
|
|
}
|
|
}
|
|
|
|
- name: Check out Devolutions/actions
|
|
if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }}
|
|
uses: actions/checkout@v7
|
|
with:
|
|
repository: Devolutions/actions
|
|
ref: v1
|
|
token: ${{ secrets.DEVOLUTIONSBOT_TOKEN }}
|
|
path: ./.github/workflows
|
|
|
|
- name: Install Devolutions Toolbox
|
|
if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }}
|
|
uses: ./.github/workflows/toolbox-install
|
|
with:
|
|
github_token: ${{ secrets.DEVOLUTIONSBOT_TOKEN }}
|
|
|
|
- name: Stage files for OneDrive upload
|
|
if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }}
|
|
shell: pwsh
|
|
run: |
|
|
$PackageVersion = '${{ needs.preflight.outputs.package-version }}'
|
|
New-Item -Path "onedrive-staging" -ItemType Directory -Force | Out-Null
|
|
|
|
$OneDriveVersion = '${{ needs.preflight.outputs.onedrive-version }}'
|
|
|
|
$Mappings = @{
|
|
"output/UniGetUI-release-x64/UniGetUI.Installer.x64.exe" = "Devolutions.UniGetUI.win-x64.$OneDriveVersion.exe"
|
|
"output/UniGetUI-release-arm64/UniGetUI.Installer.arm64.exe" = "Devolutions.UniGetUI.win-arm64.$OneDriveVersion.exe"
|
|
"output/UniGetUI-release-x64/UniGetUI.x64.zip" = "Devolutions.UniGetUI.win-x64.$OneDriveVersion.zip"
|
|
"output/UniGetUI-release-arm64/UniGetUI.arm64.zip" = "Devolutions.UniGetUI.win-arm64.$OneDriveVersion.zip"
|
|
"output/UniGetUI-macos-x64/UniGetUI.macos-x64.dmg" = "Devolutions.UniGetUI.macos-x64.$OneDriveVersion.dmg"
|
|
"output/UniGetUI-macos-arm64/UniGetUI.macos-arm64.dmg" = "Devolutions.UniGetUI.macos-arm64.$OneDriveVersion.dmg"
|
|
"output/UniGetUI-macos-x64/UniGetUI.macos-x64.tar.gz" = "Devolutions.UniGetUI.macos-x64.$OneDriveVersion.tar.gz"
|
|
"output/UniGetUI-macos-arm64/UniGetUI.macos-arm64.tar.gz" = "Devolutions.UniGetUI.macos-arm64.$OneDriveVersion.tar.gz"
|
|
"output/UniGetUI-linux-x64/UniGetUI.linux-x64.deb" = "Devolutions.UniGetUI.linux-x64.$OneDriveVersion.deb"
|
|
"output/UniGetUI-linux-arm64/UniGetUI.linux-arm64.deb" = "Devolutions.UniGetUI.linux-arm64.$OneDriveVersion.deb"
|
|
"output/UniGetUI-linux-x64/UniGetUI.linux-x64.rpm" = "Devolutions.UniGetUI.linux-x64.$OneDriveVersion.rpm"
|
|
"output/UniGetUI-linux-arm64/UniGetUI.linux-arm64.rpm" = "Devolutions.UniGetUI.linux-arm64.$OneDriveVersion.rpm"
|
|
"output/UniGetUI-linux-x64/UniGetUI.linux-x64.tar.gz" = "Devolutions.UniGetUI.linux-x64.$OneDriveVersion.tar.gz"
|
|
"output/UniGetUI-linux-arm64/UniGetUI.linux-arm64.tar.gz" = "Devolutions.UniGetUI.linux-arm64.$OneDriveVersion.tar.gz"
|
|
}
|
|
|
|
foreach ($entry in $Mappings.GetEnumerator()) {
|
|
if (-not (Test-Path $entry.Key)) {
|
|
throw "File not found: $($entry.Key)"
|
|
}
|
|
Copy-Item -Path $entry.Key -Destination "onedrive-staging/$($entry.Value)"
|
|
Write-Host "Staged: $($entry.Key) -> $($entry.Value)"
|
|
}
|
|
|
|
- name: Upload to OneDrive
|
|
if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }}
|
|
uses: ./.github/workflows/onedrive-upload
|
|
with:
|
|
azure_client_id: ${{ secrets.ONEDRIVE_AUTOMATION_CLIENT_ID }}
|
|
azure_client_secret: ${{ secrets.ONEDRIVE_AUTOMATION_CLIENT_SECRET }}
|
|
conflict_behavior: replace
|
|
destination_path: /UniGetUI/${{ needs.preflight.outputs.onedrive-version }}
|
|
remote: releases
|
|
source_path: Devolutions.UniGetUI.*
|
|
working_directory: onedrive-staging
|
|
|
|
size-report:
|
|
name: Consolidated Size Report
|
|
runs-on: ubuntu-latest
|
|
needs: [preflight, build, build-avalonia]
|
|
if: always() && needs.build.result != 'cancelled' && needs.build-avalonia.result != 'cancelled'
|
|
|
|
steps:
|
|
- name: Download artifacts
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
path: output
|
|
|
|
- name: Generate consolidated size report
|
|
shell: pwsh
|
|
working-directory: output
|
|
run: |
|
|
$SizeFiles = Get-ChildItem -Path . -Recurse -File -Filter "sizes.*.json" | Sort-Object Name
|
|
if (-not $SizeFiles) {
|
|
Write-Host "No size files found."
|
|
return
|
|
}
|
|
|
|
$Rows = foreach ($file in $SizeFiles) {
|
|
$data = Get-Content $file.FullName -Raw | ConvertFrom-Json
|
|
$uncompressedMiB = [math]::Round($data.UncompressedSizeBytes / 1MB, 2)
|
|
foreach ($artifact in $data.Artifacts) {
|
|
[PSCustomObject]@{
|
|
Platform = $data.Platform
|
|
RID = $data.RuntimeIdentifier
|
|
Artifact = $artifact.Name
|
|
CompressedMiB = [math]::Round($artifact.SizeBytes / 1MB, 2)
|
|
UncompressedMiB = $uncompressedMiB
|
|
}
|
|
}
|
|
}
|
|
|
|
$SizeReportFile = Join-Path $PWD "sizes.md"
|
|
$Lines = @("# UniGetUI Release Artifact Sizes", "")
|
|
$Lines += $Rows | Format-Table -AutoSize | Out-String -Stream | Where-Object { $_.Trim() -ne '' }
|
|
Set-Content -Path $SizeReportFile -Value $Lines -Encoding utf8NoBOM
|
|
|
|
echo "::group::sizes"
|
|
Get-Content $SizeReportFile
|
|
echo "::endgroup::"
|
|
|
|
- name: Upload size report
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: UniGetUI-size-report
|
|
path: output/sizes.md
|