Files
DevOpsExamples/.gitea/workflows/main_build-maui_release.yml
T
Lance McCarthy 82e0ef17e9
ASP.NET AJAX / build_web_app (push) Waiting to run
Angular / build_angular (push) Waiting to run
ASP.NET Core (with Reporting) / build_windows (push) Waiting to run
Blazor (with Reporting) / build_windows (push) Waiting to run
Blazor (with Reporting) / build_linux (push) Waiting to run
Console (.NET) / build_console (arm64, linux) (push) Waiting to run
Console (.NET) / build_console (arm64, win) (push) Waiting to run
Console (.NET) / build_console (x64, linux) (push) Waiting to run
Console (.NET) / build_console (x64, win) (push) Waiting to run
MAUI / Windows Smoketest (push) Waiting to run
MAUI / Android Smoketest (push) Waiting to run
MAUI / iOS Smoketest (push) Waiting to run
MAUI / MacCatalyst Smoketest (push) Waiting to run
WinForms (.NET Framework) / build_desktop (Release, x64) (push) Waiting to run
WinForms (.NET Framework) / build_desktop (Release, x86) (push) Waiting to run
WinUI3 / build-windows (push) Waiting to run
WPF (.NET Framework) / build_desktop (Release, x64) (push) Waiting to run
WPF (.NET Framework) / build_desktop (Release, x86) (push) Waiting to run
ASP.NET Core (with Reporting) - Docker / Microsoft Base - Publish to Docker Hub (push) Waiting to run
ASP.NET Core (with Reporting) - Docker / CentOS Base - Publish to Docker Hub (push) Waiting to run
Blazor (with Reporting) - Docker / Dockerfile Build and Publish (push) Waiting to run
First push
2026-05-21 15:10:03 -04:00

771 lines
36 KiB
YAML

name: MAUI (Distribution)
on:
workflow_dispatch:
defaults:
run:
shell: pwsh
permissions:
actions: write # Needed to delete artifacts after bundle upload
contents: write # Needed to create GitHub Releases
id-token: write # Needed for Azure auth (OIDC)
env:
# ____Global____________________________________________
APP_NAME: "MauiDemo"
PROJECT_PATH: "src/MAUI/MauiDemo.csproj"
PROJECT_DIRECTORY: "src/MAUI"
SDK_VERSION: '10.0.x'
NET_TFM: 'net10.0'
XCODE_VERSION: '26.4'
MAC_DOTNET_VERSION: '10.0.203'
NUGET_CONFIG_PATH: 'src/nuget.config'
TELERIK_NUGET_KEY: ${{secrets.TELERIK_NUGET_KEY}}
TELERIK_LICENSE: ${{secrets.TELERIK_LICENSE_KEY}}
# ____Android___________________________________________
ANDROID_ARTIFACTS_PATH: "artifacts_android"
ANDROID_KEYSTORE_PATH: "${{github.workspace}}/android-upload.keystore"
JAVA_VERSION: '17'
JAVA_DISTRIBUTION: 'microsoft'
# ____MacCatalyst_______________________________________
APPLE_DEV_ID_APP_CERT_NAME: "Developer ID Application: Lancelot Software, LLC (L65255N3F7)"
APPLE_DEV_ID_INSTALLER_CERT_NAME: "Developer ID Installer: Lancelot Software, LLC (L65255N3F7)"
APPLE_NOTARY_TEAM_ID: "L65255N3F7"
MAC_PACKAGE_NAME: "MauiDemo-MacCatalyst"
MAC_APP_BUNDLE_PATH: "src/Maui/bin/Release/net10.0-maccatalyst/MauiDemo.app"
MAC_ARTIFACTS_PATH: "artifacts/maccatalyst"
# ____iOS______________________________________________ (also check matrix vars)
APPLE_APP_ID: "com.lancelotsoftware.MauiDemoApp"
IOS_RID: ios-arm64
# ____Windows__________________________________________ (also check matrix vars)
WINDOWS_ARTIFACTS_PATH: "artifacts_windows"
SERVICE_PRINCIPAL_CLIENT_ID: "32daa13b-f4bb-4809-8ef6-58cb39051acd"
SERVICE_PRINCIPAL_TENANT_ID: "bd47e796-3473-4b8a-9101-1f4c0c7af31a"
SERVICE_PRINCIPAL_SUBSCRIPTION_ID: "48ab4839-62af-4ab3-afe6-043ea4d7c137"
SIGNING_ACCT_NAME: "PrimaryCodeSign"
SIGNING_ACCT_CERT_PROFILE: "lancemccarthylivepublic"
SIGNING_ACCT_ENDPOINT_URL: "https://eus.codesigning.azure.net/"
# ------------------ REQUIRED SECRETS ---------------- #
# ____Global____________________________________________
# TELERIK_NUGET_KEY
# TELERIK_LICENSE_KEY
# ____Android___________________________________________
# ANDROID_SIGNING_KEYSTORE_BASE64
# ANDROID_SIGNING_KEYSTORE_PASS
# ANDROID_SIGNING_KEY_ALIAS
# ANDROID_SIGNING_KEY_PASS
# ____MacCatalyst_______________________________________
# APPLE_DEVELOPER_ID_INSTALLER_CERT_BASE64
# APPLE_DEVELOPER_ID_INSTALLER_CERT_PASSWORD
# APPLE_DEVELOPER_ID_APPLICATION_CERT_BASE64
# APPLE_NOTARY_APPLE_ID
# APPLE_NOTARY_APP_PASSWORD
# ____iOS______________________________________________
# APPLE_DISTRIBUTION_CERT_BASE64
# APPLE_DISTRIBUTION_CERT_PASSWORD
# APPSTORE_API_ISSUER_ID
# APPSTORE_API_KEY_ID
# APPSTORE_API_PRIVATE_KEY
# ____Windows__________________________________________
# No additional secrets needed, uses Azure Trusted Signing.
jobs:
# ********************************************************************************** #
# Shared Resources #
# ********************************************************************************** #
shared-resources:
name: Create Shared Resources
runs-on: windows-latest
outputs:
app_version: ${{steps.version-creator.outputs.app_version}}
steps:
# Generates a version number using year.monthday.run_number (e.g., 2024.824.1)
- name: Generate version number using date and run number
id: version-creator
shell: pwsh
run: |
$buildDay = Get-Date -Format "yyyy.Mdd"
$runNumber = "$env:GITHUB_RUN_NUMBER"
$ver = $buildDay + "." + $runNumber
echo "app_version=$ver" >> $env:GITHUB_OUTPUT
# ********************************************************************************** #
# Android #
# ********************************************************************************** #
android:
name: Build Android (Store Upload)
runs-on: windows-latest
needs: shared-resources
if: ${{ success() && needs.shared-resources.outputs.app_version != '' }}
steps:
- uses: actions/checkout@v6
- name: Decode the Keystore
shell: pwsh
run: |
$file_bytes = [System.Convert]::FromBase64String("${{secrets.ANDROID_SIGNING_KEYSTORE_BASE64}}")
[IO.File]::WriteAllBytes("${{env.ANDROID_KEYSTORE_PATH}}", $file_bytes)
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{env.SDK_VERSION}}
- uses: actions/setup-java@v5
with:
distribution: ${{env.JAVA_DISTRIBUTION}}
java-version: ${{env.JAVA_VERSION}}
- name: Install MAUI Workloads
run: dotnet workload install maui --source https://api.nuget.org/v3/index.json
- name: Set Telerik NuGet Credentials
run: dotnet nuget update source 'Telerik_v3_Feed' -s 'https://nuget.telerik.com/v3/index.json' -u 'api-key' -p "${{secrets.TELERIK_NUGET_KEY}}" --configfile '${{env.NUGET_CONFIG_PATH}}' --store-password-in-clear-text
- name: Restore NuGet packages
run: dotnet restore ${{env.PROJECT_PATH}} --configfile ${{env.NUGET_CONFIG_PATH}}
- name: Publish MAUI Android
run: |
dotnet publish ${{env.PROJECT_PATH}} -c Release -f ${{env.NET_TFM}}-android /p:AndroidKeyStore=true /p:AndroidSigningKeyStore=${{env.ANDROID_KEYSTORE_PATH}} /p:AndroidSigningStorePass=${{secrets.ANDROID_SIGNING_KEYSTORE_PASS}} /p:AndroidSigningKeyAlias=${{secrets.ANDROID_SIGNING_KEY_ALIAS}} /p:AndroidSigningKeyPass=${{secrets.ANDROID_SIGNING_KEY_PASS}}
- name: Create artifacts folder
run: |
New-Item -ItemType Directory -Force -Path ${{env.ANDROID_ARTIFACTS_PATH}} | Out-Null
- name: Copy signed APKs & AABs
run: |
$publishRoot = "src/Maui/bin/Release/${{env.NET_TFM}}-android"
$apkFiles = Get-ChildItem -Path $publishRoot -Filter *-Signed.apk -File -Recurse -ErrorAction SilentlyContinue
$aabFiles = Get-ChildItem -Path $publishRoot -Filter *-Signed.aab -File -Recurse -ErrorAction SilentlyContinue
if ($apkFiles.Count -eq 0 -and $aabFiles.Count -eq 0) {
throw "No signed Android APK/AAB files found under $publishRoot"
}
$apkFiles | ForEach-Object { Copy-Item -Path $_.FullName -Destination ${{env.ANDROID_ARTIFACTS_PATH}} -Force }
$aabFiles | ForEach-Object { Copy-Item -Path $_.FullName -Destination ${{env.ANDROID_ARTIFACTS_PATH}} -Force }
- name: Publish Android build artifacts
uses: actions/upload-artifact@v7
with:
name: "${{env.APP_NAME}}_v${{needs.shared-resources.outputs.app_version}}_signed-android"
path: "${{env.ANDROID_ARTIFACTS_PATH}}/*"
if-no-files-found: error
retention-days: 60
# ********************************************************************************** #
# Windows (Sideload - msixbundle) #
# ********************************************************************************** #
windows-sideload-packages:
name: Build Windows (Sideload)
needs: shared-resources
runs-on: windows-latest
if: ${{ needs.shared-resources.outputs.app_version != ''}}
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runtime_id: win-x64
platform: x64
- arch: arm64
runtime_id: win-arm64
platform: ARM64
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{env.SDK_VERSION}}
# Needed only for WinUI builds
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v3
- name: Install MAUI workloads
run: dotnet workload install maui --source https://api.nuget.org/v3/index.json
- name: Set Telerik NuGet Credentials
run: dotnet nuget update source 'Telerik_v3_Feed' -s 'https://nuget.telerik.com/v3/index.json' -u 'api-key' -p "${{secrets.TELERIK_NUGET_KEY}}" --configfile '${{env.NUGET_CONFIG_PATH}}' --store-password-in-clear-text
- name: Restore NuGet packages
run: dotnet restore ${{env.PROJECT_PATH}} --configfile ${{env.NUGET_CONFIG_PATH}}
- name: Update manifest for sideload build
run: |
[xml]$manifest = Get-Content '${{env.PROJECT_DIRECTORY}}\Platforms\Windows\Package.appxmanifest'
$manifest.Package.Identity.Version = "${{needs.shared-resources.outputs.app_version}}.0"
$manifest.Save('${{env.PROJECT_DIRECTORY}}\Platforms\Windows\Package.appxmanifest')
- name: Publish ${{matrix.arch}} Windows package
id: publish-package
run: |
$appVersion = "${{needs.shared-resources.outputs.app_version}}.0"
$artifactDir = "${{github.workspace}}\${{env.WINDOWS_ARTIFACTS_PATH}}\${{matrix.arch}}"
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
Write-Host "Publishing Windows ${{matrix.arch}}..."
$publishArgs = @(
'publish'
'${{env.PROJECT_PATH}}'
'-c', 'Release'
'-f', '${{env.NET_TFM}}-windows10.0.19041.0'
'-p:Platform=${{matrix.platform}}'
'-p:AppxPackageBuildPlatform=${{matrix.platform}}'
'-p:RuntimeIdentifierOverride=${{matrix.runtime_id}}'
'-p:GenerateAppxPackageOnBuild=true'
'-p:AppxPackageSigningEnabled=false'
'-p:UapAppxPackageBuildMode=SideloadOnly'
'-p:AppxBundle=Never'
'-p:WindowsPackageType=MSIX'
)
& dotnet @publishArgs
if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed for ${{matrix.arch}} with exit code $LASTEXITCODE" }
# MAUI writes MSIX to src\${{env.APP_NAME}}\AppPackages\<name>_<ver>_<arch>_Test\*.msix
$appPackagesRoot = "src\${{env.APP_NAME}}\AppPackages"
$candidates = Get-ChildItem -Path $appPackagesRoot -Filter *.msix -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch '\\Dependencies\\' -and $_.Name -match "_${{matrix.arch}}\.msix$" }
Write-Host "Found $($candidates.Count) candidate MSIX file(s) for ${{matrix.arch}}:"
$candidates | ForEach-Object { Write-Host " $($_.FullName)" }
$msix = $candidates | Sort-Object LastWriteTime -Descending | Select-Object -ExpandProperty FullName -First 1
if (-not $msix) { throw "No .msix found after publish for ${{matrix.arch}}" }
Write-Host "Found ${{matrix.arch}} package: $msix"
Copy-Item -Path $msix -Destination "$artifactDir\${{env.APP_NAME}}.${{matrix.arch}}.msix" -Force
echo "PACKAGE_PATH=$artifactDir\${{env.APP_NAME}}.${{matrix.arch}}.msix" >> $env:GITHUB_OUTPUT
- name: Upload ${{matrix.arch}} artifact
uses: actions/upload-artifact@v7
with:
name: "${{env.APP_NAME}}_v${{needs.shared-resources.outputs.app_version}}_${{matrix.runtime_id}}"
path: ${{steps.publish-package.outputs.PACKAGE_PATH}}
if-no-files-found: error
retention-days: 30
windows-generate-msixbundle:
name: Bundle MSIX packages
needs: [shared-resources, windows-sideload-packages]
runs-on: windows-latest
steps:
- name: Clean bundle workspace
run: |
Remove-Item -Recurse -Force "${{github.workspace}}\bundle-input" -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force "${{github.workspace}}\bundle-output" -ErrorAction SilentlyContinue
- name: Download x64 artifact
uses: actions/download-artifact@v8
with:
name: "${{env.APP_NAME}}_v${{needs.shared-resources.outputs.app_version}}_win-x64"
path: ${{github.workspace}}\bundle-input
- name: Download arm64 artifact
uses: actions/download-artifact@v8
with:
name: "${{env.APP_NAME}}_v${{needs.shared-resources.outputs.app_version}}_win-arm64"
path: ${{github.workspace}}\bundle-input
- name: Create MSIX bundle
id: bundle
run: |
$appVersion = "${{needs.shared-resources.outputs.app_version}}.0"
$inputDir = "${{github.workspace}}\bundle-input"
$outputDir = "${{github.workspace}}\bundle-output"
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
# Locate makeappx.exe from the latest installed Windows 10 SDK
$sdkRoot = "C:\Program Files (x86)\Windows Kits\10\bin"
$makeAppx = Get-ChildItem -Path $sdkRoot -Recurse -Filter makeappx.exe -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match '\\x64\\makeappx\.exe$' } |
Sort-Object FullName -Descending | Select-Object -First 1
if (-not $makeAppx) { throw "makeappx.exe not found under $sdkRoot" }
Write-Host "Using makeappx: $($makeAppx.FullName)"
Get-ChildItem $inputDir -Filter *.msix | ForEach-Object { Write-Host "Input: $($_.FullName)" }
$bundlePath = "$outputDir\${{env.APP_NAME}}_v${{needs.shared-resources.outputs.app_version}}.msixbundle"
& $makeAppx.FullName bundle /d $inputDir /p $bundlePath /bv $appVersion /o
if ($LASTEXITCODE -ne 0) { throw "makeappx bundle failed with exit code $LASTEXITCODE" }
echo "BUNDLE_PATH=$bundlePath" >> $env:GITHUB_OUTPUT
# Entra ID App Registration (Akeyless OIDC Provider) > Certificates and Secrets > Federated Credentials
- name: Azure login using OIDC via GitHub
uses: azure/login@v3
id: azlogin
with:
client-id: ${{env.SERVICE_PRINCIPAL_CLIENT_ID}}
tenant-id: ${{env.SERVICE_PRINCIPAL_TENANT_ID}}
subscription-id: ${{env.SERVICE_PRINCIPAL_SUBSCRIPTION_ID}}
- name: Sign MSIX bundle
uses: azure/trusted-signing-action@v1.2.0
with:
endpoint: ${{env.SIGNING_ACCT_ENDPOINT_URL}}
signing-account-name: ${{env.SIGNING_ACCT_NAME}}
certificate-profile-name: ${{env.SIGNING_ACCT_CERT_PROFILE}}
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
file-digest: SHA256
files: ${{steps.bundle.outputs.BUNDLE_PATH}}
exclude-azure-cli-credential: false
exclude-environment-credential: true
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Upload MSIX bundle artifact
uses: actions/upload-artifact@v7
with:
name: "${{env.APP_NAME}}_v${{needs.shared-resources.outputs.app_version}}_signed-windows.msixbundle"
path: ${{steps.bundle.outputs.BUNDLE_PATH}}
if-no-files-found: error
retention-days: 30
# ********************************************************************************** #
# Windows (Store - msixupload) #
# ********************************************************************************** #
windows-store:
name: Build Windows (Store Upload)
runs-on: windows-latest
needs: shared-resources
if: ${{ success() && needs.shared-resources.outputs.app_version != '' }}
steps:
- uses: actions/checkout@v6
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{env.SDK_VERSION}}
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v3
- name: Install MAUI Workloads
run: dotnet workload install maui --source https://api.nuget.org/v3/index.json
- name: Set Telerik NuGet Credentials
run: dotnet nuget update source 'Telerik_v3_Feed' -s 'https://nuget.telerik.com/v3/index.json' -u 'api-key' -p "${{secrets.TELERIK_NUGET_KEY}}" --configfile '${{env.NUGET_CONFIG_PATH}}' --store-password-in-clear-text
- name: Restore NuGet packages
run: dotnet restore ${{env.PROJECT_PATH}} --configfile ${{env.NUGET_CONFIG_PATH}}
- name: Update manifest for store build
run: |
[xml]$manifest = Get-Content '${{env.PROJECT_DIRECTORY}}\Platforms\Windows\Package.appxmanifest'
$manifest.Package.Identity.Version = "${{needs.shared-resources.outputs.app_version}}.0"
$manifest.Save('${{env.PROJECT_DIRECTORY}}\Platforms\Windows\Package.appxmanifest')
- name: Build Maui WinUI project
run: dotnet publish ${{env.PROJECT_PATH}} -c Release -f ${{env.NET_TFM}}-windows10.0.19041.0 -p:AppxPackageSigningEnabled=false -p:AppxSymbolPackageEnabled=false -p:UapAppxPackageBuildMode=StoreUpload -p:AppxBundle=Always "-p:AppxBundlePlatforms=x64|arm64" -p:PlatformTarget=AnyCPU
- name: Publish build artifacts
uses: actions/upload-artifact@v7
with:
name: "${{env.APP_NAME}}_v${{needs.shared-resources.outputs.app_version}}_storeupload-windows"
path: "**/*.msixupload"
if-no-files-found: error
retention-days: 60
# ********************************************************************************** #
# MacCatalyst #
# ********************************************************************************** #
maccatalyst:
name: Build MacCatalyst (Signed & Notiarized)
runs-on: macos-26 # https://github.com/actions/runner-images#available-images
needs: shared-resources
if: ${{ success() && needs.shared-resources.outputs.app_version != '' }}
steps:
- uses: actions/checkout@v6
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{env.XCODE_VERSION}}
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{env.SDK_VERSION}}
- name: Install MAUI Workloads
run: dotnet workload install maui --source https://api.nuget.org/v3/index.json
- name: Set Telerik NuGet Credentials
run: dotnet nuget update source 'Telerik_v3_Feed' -s 'https://nuget.telerik.com/v3/index.json' -u 'api-key' -p "${{secrets.TELERIK_NUGET_KEY}}" --configfile '${{env.NUGET_CONFIG_PATH}}' --store-password-in-clear-text
- name: Restore NuGet packages
run: dotnet restore ${{env.PROJECT_PATH}} --configfile ${{env.NUGET_CONFIG_PATH}}
- name: Import Developer ID Installer Certificates
uses: Apple-Actions/import-codesign-certs@v7
with:
p12-file-base64: "${{secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT_BASE64}}"
p12-password: "${{secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT_PASSWORD}}"
keychain: installer_signing_temp
- name: Import Developer ID Application Certificates
uses: Apple-Actions/import-codesign-certs@v7
with:
p12-file-base64: "${{secrets.APPLE_DEVELOPER_ID_APPLICATION_CERT_BASE64}}"
p12-password: "${{secrets.APPLE_DEVELOPER_ID_APPLICATION_CERT_PASSWORD}}"
keychain: application_signing_temp
# Each import-codesign-certs call rewrites the keychain search list. The second import step (Application cert) drops the first (Installer) keychain, so product build cannot find the "Developer ID Installer" identity. This step re-adds both keychains explicitly before any signing occurs.
- name: Ensure both signing keychains are in the search list
shell: bash
run: |
set -euo pipefail
security list-keychains -d user -s "$HOME/Library/Keychains/installer_signing_temp.keychain-db" "$HOME/Library/Keychains/application_signing_temp.keychain-db" "$HOME/Library/Keychains/login.keychain-db"
echo "Keychain search list:"
security list-keychains -d user
# Finally make sure both certs are available before we build
- name: Verify Developer ID identities are available
shell: bash
run: |
set -euo pipefail
APPLE_DEV_ID_APP_CERT_NAME="${{env.APPLE_DEV_ID_APP_CERT_NAME}}"
APPLE_DEV_ID_INSTALLER_CERT_NAME="${{env.APPLE_DEV_ID_INSTALLER_CERT_NAME}}"
security find-identity -v -p codesigning
security find-certificate -a -c "$APPLE_DEV_ID_INSTALLER_CERT_NAME" || true
if ! security find-identity -v -p codesigning | grep -F "$APPLE_DEV_ID_APP_CERT_NAME" >/dev/null; then
echo "Missing Developer ID Application identity: $APPLE_DEV_ID_APP_CERT_NAME"
exit 1
fi
if ! security find-certificate -a -c "$APPLE_DEV_ID_INSTALLER_CERT_NAME" >/dev/null; then
echo "Missing Developer ID Installer certificate: $APPLE_DEV_ID_INSTALLER_CERT_NAME"
exit 1
fi
- name: Build and sign MacCatalyst artifacts
shell: bash
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE"
if [[ ! -f "$PROJECT_PATH" ]]; then
echo "Project file not found: $PROJECT_PATH"
echo "Current directory: $(pwd)"
exit 1
fi
mkdir -p "$ARTIFACTS_DIR"
echo "Cleaning project..."
dotnet clean "$PROJECT_PATH" -c "$CONFIGURATION" -f "$TFM"
publish_args=(
"$PROJECT_PATH"
-c "$CONFIGURATION"
-f "$TFM"
"-p:CodesignKey=\"$CODESIGN_KEY\""
"-p:ApplicationVersion=$APP_VERSION"
"-p:ApplicationDisplayVersion=$APP_VERSION"
-p:UseHardenedRuntime=true
)
echo "Publishing project..."
dotnet publish "${publish_args[@]}"
if [[ ! -d "$APP_BUNDLE_PATH" ]]; then
echo "Expected app bundle not found at $APP_BUNDLE_PATH"
exit 1
fi
echo "Verifying app signature..."
codesign -dv --verbose=2 "$APP_BUNDLE_PATH" >/dev/null 2>&1
echo "Creating signed app zip and installer pkg..."
rm -f "$SIGNED_ZIP_PATH" "$SIGNED_PKG_PATH"
ditto -c -k --sequesterRsrc --keepParent "$APP_BUNDLE_PATH" "$SIGNED_ZIP_PATH"
productbuild --component "$APP_BUNDLE_PATH" /Applications --sign "$INSTALLER_SIGN_ID" "$SIGNED_PKG_PATH"
echo "Done. Signed artifacts:"
echo "- $SIGNED_PKG_PATH"
echo "- $SIGNED_ZIP_PATH"
env:
PROJECT_PATH: "${{env.PROJECT_PATH}}"
TFM: "${{env.NET_TFM}}-maccatalyst"
CONFIGURATION: "Release"
APP_VERSION: "${{needs.shared-resources.outputs.app_version}}"
ARTIFACTS_DIR: "${{env.MAC_ARTIFACTS_PATH}}"
APP_BUNDLE_PATH: "${{env.MAC_APP_BUNDLE_PATH}}"
CODESIGN_KEY: "${{env.APPLE_DEV_ID_APP_CERT_NAME}}"
INSTALLER_SIGN_ID: "${{ env.APPLE_DEV_ID_INSTALLER_CERT_NAME}}"
SIGNED_ZIP_PATH: "$ARTIFACTS_DIR/${{env.MAC_PACKAGE_NAME}}-Release-signed.zip"
SIGNED_PKG_PATH: "$ARTIFACTS_DIR/${{env.MAC_PACKAGE_NAME}}-Release-signed.pkg"
- name: Notarize and staple MacCatalyst artifacts
shell: bash
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE"
if [[ -z "$APPLE_NOTARY_APPLE_ID" || -z "$APPLE_NOTARY_APP_PASSWORD" || -z "$APPLE_NOTARY_TEAM_ID" ]]; then
echo "Missing notarization credentials."
echo "Set APPLE_NOTARY_APPLE_ID, APPLE_NOTARY_APP_PASSWORD, and APPLE_NOTARY_TEAM_ID."
exit 1
fi
if [[ ! -d "$APP_BUNDLE_PATH" ]]; then
echo "Expected app bundle not found at $APP_BUNDLE_PATH"
exit 1
fi
if [[ ! -f "$SIGNED_PKG_PATH" || ! -f "$SIGNED_ZIP_PATH" ]]; then
echo "Expected signed artifacts not found in $ARTIFACTS_DIR"
exit 1
fi
echo "Submitting pkg for notarization..."
pkg_submit_json="$(xcrun notarytool submit "$SIGNED_PKG_PATH" --apple-id "$APPLE_NOTARY_APPLE_ID" --team-id "$APPLE_NOTARY_TEAM_ID" --password "$APPLE_NOTARY_APP_PASSWORD" --wait --output-format json)"
echo "$pkg_submit_json"
if ! grep -Eq '"status"[[:space:]]*:[[:space:]]*"Accepted"' <<< "$pkg_submit_json"; then
echo "Notarization failed for pkg."
exit 1
fi
echo "Stapling and validating pkg..."
xcrun stapler staple "$SIGNED_PKG_PATH"
xcrun stapler validate "$SIGNED_PKG_PATH"
echo "Submitting signed app zip for notarization..."
zip_submit_json="$(xcrun notarytool submit "$SIGNED_ZIP_PATH" --apple-id "$APPLE_NOTARY_APPLE_ID" --team-id "$APPLE_NOTARY_TEAM_ID" --password "$APPLE_NOTARY_APP_PASSWORD" --wait --output-format json)"
echo "$zip_submit_json"
if ! grep -Eq '"status"[[:space:]]*:[[:space:]]*"Accepted"' <<< "$zip_submit_json"; then
echo "Notarization failed for app zip."
exit 1
fi
echo "Stapling and validating app..."
xcrun stapler staple "$APP_BUNDLE_PATH"
xcrun stapler validate "$APP_BUNDLE_PATH"
# Recreate the final distributable zip from the stapled app.
ditto -c -k --sequesterRsrc --keepParent "$APP_BUNDLE_PATH" "$NOTARIZED_ZIP_PATH"
echo "Gatekeeper checks..."
spctl -a -t exec -vv "$APP_BUNDLE_PATH"
spctl -a -t install -vv "$SIGNED_PKG_PATH"
echo "Done. Artifacts:"
echo "- $SIGNED_PKG_PATH"
echo "- $NOTARIZED_ZIP_PATH"
env:
ARTIFACTS_DIR: "${{env.MAC_ARTIFACTS_PATH}}"
APP_BUNDLE_PATH: "${{env.MAC_APP_BUNDLE_PATH}}"
APPLE_NOTARY_APPLE_ID: "${{secrets.APPLE_NOTARY_APPLE_ID}}"
APPLE_NOTARY_APP_PASSWORD: "${{secrets.APPLE_NOTARY_APP_PASSWORD}}"
APPLE_NOTARY_TEAM_ID: "${{env.APPLE_NOTARY_TEAM_ID}}"
SIGNED_ZIP_PATH: "${{env.MAC_ARTIFACTS_PATH}}/${{env.MAC_PACKAGE_NAME}}-Release-signed.zip"
NOTARIZED_ZIP_PATH: "${{env.MAC_ARTIFACTS_PATH}}/${{env.MAC_PACKAGE_NAME}}-Release-notarized.zip"
SIGNED_PKG_PATH: "${{env.MAC_ARTIFACTS_PATH}}/${{env.MAC_PACKAGE_NAME}}-Release-signed.pkg"
- name: Publish MacCatalyst build artifacts
uses: actions/upload-artifact@v7
with:
name: "${{env.APP_NAME}}_v${{needs.shared-resources.outputs.app_version}}_signed-maccatalyst"
path: ${{env.MAC_ARTIFACTS_PATH}}/${{env.MAC_PACKAGE_NAME}}-Release-signed.pkg
if-no-files-found: error
retention-days: 30
# ********************************************************************************** #
# iOS #
# ********************************************************************************** #
ios:
name: Build iOS (${{matrix.distribution_name}})
runs-on: macos-26 # https://github.com/actions/runner-images#available-images
needs: shared-resources
if: ${{ success() && needs.shared-resources.outputs.app_version != '' }}
strategy:
fail-fast: false
matrix:
include:
- distribution_name: Store Upload IPA
artifact_suffix: storeupload-ios
provisioning_profile: MauiDemo_AppStore_Distribution
provisioning_profile_type: IOS_APP_STORE
rid: ios-arm64
- distribution_name: Ad Hoc Sideload IPA
artifact_suffix: sideload-ios
provisioning_profile: MauiDemo_AdHoc_Distribution
provisioning_profile_type: IOS_APP_ADHOC
rid: ios-arm64
env:
APPLE_PROV_PROFILE: ${{matrix.provisioning_profile}}
APPLE_PROV_PROFILE_TYPE: ${{matrix.provisioning_profile_type}}
steps:
- uses: actions/checkout@v6
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{env.XCODE_VERSION}}
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{env.SDK_VERSION}}
- name: Install MAUI Workloads
run: dotnet workload install maui --source https://api.nuget.org/v3/index.json
- name: Set Telerik NuGet Credentials
run: dotnet nuget update source 'Telerik_v3_Feed' -s 'https://nuget.telerik.com/v3/index.json' -u 'api-key' -p "${{secrets.TELERIK_NUGET_KEY}}" --configfile '${{env.NUGET_CONFIG_PATH}}' --store-password-in-clear-text
- name: Restore NuGet packages
run: dotnet restore ${{env.PROJECT_PATH}} --configfile ${{env.NUGET_CONFIG_PATH}}
# Docs https://github.com/Apple-Actions/import-codesign-certs
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@v7
with:
p12-file-base64: "${{secrets.APPLE_DISTRIBUTION_CERT_BASE64}}"
p12-password: "${{secrets.APPLE_DISTRIBUTION_CERT_PASSWORD}}"
# Docs https://github.com/Apple-Actions/download-provisioning-profiles
- id: provisioning-profiles
uses: Apple-Actions/download-provisioning-profiles@v6
with:
profile-type: "${{env.APPLE_PROV_PROFILE_TYPE}}"
bundle-id: "${{env.APPLE_APP_ID}}"
issuer-id: "${{secrets.APPSTORE_API_ISSUER_ID}}"
api-key-id: "${{secrets.APPSTORE_API_KEY_ID}}"
api-private-key: "${{secrets.APPSTORE_API_PRIVATE_KEY}}"
- name: Verify provisioning profile
run: |
$profiles = '${{steps.provisioning-profiles.outputs.profiles}}' | ConvertFrom-Json
$profile = $profiles | Where-Object { $_.name -eq $env:APPLE_PROV_PROFILE -and $_.type -eq $env:APPLE_PROV_PROFILE_TYPE } | Select-Object -First 1
if ($null -eq $profile) {
$profiles | Format-Table -AutoSize | Out-String | Write-Host
throw "Provisioning profile '$env:APPLE_PROV_PROFILE' with type '$env:APPLE_PROV_PROFILE_TYPE' was not downloaded."
}
- name: Verify iOS signing identity is available
shell: bash
run: |
set -euo pipefail
security find-identity -v -p codesigning
if ! security find-identity -v -p codesigning | grep -F "Apple Distribution" >/dev/null; then
echo "Missing Apple Distribution identity"
exit 1
fi
# Docs https://learn.microsoft.com/en-us/dotnet/maui/ios/deployment/publish-cli?view=net-maui-9.0
- name: Publish MAUI iOS IPA
run: |
# Query the actual signing identity to avoid comma parsing issues
$identityOutput = security find-identity -v -p codesigning | Select-String "Apple Distribution"
if (-not $identityOutput) { throw "No Apple Distribution identity found in keychain" }
$codesignKey = [regex]::Match($identityOutput.Line, '"(?<name>Apple Distribution:[^"]+)"').Groups['name'].Value
if (-not $codesignKey) { throw "Could not parse Apple Distribution identity from keychain output" }
Write-Host "Using codesign key: $codesignKey"
$quotedCodesignKey = '"' + $codesignKey + '"'
$publishArgs = @(
'publish'
'${{env.PROJECT_PATH}}'
'-f', '${{env.NET_TFM}}-ios'
'-c', 'Release'
'-p:ArchiveOnBuild=true'
'-p:RuntimeIdentifier=${{matrix.rid}}'
'-p:MtouchLink=SdkOnly'
'-p:ApplicationId=${{env.APPLE_APP_ID}}'
'-p:ApplicationVersion=${{needs.shared-resources.outputs.app_version}}'
'-p:CodesignProvision=${{env.APPLE_PROV_PROFILE}}'
"-p:CodesignKey=$quotedCodesignKey"
)
& dotnet @publishArgs
if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed with exit code $LASTEXITCODE" }
- name: Publish iOS build artifacts
uses: actions/upload-artifact@v7
with:
name: "${{env.APP_NAME}}_v${{needs.shared-resources.outputs.app_version}}_${{matrix.artifact_suffix}}"
path: "${{env.PROJECT_DIRECTORY}}/bin/Release/${{env.NET_TFM}}-ios/${{matrix.rid}}/publish/*.ipa"
if-no-files-found: error
retention-days: 60
# ********************************************************************************** #
# GitHub Release #
# ********************************************************************************** #
# create-release:
# name: Create GitHub Release
# runs-on: ubuntu-latest
# needs: [shared-resources, android, windows-sideload-packages, windows-generate-msixbundle, windows-store, maccatalyst, ios]
# steps:
# - name: Download all artifacts
# uses: actions/download-artifact@v8
# with:
# path: release-artifacts
# - name: List downloaded artifacts
# shell: bash
# run: find release-artifacts -type f | sort
# - name: Prepare release files
# shell: bash
# run: |
# set -euo pipefail
# VER="${{needs.shared-resources.outputs.app_version}}"
# PREFIX="${{env.APP_NAME}}_v${VER}"
# mkdir -p release-upload
# copy_one() {
# local artifact_dir="$1"
# local pattern="$2"
# local destination="$3"
# local -a matches=()
# if [[ ! -d "$artifact_dir" ]]; then
# echo "Expected artifact directory not found: $artifact_dir" >&2
# exit 1
# fi
# mapfile -d '' matches < <(find "$artifact_dir" -type f -name "$pattern" -print0 | sort -z)
# if [[ ${#matches[@]} -ne 1 ]]; then
# echo "Expected exactly one match for '$pattern' under '$artifact_dir', found ${#matches[@]}." >&2
# find "$artifact_dir" -type f | sort >&2
# exit 1
# fi
# cp -- "${matches[0]}" "$destination"
# }
# # Android
# copy_one "release-artifacts/${PREFIX}_signed-android" "*-Signed.apk" "release-upload/${PREFIX}_android-signed.apk"
# copy_one "release-artifacts/${PREFIX}_signed-android" "*-Signed.aab" "release-upload/${PREFIX}_android-signed.aab"
# # Windows msixbundle (signed)
# copy_one "release-artifacts/${PREFIX}_signed-windows.msixbundle" "*.msixbundle" "release-upload/${PREFIX}_windows.msixbundle"
# # Windows Store
# copy_one "release-artifacts/${PREFIX}_storeupload-windows" "*.msixupload" "release-upload/${PREFIX}_msstore.msixupload"
# # MacCatalyst
# copy_one "release-artifacts/${PREFIX}_signed-maccatalyst" "*.pkg" "release-upload/${PREFIX}_mac.pkg"
# # iOS
# copy_one "release-artifacts/${PREFIX}_ios-adhoc" "*.ipa" "release-upload/${PREFIX}_ios-adhoc.ipa"
# copy_one "release-artifacts/${PREFIX}_ios-store" "*.ipa" "release-upload/${PREFIX}_ios-store.ipa"
# echo "Files prepared for release:"
# ls -lh release-upload/
# - name: Create GitHub Release
# uses: softprops/action-gh-release@v3.0.0
# with:
# tag_name: "v${{needs.shared-resources.outputs.app_version}}"
# name: "${{env.APP_NAME}} v${{needs.shared-resources.outputs.app_version}}"
# draft: false
# prerelease: false
# generate_release_notes: true
# files: release-upload/*