Skip to content

build(mobile): 在移动构建中排除 SoundFlow 原生资产 #121

build(mobile): 在移动构建中排除 SoundFlow 原生资产

build(mobile): 在移动构建中排除 SoundFlow 原生资产 #121

Workflow file for this run

name: Build & Publish
on:
push:
branches:
- '**'
workflow_dispatch:
inputs:
release_tag:
description: "发布标签(例如 v2.3.100)"
required: true
type: string
permissions:
contents: read
jobs:
build_desktop:
name: Build_${{ matrix.os }}_${{ matrix.arch }}
runs-on: ${{ matrix.os == 'linux' && 'ubuntu-24.04' || (matrix.os == 'macos' && 'macos-15' || 'windows-2022') }}
strategy:
fail-fast: false
matrix:
os: [windows, linux, macos]
arch: [x64, x86, arm64]
exclude:
- os: linux
arch: x86
- os: macos
arch: x86
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || github.ref }}
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Restore
shell: pwsh
env:
RID: ${{ matrix.os == 'windows' && format('win-{0}', matrix.arch) || (matrix.os == 'linux' && format('linux-{0}', matrix.arch) || format('osx-{0}', matrix.arch)) }}
PLATFORM: ${{ matrix.os == 'windows' && 'Windows' || (matrix.os == 'linux' && 'Linux' || 'MacOs') }}
run: |
dotnet restore SecRandom.Desktop/SecRandom.Desktop.csproj -r $env:RID -p:SecRandomPlatform=$env:PLATFORM
dotnet restore SecRandom.Launcher/SecRandom.Launcher.csproj -r $env:RID
- name: Get Latest Tag
id: get_tag
if: ${{ github.event_name != 'workflow_dispatch' }}
shell: pwsh
run: |
$latestTag = git describe --tags --abbrev=0
echo "LATEST_TAG=$latestTag" >> $env:GITHUB_OUTPUT
echo "Latest tag: $latestTag"
- name: Publish Desktop
shell: pwsh
env:
RID: ${{ matrix.os == 'windows' && format('win-{0}', matrix.arch) || (matrix.os == 'linux' && format('linux-{0}', matrix.arch) || format('osx-{0}', matrix.arch)) }}
PLATFORM: ${{ matrix.os == 'windows' && 'Windows' || (matrix.os == 'linux' && 'Linux' || 'MacOs') }}
run: |
$releaseRoot = 'artifacts/release'
$fullDir = "$releaseRoot/publish/full/$env:RID"
$lightDir = "$releaseRoot/publish/light/$env:RID"
New-Item -ItemType Directory -Path $fullDir, $lightDir -Force | Out-Null
dotnet publish SecRandom.Desktop/SecRandom.Desktop.csproj `
-c Release `
-r $env:RID `
-p:SecRandomPlatform=$env:PLATFORM `
--self-contained `
-p:PublishTrimmed=false `
-p:UseAppHost=true `
-p:DebugType=None `
-p:DebugSymbols=false `
--no-restore `
-o $fullDir
dotnet publish SecRandom.Desktop/SecRandom.Desktop.csproj `
-c Release `
-r $env:RID `
-p:SecRandomPlatform=$env:PLATFORM `
--self-contained false `
-p:PublishTrimmed=false `
-p:UseAppHost=true `
-p:DebugType=None `
-p:DebugSymbols=false `
--no-restore `
-o $lightDir
- name: Verify bundled audio runtime
shell: pwsh
env:
RID: ${{ matrix.os == 'windows' && format('win-{0}', matrix.arch) || (matrix.os == 'linux' && format('linux-{0}', matrix.arch) || format('osx-{0}', matrix.arch)) }}
run: |
$nativeAsset = if ($env:RID.StartsWith('win-')) { 'miniaudio.dll' } elseif ($env:RID.StartsWith('linux-')) { 'libminiaudio.so' } else { 'libminiaudio.dylib' }
$assetPath = Join-Path "artifacts/release/publish/full/$env:RID" $nativeAsset
if (!(Test-Path -LiteralPath $assetPath)) {
throw "SoundFlow native audio runtime was not published: $assetPath"
}
- name: Pack Portable Zips (Windows)
if: ${{ matrix.os == 'windows' }}
shell: pwsh
env:
VERSION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || steps.get_tag.outputs.LATEST_TAG }}
RID: ${{ matrix.os == 'windows' && format('win-{0}', matrix.arch) || (matrix.os == 'linux' && format('linux-{0}', matrix.arch) || format('osx-{0}', matrix.arch)) }}
WINDOWS_CODESIGN_PFX_BASE64: ${{ secrets.WINDOWS_CODESIGN_PFX_BASE64 }}
WINDOWS_CODESIGN_PFX_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PFX_PASSWORD }}
WINDOWS_CODESIGN_TIMESTAMP_URL: ${{ secrets.WINDOWS_CODESIGN_TIMESTAMP_URL }}
run: |
function Write-Marker([string]$path, [string]$runtimeKind) {
@{ schemaVersion = 1; product = 'SecRandom'; rid = $env:RID; packageKind = 'portable-zip'; runtimeKind = $runtimeKind } |
ConvertTo-Json -Compress | Set-Content -LiteralPath (Join-Path $path 'SecRandom.package.json') -Encoding utf8NoBOM
}
function Publish-Launcher([string]$path, [bool]$selfContained) {
$assemblyVersion = $env:VERSION.TrimStart('v', 'V')
$arguments = @('publish', 'SecRandom.Launcher/SecRandom.Launcher.csproj', '-c', 'Release', '-r', $env:RID, '-p:UseAppHost=true', '-p:PublishSingleFile=true', '-p:IncludeNativeLibrariesForSelfExtract=true', '-p:DebugType=None', '-p:DebugSymbols=false', "-p:Version=$assemblyVersion", '--no-restore', '-o', $path)
if ($selfContained) { $arguments += '--self-contained' } else { $arguments += @('--self-contained', 'false') }
& dotnet @arguments
if ($LASTEXITCODE -ne 0) { throw "Launcher publish failed with exit code $LASTEXITCODE." }
}
function Sign-File([string]$path, [string]$signTool, [string]$certificatePath) {
$arguments = @('sign', '/fd', 'SHA256', '/f', $certificatePath, '/p', $env:WINDOWS_CODESIGN_PFX_PASSWORD)
if (-not [string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_TIMESTAMP_URL)) {
$arguments += @('/tr', $env:WINDOWS_CODESIGN_TIMESTAMP_URL, '/td', 'SHA256')
}
$arguments += $path
& $signTool @arguments
if ($LASTEXITCODE -ne 0) { throw "signtool failed for $path with exit code $LASTEXITCODE." }
}
$releaseRoot = 'artifacts/release'
$zipRoot = "$releaseRoot/portable/$env:RID"
$fullRoot = "$zipRoot/full"
$lightRoot = "$zipRoot/light"
Remove-Item $zipRoot -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $fullRoot, $lightRoot, "$releaseRoot/dist" -Force | Out-Null
$fullApp = Join-Path $fullRoot "app-$env:VERSION-0"
$lightApp = Join-Path $lightRoot "app-$env:VERSION-0"
Copy-Item "$releaseRoot/publish/full/$env:RID" $fullApp -Recurse
Copy-Item "$releaseRoot/publish/light/$env:RID" $lightApp -Recurse
Write-Marker $fullApp 'self-contained'
Write-Marker $lightApp 'framework-dependent'
Publish-Launcher $fullRoot $true
Publish-Launcher $lightRoot $false
if (-not [string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_PFX_BASE64) -and -not [string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_PFX_PASSWORD)) {
$certificatePath = Join-Path $env:RUNNER_TEMP 'secrandom-portable.pfx'
try {
[IO.File]::WriteAllBytes($certificatePath, [Convert]::FromBase64String($env:WINDOWS_CODESIGN_PFX_BASE64))
$signTool = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe' | Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty FullName
if ([string]::IsNullOrWhiteSpace($signTool)) { throw 'signtool.exe was not found on the Windows runner.' }
foreach ($file in @(
(Join-Path $fullRoot 'SecRandomLauncher.exe'),
(Join-Path $lightRoot 'SecRandomLauncher.exe'),
(Join-Path $fullApp 'SecRandom.Desktop.exe'),
(Join-Path $lightApp 'SecRandom.Desktop.exe')
)) {
Sign-File $file $signTool $certificatePath
}
}
finally {
Remove-Item $certificatePath -Force -ErrorAction SilentlyContinue
}
}
else {
Write-Warning 'Windows code-signing secrets are unavailable; portable executables will be unsigned.'
}
Compress-Archive -Path "$fullRoot/*" -DestinationPath "$releaseRoot/dist/SecRandom-$env:VERSION-$env:RID-portable-full.zip" -Force
Compress-Archive -Path "$lightRoot/*" -DestinationPath "$releaseRoot/dist/SecRandom-$env:VERSION-$env:RID-portable-light.zip" -Force
- name: Pack Portable Zips (Unix)
if: ${{ matrix.os != 'windows' }}
shell: bash
env:
VERSION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || steps.get_tag.outputs.LATEST_TAG }}
RID: ${{ matrix.os == 'windows' && format('win-{0}', matrix.arch) || (matrix.os == 'linux' && format('linux-{0}', matrix.arch) || format('osx-{0}', matrix.arch)) }}
run: |
set -euo pipefail
release_root="artifacts/release"
zip_root="${release_root}/portable/${RID}"
full_root="${zip_root}/full"
light_root="${zip_root}/light"
rm -rf "$zip_root"
mkdir -p "$full_root" "$light_root" "${release_root}/dist"
full_app="$full_root/app-${VERSION}-0"
light_app="$light_root/app-${VERSION}-0"
assembly_version="${VERSION#v}"
cp -a "${release_root}/publish/full/${RID}" "$full_app"
cp -a "${release_root}/publish/light/${RID}" "$light_app"
printf '{"schemaVersion":1,"product":"SecRandom","rid":"%s","packageKind":"portable-zip","runtimeKind":"self-contained"}\n' "$RID" > "$full_app/SecRandom.package.json"
printf '{"schemaVersion":1,"product":"SecRandom","rid":"%s","packageKind":"portable-zip","runtimeKind":"framework-dependent"}\n' "$RID" > "$light_app/SecRandom.package.json"
dotnet publish SecRandom.Launcher/SecRandom.Launcher.csproj -c Release -r "$RID" --self-contained -p:UseAppHost=true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -p:Version="$assembly_version" --no-restore -o "$full_root"
dotnet publish SecRandom.Launcher/SecRandom.Launcher.csproj -c Release -r "$RID" --self-contained false -p:UseAppHost=true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -p:Version="$assembly_version" --no-restore -o "$light_root"
(cd "$full_root" && zip -qr "../../../dist/SecRandom-${VERSION}-${RID}-portable-full.zip" .)
(cd "$light_root" && zip -qr "../../../dist/SecRandom-${VERSION}-${RID}-portable-light.zip" .)
- name: Install Inno Setup Languages
if: ${{ matrix.os == 'windows' }}
shell: pwsh
run: |
$langDir = "C:\Program Files (x86)\Inno Setup 6\Languages"
if (!(Test-Path $langDir)) {
New-Item -ItemType Directory -Path $langDir -Force
}
$languages = @{
'ChineseSimplified.isl' = 'https://raw.githubusercontent.com/kira-96/Inno-Setup-Chinese-Simplified-Translation/main/ChineseSimplified.isl'
'Japanese.isl' = 'https://raw.githubusercontent.com/jrsoftware/issrc/main/Files/Languages/Japanese.isl'
}
foreach ($language in $languages.GetEnumerator()) {
$path = Join-Path $langDir $language.Key
Invoke-WebRequest -Uri $language.Value -OutFile $path
Write-Host "Downloaded Inno Setup language: $($language.Key)"
}
- name: Build Windows Setup
if: ${{ matrix.os == 'windows' }}
shell: pwsh
env:
VERSION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || steps.get_tag.outputs.LATEST_TAG }}
ARCH: ${{ matrix.arch }}
RID: ${{ matrix.os == 'windows' && format('win-{0}', matrix.arch) || (matrix.os == 'linux' && format('linux-{0}', matrix.arch) || format('osx-{0}', matrix.arch)) }}
PLATFORM: Windows
WINDOWS_CODESIGN_PFX_BASE64: ${{ secrets.WINDOWS_CODESIGN_PFX_BASE64 }}
WINDOWS_CODESIGN_PFX_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PFX_PASSWORD }}
WINDOWS_CODESIGN_TIMESTAMP_URL: ${{ secrets.WINDOWS_CODESIGN_TIMESTAMP_URL }}
run: |
$uiAccessBuild = -not [string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_PFX_BASE64) -and -not [string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_PFX_PASSWORD)
if ($uiAccessBuild) {
$outDir = "artifacts/release/installer/$env:RID"
Remove-Item $outDir -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
dotnet publish SecRandom.Desktop/SecRandom.Desktop.csproj `
-c Release `
-r $env:RID `
-p:SecRandomPlatform=$env:PLATFORM `
--self-contained `
-p:PublishTrimmed=false `
-p:UseAppHost=true `
-p:EnableUiAccess=true `
--no-restore `
-o $outDir
$certificatePath = Join-Path $env:RUNNER_TEMP 'secrandom-uiaccess.pfx'
try {
[IO.File]::WriteAllBytes($certificatePath, [Convert]::FromBase64String($env:WINDOWS_CODESIGN_PFX_BASE64))
$signTool = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe' |
Sort-Object FullName -Descending |
Select-Object -First 1 -ExpandProperty FullName
if ([string]::IsNullOrWhiteSpace($signTool)) {
throw 'signtool.exe was not found on the Windows runner.'
}
$arguments = @('sign', '/fd', 'SHA256', '/f', $certificatePath, '/p', $env:WINDOWS_CODESIGN_PFX_PASSWORD)
if (-not [string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_TIMESTAMP_URL)) {
$arguments += @('/tr', $env:WINDOWS_CODESIGN_TIMESTAMP_URL, '/td', 'SHA256')
}
$arguments += "$outDir/SecRandom.Desktop.exe"
& $signTool @arguments
if ($LASTEXITCODE -ne 0) { throw "signtool failed with exit code $LASTEXITCODE." }
& $signTool verify /pa /v "$outDir/SecRandom.Desktop.exe"
if ($LASTEXITCODE -ne 0) { throw "signtool verification failed with exit code $LASTEXITCODE." }
}
finally {
Remove-Item $certificatePath -Force -ErrorAction SilentlyContinue
}
}
else {
$outDir = "artifacts/release/installer/$env:RID"
Remove-Item $outDir -Recurse -Force -ErrorAction SilentlyContinue
Copy-Item "artifacts/release/publish/full/$env:RID" $outDir -Recurse
Write-Warning "Windows code-signing secrets are unavailable; building the installer without the UIAccess manifest."
}
@{ schemaVersion = 1; product = 'SecRandom'; rid = $env:RID; packageKind = 'windows-exe'; runtimeKind = 'self-contained' } |
ConvertTo-Json -Compress | Set-Content -LiteralPath (Join-Path $outDir 'SecRandom.package.json') -Encoding utf8NoBOM
$setupOutput = 'artifacts/release/setup'
New-Item -ItemType Directory -Path $setupOutput -Force | Out-Null
$isccArguments = @(
"/DMyAppVersion=$env:VERSION"
"/DMyAppOutDir=$outDir"
"/FSecRandom-$env:VERSION-$env:RID-setup"
"/O$setupOutput"
)
if ($uiAccessBuild) { $isccArguments += '/DUiAccessBuild' }
switch ($env:ARCH) {
'x86' { $isccArguments += '/DBuildArchX86' }
'x64' { $isccArguments += '/DBuildArchX64' }
'arm64' { $isccArguments += '/DBuildArchArm64' }
default { throw "Unsupported Windows architecture: $env:ARCH" }
}
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" Setup.iss @isccArguments
if ($LASTEXITCODE -ne 0) { throw "ISCC failed with exit code $LASTEXITCODE." }
- name: Build Linux deb
if: ${{ matrix.os == 'linux' }}
env:
ARCH: ${{ matrix.arch }}
VERSION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || steps.get_tag.outputs.LATEST_TAG }}
RID: ${{ matrix.os == 'windows' && format('win-{0}', matrix.arch) || (matrix.os == 'linux' && format('linux-{0}', matrix.arch) || format('osx-{0}', matrix.arch)) }}
shell: bash
run: |
set -euo pipefail
version_no_v="${VERSION#v}"
release_root="artifacts/release"
mkdir -p "${release_root}/dist"
deb_arch="amd64"
case "${{ matrix.arch }}" in
x64) deb_arch="amd64" ;;
arm64) deb_arch="arm64" ;;
esac
work="$(pwd)/${release_root}/linux/${RID}"
pkgroot="${work}/package-root"
rm -rf "$work"
mkdir -p "$pkgroot/DEBIAN"
mkdir -p "$pkgroot/usr/lib/secrandom"
mkdir -p "$pkgroot/usr/bin"
mkdir -p "$pkgroot/usr/share/applications"
mkdir -p "$pkgroot/usr/share/icons/hicolor/256x256/apps"
cp -a "${release_root}/publish/full/${RID}/." "$pkgroot/usr/lib/secrandom/"
printf '{"schemaVersion":1,"product":"SecRandom","rid":"%s","packageKind":"linux-deb","runtimeKind":"self-contained"}\n' "$RID" > "$pkgroot/usr/lib/secrandom/SecRandom.package.json"
chmod +x "$pkgroot/usr/lib/secrandom/SecRandom.Desktop" || true
cat > "$pkgroot/usr/bin/secrandom" << 'EOF'
#!/bin/sh
exec /usr/lib/secrandom/SecRandom.Desktop "$@"
EOF
chmod +x "$pkgroot/usr/bin/secrandom"
cat > "$pkgroot/usr/share/applications/secrandom.desktop" << 'EOF'
[Desktop Entry]
Type=Application
Name=SecRandom
Exec=secrandom
Icon=secrandom
Categories=Utility;
Terminal=false
EOF
if [ -f "resources/secrandom-icon-paper.png" ]; then
cp "resources/secrandom-icon-paper.png" "$pkgroot/usr/share/icons/hicolor/256x256/apps/secrandom.png"
fi
cat > "$pkgroot/DEBIAN/control" << EOF
Package: secrandom
Version: ${version_no_v}
Section: utils
Priority: optional
Architecture: ${deb_arch}
Maintainer: SECTL
Description: SecRandom
EOF
dpkg-deb --build "$pkgroot" "${release_root}/dist/SecRandom-${VERSION}-${RID}.deb"
- name: Build macOS pkg
if: ${{ matrix.os == 'macos' }}
env:
ARCH: ${{ matrix.arch }}
VERSION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || steps.get_tag.outputs.LATEST_TAG }}
RID: ${{ matrix.os == 'windows' && format('win-{0}', matrix.arch) || (matrix.os == 'linux' && format('linux-{0}', matrix.arch) || format('osx-{0}', matrix.arch)) }}
MACOS_SIGNING_P12_BASE64: ${{ secrets.MACOS_SIGNING_P12_BASE64 }}
MACOS_SIGNING_P12_PASSWORD: ${{ secrets.MACOS_SIGNING_P12_PASSWORD }}
MACOS_APPLICATION_SIGNING_IDENTITY: ${{ secrets.MACOS_APPLICATION_SIGNING_IDENTITY }}
MACOS_INSTALLER_SIGNING_IDENTITY: ${{ secrets.MACOS_INSTALLER_SIGNING_IDENTITY }}
MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
MACOS_NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
shell: bash
run: |
set -euo pipefail
version_no_v="${VERSION#v}"
release_root="artifacts/release"
mkdir -p "${release_root}/dist"
work="$(pwd)/${release_root}/macos/${RID}"
rm -rf "$work"
mkdir -p "$work/bundle/SecRandom.app/Contents/MacOS"
mkdir -p "$work/bundle/SecRandom.app/Contents/Resources"
cp -a "${release_root}/publish/full/${RID}/." "$work/bundle/SecRandom.app/Contents/MacOS/"
printf '{"schemaVersion":1,"product":"SecRandom","rid":"%s","packageKind":"macos-app","runtimeKind":"self-contained"}\n' "$RID" > "$work/bundle/SecRandom.app/Contents/MacOS/SecRandom.package.json"
chmod +x "$work/bundle/SecRandom.app/Contents/MacOS/SecRandom.Desktop" || true
cat > "$work/bundle/SecRandom.app/Contents/Info.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key><string>SecRandom</string>
<key>CFBundleDisplayName</key><string>SecRandom</string>
<key>CFBundleIdentifier</key><string>top.sectl.secrandom</string>
<key>CFBundleVersion</key><string>${version_no_v}</string>
<key>CFBundleShortVersionString</key><string>${version_no_v}</string>
<key>CFBundleExecutable</key><string>SecRandom.Desktop</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>LSMinimumSystemVersion</key><string>11.0</string>
<key>CFBundleIconFile</key><string>AppIcon</string>
</dict>
</plist>
EOF
icon_png="resources/secrandom-icon-paper.png"
if [ -f "$icon_png" ]; then
iconset="$work/AppIcon.iconset"
mkdir -p "$iconset"
for size in 16 32 64 128 256 512; do
sips -z "$size" "$size" "$icon_png" --out "$iconset/icon_${size}x${size}.png" >/dev/null
done
for size in 16 32 128 256 512; do
double=$((size*2))
sips -z "$double" "$double" "$icon_png" --out "$iconset/icon_${size}x${size}@2x.png" >/dev/null
done
iconutil -c icns "$iconset" -o "$work/bundle/SecRandom.app/Contents/Resources/AppIcon.icns"
fi
if [ -z "$MACOS_SIGNING_P12_BASE64" ] || [ -z "$MACOS_SIGNING_P12_PASSWORD" ] || [ -z "$MACOS_APPLICATION_SIGNING_IDENTITY" ] || [ -z "$MACOS_INSTALLER_SIGNING_IDENTITY" ] || [ -z "$MACOS_NOTARY_APPLE_ID" ] || [ -z "$MACOS_NOTARY_TEAM_ID" ] || [ -z "$MACOS_NOTARY_PASSWORD" ]; then
echo "macOS signing or notarization credentials are unavailable; keeping unsigned APP and skipping signed PKG."
ditto -c -k --sequesterRsrc --keepParent \
"$work/bundle/SecRandom.app" \
"${release_root}/dist/SecRandom-${VERSION}-${RID}.app.zip"
exit 0
fi
keychain="$RUNNER_TEMP/secrandom-signing.keychain-db"
certificate="$RUNNER_TEMP/secrandom-signing.p12"
trap 'security delete-keychain "$keychain" >/dev/null 2>&1 || true; rm -f "$certificate"' EXIT
echo "$MACOS_SIGNING_P12_BASE64" | base64 --decode > "$certificate"
security create-keychain -p "$MACOS_SIGNING_P12_PASSWORD" "$keychain"
security set-keychain-settings -lut 21600 "$keychain"
security unlock-keychain -p "$MACOS_SIGNING_P12_PASSWORD" "$keychain"
security import "$certificate" -k "$keychain" -P "$MACOS_SIGNING_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productbuild
security list-keychain -d user -s "$keychain"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_SIGNING_P12_PASSWORD" "$keychain"
codesign --force --deep --options runtime --sign "$MACOS_APPLICATION_SIGNING_IDENTITY" "$work/bundle/SecRandom.app"
codesign --verify --deep --strict "$work/bundle/SecRandom.app"
ditto -c -k --sequesterRsrc --keepParent \
"$work/bundle/SecRandom.app" \
"${release_root}/dist/SecRandom-${VERSION}-${RID}.app.zip"
printf '{"schemaVersion":1,"product":"SecRandom","rid":"%s","packageKind":"macos-pkg","runtimeKind":"self-contained"}\n' "$RID" > "$work/bundle/SecRandom.app/Contents/MacOS/SecRandom.package.json"
pkgroot="$work/pkgroot"
mkdir -p "$pkgroot/Applications"
cp -a "$work/bundle/SecRandom.app" "$pkgroot/Applications/"
pkgbuild \
--root "$pkgroot" \
--identifier "top.sectl.secrandom" \
--version "$version_no_v" \
--install-location "/" \
--sign "$MACOS_INSTALLER_SIGNING_IDENTITY" \
"${release_root}/dist/SecRandom-${VERSION}-${RID}.pkg"
xcrun notarytool submit "${release_root}/dist/SecRandom-${VERSION}-${RID}.pkg" --apple-id "$MACOS_NOTARY_APPLE_ID" --team-id "$MACOS_NOTARY_TEAM_ID" --password "$MACOS_NOTARY_PASSWORD" --wait
xcrun stapler staple "${release_root}/dist/SecRandom-${VERSION}-${RID}.pkg"
pkgutil --check-signature "${release_root}/dist/SecRandom-${VERSION}-${RID}.pkg"
- name: Upload Portable Artifact
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.os == 'windows' && format('win-{0}', matrix.arch) || (matrix.os == 'linux' && format('linux-{0}', matrix.arch) || format('osx-{0}', matrix.arch)) }}_portable
path: artifacts/release/dist/*.zip
- name: Upload Setup Artifact
if: ${{ matrix.os == 'windows' }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.os == 'windows' && format('win-{0}', matrix.arch) || (matrix.os == 'linux' && format('linux-{0}', matrix.arch) || format('osx-{0}', matrix.arch)) }}_setup
path: artifacts/release/setup/*.exe
- name: Upload deb Artifact
if: ${{ matrix.os == 'linux' }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.os == 'windows' && format('win-{0}', matrix.arch) || (matrix.os == 'linux' && format('linux-{0}', matrix.arch) || format('osx-{0}', matrix.arch)) }}_deb
path: artifacts/release/dist/*.deb
- name: Upload pkg Artifact
if: ${{ matrix.os == 'macos' }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.os == 'windows' && format('win-{0}', matrix.arch) || (matrix.os == 'linux' && format('linux-{0}', matrix.arch) || format('osx-{0}', matrix.arch)) }}_pkg
path: artifacts/release/dist/*.pkg
if-no-files-found: ignore
build_mobile_shell:
name: Build_Mobile_Android
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
target: [android]
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || github.ref }}
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.102
- name: Pin Android SDK version
shell: pwsh
run: |
@'
{
"sdk": {
"version": "10.0.102",
"rollForward": "disable"
}
}
'@ | Set-Content -LiteralPath global.json -NoNewline
dotnet --version
- name: Get Latest Tag
id: get_tag
if: ${{ github.event_name != 'workflow_dispatch' }}
shell: pwsh
run: |
$latestTag = git describe --tags --abbrev=0
"LATEST_TAG=$latestTag" >> $env:GITHUB_OUTPUT
- name: Setup Android build tools
if: ${{ matrix.target == 'android' }}
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '21'
- name: Install Android SDK packages
if: ${{ matrix.target == 'android' }}
shell: pwsh
run: |
& "$env:ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "platform-tools" "platforms;android-36" "build-tools;36.0.0"
- name: Install mobile workload
shell: pwsh
run: dotnet workload install ${{ matrix.target }}
- name: Build mobile shell
shell: pwsh
env:
VERSION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || steps.get_tag.outputs.LATEST_TAG }}
ANDROID_SIGNING_KEYSTORE_BASE64: ${{ secrets.ANDROID_SIGNING_KEYSTORE_BASE64 }}
ANDROID_SIGNING_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_SIGNING_KEYSTORE_PASSWORD }}
ANDROID_SIGNING_KEY_ALIAS: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}
ANDROID_SIGNING_KEY_PASSWORD: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}
run: |
$version = $env:VERSION.TrimStart('v', 'V')
$arguments = @('build', 'SecRandom.Android/SecRandom.Android.csproj', '-c', 'Release', '-r', 'android-arm64', '-p:BuildMobile=true', "-p:Version=$version", '-p:ApplicationVersion=${{ github.run_number }}')
$hasSigningSecrets = @($env:ANDROID_SIGNING_KEYSTORE_BASE64, $env:ANDROID_SIGNING_KEYSTORE_PASSWORD, $env:ANDROID_SIGNING_KEY_ALIAS, $env:ANDROID_SIGNING_KEY_PASSWORD) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
if ($hasSigningSecrets.Count -gt 0 -and $hasSigningSecrets.Count -ne 4) {
throw 'All Android signing secrets must be configured together.'
}
if ($hasSigningSecrets.Count -eq 4) {
$keystorePath = Join-Path $env:RUNNER_TEMP 'secrandom-android.keystore'
[IO.File]::WriteAllBytes($keystorePath, [Convert]::FromBase64String($env:ANDROID_SIGNING_KEYSTORE_BASE64))
$arguments += @('-p:AndroidKeyStore=true', "-p:AndroidSigningKeyStore=$keystorePath", "-p:AndroidSigningStorePass=$env:ANDROID_SIGNING_KEYSTORE_PASSWORD", "-p:AndroidSigningKeyAlias=$env:ANDROID_SIGNING_KEY_ALIAS", "-p:AndroidSigningKeyPass=$env:ANDROID_SIGNING_KEY_PASSWORD")
}
elseif ('${{ github.event_name }}' -eq 'workflow_dispatch') {
throw 'Release publication requires all Android signing secrets.'
}
& dotnet @arguments
if ($LASTEXITCODE -ne 0) {
throw "Android build failed with exit code $LASTEXITCODE."
}
- name: Prepare Android APK
shell: pwsh
env:
VERSION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || steps.get_tag.outputs.LATEST_TAG }}
run: |
$apk = Get-ChildItem 'SecRandom.Android/bin/Release/net10.0-android/android-arm64' -Filter '*-Signed.apk' |
Select-Object -First 1 -ExpandProperty FullName
if ([string]::IsNullOrWhiteSpace($apk)) {
throw 'The Android build did not produce a signed APK.'
}
New-Item -ItemType Directory -Path 'artifacts/mobile' -Force | Out-Null
Copy-Item $apk "artifacts/mobile/SecRandom-v$($env:VERSION.TrimStart('v', 'V'))-Android-arm64.apk"
- name: Upload Android APK
if: ${{ matrix.target == 'android' }}
uses: actions/upload-artifact@v7
with:
name: SecRandom-Android-${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || steps.get_tag.outputs.LATEST_TAG }}
path: artifacts/mobile/SecRandom-v*-Android-arm64.apk
retention-days: 14
if-no-files-found: error
build_mobile_ios:
name: Build_Mobile_iOS
runs-on: macos-26
env:
DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || github.ref }}
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.102
- name: Get Latest Tag
id: get_tag
if: ${{ github.event_name != 'workflow_dispatch' }}
shell: bash
run: echo "LATEST_TAG=$(git describe --tags --abbrev=0)" >> "$GITHUB_OUTPUT"
- name: Pin iOS SDK version
shell: bash
run: |
cat > global.json <<'EOF'
{
"sdk": {
"version": "10.0.102",
"rollForward": "disable"
}
}
EOF
dotnet --version
- name: Verify iOS toolchain
shell: bash
run: |
test "$(uname -m)" = arm64
test -d "$DEVELOPER_DIR"
xcodebuild -version
dotnet --version
- name: Install iOS workload
shell: bash
run: dotnet workload install ios --version 10.0.101.1
- name: Build iOS simulator target
shell: bash
env:
VERSION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || steps.get_tag.outputs.LATEST_TAG }}
run: >
dotnet build SecRandom.iOS/SecRandom.iOS.csproj
-c Release
-r iossimulator-arm64
-p:BuildMobile=true
-p:Version=${VERSION#v}
-p:EnableCodeSigning=false
-p:CodesignRequireProvisioningProfile=false
-p:BuildIpa=false
-p:ArchiveOnBuild=false
-p:EnableAssemblyILStripping=false
- name: Publish unsigned iOS arm64 IPA
shell: bash
env:
VERSION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || steps.get_tag.outputs.LATEST_TAG }}
run: |
version="${VERSION#v}"
version="${version#V}"
output="artifacts/mobile/SecRandom-v${version}-iOS-arm64-unsigned.ipa"
mkdir -p artifacts/mobile
dotnet publish SecRandom.iOS/SecRandom.iOS.csproj \
-c Release \
-f net10.0-ios \
-r ios-arm64 \
-p:BuildMobile=true \
-p:Version="$version" \
-p:ApplicationVersion=${{ github.run_number }} \
-p:EnableCodeSigning=false \
-p:CodesignRequireProvisioningProfile=false \
-p:BuildIpa=true \
-p:ArchiveOnBuild=false \
-p:EnableAssemblyILStripping=false \
-p:IpaPackagePath="$GITHUB_WORKSPACE/$output"
test -s "$output"
unzip -l "$output" | grep -q 'Payload/.*\.app/Info.plist'
- name: Upload unsigned iOS IPA
uses: actions/upload-artifact@v7
with:
name: SecRandom-iOS-${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || steps.get_tag.outputs.LATEST_TAG }}-unsigned
path: artifacts/mobile/SecRandom-v*-iOS-arm64-unsigned.ipa
retention-days: 14
if-no-files-found: error
publish:
name: Publish_Release
runs-on: windows-2022
needs: [build_desktop, build_mobile_shell, build_mobile_ios]
if: ${{ always() && success('build_desktop') && success('build_mobile_shell') && success('build_mobile_ios') && github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag && github.event_name != 'pull_request' }}
permissions:
contents: write
concurrency:
group: publish-public
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
ref: ${{ github.event.inputs.release_tag }}
- name: Download Artifacts
uses: actions/download-artifact@v8
with:
path: ./artifacts/release/downloaded
- name: Combine Artifacts
shell: pwsh
run: |
$releaseOutput = './artifacts/release/output'
New-Item -ItemType Directory -Path $releaseOutput -Force | Out-Null
Get-ChildItem ./artifacts/release/downloaded -Recurse -File -Include *.zip,*.exe,*.deb,*.pkg,*.apk,*.ipa | ForEach-Object {
Copy-Item $_.FullName $releaseOutput -Force
}
Write-Host "Combined files:"
Get-ChildItem $releaseOutput -File | Select-Object Name, Length
- name: Generate signed update manifest
shell: pwsh
env:
RELEASE_TAG: ${{ github.event.inputs.release_tag }}
UPDATE_MANIFEST_PRIVATE_KEY_PEM_BASE64: ${{ secrets.UPDATE_MANIFEST_PRIVATE_KEY_PEM_BASE64 }}
run: |
if ([string]::IsNullOrWhiteSpace($env:UPDATE_MANIFEST_PRIVATE_KEY_PEM_BASE64)) {
throw 'Release publication requires the UPDATE_MANIFEST_PRIVATE_KEY_PEM_BASE64 secret.'
}
$privateKeyPath = Join-Path $env:RUNNER_TEMP 'secrandom-update-ed25519.pem'
$publicDerPath = Join-Path $env:RUNNER_TEMP 'secrandom-update-ed25519-public.der'
try {
[IO.File]::WriteAllBytes($privateKeyPath, [Convert]::FromBase64String($env:UPDATE_MANIFEST_PRIVATE_KEY_PEM_BASE64))
& openssl pkey -in $privateKeyPath -pubout -outform DER -out $publicDerPath
if ($LASTEXITCODE -ne 0) { throw "OpenSSL Ed25519 public-key export failed with exit code $LASTEXITCODE." }
$publicDer = [IO.File]::ReadAllBytes($publicDerPath)
if ($publicDer.Length -lt 32) { throw 'The update-signing public key is invalid.' }
$derivedPublicKey = [Convert]::ToBase64String($publicDer[($publicDer.Length - 32)..($publicDer.Length - 1)])
$embeddedPublicKey = (Get-Content -LiteralPath 'SecRandom/Assets/Updates/release-public-key.txt' -Raw).Trim()
if ($derivedPublicKey -cne $embeddedPublicKey) {
throw 'UPDATE_MANIFEST_PRIVATE_KEY_PEM_BASE64 does not match SecRandom/Assets/Updates/release-public-key.txt.'
}
$releaseTag = $env:RELEASE_TAG
$channel = if ($releaseTag -match '-alpha(?:\.|$)') { 'alpha' } elseif ($releaseTag -match '-beta(?:\.|$)') { 'beta' } elseif ($releaseTag -match '-') { throw "Unsupported prerelease tag: $releaseTag" } else { 'release' }
$metadata = Get-Content -LiteralPath metadata.yaml -Raw
$channelMatch = [regex]::Match($metadata, "(?ms)^ $channel:\s*\r?\n\s+tag:\s*(?<tag>\S+)\s*$")
if (-not $channelMatch.Success -or $channelMatch.Groups['tag'].Value -ne $releaseTag) {
throw "metadata.yaml channel '$channel' must point to release tag $releaseTag."
}
$releaseOutput = './artifacts/release/output'
$artifacts = Get-ChildItem $releaseOutput -File | ForEach-Object {
$name = $_.Name
$kind, $runtimeKind, $os, $arch = if ($name -match '^SecRandom-v.+-(?<rid>(win|linux|osx)-(?<arch>x64|x86|arm64))-portable-(?<runtime>full|light)\.zip$') {
$platform = switch ($Matches[2]) { 'win' { 'windows' } 'linux' { 'linux' } 'osx' { 'macos' } }
@('portable-zip', $(if ($Matches.runtime -eq 'full') { 'self-contained' } else { 'framework-dependent' }), $platform, $Matches.arch)
} elseif ($name -match '^SecRandom-v.+-(?<rid>win-(?<arch>x64|x86|arm64))-setup\.exe$') {
@('windows-exe', 'self-contained', 'windows', $Matches.arch)
} elseif ($name -match '^SecRandom-v.+-(?<rid>linux-(?<arch>x64|arm64))\.deb$') {
@('linux-deb', 'self-contained', 'linux', $Matches.arch)
} elseif ($name -match '^SecRandom-v.+-(?<rid>osx-(?<arch>x64|arm64))\.pkg$') {
@('macos-pkg', 'self-contained', 'macos', $Matches.arch)
} elseif ($name -match '^SecRandom-v.+-(?<rid>osx-(?<arch>x64|arm64))\.app\.zip$') {
@('macos-app', 'self-contained', 'macos', $Matches.arch)
} elseif ($name -match '^SecRandom-v.+-Android-(?<arch>arm64)\.apk$') {
@('android-apk', 'self-contained', 'android', $Matches.arch)
} elseif ($name -match '^SecRandom-v.+-iOS-(?<arch>arm64)-unsigned\.ipa$') {
@('ios-ipa', 'self-contained', 'ios', $Matches.arch)
} else {
throw "Unrecognized release artifact name: $name"
}
[ordered]@{
id = "$os-$arch-$kind-$runtimeKind"
os = $os
arch = $arch
kind = $kind
runtimeKind = $runtimeKind
assetName = $name
byteLength = $_.Length
sha512 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA512).Hash
}
}
$manifest = [ordered]@{
schemaVersion = 1
product = 'SecRandom'
channel = $channel
tag = $releaseTag
version = $releaseTag.TrimStart('v')
publishedAt = [DateTimeOffset]::UtcNow.ToString('O')
artifacts = @($artifacts)
}
$manifestPath = "$releaseOutput/SecRandom-update-manifest.v1.json"
$signaturePath = "$releaseOutput/SecRandom-update-manifest.v1.sig"
$manifest | ConvertTo-Json -Depth 5 -Compress | Set-Content -LiteralPath $manifestPath -Encoding utf8NoBOM -NoNewline
& openssl pkeyutl -sign -rawin -inkey $privateKeyPath -in $manifestPath -out $signaturePath
if ($LASTEXITCODE -ne 0) { throw "OpenSSL Ed25519 signing failed with exit code $LASTEXITCODE." }
}
finally {
Remove-Item $privateKeyPath -Force -ErrorAction SilentlyContinue
Remove-Item $publicDerPath -Force -ErrorAction SilentlyContinue
}
- name: Generate Release Note
env:
tagName: ${{ github.event.inputs.release_tag }}
repoName: ${{ github.repository }}
run: pwsh -ep bypass ./scripts/gen-release-note.ps1
- name: Upload APP to release
uses: ncipollo/release-action@v1
with:
name: SecRandom ${{ github.event.inputs.release_tag }}
artifacts: "./artifacts/release/output/*.zip,./artifacts/release/output/*.exe,./artifacts/release/output/*.deb,./artifacts/release/output/*.pkg,./artifacts/release/output/*.apk,./artifacts/release/output/*.ipa,./artifacts/release/output/SecRandom-update-manifest.v1.json,./artifacts/release/output/SecRandom-update-manifest.v1.sig"
draft: true
bodyFile: ./release-note.md
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.event.inputs.release_tag }}