| name | dotnet-maui-cicd |
| description | GitHub Actions CI/CD patterns for .NET MAUI Windows applications with MSIX packaging |
| domain | ci-cd, deployment, windows-store |
| confidence | low |
| source | earned |
Context
When building CI/CD pipelines for .NET MAUI Windows applications destined for the Windows Store, you need to handle:
- .NET preview versions (e.g., .NET 10)
- MAUI workload installation
- MSIX package generation
- Code signing for store submission
- Multi-tier workflow strategy (PR validation, CI, releases)
This skill captures patterns for GitHub Actions workflows that handle these requirements efficiently.
Patterns
1. Three-Tier Workflow Strategy
PR Validation - Fast feedback loop
- Build + test only
- No packaging or signing
- Target time: 5-8 minutes
- Fail fast on test failures
Continuous Integration - Extended validation
- Build + test + code coverage
- Artifact retention for debugging
- Run on main branch only
- Target time: 6-10 minutes
Release - Production packaging
- Build + test + MSIX packaging
- Code signing with certificates
- Artifact retention for store submission
- Triggered by Git tags or manual dispatch
- Target time: 10-15 minutes
2. .NET Preview Setup
For preview .NET versions (like .NET 10), use:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
dotnet-quality: 'preview'
3. MAUI Workload Installation
MSIX packaging requires MAUI workloads:
- name: Install MAUI Workloads
run: dotnet workload install maui
Only needed in release workflow, not PR validation (speeds up PR builds).
4. Conditional MSIX Packaging
Use MSBuild property to control packaging:
- name: Build solution
run: dotnet build --configuration Release /p:WindowsPackageType=None
- name: Build MSIX Package
run: dotnet publish -c Release -f net10.0-windows10.0.19041.0 /p:WindowsPackageType=MSIX
5. Secure Code Signing
Store certificate as Base64, decode at runtime, clean up after:
- name: Decode signing certificate
if: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 != '' }}
shell: pwsh
run: |
$certBytes = [Convert]::FromBase64String("${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}")
$certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx"
[IO.File]::WriteAllBytes($certPath, $certBytes)
echo "CERTIFICATE_PATH=$certPath" >> $env:GITHUB_ENV
- name: Build with signing
run: |
dotnet publish /p:PackageCertificateKeyFile="$env:CERTIFICATE_PATH" \
/p:PackageCertificatePassword="${{ secrets.CERTIFICATE_PASSWORD }}"
- name: Cleanup certificate
if: always() && env.CERTIFICATE_PATH != ''
shell: pwsh
run: |
if (Test-Path $env:CERTIFICATE_PATH) {
Remove-Item $env:CERTIFICATE_PATH -Force
}
Key points:
- Use
$env:RUNNER_TEMP (auto-cleaned by GitHub)
- Conditional check prevents failure when cert not configured
- Always cleanup in finally-style step
- Never commit certificates to repository
6. Version from Git Tags
Extract version from Git tags or workflow inputs:
- name: Determine Version
id: version
shell: pwsh
run: |
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
$version = "${{ github.event.inputs.version }}"
} elseif ("${{ github.ref }}" -like "refs/tags/v*") {
$version = "${{ github.ref }}".Replace("refs/tags/v", "")
} else {
$version = "1.0.0"
}
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
- name: Use version
run: dotnet publish /p:ApplicationVersion=${{ steps.version.outputs.VERSION }}.0
7. Test Execution with Reporting
Use TRX format and upload for visualization:
- name: Run tests
run: dotnet test --logger "trx;LogFileName=test-results.trx" --collect:"XPlat Code Coverage"
- name: Publish test results
uses: dorny/test-reporter@v1
if: always()
with:
name: Test Results
path: '**/test-results.trx'
reporter: dotnet-trx
fail-on-error: true
8. Tiered Artifact Retention
Different retention for different artifact types:
- uses: actions/upload-artifact@v4
with:
retention-days: 7
- uses: actions/upload-artifact@v4
with:
retention-days: 14
- uses: actions/upload-artifact@v4
with:
retention-days: 90
9. Path Ignore Patterns
Skip CI for documentation changes:
on:
pull_request:
paths-ignore:
- '**.md'
- 'docs/**'
- '.ai-team/**'
- 'LICENSE'
Examples
Complete PR Validation Workflow
name: PR Validation
on:
pull_request:
branches: [ main ]
paths-ignore: ['**.md', 'docs/**']
jobs:
build-and-test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
dotnet-quality: 'preview'
- run: dotnet restore
- run: dotnet build --configuration Release --no-restore /p:WindowsPackageType=None
- run: dotnet test --configuration Release --no-build --logger "trx"
- uses: dorny/test-reporter@v1
if: always()
with:
name: Test Results
path: '**/test-results.trx'
reporter: dotnet-trx
Complete Release Workflow
See .github/workflows/release.yml in VideoSplitter project for full example.
Anti-Patterns
❌ Don't build MSIX on every PR
- Slow (adds MAUI workload installation time)
- Unnecessary (not testing packaging in PRs)
- Use WindowsPackageType=None for PR/CI builds
❌ Don't store certificates in repository
- Security risk
- Use GitHub Secrets with Base64 encoding
- Decode at runtime, clean up after
❌ Don't hardcode versions
- Use Git tags as source of truth
- Allow manual override with workflow_dispatch
- Inject version at build time
❌ Don't skip tests in release builds
- Always run tests before packaging
- Fail the release if tests fail
- Test results = quality gate
❌ Don't use same retention for all artifacts
- PR artifacts: 7 days (debug only)
- CI artifacts: 14 days (troubleshooting)
- Release packages: 90 days (store submission)
❌ Don't forget to clean up sensitive files
- Use
if: always() for cleanup steps
- Delete certificates after use
- Use $env:RUNNER_TEMP for temp files
Related Skills
dotnet-testing-setup - Setting up xUnit test infrastructure
testing-external-dependencies - Mocking for testable code