Skip to content

[DO NOT MERGE] Add template app distribution workflow - #36571

Draft
kubaflo wants to merge 30 commits into
dotnet:mainfrom
kubaflo:template-app-distribution
Draft

kubaflo wants to merge 30 commits into
dotnet:mainfrom
kubaflo:template-app-distribution

Conversation

@kubaflo

@kubaflo kubaflo commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

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 installable or sideloadable GitHub artifacts; publish=true optionally ships to Google Play test tracks and TestFlight via fastlane, gated on the protected template-app-distribution environment 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 an "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 with EXC_CRASH (SIGKILL - Code Signature Invalid).

This version provides a usable test path for every platform:

Platform Problem in #36136 Fix
Android Only an .aab was produced (cannot be sideloaded) Build a signed, directly-installable APK (v2+v3)
Windows Framework-dependent (tester saw a runtime-missing screen) Publish self-contained (SelfContained=true + WindowsAppSDKSelfContained=true)
iOS Unsigned device .app / TestFlight-profile IPA installs nowhere Upload an arm64 iOS Simulator app plus a best-effort unsigned device IPA; the IPA must be re-signed by AltStore/Sideloadly, while directly installable ad-hoc/TestFlight builds remain secret-gated
Mac Catalyst Store .pkg / Rosetta x64 build then SIGKILL Code Signature Invalid Build an arm64-native .app on macos-26, zip with ditto, and ad-hoc re-sign inside-out so it launches on current macOS runners and test machines

Current .NET 11 Apple toolchain

Current .NET 11 Apple workloads require Xcode 26.6 with the iOS/macOS 26.5 SDK. The previous macos-15 jobs selected Xcode 26.3 / SDK 26.2 and failed with MT0180 and MT4162. Both iOS and Mac Catalyst jobs now run on macos-26 and select Xcode 26.6.

Full-SHA dry runs also reset PowerShell's native exit status after expected Git trust probes, preventing a successfully resolved source SHA from being reported as a failed prepare job.

Validation

Workflow run 31022037238 passed all eight blank/sample jobs for Android, iOS, Mac Catalyst, and Windows. Its Apple logs confirmed Xcode 26.6 and SDK 26.5.

Artifacts were then validated end-to-end from CI output, not only built:

  • Android: APK signature verified, installed with adb install, launched, rendered, and remained alive.
  • iOS: arm64 Simulator app installed and launched on iOS 18.5 and iOS 26.5; the blank app rendered and remained alive.
  • Mac Catalyst: arm64 ad-hoc app launched natively and remained alive on macOS 26.5.2.
  • Windows: self-contained payload confirmed structurally with CoreCLR and WindowsAppSDK native dependencies bundled.

The sample-content app also exposed a separate template-source seed initialization defect on iOS 26.5. That source issue is fixed and empirically validated across iOS 26.5, iOS 18.5, Android 16, and Mac Catalyst in #37122; it is not a distribution-workflow failure.

See .github/scripts/template-app-distribution/README.md for per-platform tester install steps, including the AltStore/Sideloadly requirement for the unsigned iOS device IPA.

Setup still needed before publish=true works

The template-app-distribution GitHub Environment and its signing / store secrets must be created in dotnet/maui. Dry-run artifact builds run without any secrets.

Publishing from any source branch beyond the default branch also requires the repository variable TEMPLATE_APP_TRUSTED_PUBLISH_BRANCHES to list that exact protected branch name. Wildcards such as release/* are rejected. Publish jobs deliberately do not create or upload MSBuild binlogs because structured logs can capture environment-derived signing and store credentials.

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 to main (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.

Copilot AI lite review requested due to automatic review settings July 14, 2026 20:25
@kubaflo
kubaflo had a problem deploying to copilot-pat-pool July 14, 2026 20:25 — with GitHub Actions Failure
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 36571

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 36571"

@azure-pipelines

Copy link
Copy Markdown
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml with 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.

Copilot AI review requested due to automatic review settings July 15, 2026 12:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 4 comments.

@kubaflo

kubaflo commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Correction - October 2, 2026: the former "installable IPA" heading was misleading. This historical dry-run output was unsigned and not directly installable on an iPhone/iPad. It was not a TestFlight release. Do not use it as a substitute for the signed delivery requested for Pooja.

Historical iOS dry-run output: unsigned device IPA and Simulator app

The July change added an unsigned ios-arm64 device build wrapped as Payload/*.app in an IPA, alongside the arm64 Simulator .app.zip. Historical run 29414424580 used source_ref=net11.0 and publish=false.

The inspected device archive had an arm64 iOS executable, no embedded provisioning profile and no code signature. Those checks proved archive contents, not physical-device installability. The Simulator app is a separate artifact and does not establish that the device IPA can be installed.

Physical-device delivery requires a successfully processed TestFlight build with tester access, or a distribution-signed ad-hoc IPA whose provisioning profile includes the tester's device. No such delivery or invitation is established by this comment. Current sample-download status is maintained in the distribution index.

@kubaflo kubaflo added the area-infrastructure CI, Maestro / Coherency, upstream dependencies/versions label Jul 15, 2026
Copilot AI review requested due to automatic review settings July 16, 2026 11:58
@kubaflo

kubaflo commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the Copilot review feedback in b203ffa912b:

  • base64 trim — Write-Base64File now .Trim()s before FromBase64String so a secret with a trailing newline doesn't throw (matches the existing keystore trim in the same script).
  • checkout pin — all 6 actions/checkout@v7 → @v4 (repo standard; v7 doesn't exist).
  • template-args injection — matrix.templateArgsJson is now passed via a TEMPLATE_ARGS_JSON env var in both the dry-run and publish jobs instead of single-quote interpolation, removing the quoting/command-injection vector.

All 5 review threads resolved. Ready for re-review.

🔍 This is an AI-generated comment posted on @kubaflo's behalf.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.

Copilot AI review requested due to automatic review settings July 16, 2026 12:09
@kubaflo

kubaflo commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the latest Copilot re-review in 06e4349c6f0:

# Finding Resolution
1 Mac installer cert required in pre-flight validation Removed the hard requirement — Install-AppleSigningAssets.ps1 accepts an installer identity from any imported cert (the main IOS_CERTIFICATE p12 can contain one) and enforces it itself
2 README "two files" for iOS dry-run Now "one or two files" (unsigned device .ipa is best-effort)
3 Error message "iOS publishing" Now "iOS/Mac Catalyst publishing" (step runs for both)
4 Header lists ANDROID_KEY_PASSWORD as required Moved to Optional (defaults to keystore password)
5 & 6 Per-matrix secret scoping (validate + build steps) Explained trade-off inline — step-scoped exposure is already limited; per-platform gating is a larger, untestable change better done as a focused follow-up

All six threads addressed and resolved. Ready for another look, @copilot.

🔍 This is an AI-generated comment posted on @kubaflo's behalf.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 5 comments.

Copilot AI review requested due to automatic review settings July 16, 2026 12:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

Copilot AI review requested due to automatic review settings July 16, 2026 12:30
@kubaflo

kubaflo commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@copilot-pull-request-reviewer this round is addressed in 8dd0d23:

  • fetch-tags: true added to the source checkout so tag-based source_ref inputs resolve and trust correctly.
  • Info.plist key injection now anchors to the root </dict> before </plist> and replaces only once, preventing duplicate keys if a stray column-0 </dict> appears.

All review threads are resolved. Ready for re-review. Thanks!

🔍 This is an AI-generated comment posted on @kubaflo's behalf.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings July 16, 2026 12:39
Copilot AI and others added 28 commits October 26, 2026 11:58
…late args via env

- Build-TemplateApp.ps1: Write-Base64File now trims surrounding whitespace/newlines before
  [Convert]::FromBase64String (secrets often carry a trailing newline, which otherwise throws
  and breaks keystore/cert materialization). Matches the existing trim pattern used for the
  Android keystore later in the same script.
- template-app-distribution.yml: pin all 6 actions/checkout@v7 -> @v4 to match the repo
  standard and avoid a runtime 'unresolved action' failure (v7 does not exist).
- template-app-distribution.yml: pass matrix.templateArgsJson to New-TemplateApp.ps1 via a
  TEMPLATE_ARGS_JSON env var ($env:TEMPLATE_ARGS_JSON) in both the dry-run and publish jobs,
  instead of single-quote-interpolating it into the PowerShell command text. Removes the
  quoting/command-injection vector if the JSON ever contains a single quote.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
- README: iOS dry-run artifact is "one or two files" (unsigned device .ipa is
  best-effort, so it may be absent, leaving only the Simulator .app.zip).
- Header: TEMPLATE_APP_ANDROID_KEY_PASSWORD is Optional (defaults to the
  keystore password when unset), not Required.
- Pre-flight validation no longer hard-requires MAC_INSTALLER_CERTIFICATE_* for
  maccatalyst: Install-AppleSigningAssets.ps1 accepts an installer identity from
  any imported cert (the main IOS_CERTIFICATE p12 can already contain one), so
  requiring the dedicated secret was a false-negative.
- App Store Connect private-key check message now says "iOS/Mac Catalyst
  publishing" (the step runs for both platforms).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
… codes

- actions/upload-artifact@v7 -> @v4 (v7 does not exist; @v4 is the repo standard)
  in both the dry-run and publish jobs.
- Pack-Templates.ps1: throw when `dotnet build`/`dotnet pack` fail. With
  $ErrorActionPreference='Stop', native-command failures still don't throw in
  PowerShell, so an earlier failing build could otherwise fall through to a stale
  package. Matches the explicit $LASTEXITCODE checks in Build-TemplateApp.ps1.
- New-TemplateApp.ps1: throw when `dotnet new install` or `dotnet new <template>`
  fail, for the same reason.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
- template-app-distribution.yml: add fetch-tags: true to the source
  checkout so tag-based source_ref values resolve/trust correctly
  (Resolve-SourceRef.ps1 uses git rev-list refs/tags/<tag>).
- New-TemplateApp.ps1: anchor the missing-key injection to the root
  </dict> immediately preceding </plist> and replace only once, so a
  stray column-0 </dict> can't duplicate the injected key.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
codesign warns 'Specifying ":" in the path is deprecated and will not
work in a future release' for the -d --entitlements :<path> form. Switch
to '-d --xml --entitlements -' (verified on macOS 26.5.2: identical XML,
no deprecation warning), capture stdout, and write the file only when the
bundle actually declares entitlements so the hardened-runtime re-sign
preserves them instead of silently dropping them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
The Android path built the APK/AAB without -r, so the RuntimeIdentifier
matrix value (e.g. android-arm64) was ignored and the parameter was
misleading. Append -r $RuntimeIdentifier to the shared $commonArgs when
non-empty (so both the APK and AAB builds inherit it), consistent with
the iOS/Windows/Developer ID paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Pack-Templates.ps1 ran dotnet build -t:Rebuild then dotnet pack without
--no-build, so the project built twice. The template package is content-
only (IncludeBuildOutput=false) and its content is assembled by the
project's BeforePack target (_UpdateTemplateVersions), which runs during
pack regardless of --no-build, so packing with --no-build drops the
duplicate build without losing any packaged content.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
- Upload the dry-run artifact with if: always() so the (partial) binlog is
  still collected when the build step fails, matching the publish job.
- Emit binlog_path to $GITHUB_OUTPUT up-front in Build-TemplateApp.ps1 so a
  throwing build still exposes the binlog path to the upload step.
- Mention the IOS_PROVISIONING_* fallbacks in the missing-profile error so the
  message isn't misleading when those variables are configured.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Addresses review feedback (PureWeen):

- Security: pass every matrix-controlled value (project/display names, template,
  application/bundle IDs, platform, TFM, RID, variant) through env: and consume
  $env:... in the PowerShell/Fastlane steps, instead of interpolating repo-
  variable-derived values directly into command text. A quote/backtick/$() in a
  configured value is now treated as literal data, not parsed as script — matching
  the existing TEMPLATE_ARGS_JSON pattern. This matters most in publish, which
  holds signing/store secrets.
- Error handling: the publish build now passes -CreateBinlog and uploads the
  binlog via a dedicated always() step, so signing/Xcode/MSBuild failures leave a
  binlog even when no package output is produced.
- Regression: stop applying the matrix RID to the shared Android args. The
  sideload APK is built universal again (installs on any device/emulator, per the
  README); the RID is applied only to the Google Play AAB.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Reset LASTEXITCODE after expected Git trust probes so valid untrusted full-SHA dry runs do not fail after resolving successfully.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0e65f721-890b-4b33-939b-af632286de45
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0e65f721-890b-4b33-939b-af632286de45
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Validate paired Developer ID assets and required custom variant metadata. Fail TestFlight review conflicts when external distribution was requested, and replace source-text assertions with executable fake-tool regression tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Run the behavioral suite before matrix preparation, pin trusted checkouts to the tested SHA, and upload dedicated iOS ad-hoc and Mac Catalyst Developer ID binlogs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Reject custom matrix variant names that are not safe single path segments before they can reach recursive build-directory cleanup. Cover accepted normalized names and traversal or separator rejection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Build framework, SDK and templates from the same source commit. Restrict MAUI restore to verified local packages and validate resolved package hashes plus archive assembly provenance before exposing app artifacts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep duplicate identity validation while staging only runtime nupkgs and use the existing snupkg symbol format.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Exercise the production entry point without test-only helper injection and verify exact managed payload bytes inside Android assembly wrappers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Override Arcade's default commit omission for non-date-stamped builds, preserving fail-closed payload verification.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 21e641be-a7f9-431e-b067-f011fa79aabf
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 21e641be-a7f9-431e-b067-f011fa79aabf
Keep workload installation and generated app builds on the source-selected SDK instead of different preinstalled feature bands.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 77db17ed-2e47-4ac3-aa4b-cdd72966c34d
@kubaflo

This comment has been minimized.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
3 pipeline(s) were filtered out due to trigger conditions.

This branch was previously deployed

1 inactive (outdated) deployment
copilot-pat-pool — 2a1937fd Deployed Sep 9, 2026 by kubaflo via conclusion #1825
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-infrastructure CI, Maestro / Coherency, upstream dependencies/versions do-not-merge Don't merge this PR s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants