CI/CD para .NET MAUI com GitHub Actions: Pipeline Completo para iOS e Android em 2026
Pipeline completo de CI/CD para .NET MAUI no GitHub Actions: builds paralelos de iOS e Android, assinatura correta com certificados e keystores, versionamento automático e upload direto para TestFlight, Google Play e Firebase App Distribution.
Um pipeline de CI/CD para .NET MAUI com GitHub Actions é um workflow YAML que compila, assina e distribui um app net10.0-ios, net10.0-android e net10.0-maccatalyst a partir de um único repositório, usando runners macos-15 para builds Apple e ubuntu-24.04 para Android. A maioria das equipes que herda um projeto Xamarin.Forms migrado tenta reaproveitar scripts antigos de msbuild, quebra na etapa de assinatura, e só então descobre que o workload maui precisa ser restaurado explicitamente em cada job. Honestamente, foi assim que perdi um final de semana inteiro no meu primeiro release .NET MAUI. Este guia mostra o pipeline completo, do checkout ao upload no TestFlight e Google Play.
Builds de iOS e macCatalyst exigem runners macos-15 com Xcode 16.3+. Android roda em ubuntu-24.04 com JDK 17 e economiza 60% de custo por minuto.
A assinatura no iOS deve usar apple-actions/import-codesign-certs para importar o .p12 e apple-actions/download-provisioning-profiles para provisioning. Nunca versione certificados no repositório.
Para Android, o keystore .jks deve ser armazenado como GitHub Secret em base64 e decodificado no runner com echo "$KEYSTORE_B64" | base64 -d > app.keystore.
Cache de ~/.nuget/packages e do workload maui reduz builds típicos de 12 min para 3–4 min por plataforma.
O App Center foi descontinuado em 31 de março de 2025. Use Firebase App Distribution ou TestFlight/Google Play para distribuição interna.
Bump automático de ApplicationVersion e ApplicationDisplayVersion por ${{ github.run_number }} evita rejeições por versão duplicada nas lojas.
Pré-requisitos e estrutura do repositório
Na maioria dos projetos que peguei nos últimos dois anos, o repositório tinha um .csproj multi-target no formato net10.0-android;net10.0-ios;net10.0-maccatalyst;net10.0-windows10.0.19041.0 e nenhum arquivo global.json. Isso é a primeira coisa que quebra o CI. O runner do GitHub pode ter uma SDK diferente da que a equipe usa localmente, e o build falha em NETSDK1045. Já perdi tarde da noite debugando exatamente isso.
Antes de escrever qualquer YAML, garanta o seguinte na raiz do repositório:
Um global.json fixando a versão exata do .NET 10 SDK (por exemplo, 10.0.100).
Um arquivo .editorconfig e um Directory.Build.props compartilhado onde ApplicationVersion e ApplicationDisplayVersion são declarados como propriedades MSBuild. Nunca deixe hardcoded no .csproj.
Um projeto de testes unitários separado em tests/ que compila em net10.0 puro (sem TFMs móveis), para poder rodar em qualquer runner sem workloads.
Um arquivo nuget.config comitado, apontando apenas para api.nuget.org. Feeds internos autenticados exigem configuração adicional de secrets.
Se você está vindo de Xamarin, dê uma olhada no nosso guia de migração do Xamarin.Forms para .NET MAUI antes de configurar o pipeline. Headers de .csproj pré-migração produzem artefatos que nenhum job de assinatura consegue processar.
Workflow base do GitHub Actions para .NET MAUI
O pipeline começa com um workflow único que dispara em push na branch main e em pull requests. Divido em três jobs paralelos: test, build-android, build-ios. O truque para .NET MAUI é sempre restaurar o workload depois do checkout, porque a imagem do runner traz apenas o SDK puro. Salve o arquivo abaixo em .github/workflows/build.yml:
O job test roda em Ubuntu porque testes unitários puros não precisam de workloads móveis, e isso corta o custo em minutos GitHub-Actions à metade em comparação com rodar em macOS. Se seus testes tocam código específico de plataforma, mova-os para um job matrix em macos-15. Para uma cobertura mais profunda de estratégia de testes, veja o guia completo de testes em .NET MAUI.
Como construir e assinar um APK/AAB Android no CI
Android é a plataforma mais barata de rodar no CI porque não exige runner macOS. O fluxo é direto: decodificar o keystore de um secret base64, restaurar o workload maui-android, publicar o AAB assinado, e fazer upload do artefato. Adicione o job abaixo ao build.yml:
Note que o keystore nunca é escrito no diretório do repositório. Ele fica só em $RUNNER_TEMP, que o runner limpa automaticamente ao final. O parâmetro ApplicationVersion deve ser um inteiro monotonicamente crescente, e usar github.run_number resolve isso sem lógica extra. Para criar o keystore inicialmente, use keytool -genkey -v -keystore app.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias upload, e converta para base64 com base64 -i app.keystore | pbcopy (macOS) ou base64 -w 0 app.keystore (Linux) antes de colar como secret.
Como buildar e assinar iOS no GitHub Actions
Builds iOS são onde 80% dos pipelines falham. Aviso: preparei mais café pra essa seção do que qualquer outra na minha carreira. Você precisa de: certificado .p12 exportado do Keychain, provisioning profile .mobileprovision, App Store Connect API key para downloads automáticos, e um runner macos-15 com Xcode selecionado corretamente. Aqui vai o job completo:
O CodesignProvision deve corresponder exatamente ao nome do provisioning profile no App Store Connect, sem o UUID e sem prefixos. Se o build reclama de "No valid provisioning profile found", 99% das vezes é o nome errado, não certificado errado. Rode security find-identity -p codesigning no runner (adicione um step de debug) para listar identidades disponíveis. A action oficial apple-actions/import-codesign-certs cuida do keychain automaticamente e destrói o keychain temporário ao final do job.
Versionamento automático e bump de build number
Nada bloqueia mais releases do que subir para o TestFlight e receber ERROR ITMS-90062: Invalid Value. The value for the bundle version must be a higher number than the previously uploaded version. A solução é derivar a versão do build do próprio pipeline, e nunca versionar manualmente no .csproj. Existem duas estratégias que uso:
Estratégia 1: github.run_number como build number
É a mais simples. Cada execução do workflow incrementa github.run_number, e você injeta como ApplicationVersion. Para ApplicationDisplayVersion (a versão user-facing "1.2.3"), extraia da tag Git:
Se você já usa Conventional Commits, o GitVersion calcula a versão a partir do histórico automaticamente. Adicione um step gittools/actions/gitversion/execute@v3, e use ${{ env.GitVersion_MajorMinorPatch }}. Prefiro essa estratégia em monorepos com múltiplos apps, onde cada app tem sua própria trajetória de versionamento.
Como gerenciar secrets e certificados com segurança
A regra número um: nunca comitar um .p12, .mobileprovision, .jks, ou qualquer credencial da App Store Connect. Já vi equipes vazarem certificados de distribuição num commit "temporário" e passarem semanas para revogar tudo. A abordagem correta usa três camadas:
GitHub Encrypted Secrets no nível do repositório para tudo que muda pouco (keystore Android, senha do keystore, API key da App Store).
GitHub Environments para separar staging vs production com regras de aprovação. Crie um environment production que exija revisão manual antes de rodar jobs de release.
OpenID Connect (OIDC) para autenticar em Azure/AWS sem armazenar credenciais de longa duração. Se você usa .NET Aspire para o backend, provavelmente já tem infraestrutura Azure e OIDC é o caminho mais seguro.
Para gerar a API key da App Store Connect, vá em Users and Access → Integrations → App Store Connect API, crie uma key com role "App Manager", baixe o .p8 (você só pode baixar uma vez), e armazene como três secrets separados: APPSTORE_ISSUER_ID (UUID), APPSTORE_KEY_ID (10 caracteres alfanuméricos), e APPSTORE_PRIVATE_KEY (o conteúdo completo do .p8 incluindo as linhas -----BEGIN PRIVATE KEY-----).
Distribuição para TestFlight, Google Play e Firebase
Depois que os artefatos assinados estão em upload-artifact, o próximo passo é subir para as lojas. O App Center foi oficialmente descontinuado em 31 de março de 2025, então esqueça-o completamente. As opções atuais são TestFlight (iOS), Google Play Internal Testing (Android), e Firebase App Distribution (ambos, para builds internos rápidos).
Aspecto
TestFlight
Google Play Internal
Firebase App Distribution
Plataformas
iOS, macCatalyst
Android
iOS + Android
Tempo até build ficar disponível
10–30 min (processamento)
5–10 min
1–2 min
Limite de testers
10.000 externos
100 internos
Sem limite prático
Distribuição por email
Sim, mas exige App Store Connect
Sim, via Google Groups
Sim, sem conta Google necessária
Automação via GitHub Actions
upload-testflight-build, fastlane pilot
r0adkll/upload-google-play
wzieba/Firebase-Distribution-Github-Action
Custo
Incluído no dev program
Incluído no Play Console
Free até 50k testers
Para upload no TestFlight, adicione depois do build iOS:
Para Google Play, você precisa de uma service account JSON com role Release Manager:
- name: Upload to Google Play
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
packageName: com.mycompany.myapp
releaseFiles: '**/*.aab'
track: internal
status: completed
Como reduzir o tempo do pipeline com cache e matriz
Um pipeline .NET MAUI sem otimização leva 12–18 minutos por plataforma. Com cache correto, cai para 3–5 minutos. Três otimizações valem o esforço:
Cache do ~/.nuget/packages: chave baseada no hash de todos os .csproj, com fallback para chave parcial. Isso economiza 60–90 segundos por job.
Cache do workload MAUI: o diretório ~/.dotnet/sdk-manifests contém os manifests de workload. Restaure-o em cache separado, e pule dotnet workload install se o cache bater (economiza 2–3 min).
Matriz para plataformas paralelas: ao invés de sequenciar Android e iOS, rode em paralelo. GitHub Actions cobra por minuto/runner, mas o wall clock cai pela metade, o que importa mais para desenvolvedores esperando o CI.
Se você processa muitos assets (imagens, fontes, arquivos de Resources/Raw), considere também mover o build para runners self-hosted em macOS Mini M4 para iOS. Um Mac Mini de $600 amortiza em 2–3 meses comparado com o custo de macos-15 a $0.16/min para uma equipe que faz 20+ builds por dia. Fiz essa migração na minha equipe atual e o ROI apareceu no primeiro trimestre.
Erros comuns em pipelines .NET MAUI e como corrigir
Depois de configurar dezenas desses pipelines, esses são os erros que aparecem com mais frequência e como resolvê-los:
NETSDK1147: To build this project, the following workloads must be installed: maui-ios: você esqueceu o dotnet workload install antes do publish. Adicione o step correspondente à plataforma.
error MSB4018: Task "GenerateBundleName" failed unexpectedly: o Xcode instalado no runner não corresponde ao esperado pelo workload. Fixe a versão com xcode-select -s.
Could not find any provisioning profiles matching '...': o nome em CodesignProvision não bate. Rode ls ~/Library/MobileDevice/Provisioning\ Profiles/ e valide os nomes reais dos profiles baixados.
Keystore was tampered with, or password was incorrect: você fez base64 do keystore em Windows e o CRLF foi incluído. Use base64 -w 0 em Linux ou WSL.
ITMS-90189: Redundant Binary Upload: o build number é o mesmo que um upload anterior. Sempre incremente ApplicationVersion automaticamente por run.
Build passa localmente mas falha no CI: 90% das vezes é diferença de SDK. Fixe com global.json e revalide com dotnet --info no início do job.
Se você mantém múltiplas variantes do app (dev, staging, prod), consolide as configurações em Directory.Build.props e use MSBuild conditions ao invés de branches YAML separadas. Um workflow que cresceu para 800 linhas de YAML sempre foi mais fácil de refatorar reduzindo a lógica no MSBuild do que dividindo em vários arquivos.
Perguntas frequentes
Posso buildar apps iOS no GitHub Actions sem um Mac?
Sim, usando runners hospedados macos-15, que são VMs macOS na infraestrutura do GitHub. Não é necessário Mac físico para builds, mas para debugar problemas de assinatura ou provisioning você ainda precisa de acesso ao Apple Developer Portal, que exige uma conta paga ($99/ano).
Qual é a diferença entre ApplicationVersion e ApplicationDisplayVersion no .NET MAUI?
ApplicationVersion é o build number interno (inteiro, deve ser único por upload nas lojas). ApplicationDisplayVersion é a versão user-facing no formato semver (por exemplo, "1.2.3"). No CI, use github.run_number para o primeiro e uma tag Git para o segundo.
GitHub Actions ou Azure DevOps para pipelines .NET MAUI?
Para times pequenos e projetos open-source, GitHub Actions é gratuito até 2.000 minutos/mês e integra melhor com repositórios GitHub. Azure DevOps é preferível quando você já tem infra empresarial em Azure, precisa de agent pools self-hosted em escala, ou usa Azure Test Plans. As duas suportam .NET MAUI com esforço semelhante.
Como faço o rollback de um release enviado por engano ao TestFlight?
Você não pode deletar um build do TestFlight, mas pode expirá-lo em App Store Connect indo em TestFlight → Builds → Expire. Para prevenir uploads acidentais, use GitHub Environments com approval obrigatório para o job upload-testflight, e nunca rode uploads em push automático, apenas em tags v*.
Preciso de fastlane com .NET MAUI se estou usando GitHub Actions?
Não. As apple-actions/* cobrem 95% dos casos (importar certificado, baixar provisioning, upload TestFlight) sem precisar de fastlane. Só considere fastlane se você precisa de features específicas como match para gerenciamento de certificados compartilhados entre equipes ou snapshot para screenshots automatizados.