Files
wehub-resource-sync 643e9f9fcb
.NET Tests / test-codebase (push) Waiting to run
Translation Validation / validate-translations (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:01:52 +08:00

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