Ir para o conteúdo

iOS + Github Actions + AppCenter

8 min. de leitura

Avatar de Luís Ricardo Jaeger

Luís Ricardo Jaeger Autor


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.

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 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.

You Only Live Once

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.

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. 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:

Projeto iOS

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/[email protected]

    - name: Cache Pods
      id: cache-pods
      uses: actions/[email protected]
      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/[email protected]
      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/[email protected]
      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/[email protected]

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/[email protected]
  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 installbuildarchive 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/[email protected]
  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/[email protected]
  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.

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.

Referências

 

Gostou?