Blog > Tecnologia > iOS + Github Actions + AppCenter

iOS + Github Actions + AppCenter

19/01/2021
  • 8 min. 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.

    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/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/AppCenter-Github-Action@v1.3.1
          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 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/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/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.

    Referências

    Gostou? 11
    Compartilhe esse post: