Tecnologia
iOS + Github Actions + AppCenter
8 minutos de leitura
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, para o iOS conseguimos ter 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.
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 um dos arquivos com uma senha forte e diferente, mas como só se vive uma vez, nesse exemplo utilizaremos a mesma senha para os dois.
Agora com nossos arquivos encriptados, vamos adicioná-los na a 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.
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><none></string>
</dict>
</plist>
Os pontos destacados devem ser substituídos de acordo com o seu projeto e time. Este plist pode ser salvo, junto dos 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:
Agora sim, dentro do nosso ios.yml nós teremos esse pequeno script:
name: Build Sample
on:
push:
branches:
- master
- develop
jobs:
build:
runs-on: macos-latest
env:
CERT_KEY: ${{ secrets.CERT_KEY }}
DECRYPT_KEY: ${{ secrets.DECRYPT_KEY }}
KEYCHAIN: ${{ 'some.keychain' }}
steps:
- uses: actions/checkout@v2
- name: Cache Pods
id: cache-pods
uses: actions/cache@v2
with:
path: Pods
key: ${{ runner.os }}-pods
- name: Set Xcode Version
run: sudo xcode-select -s /Applications/Xcode_12.app/Contents/Developer
- 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
- 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
- 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
- name: Check files
run: ls -R
- name: Save ipa
uses: actions/upload-artifact@v2
with:
name: ios-artifact
path: ActionSample.ipa
upload:
runs-on: ubuntu-latest
needs: build
env:
APPCENTER_TOKEN: ${{ secrets.APPCENTER_TOKEN }}
steps:
- name: Get ipa
uses: actions/download-artifact@v2
with:
name: ios-artifact
- name: Check files
run: ls -R
- name: Upload ipa to App Center
uses: wzieba/[email protected]
with:
appName: luisrjaeger/Action-Sample
token: ${{ secrets.APPCENTER_TOKEN }}
group: testers
file: ActionSample.ipa
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?
Actions vem para nos ajustar de novo, 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 checkando 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/[email protected]
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.
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.
Referências