Skip to content

feat: support gender filtering for SecAgent draws #231

feat: support gender filtering for SecAgent draws

feat: support gender filtering for SecAgent draws #231

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
$publishArguments = @(
'publish', 'SecRandom.Desktop/SecRandom.Desktop.csproj',
'-c', 'Release',
'-r', $env:RID,
"-p:SecRandomPlatform=$env:PLATFORM",
'-p:PublishTrimmed=false',
'-p:UseAppHost=true',
'-p:DebugType=None',
'-p:DebugSymbols=false',
'-p:BuildInParallel=false',
'-p:UseSharedCompilation=false',
'--no-restore'
)
& dotnet @publishArguments --self-contained -o $fullDir
if ($LASTEXITCODE -ne 0) {
throw "Full desktop publish failed with exit code $LASTEXITCODE."
}
& dotnet @publishArguments --self-contained false -o $lightDir
if ($LASTEXITCODE -ne 0) {
throw "Light desktop publish failed with exit code $LASTEXITCODE."
}
- 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 -replace '[^A-Za-z0-9+/=]', '')))
$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 `
-p:BuildInParallel=false `
-p:UseSharedCompilation=false `
--no-restore `
-o $outDir
if ($LASTEXITCODE -ne 0) {
throw "UIAccess desktop publish failed with exit code $LASTEXITCODE."
}
$certificatePath = Join-Path $env:RUNNER_TEMP 'secrandom-uiaccess.pfx'
try {
[IO.File]::WriteAllBytes($certificatePath, [Convert]::FromBase64String(($env:WINDOWS_CODESIGN_PFX_BASE64 -replace '[^A-Za-z0-9+/=]', '')))
$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_pluginsdk:
name: Build_PluginSDK
runs-on: ubuntu-24.04
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: Get Version
id: get_version
shell: pwsh
env:
RELEASE_TAG: ${{ github.event.inputs.release_tag }}
run: |
$semverPattern = '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z.-]+)?$'
if ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch') {
$version = $env:RELEASE_TAG.TrimStart('v', 'V')
if ($version -notmatch $semverPattern) {
throw "Release tag must be a v-prefixed semantic version (vMAJOR.MINOR.PATCH), got '$env:RELEASE_TAG'."
}
} else {
$tag = git describe --tags --abbrev=0 2>$null
if ($LASTEXITCODE -eq 0 -and $tag) {
$version = $tag.TrimStart('v', 'V')
if ($version -notmatch $semverPattern) {
Write-Host "WARNING: latest tag '$tag' is not semantic versioning; using it as-is for the routine build artifact."
}
} else {
$version = '0.0.0-dev'
}
}
echo "PKG_VERSION=$version" >> $env:GITHUB_OUTPUT
Write-Host "Plugin SDK package version: $version"
- name: Verify API version matches main version
shell: pwsh
env:
PKG_VERSION: ${{ steps.get_version.outputs.PKG_VERSION }}
run: |
$api = Get-Content 'SecRandom.PluginSdk/PluginApiVersions.cs' -Raw
$match = [regex]::Match($api, 'new\((?<major>\d+),\s*\d+')
if (-not $match.Success) { throw 'Unable to parse PluginApiVersions.Current.' }
$apiMajor = $match.Groups['major'].Value
$pkgMajor = ($env:PKG_VERSION -split '\.')[0]
if ($apiMajor -ne $pkgMajor) {
throw "PluginApiVersions.Current major ($apiMajor) must match the package version major ($pkgMajor)."
}
- name: Pack Plugin SDK
shell: pwsh
env:
PKG_VERSION: ${{ steps.get_version.outputs.PKG_VERSION }}
run: |
$outDir = 'artifacts/pluginsdk'
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
foreach ($project in @(
'SecRandom.Shared/SecRandom.Shared.csproj',
'SecRandom.Core/SecRandom.Core.csproj',
'SecRandom.PluginSdk/SecRandom.PluginSdk.csproj'
)) {
& dotnet pack $project -c Release -o $outDir "-p:PackageVersion=$env:PKG_VERSION" '-p:BuildInParallel=false' '-p:UseSharedCompilation=false'
if ($LASTEXITCODE -ne 0) { throw "dotnet pack failed for $project with exit code $LASTEXITCODE." }
}
- name: Upload Plugin SDK Packages
uses: actions/upload-artifact@v7
with:
name: pluginsdk
path: artifacts/pluginsdk/*.nupkg
if-no-files-found: error
build_android:
name: Build_Android_${{ matrix.arch }}
runs-on: ubuntu-24.04
env:
arch: ${{ matrix.arch }}
strategy:
fail-fast: false
matrix:
arch: ['arm64', 'x64']
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: 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 JDK 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '21'
- name: Install Android SDK packages
shell: pwsh
run: |
& "$env:ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "platform-tools" "platforms;android-36" "build-tools;36.0.0"
- name: Install Android workload
shell: pwsh
run: dotnet workload install android
- name: Build Android APK
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-${{ matrix.arch }}', "-p:Version=$version", '-p:ApplicationVersion=${{ github.run_number }}', '-p:BuildInParallel=false', '-p:UseSharedCompilation=false')
$signingValues = @{
ANDROID_SIGNING_KEYSTORE_BASE64 = $env:ANDROID_SIGNING_KEYSTORE_BASE64
ANDROID_SIGNING_KEYSTORE_PASSWORD = $env:ANDROID_SIGNING_KEYSTORE_PASSWORD
ANDROID_SIGNING_KEY_ALIAS = $env:ANDROID_SIGNING_KEY_ALIAS
ANDROID_SIGNING_KEY_PASSWORD = $env:ANDROID_SIGNING_KEY_PASSWORD
}
$missingSigningValues = @($signingValues.GetEnumerator() | Where-Object { [string]::IsNullOrWhiteSpace($_.Value) } | Select-Object -ExpandProperty Key)
if ($missingSigningValues.Count -gt 0) {
throw "Android release signing is required for every build; missing secrets: $($missingSigningValues -join ', ')."
}
$keystorePath = Join-Path $env:RUNNER_TEMP 'secrandom-android.keystore'
$keystoreBase64 = $env:ANDROID_SIGNING_KEYSTORE_BASE64
if ($keystoreBase64.Length -gt 0 -and [int][char]$keystoreBase64[0] -eq 0xFEFF) {
$keystoreBase64 = $keystoreBase64.Substring(1)
}
$keystoreBase64 = $keystoreBase64.Trim()
$keystoreBase64 = $keystoreBase64 -replace '-', '+'
$keystoreBase64 = $keystoreBase64 -replace '_', '/'
$keystoreBytes = $null
try {
$keystoreBytes = [Convert]::FromBase64String($keystoreBase64)
} catch {
$firstInvalid = $keystoreBase64.ToCharArray() | Where-Object { [char]::IsWhiteSpace($_) -eq $false -and $_ -notmatch '[A-Za-z0-9+/=]' } | Select-Object -First 1
$invalidCodePoint = if ($null -ne $firstInvalid) { [int][char]$firstInvalid } else { -1 }
throw "ANDROID_SIGNING_KEYSTORE_BASE64 is not valid base64 even after trimming whitespace and translating URL-safe characters; first invalid character has code point $invalidCodePoint. Re-provision the secret with a clean base64 encoding of the keystore."
}
if ($keystoreBytes.Length -lt 16) {
throw 'The Android release keystore secret decoded to an implausibly small file; re-check ANDROID_SIGNING_KEYSTORE_BASE64.'
}
[IO.File]::WriteAllBytes($keystorePath, $keystoreBytes)
$keytoolArguments = @('-J-Duser.language=en', '-J-Duser.country=US', '-list', '-v', '-keystore', $keystorePath, '-storepass', $env:ANDROID_SIGNING_KEYSTORE_PASSWORD, '-alias', $env:ANDROID_SIGNING_KEY_ALIAS)
$keystoreVerification = & keytool @keytoolArguments 2>&1
if ($LASTEXITCODE -ne 0) {
throw "Android signing keystore validation failed. $($keystoreVerification -join ' ')"
}
$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")
& 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 }}
ANDROID_SIGNING_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_SIGNING_KEYSTORE_PASSWORD }}
ANDROID_SIGNING_KEY_ALIAS: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}
run: |
$apk = Get-ChildItem 'SecRandom.Android/bin/Release/net10.0-android36.0/android-${{ matrix.arch }}' -Filter '*-Signed.apk' |
Select-Object -First 1 -ExpandProperty FullName
if ([string]::IsNullOrWhiteSpace($apk)) {
throw 'The Android build did not produce a signed APK.'
}
$keystorePath = Join-Path $env:RUNNER_TEMP 'secrandom-android.keystore'
if (!(Test-Path -LiteralPath $keystorePath)) {
throw 'The Android signing keystore is missing after the build.'
}
$keytoolArguments = @('-J-Duser.language=en', '-J-Duser.country=US', '-list', '-v', '-keystore', $keystorePath, '-storepass', $env:ANDROID_SIGNING_KEYSTORE_PASSWORD, '-alias', $env:ANDROID_SIGNING_KEY_ALIAS)
$keystoreCertificate = & keytool @keytoolArguments 2>&1
if ($LASTEXITCODE -ne 0) {
throw 'Unable to read the Android release certificate from the signing keystore.'
}
$expectedFingerprint = [regex]::Match(($keystoreCertificate -join "`n"), '(?im)^\s*SHA256:\s*([0-9A-F:]+)\s*$').Groups[1].Value.Replace(':', '').ToUpperInvariant()
if ([string]::IsNullOrWhiteSpace($expectedFingerprint)) {
throw 'The Android signing keystore did not expose a SHA-256 certificate fingerprint.'
}
$apksigner = Get-ChildItem "$env:ANDROID_HOME/build-tools/*/apksigner" -File | Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty FullName
if ([string]::IsNullOrWhiteSpace($apksigner)) {
throw 'apksigner was not found on the Android runner.'
}
$verification = & $apksigner verify --verbose --print-certs $apk 2>&1
if ($LASTEXITCODE -ne 0) {
throw 'The Android APK signature verification failed.'
}
$actualFingerprint = [regex]::Match(($verification -join "`n"), '(?im)certificate SHA-256 digest:\s*([0-9A-F:]+)').Groups[1].Value.Replace(':', '').ToUpperInvariant()
if ([string]::IsNullOrWhiteSpace($actualFingerprint) -or $actualFingerprint -cne $expectedFingerprint) {
throw "The Android APK certificate fingerprint does not match the release keystore. Expected $expectedFingerprint, got $actualFingerprint."
}
Write-Host "Verified Android release certificate SHA-256: $actualFingerprint"
New-Item -ItemType Directory -Path 'artifacts/mobile' -Force | Out-Null
Copy-Item $apk "artifacts/mobile/SecRandom-v$($env:VERSION.TrimStart('v', 'V'))-android-${{ matrix.arch }}.apk"
- name: Upload Android APK
uses: actions/upload-artifact@v7
with:
name: android-${{ matrix.arch }}_apk
path: artifacts/mobile/*.apk
retention-days: 14
if-no-files-found: error
build_ios:
name: Build_iOS
runs-on: macos-26
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: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '26.6'
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- 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: Verify iOS toolchain
shell: bash
run: |
test "$(uname -m)" = arm64
xcode-select -p
xcodebuild -version
xcrun --find strip
dotnet --version
- name: Install iOS workload
shell: bash
run: dotnet workload install ios
- 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: |
set -euo pipefail
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:Version="$version" \
-p:ApplicationVersion=${{ github.run_number }} \
-p:EnableCodeSigning=false \
-p:CodesignRequireProvisioningProfile=false \
-p:BuildIpa=true \
-p:ArchiveOnBuild=false \
-p:BuildInParallel=false \
-p:UseSharedCompilation=false \
-p:IpaPackagePath="$GITHUB_WORKSPACE/$output"
test -s "$output"
unzip -l "$output" | grep -q 'Payload/.*\.app/Info.plist'
unzip -p "$output" 'Payload/*.app/Info.plist' > "$RUNNER_TEMP/SecRandom-iOS-Info.plist"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleName' "$RUNNER_TEMP/SecRandom-iOS-Info.plist")" = 'SecRandom'
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconName' "$RUNNER_TEMP/SecRandom-iOS-Info.plist")" = 'AppIcon'
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$RUNNER_TEMP/SecRandom-iOS-Info.plist")" = "$version"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$RUNNER_TEMP/SecRandom-iOS-Info.plist")" = "${{ github.run_number }}"
- name: Upload unsigned iOS IPA
uses: actions/upload-artifact@v7
with:
name: ios-arm64_unsigned_ipa
path: artifacts/mobile/*.ipa
retention-days: 14
if-no-files-found: error
publish:
name: Publish_Release
runs-on: windows-2022
needs: [build_desktop, build_android, build_ios, build_pluginsdk]
if: ${{ always() && success('build_desktop') && success('build_android') && success('build_ios') && success('build_pluginsdk') && github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag && github.event_name != 'pull_request' }}
permissions:
contents: write
id-token: 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: NuGet login (OIDC → temp API key)
uses: NuGet/login@v1
id: login
with:
user: ${{ secrets.NUGET_USER }}
- name: NuGet push
shell: pwsh
env:
NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }}
run: |
$packages = Get-ChildItem './artifacts/release/downloaded/pluginsdk' -Filter *.nupkg -Recurse | Select-Object -ExpandProperty FullName
if (-not $packages) { throw 'No Plugin SDK nupkg artifacts found to publish.' }
foreach ($nupkg in $packages) {
& dotnet nuget push $nupkg --api-key $env:NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate
if ($LASTEXITCODE -ne 0) { throw "NuGet push failed for $nupkg with exit code $LASTEXITCODE." }
}
- 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 -replace '[^A-Za-z0-9+/=]', '')))
& 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|x64)\.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.json"
$signaturePath = "$releaseOutput/SecRandom-update-manifest.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.json,./artifacts/release/output/SecRandom-update-manifest.sig"
draft: true
bodyFile: ./release-note.md
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.event.inputs.release_tag }}