Add template app distribution workflow#36571
Conversation
Supersedes dotnet#36136. Adds a manually triggered workflow that builds the MAUI templates into tester apps for Android, iOS, Mac Catalyst, and Windows, in both blank and sample-content variants. The dry-run path (publish=false) uploads directly-installable GitHub artifacts; publish=true optionally ships to Google Play test tracks and TestFlight via fastlane, gated on the protected template-app-distribution environment secrets. The original PR produced store-shaped artifacts that testers could not install or launch. This version fixes each platform so the uploaded artifact runs on a tester device with no extra tooling: - Android: build a signed, directly-installable APK (v2+v3) for the dry-run path instead of an AAB, which cannot be sideloaded. - Windows: publish self-contained (SelfContained=true plus WindowsAppSDKSelfContained=true) so testers do not need a matching .NET or WindowsAppSDK runtime installed. - iOS: the dry-run now builds an arm64 iOS Simulator app (dotnet build -r iossimulator-arm64, with -p:UseMonoRuntime=false for the net11 CoreCLR build) that actually launches; the device path stays gated behind ad-hoc signing secrets. - Mac Catalyst: pin the arm64-native RID, zip the .app with ditto instead of Compress-Archive (which strips the exec bit and framework symlinks), and re-sign the bundle ad-hoc inside-out so it launches on macOS 15+ and macOS 26 without a Code Signature Invalid SIGKILL. All four artifacts were validated end-to-end from CI output (installed and launched, not just built). See .github/scripts/template-app-distribution/README.md for per-platform tester install steps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 92e506ca-933d-4c5b-ab06-62cf471e259c
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 36571Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 36571" |
|
Azure Pipelines: 1 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
This PR introduces a new manually-triggered “Template App Distribution” GitHub Actions workflow plus supporting scripts to (1) pack the MAUI templates, (2) generate per-platform apps (blank + sample), (3) produce sideloadable artifacts for dry-runs, and (4) optionally publish to Google Play / TestFlight using fastlane behind a protected environment.
Changes:
- Adds
.github/workflows/template-app-distribution.ymlwith a prepare → dry-run-build / publish job split and a platform/variant matrix. - Adds PowerShell scripts under
.github/scripts/template-app-distribution/to resolve the source ref + SDK, pack templates, generate apps, and build platform-appropriate distributables. - Adds a dedicated fastlane bundle (Gemfile/Gemfile.lock + Fastfile) and a README documenting artifact types and tester install steps.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| .github/workflows/template-app-distribution.yml | New workflow_dispatch pipeline to build and (optionally) publish template-based tester apps across platforms/variants. |
| .github/scripts/template-app-distribution/Resolve-SourceRef.ps1 | Resolves/validates source_ref and enforces trust rules for publish=true. |
| .github/scripts/template-app-distribution/Resolve-DotNetSdk.ps1 | Resolves SDK version (global.json) and derives the netX.Y TFM used by matrix generation. |
| .github/scripts/template-app-distribution/Prepare-Matrix.ps1 | Builds the variant/platform matrix and default app identifiers/bundle IDs. |
| .github/scripts/template-app-distribution/Pack-Templates.ps1 | Builds/packs Microsoft.Maui.Templates into a local nupkg for dotnet new install. |
| .github/scripts/template-app-distribution/New-TemplateApp.ps1 | Generates an app from the packed templates and normalizes app identity/version settings. |
| .github/scripts/template-app-distribution/Install-AppleSigningAssets.ps1 | Installs Apple certs/profiles into a temporary keychain and exports signing env vars. |
| .github/scripts/template-app-distribution/Build-TemplateApp.ps1 | Builds platform-specific outputs (APK/AAB, iOS IPA + simulator .app zip, Mac Catalyst .pkg + .app zip, Windows self-contained zip). |
| .github/scripts/template-app-distribution/README.md | Tester-facing documentation for what artifacts are produced and how to install/run them. |
| .github/scripts/template-app-distribution/fastlane/Gemfile | Pins fastlane dependency for publish lanes. |
| .github/scripts/template-app-distribution/fastlane/Gemfile.lock | Bundler lockfile for deterministic fastlane installs in CI. |
| .github/scripts/template-app-distribution/fastlane/Fastfile | Implements the fastlane lanes used to upload to Google Play / TestFlight. |
| function Write-Base64File([string]$Base64Value, [string]$Path) { | ||
| $bytes = [Convert]::FromBase64String($Base64Value) | ||
| [System.IO.File]::WriteAllBytes($Path, $bytes) | ||
| } |
The iOS dry-run only produced a Simulator .app.zip, so testers reported there was no .ipa to install on a real iPhone/iPad. Add a best-effort unsigned ios-arm64 device build wrapped as a Payload/*.app .ipa, which testers can install via AltStore/Sideloadly (re-signed with their own Apple ID). The Simulator app is still uploaded as an additional artifact for Mac-only smoke testing, and the device IPA build is wrapped in try/catch so a failure never regresses the existing Simulator artifact. A directly-installable device build still requires the secret-gated ad-hoc IPA or TestFlight publish path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 92e506ca-933d-4c5b-ab06-62cf471e259c
| git config --global advice.defaultBranchName false | ||
|
|
||
| - name: Checkout workflow scripts | ||
| uses: actions/checkout@v7 |
| - name: Create generated app | ||
| id: app | ||
| shell: pwsh | ||
| run: | | ||
| & "${{ github.workspace }}/trusted/.github/scripts/template-app-distribution/New-TemplateApp.ps1" ` | ||
| -TemplatePackagePath "${{ steps.pack.outputs.template_package_path }}" ` | ||
| -BuildRoot "${{ runner.temp }}/template-app-build" ` | ||
| -Variant "${{ matrix.variant }}-${{ matrix.platform }}" ` | ||
| -ProjectName "${{ matrix.projectName }}" ` | ||
| -Template "${{ matrix.template }}" ` | ||
| -TemplateArgsJson '${{ matrix.templateArgsJson }}' ` | ||
| -DotNetTfm "${{ needs.prepare.outputs.dotnet_tfm }}" ` | ||
| -TargetFramework "${{ matrix.targetFramework }}" ` | ||
| -ApplicationId "${{ matrix.applicationId }}" ` | ||
| -DisplayName "${{ matrix.displayName }}" ` | ||
| -DotNetSdk "${{ needs.prepare.outputs.dotnet_sdk }}" ` | ||
| -AppDisplayVersion "$env:APP_DISPLAY_VERSION" ` | ||
| -AppBuildNumber "$env:APP_BUILD_NUMBER" ` | ||
| -NuGetConfigPath "${{ github.workspace }}/source/NuGet.config" |
| - name: Create generated app | ||
| id: app | ||
| shell: pwsh | ||
| run: | | ||
| & "${{ github.workspace }}/trusted/.github/scripts/template-app-distribution/New-TemplateApp.ps1" ` | ||
| -TemplatePackagePath "${{ steps.pack.outputs.template_package_path }}" ` | ||
| -BuildRoot "${{ runner.temp }}/template-app-build" ` | ||
| -Variant "${{ matrix.variant }}-${{ matrix.platform }}" ` | ||
| -ProjectName "${{ matrix.projectName }}" ` | ||
| -Template "${{ matrix.template }}" ` | ||
| -TemplateArgsJson '${{ matrix.templateArgsJson }}' ` | ||
| -DotNetTfm "${{ needs.prepare.outputs.dotnet_tfm }}" ` | ||
| -TargetFramework "${{ matrix.targetFramework }}" ` | ||
| -ApplicationId "${{ matrix.applicationId }}" ` | ||
| -DisplayName "${{ matrix.displayName }}" ` | ||
| -DotNetSdk "${{ needs.prepare.outputs.dotnet_sdk }}" ` | ||
| -AppDisplayVersion "$env:APP_DISPLAY_VERSION" ` | ||
| -AppBuildNumber "$env:APP_BUILD_NUMBER" ` | ||
| -NuGetConfigPath "${{ github.workspace }}/source/NuGet.config" |
| function Write-Base64File([string]$Base64Value, [string]$Path) { | ||
| $bytes = [Convert]::FromBase64String($Base64Value) | ||
| [System.IO.File]::WriteAllBytes($Path, $bytes) | ||
| } |
iOS dry-run now emits an installable
|
| Check | Result |
|---|---|
| Archive layout | Payload/MauiTemplateBlank.app/… at root ✓ |
| Binary | Mach-O 64-bit executable arm64 ✓ |
| Platform | LC_BUILD_VERSION platform 2 (iOS device, not simulator) ✓ |
DTPlatformName |
iphoneos ✓ |
| Embedded profile | none — so no app-store/Beta profile to reject (the original 0xe800801f error) ✓ |
| Signature | not signed at all → AltStore/Sideloadly re-signs ✓ |
Both blank and sample iOS jobs now produce MauiTemplate*.app.ipa (device) + MauiTemplate*.app.zip (Simulator).
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Summary
Supersedes #36136.
Adds a manually triggered Template App Distribution workflow that builds the MAUI templates into tester apps for Android, iOS, Mac Catalyst, and Windows, in both blank and sample-content variants. The dry-run path (
publish=false) uploads directly-installable GitHub artifacts;publish=trueoptionally ships to Google Play test tracks and TestFlight via fastlane, gated on the protectedtemplate-app-distributionenvironment secrets.Why this supersedes #36136
#36136 produced store-shaped artifacts that testers could not install or launch. Reported failures: Android had no APK, Windows showed a "install the .NET runtime" screen, iOS failed with
0xe800801f Attempted to install a Beta profile without the proper entitlement, and Mac Catalyst crashed on launch withEXC_CRASH (SIGKILL - Code Signature Invalid).This version fixes each platform so the uploaded dry-run artifact runs on a tester device with no extra tooling:
.aabwas produced (cannot be sideloaded)SelfContained=true+WindowsAppSDKSelfContained=true).app/ TestFlight-profile IPA installs nowhere.pkg/ Rosetta x64 build thenSIGKILL Code Signature Invalid.app, zipped withditto, ad-hoc re-signed inside-out so it launches on macOS 15+ / 26Validation
All four artifacts were validated end-to-end from CI output — installed and launched on a real target (arm64 macOS 26.5.2, an Android emulator, an iOS Simulator), not just built:
adb installthenam start; process stayed alive with noFATAL..app(arm64, ad-hoc), thensimctl install/launch; UIKit app running, no crash..app(arm64, ad-hoc) launches natively and the Hello-World window renders; tester quarantine then clear then launch also confirmed.See
.github/scripts/template-app-distribution/README.mdfor per-platform tester install steps.Setup still needed before
publish=trueworksThe
template-app-distributionGitHub Environment and its signing / store secrets must be created indotnet/maui. Dry-run artifact builds run without any secrets.Note on scope
Unlike #36136 this PR does not touch
src/Templates/src/templates/maui-mobile/MauiApp.1.csproj; that change was an incidental package-version edit from an older base and is now stale relative tomain(which has since moved those versions forward via #36074 and #35608). The workflow does not require it./cc @PureWeen @jfversluis
🔍 This PR was prepared by GitHub Copilot CLI on behalf of @kubaflo, standing in for @jfversluis while he is away. All four platform artifacts were launched, not just built.