.NET MAUI CI/CD自动化部署完全指南:用GitHub Actions搞定iOS与Android自动构建发布

手把手教你用GitHub Actions为.NET MAUI应用搭建完整CI/CD流水线,覆盖Android密钥库签名与Google Play发布、iOS证书配置与TestFlight上传、版本号自动管理、构建成本优化等实战内容。

为什么.NET MAUI项目需要CI/CD流水线

如果你手动构建和发布过.NET MAUI应用,你大概率知道那种痛苦——每次发版要在不同平台上分别编译、签名、打包,然后手动上传到Google Play Console和App Store Connect。说实话,干过一两次之后就会想:能不能让机器自动帮我搞定这些?

答案当然是可以的。CI/CD(持续集成/持续部署)流水线就是干这个的。

自动化这些重复步骤能带来几个实实在在的好处:

  • 一致性:每次构建和部署走完全相同的流程,不会因为"忘了勾某个选项"翻车
  • 速度:Push代码后自动触发构建,你可以去喝杯咖啡等结果
  • 可追溯性:每个版本都关联到具体的Git提交,出了问题能快速定位
  • 团队协作:开发人员不用在本地折腾签名证书和密钥库(这个真的太香了)

GitHub Actions是GitHub内置的CI/CD平台,跟.NET MAUI项目配合得非常好。接下来我会从零开始,带你搭建一套完整的Android和iOS自动构建与发布流水线,基于.NET 10和最新的MAUI工作负载。

前置准备与项目结构

开发环境要求

  • .NET 10 SDK
  • GitHub仓库(用于托管代码和跑Actions)
  • Android签名密钥库(Keystore)
  • Apple开发者账号(用于iOS证书和描述文件)
  • Google Play Console开发者账号(用于Android发布)
  • App Store Connect API密钥(用于TestFlight上传)

推荐的项目目录结构

MyMauiApp/
├── .github/
│   └── workflows/
│       ├── ci.yml              # 持续集成工作流
│       ├── cd-android.yml      # Android CD工作流
│       └── cd-ios.yml          # iOS CD工作流
├── src/
│   └── MyMauiApp/
│       ├── MyMauiApp.csproj
│       └── ...
├── tests/
│   └── MyMauiApp.Tests/
└── global.json

建议把工作流文件按平台分开管理,这样可以独立触发和调试,排查问题时会轻松不少。另外,强烈推荐用global.json锁定SDK版本——我踩过本地和CI环境SDK版本不一致的坑,排查起来真的很费时间。

创建基础CI工作流

先来搞定持续集成部分。我们需要一个工作流,在每次Push和Pull Request时自动编译项目并运行测试。

name: CI Build

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  build-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'

      - name: Install MAUI Workload
        run: dotnet workload install maui-android --ignore-failed-sources

      - name: Restore Dependencies
        run: dotnet restore src/MyMauiApp/MyMauiApp.csproj

      - name: Build Android
        run: dotnet build src/MyMauiApp/MyMauiApp.csproj -c Release -f net10.0-android --no-restore

  build-ios:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'

      - name: Install MAUI Workload
        run: dotnet workload install maui-ios --ignore-failed-sources

      - name: Restore Dependencies
        run: dotnet restore src/MyMauiApp/MyMauiApp.csproj

      - name: Build iOS
        run: dotnet build src/MyMauiApp/MyMauiApp.csproj -c Release -f net10.0-ios --no-restore

  run-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'

      - name: Run Unit Tests
        run: dotnet test tests/MyMauiApp.Tests/MyMauiApp.Tests.csproj -c Release --logger trx

几个关键点要注意

  • Android构建可以跑在ubuntu-latest上,成本最低
  • iOS构建必须用macos-15运行器——没办法,Xcode工具链只有macOS上有
  • 加上--ignore-failed-sources可以避免NuGet源临时抽风导致工作负载装不上
  • 始终对项目文件(.csproj)而不是解决方案文件(.sln)执行dotnet builddotnet publish,不然可能会把不该编译的项目也带上

Android自动构建与签名

第一步:生成签名密钥库

还没有密钥库?没关系,用下面的命令生成一个:

keytool -genkey -v \
  -keystore myapp-release.keystore \
  -alias myapp \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000

划重点:一定要备份好密钥库文件和密码。丢了密钥库就意味着没法用同一签名去更新你的应用了,这可不是闹着玩的。

第二步:把密钥库存到GitHub Secret里

密钥库是二进制文件,不能直接当Secret用。得先转成Base64字符串:

# macOS / Linux
base64 -i myapp-release.keystore -o keystore-base64.txt

# Windows PowerShell
[Convert]::ToBase64String([IO.File]::ReadAllBytes("myapp-release.keystore")) | Set-Content keystore-base64.txt

然后到GitHub仓库的 Settings → Secrets and variables → Actions,创建这几个Secrets:

  • ANDROID_KEYSTORE_BASE64:密钥库的Base64编码内容
  • ANDROID_KEYSTORE_PASSWORD:密钥库密码
  • ANDROID_KEY_ALIAS:密钥别名
  • ANDROID_KEY_PASSWORD:密钥密码
  • PLAYSTORE_SERVICE_ACCOUNT_JSON:Google Play服务账号JSON内容

第三步:Android CD工作流

这才是核心部分——当你Push一个版本标签时,自动完成构建、签名和上传到Google Play。

name: Android CD

on:
  push:
    tags:
      - 'v*'

env:
  PROJECT_PATH: src/MyMauiApp/MyMauiApp.csproj
  DOTNET_VERSION: '10.0.x'

jobs:
  deploy-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'microsoft'
          java-version: '17'

      - name: Install MAUI Workload
        run: dotnet workload install maui-android --ignore-failed-sources

      - name: Decode Keystore
        run: |
          echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > myapp-release.keystore

      - name: Publish Android App
        run: |
          dotnet publish ${{ env.PROJECT_PATH }} \
            -f net10.0-android \
            -c Release \
            -p:AndroidKeyStore=true \
            -p:AndroidSigningKeyStore=${{ github.workspace }}/myapp-release.keystore \
            -p:AndroidSigningKeyAlias=${{ secrets.ANDROID_KEY_ALIAS }} \
            -p:AndroidSigningKeyPass=${{ secrets.ANDROID_KEY_PASSWORD }} \
            -p:AndroidSigningStorePass=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}

      - name: Upload to Google Play
        uses: r0adkll/[email protected]
        with:
          serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_SERVICE_ACCOUNT_JSON }}
          packageName: com.yourcompany.myapp
          releaseFiles: src/MyMauiApp/bin/Release/net10.0-android/publish/*-Signed.aab
          track: internal
          status: draft

Android签名参数一览

参数说明
AndroidKeyStore=true启用密钥库签名
AndroidSigningKeyStore密钥库文件的绝对路径
AndroidSigningKeyAlias密钥库中的密钥别名
AndroidSigningKeyPass密钥密码,支持env:file:前缀
AndroidSigningStorePass密钥库密码,支持env:file:前缀

顺便说一下,在.NET 10中dotnet publish默认就是Release配置了,所以-c Release其实可以省略。不过我习惯写上,看着明确一些。

iOS自动构建与TestFlight发布

iOS这边会稍微麻烦一点(苹果的生态嘛,你懂的),但跟着步骤走其实也不复杂。

第一步:配置App Store Connect API密钥

App Store Connect → 用户和访问 → 集成 → App Store Connect API,创建一个新的API密钥。创建完你会拿到三样东西:

  • Issuer ID:发行者ID
  • Key ID:密钥ID
  • Private Key (.p8):私钥文件——注意,这个只能下载一次,千万别弄丢了

把这三个值存为GitHub Secrets:

  • APPSTORE_ISSUER_ID
  • APPSTORE_KEY_ID
  • APPSTORE_PRIVATE_KEY:.p8文件的完整内容

第二步:准备签名证书和描述文件

iOS应用签名需要两个东西:

  • Distribution Certificate (.p12):Apple Distribution签名证书及其私钥
  • Provisioning Profile (.mobileprovision):把应用ID、证书和设备绑在一起的描述文件

把证书导出为.p12之后,用Base64编码存到GitHub Secret里:

  • IOS_CERTIFICATE_BASE64:证书的Base64编码
  • IOS_CERTIFICATE_PASSWORD:证书导出时设的密码

第三步:iOS CD工作流

name: iOS CD

on:
  push:
    tags:
      - 'v*'

env:
  PROJECT_PATH: src/MyMauiApp/MyMauiApp.csproj
  DOTNET_VERSION: '10.0.x'

jobs:
  deploy-ios:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      - name: Setup Xcode
        uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: latest-stable

      - name: Install MAUI Workload
        run: dotnet workload install maui-ios --ignore-failed-sources

      - name: Import Code Signing Certificate
        uses: apple-actions/import-codesign-certs@v3
        with:
          p12-file-base64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
          p12-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}

      - name: Download Provisioning Profile
        uses: maui-actions/apple-provisioning@v1
        with:
          bundle-identifiers: com.yourcompany.myapp
          profile-types: IOS_APP_STORE
          issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
          api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
          api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}

      - name: Publish iOS App
        run: |
          dotnet publish ${{ env.PROJECT_PATH }} \
            -f net10.0-ios \
            -c Release \
            -p:ArchiveOnBuild=true \
            -p:RuntimeIdentifier=ios-arm64 \
            -p:CodesignKey="Apple Distribution" \
            -p:CodesignProvision="MyMauiApp"

      - name: Upload to TestFlight
        uses: apple-actions/upload-testflight-build@v3
        with:
          app-path: src/MyMauiApp/bin/Release/net10.0-ios/ios-arm64/publish/MyMauiApp.ipa
          issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
          api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
          api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}

iOS构建踩坑备忘

  • ArchiveOnBuild=true是生成.ipa文件的关键,漏掉这个参数就不会产出可安装包
  • CodesignKey的值要跟密钥链中安装的证书名称对上
  • CodesignProvision的值要和下载的描述文件名称一致
  • .NET 10中RuntimeIdentifier默认已经是ios-arm64了,但我建议还是写上,一目了然
  • maui-actions/apple-provisioning这个Action会自动创建临时密钥链,不会跟运行器的默认密钥链打架

版本号自动管理

每次上传到商店的版本号必须递增,这是硬性要求。手动改版本号?算了吧,早晚会忘的。下面是个靠谱的自动化方案。

在.csproj中配置版本属性

<PropertyGroup>
  <ApplicationDisplayVersion>1.0.0</ApplicationDisplayVersion>
  <ApplicationVersion>1</ApplicationVersion>
</PropertyGroup>
  • ApplicationDisplayVersion:用户看到的版本号(比如1.2.3)
  • ApplicationVersion:内部版本号(整数,必须逐次递增)

在工作流中自动提取和设置版本号

- name: Extract Version from Tag
  id: version
  run: |
    TAG=${GITHUB_REF#refs/tags/v}
    echo "display_version=$TAG" >> $GITHUB_OUTPUT
    echo "build_number=${{ github.run_number }}" >> $GITHUB_OUTPUT

- name: Publish with Version
  run: |
    dotnet publish ${{ env.PROJECT_PATH }} \
      -f net10.0-android \
      -c Release \
      -p:ApplicationDisplayVersion=${{ steps.version.outputs.display_version }} \
      -p:ApplicationVersion=${{ steps.version.outputs.build_number }} \
      -p:AndroidKeyStore=true \
      ...

这样一来,推送v1.2.0标签时,用户可见版本号自动变成1.2.0,内部构建号用GitHub Actions的运行编号,自动递增不重复。简单又可靠。

GitHub Secrets安全最佳实践

CI/CD流水线里塞了一堆敏感信息,安全这块不能马虎。

用Environment保护关键环境

jobs:
  deploy-android:
    runs-on: ubuntu-latest
    environment: production
    steps:
      ...

GitHub Environments可以让你给不同环境(staging、production等)配不同的Secrets。最重要的是,你可以设置"人工审批"——部署到生产环境前必须有人点个确认,这在团队协作中非常重要。

密钥管理清单

下面这张表列出了所有需要管理的Secrets,建议存好备用:

Secret名称用途获取方式
ANDROID_KEYSTORE_BASE64Android签名密钥库keytool生成后Base64编码
ANDROID_KEYSTORE_PASSWORD密钥库密码生成密钥库时设置
ANDROID_KEY_ALIAS密钥别名生成密钥库时设置
ANDROID_KEY_PASSWORD密钥密码生成密钥库时设置
PLAYSTORE_SERVICE_ACCOUNT_JSONGoogle Play发布权限Google Cloud Console创建
APPSTORE_ISSUER_IDApp Store Connect API发行者IDApp Store Connect创建
APPSTORE_KEY_IDApp Store Connect API密钥IDApp Store Connect创建
APPSTORE_PRIVATE_KEYApp Store Connect API私钥App Store Connect下载.p8文件
IOS_CERTIFICATE_BASE64iOS签名证书Keychain Access导出后Base64编码
IOS_CERTIFICATE_PASSWORD证书导出密码导出.p12时设置

优化构建性能与控制成本

GitHub Actions按分钟收费,而macOS运行器的价格是Linux的10倍。不做点优化的话,账单看着会有点肉疼。以下几个策略亲测有效。

缓存NuGet包

- name: Cache NuGet Packages
  uses: actions/cache@v4
  with:
    path: ~/.nuget/packages
    key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
    restore-keys: |
      ${{ runner.os }}-nuget-

用条件触发避免浪费

on:
  push:
    branches: [ main ]
    paths-ignore:
      - '**/*.md'
      - 'docs/**'
      - '.gitignore'

改了个README就跑一遍完整构建?大可不必。paths-ignore帮你过滤掉文档类变更。

用workload restore替代workload install

- name: Restore Workloads
  run: dotnet workload restore src/MyMauiApp/MyMauiApp.csproj

dotnet workload restore只装项目真正需要的工作负载,比dotnet workload install maui快得多——后者会把所有平台的工作负载全装上,很多你根本用不到。

成本对比

运行器类型每分钟费率适用场景
ubuntu-latest$0.008Android、测试
windows-latest$0.016Windows
macos-15$0.08iOS、macOS

所以Android构建一定要用Linux运行器,成本差距是10倍。实际体验下来,典型的Android构建大概4到7分钟,iOS构建5到10分钟。

常见问题排查

搭流水线的过程中,你大概率会遇到下面这些问题。别问我怎么知道的。

工作负载安装失败

dotnet workload install步骤报错?十有八九是NuGet源临时不可用。加个--ignore-failed-sources就行:

dotnet workload install maui-android --ignore-failed-sources

Java SDK版本不对

Android构建如果报"Java SDK 11.0 or above is required"之类的错,那就是运行器自带的Java版本太老了。在工作流里显式指定:

- name: Setup Java
  uses: actions/setup-java@v4
  with:
    distribution: 'microsoft'
    java-version: '17'

iOS描述文件扩展名的坑

有些GitHub Actions下载描述文件时,不管目标平台是啥,统一存成.mobileprovision。但如果是Mac Catalyst项目,正确的扩展名应该是.provisionprofile。遇到这个问题需要加一步重命名操作。这个坑比较隐蔽,构建到签名阶段才会报错。

dotnet publish对解决方案文件报错

记住:dotnet publish永远指向.csproj文件,别用.sln。对解决方案执行publish会尝试逐个发布每个项目,各种莫名其妙的错误保管让你怀疑人生。

完整的多平台发布流水线架构

把前面的内容串起来,一套成熟的.NET MAUI CI/CD流水线长这样:

  1. CI阶段:每次Push或PR触发 → 编译检查 + 单元测试
  2. Staging阶段:合并到develop → 自动构建并推送到内部测试轨道(Google Play Internal)和TestFlight
  3. Production阶段:推送版本标签(如v1.2.0)→ 构建签名包 → 上传到商店正式轨道(需人工审批)
name: Release Pipeline

on:
  push:
    tags:
      - 'v*'

jobs:
  run-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'
      - run: dotnet test tests/MyMauiApp.Tests/ -c Release

  deploy-android:
    needs: run-tests
    runs-on: ubuntu-latest
    environment: production
    steps:
      # ... Android构建和发布步骤(参见上方Android CD工作流)

  deploy-ios:
    needs: run-tests
    runs-on: macos-15
    environment: production
    steps:
      # ... iOS构建和发布步骤(参见上方iOS CD工作流)

needs: run-tests确保测试通过才触发部署,environment: production要求有人审批才能执行。Android和iOS部署并行跑,效率拉满。

常见问题解答

GitHub Actions构建.NET MAUI应用要多久?

Android大概4到7分钟,iOS大概5到10分钟。第一次构建会慢一些,因为要下载安装工作负载。后面用上NuGet缓存和workload restore之后会快不少。多平台构建并行跑的话,整体流水线时间就取决于最慢的那个平台。

能不能在Linux上构建iOS应用?

不行,这是苹果的限制。iOS构建必须在macOS上跑,因为Xcode工具链和代码签名工具只有macOS版本。Android构建倒是没这个限制,Linux上就能搞定,而且更便宜。

密钥库丢了怎么办?

对于Android来说,丢了签名密钥库就没法用同一身份更新应用了。好在Google Play有个Play App Signing功能,建议一开始就启用——让Google帮你管App Signing Key,你只管Upload Key就行。万一Upload Key丢了,还可以联系Google Play支持团队重置。

怎么给不同分支配不同的部署目标?

用GitHub Actions的environment功能加上分支保护规则。比如develop分支自动部署到内部测试轨道,main分支通过标签触发正式发布。每个environment可以有自己独立的Secrets和审批流程。

macOS运行器费用怎么控制?

macOS运行器每分钟$0.08,Linux只要$0.008,差了10倍。省钱的办法有几个:只在Push标签时才触发iOS构建、用NuGet缓存缩短构建时间、用paths-ignore过滤非代码变更、或者直接上自托管运行器(self-hosted runners)彻底告别按分钟计费。

关于作者 Editorial Team

Our team of expert writers and editors.