iOS + Github Actions + AppCenter

Luís Jaeger
CWI Software
Published in
8 min readJan 22, 2021

Buildando e distribuindo seu app automaticamente

Como já foi dado o kickoff do assunto Continuous Distribution usando um exemplo de projeto Android com o Github Actions e o App Center aqui, nada mais justo que continuarmos ele estendendo para sua contraparte, o iOS. Vamos implementar uma esteira de distribuição de um aplicativo iOS utilizando o Github Actions que fará a publicação Ad Hoc no App Center. Parece fácil, mas nem tudo são flores no desenvolvimento iOS.

Pedras no caminho

Existe um ponto negativo no Github Actions para o build iOS, o Github disponibiliza 2000 minutos mensais para builds em qualquer repositório. Porém nos builds que utilizam MacOS cada minuto utilizado é multiplicado por 10, ou seja, temos apenas 200 minutos de build por mês. 😢

Além disso, primeiro precisamos organizar e preparar alguns detalhes do projeto visando ser possível utilizar os certificados e assinar nosso .ipa corretamente nas máquinas do Github Actions.

Certificado e Provisioning Profile

Os certificados e provision são arquivos que, em conjunto, validam permissões de desenvolvimento, distribuição e funcionalidades do nosso aplicativo. Esses arquivos devem ser adicionados a keychain da nossa máquina para que seja possível assinar o .ipa, é aí que começa o problema. Com o certificado e provision qualquer um pode buildar e distribuir nosso aplicativo. Então como enviar nosso certificado de forma segura para a máquina que rodará o build?

Encriptando Certificados e Provision

A solução aqui será controlarmos manualmente os certificados e provisions, adicionando eles ao nosso repositório, mas para mantermos a segurança da distribuição vamos precisar encriptá-los.

Considerando que já temos instalados o certificado e provision profile que utilizaremos, vamos acessar o Keychain Access da nossa máquina, selecionar o certificado de distribuição e exportá-lo como certs.p12 utilizando uma senha forte. Salve essa senha, pois a utilizaremos logo mais.

Keychain Access

Agora o provisioning profile, em nosso exemplo, estamos utilizando apenas um provision, mas para facilitar a manipulação de vários profiles no futuro, aqui vamos utilizar o tar.gz. No diretório onde está .mobileprovision (baixado do Apple Connect) rodaremos o seguinte comando:

tar cvfz provisioning.tar.gz *.mobileprovision

E finalmente utilizaremos o gpg para encriptarmos nossos arquivos:

gpg -c certs.p12
gpg -c provisioning.tar.gz

As boas práticas nos dizem para encriptar cada arquivo com uma senha forte e diferente, mas como só se vive uma vez, nesse exemplo utilizaremos a mesma senha para os dois.

You Only Live Once

Agora com nossos arquivos encriptados, vamos adicioná-los na raiz do nosso projeto e salvaremos as duas senhas (export e encrypt) nos secrets do Github com os nomes de CERT_KEY e DECRYPT_KEY.

Github Secrets

ExportOptions.plist

Com os certificados prontos, precisamos definir através de um .plist as opções de exportação do nosso .ipa, segue exemplo:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>compileBitcode</key>
<true/>
<key>method</key>
<string>ad-hoc</string>
<key>provisioningProfiles</key>
<dict>
<key>BUNDLE ID</key>
<string>PROVISIONING NAME</string>
</dict>
<key>signingCertificate</key>
<string>iPhone Distribution</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>YOUR-TEAM-ID</string>
<key>thinning</key>
<string>&lt;none&gt;</string>
</dict>
</plist>

Os pontos destacados devem ser substituídos de acordo com o seu projeto e time. Esse plist pode ser salvo, junto com os certificados, na raiz do nosso projeto.

Hora de buildar nosso aplicativo

Finalmente terminamos de preparar os arquivos para o build e signing do nosso aplicativo, então já podemos seguir para a parte interessante. A partir da raiz do nosso projeto criaremos o arquivo /.github/workflows/ios.yml que terá nosso script de execução do build iOS, o arquivo não necessariamente precisa ter o nome "ios", porém obrigatoriamente deve estar dentro do path /.github/workflows/ para que o Github Actions seja executado. Ficando nosso projeto assim:

Projeto iOS

Agora sim, dentro do nosso ios.yml nós teremos esse pequeno script:

Distribuição iOS

Se assustou? Calma, vamos explicar cada um dos steps. Haja vontade de desenvolver iOS, ehn?! 😜

E lá vamos nós…

Levando em consideração que estaremos rodando um projeto com Cocoapods e com o Xcode 12, começamos pelo nome e quando disparar nosso workflow:

name: Build Sample
on:
push:
branches:
- master
- develop

O nome do workflow será "Build Sample" e será disparado quando forem feitos pushes nas branches master e develop.

Dividindo para conquistar, nos trechos abaixo você consegue observar que temos de forma hierárquica dois jobs no nosso workflow. O primeiro rodando em um MacOS, responsável pelo build em si, tendo o nome de "build"; e o segundo rodando em um Ubuntu, responsável por realizar o upload do .ipa para o App Center, por sua vez com nome de "upload".

jobs:

build:
runs-on: macos-latest
env:
CERT_KEY: ${{ secrets.CERT_KEY }}
DECRYPT_KEY: ${{ secrets.DECRYPT_KEY }}
KEYCHAIN: ${{ 'some.keychain' }}
...
upload:
runs-on: ubuntu-latest
needs: build
env:
APPCENTER_TOKEN: ${{ secrets.APPCENTER_TOKEN }}
...

Dentro do job upload você pode observar que temos a property needs: build para indicar que o upload deve ocorrer apenas após a execução do job build.

Essa divisão acaba sendo necessária por que a Action utilizada para o upload para o App Center foi desenvolvida apenas para sistemas Ubuntu. ¯\_(ツ)_/¯

Além da divisão de jobs, aqui podemos ver a atribuição das variáveis de ambiente como o caso das duas secrets que criamos para os certificados, o token do projeto criado no App Center e também o nome dado a keychain que vamos criar para adicionar os certificados.

Passo 1: Checkout

- uses: actions/checkout@v2

O primeiro passo a ser executado dentro de nossa máquina de build é realizar um checkout na branch correspondente a onde ocorreu o push.

Passo 2: Cache dos Pods

Para garantir um pouco mais de agilidade ao buildar nosso projeto, utilizaremos uma Action muito bem vinda e desenvolvida pela equipe do Github:

- name: Cache Pods
id: cache-pods
uses: actions/cache@v2
with:
path: Pods
key: ${{ runner.os }}-pods

Esta Action cria cache do path informado, tendo como key o sistema operacional que utilizamos (MacOS). Assim, depois que o build rodar com sucesso pela primeira vez, as próximas execuções terão os pods do nosso projeto salvos para agilizar as execuções subsequentes.

Passo 3: Definir versão do Xcode

Como estamos rodando uma versão mais recente do Xcode, precisamos definir manualmente que desejamos utilizar o Xcode 12 ao invés do 11:

- name: Set Xcode Version
run: sudo xcode-select -s /Applications/Xcode_12.app/Contents/Developer

Passo 4: Preparar keychain

Precisamos então criar, definir como padrão e desbloquear a nova keychain que utilizaremos:

- name: Keychain
run: |
security create-keychain -p "" "$KEYCHAIN"
security list-keychains -s "$KEYCHAIN"
security default-keychain -s "$KEYCHAIN"
security unlock-keychain -p "" "$KEYCHAIN"
security set-keychain-settings

Passo 5: Preparar provisioning profile e code signing

Chegamos então na parte que, acredito eu, seja a mais complexa do flow, onde precisamos decryptar os certificados e adicioná-los a nossa keychain recém criada.

- name: Prepare Provision and Code Signing
run: |
gpg -d -o ./certs.p12 --pinentry-mode=loopback --passphrase "$DECRYPT_KEY" ./certs.p12.gpg
gpg -d -o ./provisioning.tar.gz --pinentry-mode=loopback --passphrase "$DECRYPT_KEY" ./provisioning.tar.gz.gpg
security import ./certs.p12 -k "$KEYCHAIN" -P "$CERT_KEY" -A
security set-key-partition-list -S apple-tool:,apple: -s -k "" "$KEYCHAIN"
tar xzvf ./provisioning.tar.gz
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
for PROVISION in `ls ./*.mobileprovision`
do
UUID=`/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $(security cms -D -i ./$PROVISION)`
cp "./$PROVISION" "$HOME/Library/MobileDevice/Provisioning Profiles/$UUID.mobileprovision"
done

Começando com o decrypting dos arquivos usando o gpg e nossa chave salva nas secrets nós extraímos os arquivos para seus nomes originais:

gpg -d -o ./certs.p12 --pinentry-mode=loopback --passphrase "$DECRYPT_KEY" ./certs.p12.gpg
gpg -d -o ./provisioning.tar.gz --pinentry-mode=loopback --passphrase "$DECRYPT_KEY" ./provisioning.tar.gz.gpg

Hora de adicionar o certificado à keychain:

security import ./certs.p12 -k "$KEYCHAIN" -P "$CERT_KEY" -A        
security set-key-partition-list -S apple-tool:,apple: -s -k "" "$KEYCHAIN"

Extraindo os profiles do tar.gz e criando o diretório de provisioning:

tar xzvf ./provisioning.tar.gz
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"

E finalmente o momento daquele cmd + C, cmd + V maroto:

for PROVISION in `ls ./*.mobileprovision`
do
UUID=`/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $(security cms -D -i ./$PROVISION)`
cp "./$PROVISION" "$HOME/Library/MobileDevice/Provisioning Profiles/$UUID.mobileprovision"
done

Este script fica responsável por pegar todos os .mobileprovision, extrair seus UUID, definir como o nome do arquivo e copiá-los para o diretório de profiles, isso é necessário, pois o sistema operacional só reconhece provisioning profiles que tenham o nome idêntico ao seu UUID.

Passo 6: Build!!!

É agora, amigos e amigas! O momento tão esperado, o build:

- name: Build
run: |
pod install
xcodebuild build -workspace ActionSample.xcworkspace -configuration Automation -scheme ActionSample "OTHER_CODE_SIGN_FLAGS=--keychain '$KEYCHAIN'"
xcodebuild archive -workspace ActionSample.xcworkspace -scheme ActionSample -archivePath sample.xcarchive
xcodebuild -exportArchive -archivePath sample.xcarchive -exportPath . -exportOptionsPlist ./ExportOptions.plist

Em ordem, nós rodamos pod install, build, archive e export passando os nomes do workspace, projeto e export options de acordo com nosso projeto. Voilà! Temos o .ipa pronto para ser enviado ao App Center. Mas calma lá, ainda temos alguns obstáculos a vencer.

Passo 7: Verificando arquivos e salvando .ipa

Vocês lembram que comentei sobre a Action responsável de mandar o aplicativo para o App Center funcionava apenas em sistemas Ubuntu, certo? Então como vamos mandar esse .ipa do MacOS para o Ubuntu?

Com uma Action. Primeiro fazemos um rápido check nos arquivos do projeto com o ls e na sequência salvamos o .ipa dentro da nossa package de armazenamento gratuito do Github (mais uma cortesia Microsoft):

- name: Check files
run: ls -R

- name: Save ipa
uses: actions/upload-artifact@v2
with:
name: ios-artifact
path: ActionSample.ipa

Detalhe, ao fazermos o upload do artefato, o .ipa fica disponível para download através da própria página do Github ❤️.

Passo 8: Download .ipa

Encerrando o upload do .ipa no passo anterior, o Github Actions está terminando a execução do job no MacOS e iniciará o job no Ubuntu, onde precisamos fazer o download do artefato:

- name: Get ipa
uses: actions/download-artifact@v2
with:
name: ios-artifact

- name: Check files
run: ls -R

Observem que o with: name:utilizado precisa ser exatamente igual ao utilizado no passo 7 para que o job baixe corretamente os arquivos. Terminamos esse passo checando novamente os arquivos do projeto para ver se nosso app chegou corretamente ao Ubuntu.

Passo 9: Publicando .ipa

Finalizando nossa jornada pelo CD iOS, utilizaremos uma Action desenvolvida pela comunidade para envio ao App Center:

- name: Upload ipa to App Center
uses: wzieba/AppCenter-Github-Action@v1.3.1
with:
appName: luisrjaeger/Action-Sample
token: ${{ secrets.APPCENTER_TOKEN }}
group: testers
file: ActionSample.ipa

Na publicação no App Center, precisamos informar o nome da organização, projeto do aplicativo e grupo de QAs criado lá. Em nosso caso, luisrjaeger/Action-Sample e o grupo “testers”. Também precisamos informar o path (que será o caminho do output do build) do .ipa a ser publicado e o token de acesso do App Center, este previamente salvo nas secrets e definido nas variáveis de ambiente.

Rodando nosso workflow

Agora commitando o script na branch develop e/ou master, o workflow começará a rodar. Você pode observar a execução na tab “Actions” do seu projeto no Github.

Depois de pronto até que parece fácil XD

Todos os novos commits realizados nas branches develop e master vão novamente disparar o build e publicação do aplicativo. Com isso, você não precisará mais parar o que estava fazendo para gerar as versões de seu app.

Você pode ver o resultado desse projeto de exemplo aqui no Github.

Conclusão

Conseguimos! Fechamos o nosso Continuous Distribution iOS, agora chega de desculpas, hora de botar esse CD rodar ai no seu projeto e incentivar a cultura de CI/CD mobile para que utilizemos nosso tempo desenvolvendo, criando novas features, corrigindo bugs e não parados olhando para tela do computador esperando o build terminar.

--

--