Skip to content
Closed
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
62536f5
Add template app distribution workflow
jfversluis Jun 23, 2026
00241c2
Merge remote-tracking branch 'origin/main' into jfversluis/template-a…
jfversluis Jun 23, 2026
07e913d
Simplify template app distribution inputs
jfversluis Jun 23, 2026
81c671b
Validate template app publish secrets early
jfversluis Jun 23, 2026
93fec7e
Move publish secret validation to publish job
jfversluis Jun 23, 2026
3c489ce
Restrict generated template app target frameworks
jfversluis Jun 23, 2026
f1d3498
Fix template app Xcode selection
jfversluis Jun 23, 2026
46c985e
Prefer compatible Xcode SDK for template apps
jfversluis Jun 23, 2026
91b2539
Pin template app workload manifests
jfversluis Jun 23, 2026
9e193f8
Use platform-specific workload install
jfversluis Jun 23, 2026
bdfb2b2
Support Android keystore type for template app publishing
jfversluis Jun 24, 2026
333a00f
Allow draft Play releases for template apps
jfversluis Jun 24, 2026
779541d
Use static skipped dry-run job name
jfversluis Jun 24, 2026
eb10111
Add Windows template app artifact builds
jfversluis Jun 24, 2026
8a66e33
Use static template app publish job name
jfversluis Jun 24, 2026
8bcb2fe
Add Mac Catalyst template app distribution
jfversluis Jun 24, 2026
b2f54ef
Clean up template app workflow annotations
jfversluis Jun 24, 2026
e744346
Fix Mac Catalyst provisioning profile lookup
jfversluis Jun 25, 2026
ff43084
Fix Mac Catalyst TestFlight platform
jfversluis Jun 25, 2026
f966bfe
Clean up template app distribution artifacts
jfversluis Jun 25, 2026
f8dcab2
Secure template app distribution workflow
jfversluis Jun 25, 2026
36c5d6d
Clean up skipped template app job names
jfversluis Jun 25, 2026
bc2a9f3
Use static template app matrix job names
jfversluis Jun 25, 2026
f4898c9
Allow release branches for template app publishing
jfversluis Jun 25, 2026
c475b38
Use source NuGet config for template app workloads
jfversluis Jul 1, 2026
56bf468
Handle net11 template app builds
jfversluis Jul 1, 2026
c66f1db
Adjust net11 Apple template app publishing
jfversluis Jul 1, 2026
6375187
Escape Mac Catalyst runtime identifiers
jfversluis Jul 1, 2026
cd76a7f
Fix net11 template app publishing
jfversluis Jul 1, 2026
7e4e9e5
Bound TestFlight processing wait
jfversluis Jul 1, 2026
742ef07
Handle fastlane build watcher timeouts
jfversluis Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 280 additions & 0 deletions .github/scripts/template-app-distribution/Build-TemplateApp.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
#!/usr/bin/env pwsh

param(
[Parameter(Mandatory)]
[string]$ProjectPath,

[Parameter(Mandatory)]
[ValidateSet("android", "ios", "maccatalyst", "windows")]
[string]$Platform,

[Parameter(Mandatory)]
[string]$TargetFramework,

[Parameter(Mandatory)]
[AllowEmptyString()]
[string]$RuntimeIdentifier,

[Parameter(Mandatory)]
[string]$OutputPath,

[Parameter(Mandatory)]
[string]$AppDisplayVersion,

[Parameter(Mandatory)]
[string]$AppBuildNumber,

[string]$Configuration = "Release",

[switch]$Publish,

[switch]$CreateBinlog
)

$ErrorActionPreference = "Stop"

function Assert-EnvironmentValue([string]$Name) {
$value = [Environment]::GetEnvironmentVariable($Name)
if ([string]::IsNullOrWhiteSpace($value)) {
throw "Required environment variable '$Name' is not set."
}

return $value
}

function Write-Base64File([string]$Base64Value, [string]$Path) {
$bytes = [Convert]::FromBase64String($Base64Value)
[System.IO.File]::WriteAllBytes($Path, $bytes)
}
Comment on lines +45 to +48
Comment on lines +45 to +48
Comment on lines +45 to +48

function Get-NewestBuildOutput([string]$Root, [string]$Filter, [switch]$Directory) {
$itemType = if ($Directory) { "Directory" } else { "File" }
return Get-ChildItem -Path $Root -Filter $Filter -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.PSIsContainer -eq [bool]$Directory -and $_.FullName -notmatch "[\\/](obj)[\\/]" } |
Sort-Object LastWriteTimeUtc -Descending |
Select-Object -First 1
}

function Invoke-DotNetPublish([string[]]$Arguments, [string]$Description) {
& dotnet @Arguments
if ($LASTEXITCODE -ne 0) {
throw "$Description failed with exit code $LASTEXITCODE."
}
}
Comment on lines +58 to +63

$projectFile = Get-ChildItem -Path $ProjectPath -Filter "*.csproj" -Recurse | Select-Object -First 1
if (-not $projectFile) {
throw "No project file was found in '$ProjectPath'."
}

New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
$binlogPath = if ($CreateBinlog) { Join-Path $OutputPath "build.binlog" } else { $null }
$binlogArguments = if ($CreateBinlog) { @("/bl:$binlogPath") } else { @() }

switch ($Platform) {
"android" {
$arguments = @(
"publish", $projectFile.FullName,
"-f", $TargetFramework,
"-c", $Configuration,
"-p:AndroidPackageFormat=aab",
"-p:ApplicationDisplayVersion=$AppDisplayVersion",
"-p:ApplicationVersion=$AppBuildNumber",
"-o", $OutputPath
) + $binlogArguments

if ($Publish) {
$keystorePath = $env:ANDROID_KEYSTORE_PATH
if ([string]::IsNullOrWhiteSpace($keystorePath)) {
$keystoreBase64 = Assert-EnvironmentValue "ANDROID_KEYSTORE_BASE64"
$keystorePath = Join-Path $env:RUNNER_TEMP "template-app-distribution.keystore"
Write-Base64File $keystoreBase64 $keystorePath
}

$env:ANDROID_SIGNING_STORE_PASS = Assert-EnvironmentValue "ANDROID_KEYSTORE_PASSWORD"
$env:ANDROID_SIGNING_KEY_PASS = if ([string]::IsNullOrWhiteSpace($env:ANDROID_KEY_PASSWORD)) {
$env:ANDROID_SIGNING_STORE_PASS
} else {
$env:ANDROID_KEY_PASSWORD
}

$keyAlias = Assert-EnvironmentValue "ANDROID_KEY_ALIAS"
$keystoreType = [Environment]::GetEnvironmentVariable("ANDROID_KEYSTORE_TYPE")

$arguments += @(
"-p:AndroidKeyStore=true",
"-p:AndroidSigningKeyStore=$keystorePath",
"-p:AndroidSigningKeyAlias=$keyAlias",
"-p:AndroidSigningStorePass=env:ANDROID_SIGNING_STORE_PASS",
"-p:AndroidSigningKeyPass=env:ANDROID_SIGNING_KEY_PASS"
)

if (-not [string]::IsNullOrWhiteSpace($keystoreType)) {
$arguments += "-p:AndroidSigningStoreType=$keystoreType"
}
} else {
$arguments += "-p:AndroidKeyStore=false"
}

Write-Host "Building Android package for $($projectFile.FullName)"
Invoke-DotNetPublish $arguments "Android publish"

$package = Get-NewestBuildOutput $ProjectPath "*.aab"
if (-not $package) {
$package = Get-NewestBuildOutput $OutputPath "*.aab"
}
}

"ios" {
$arguments = @(
"publish", $projectFile.FullName,
"-f", $TargetFramework,
"-c", $Configuration,
"-r", $RuntimeIdentifier,
"-p:ApplicationDisplayVersion=$AppDisplayVersion",
"-p:ApplicationVersion=$AppBuildNumber",
"-p:ValidateXcodeVersion=false"
) + $binlogArguments

if ($Publish) {
$codesignKey = Assert-EnvironmentValue "IOS_CODESIGN_KEY"
$codesignProvision = Assert-EnvironmentValue "IOS_CODESIGN_PROVISION"
$arguments += @(
"-p:BuildIpa=true",
"-p:ArchiveOnBuild=true",
"-p:CodesignKey=$codesignKey",
"-p:CodesignProvision=$codesignProvision",
"-o", $OutputPath
)
} else {
$arguments += @(
"-p:_RequireCodeSigning=false",
"-p:EnableCodeSigning=false",
"-p:CodesignKey=-",
"-p:BuildIpa=false"
)
}

Write-Host "Building iOS package for $($projectFile.FullName)"
Invoke-DotNetPublish $arguments "iOS publish"

if ($Publish) {
$package = Get-NewestBuildOutput $ProjectPath "*.ipa"
if (-not $package) {
$package = Get-NewestBuildOutput $OutputPath "*.ipa"
}
} else {
$appBundle = Get-NewestBuildOutput $ProjectPath "*.app" -Directory
if ($appBundle) {
$zipPath = Join-Path $OutputPath "$($appBundle.Name).zip"
Compress-Archive -Path $appBundle.FullName -DestinationPath $zipPath -Force
$package = Get-Item $zipPath
}
}
}

"maccatalyst" {
$arguments = @(
"publish", $projectFile.FullName,
"-f", $TargetFramework,
"-c", $Configuration,
"-p:MtouchLink=SdkOnly",
"-p:ApplicationDisplayVersion=$AppDisplayVersion",
"-p:ApplicationVersion=$AppBuildNumber",
"-p:ValidateXcodeVersion=false"
) + $binlogArguments

if (-not [string]::IsNullOrWhiteSpace($RuntimeIdentifier)) {
$arguments += @("-r", $RuntimeIdentifier)
}

if ($Publish) {
$codesignKey = Assert-EnvironmentValue "APPLE_CODESIGN_KEY"
$codesignProvision = Assert-EnvironmentValue "APPLE_CODESIGN_PROVISION"
$packageSigningKey = Assert-EnvironmentValue "APPLE_PACKAGE_SIGNING_KEY"
# App Store profiles include get-task-allow=false; the SDK validator still warns on that key for Mac Catalyst.
$arguments += @(
"-p:CreatePackage=true",
"-p:EnableCodeSigning=true",
"-p:EnablePackageSigning=true",
"-p:ValidateEntitlements=disable",
"-p:CodesignKey=$codesignKey",
"-p:CodesignProvision=$codesignProvision",
"-p:CodesignEntitlements=Platforms/MacCatalyst/Entitlements.plist",
"-p:PackageSigningKey=$packageSigningKey",
"-o", $OutputPath
)
} else {
$arguments += @(
"-p:CreatePackage=false",
"-p:_RequireCodeSigning=false",
"-p:EnableCodeSigning=false",
"-p:CodesignKey=-",
"-o", $OutputPath
)
}

Write-Host "Building Mac Catalyst package for $($projectFile.FullName)"
Invoke-DotNetPublish $arguments "Mac Catalyst publish"

if ($Publish) {
$package = Get-NewestBuildOutput $ProjectPath "*.pkg"
if (-not $package) {
$package = Get-NewestBuildOutput $OutputPath "*.pkg"
}
} else {
$appBundle = Get-NewestBuildOutput $OutputPath "*.app" -Directory
if (-not $appBundle) {
$appBundle = Get-NewestBuildOutput $ProjectPath "*.app" -Directory
}

if ($appBundle) {
$zipPath = Join-Path $OutputPath "$($appBundle.Name).zip"
Compress-Archive -Path $appBundle.FullName -DestinationPath $zipPath -Force
$package = Get-Item $zipPath
}
}
}

"windows" {
$publishOutputPath = Join-Path $OutputPath "publish"
Remove-Item -Path $publishOutputPath -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $publishOutputPath -Force | Out-Null

$arguments = @(
"publish", $projectFile.FullName,
"-f", $TargetFramework,
"-c", $Configuration,
"-p:RuntimeIdentifierOverride=$RuntimeIdentifier",
"-p:WindowsPackageType=None",
"-p:WindowsAppSDKSelfContained=true",
"-p:ApplicationDisplayVersion=$AppDisplayVersion",
"-p:ApplicationVersion=$AppBuildNumber",
"-o", $publishOutputPath
) + $binlogArguments

Write-Host "Building Windows unpackaged app for $($projectFile.FullName)"
Invoke-DotNetPublish $arguments "Windows unpackaged publish"

$zipPath = Join-Path $OutputPath "$($projectFile.BaseName)-windows-unpackaged.zip"
Remove-Item -Path $zipPath -Force -ErrorAction SilentlyContinue
Compress-Archive -Path (Join-Path $publishOutputPath "*") -DestinationPath $zipPath -Force
$package = Get-Item $zipPath
}
}

if (-not $package) {
throw "Build completed but no package artifact was found for platform '$Platform'."
}

Write-Host "Package artifact: $($package.FullName)"
if ($CreateBinlog) {
Write-Host "Build binlog: $binlogPath"
}

if ($env:GITHUB_OUTPUT) {
"package_path=$($package.FullName)" >> $env:GITHUB_OUTPUT
if ($CreateBinlog) {
"binlog_path=$binlogPath" >> $env:GITHUB_OUTPUT
}
}
Loading
Loading