Configurando a pipeline de CI/CD para iOS usando Fastlane e GitHub Actions num projeto Flutter

Manoel Soares Neto
10 min readDec 23, 2023

--

Do you prefer to read in English? Click here!

Se você desenvolve aplicativos, sabe que o processo de deploy pode ser repetitivo e demorado. Fazer isso manualmente não é apenas cansativo, mas também aumenta o risco de erros. Neste artigo, vou mostrar como automatizar e agilizar o processo de deploy na Apple Store. Vamos utilizar ferramentas eficientes como o Fastlane e GitHub Actions, que transformam o deploy de uma tarefa manual em um processo automatizado, confiável e eficiente.

Este guia é direto ao ponto, ideal para quem desenvolve em Flutter e busca uma forma de tornar o deploy de aplicativos iOS mais simples e menos propenso a falhas. Com este guia, você ganhará uma base sólida, permitindo que você refine e aprimore seus processos de deploy para torná-los ainda mais seguros e eficientes.

Ao final desse guia, você será capaz de enviar seu aplicativo para a App Store apenas subindo um branch para o seu repositório no GitHub, ou clicando num simples botão.

1. Iniciando o Projeto Flutter

Vamos criar o projeto Flutter da seguinte forma: flutter create --org com.fastlaneexample.example app. A versão flutter utilizada nesse projeto foi: 3.16.2.

Estrutura do projeto e Ferramentas utilizadas

Além do Flutter, usaremos o Melos, uma ferramenta versátil para gerenciamento de monorepos em Dart e Flutter, que nos ajuda a simplificar e automatizar várias tarefas. A versão do Melos usada aqui é a 3.4.0.

A estrutura do nosso projeto se organiza da seguinte maneira:

├── app
├── android
├── ios
└── pubspec.yaml
├── melos.yaml
└── pubspec.yaml

Nota: Este artigo foca na configuração do ambiente e ferramentas necessárias para automatizar o deploy desse projeto para o TestFlight, portanto, não entraremos em detalhes sobre a implementação específica do Flutter.

2. Inicializando o Fastlane

Para integrar o Fastlane no projeto, a instalação será feita através do Homebrew, um gerenciador de pacotes para macOS, que simplifica a instalação de softwares no sistema operacional da Apple. Embora este guia utilize o Homebrew, o Fastlane pode ser instalado de diversas outras maneiras. Para alternativas de instalação, consulte a documentação oficial do Fastlane.

brew install fastlane

Após a instalação, navegue até a pasta ios do seu projeto Flutter e inicialize o Fastlane:

fastlane init

Durante o processo de inicialização, quando solicitado sobre como você deseja configurar o Fastlane, opte por 'Manual setup'. Esta escolha permite uma configuração mais detalhada e adaptada às necessidades específicas do projeto.

Ao finalizar, a estrutura inicial do Fastlane será criada com os seguintes arquivos:

├── fastlane
├── Appfile
└── Fastfile
└── Gemfile

O 'Gemfile' é usado pelo Bundler para gerenciar as dependências do Ruby, especificamente o Fastlane neste caso. O 'Appfile' contém configurações globais do seu app, como o identificador do bundle. E o 'Fastfile' define as 'lanes' que automatizam tarefas específicas, como por exemplo os builds ou deploys.

Edite o 'Appfile' para configurar o bundle do seu aplicativo:

app_identifier("com.fastlaneexample.example") # The bundle identifier of your app

Atualize o 'Gemfile' para especificar a versão do Fastlane usada no projeto:

source "https://rubygems.org"

gem "fastlane", "~> 2.217.0"

Ao especificar a versão do Fastlane, você assegura a consistência da ferramenta utilizada por todos os membros da equipe e em diferentes ambientes de desenvolvimento.

3. Configurando o Fastlane Match

O Fastlane Match simplifica o gerenciamento de certificados e perfis de provisionamento, tornando o processo de desenvolvimento e distribuição de aplicativos iOS mais eficiente e seguro. Vamos adicionar um novo app e configurar o Match seguindo estes passos:

Criando um Novo App ID

Comece criando um novo App ID para o projeto no App Developer Portal. Este ID será usado para identificar o aplicativo na App Store.

Em seguida, crie um novo app chamado Flutter Fastlane Example.

Criando um Repositório para Certificados e Perfis

Crie um novo repositório privado no GitHub para armazenar de forma segura os certificados e perfis de provisionamento gerenciados pelo Match. Por exemplo, nomeie esse repositório de 'secrets'.

Inicializando o Fastlane Match

Navegue para a pasta 'ios' do seu projeto e execute:

fastlane match init

Escolha a opção 'git' e forneça a URL do repositório recém-criado, por exemplo: https://github.com/Flutter-Fastlane-Example/secrets.git. Um 'Matchfile' será criado na pasta 'fastlane'. Edite-o da seguinte forma:

git_url("https://github.com/Flutter-Fastlane-Example/secrets.git")
storage_mode("git")
type("appstore") # The default type, can be: appstore, adhoc, enterprise or development
app_identifier(["com.fastlaneexample.example"])
username("your_github_username") # Your Apple Developer Portal username

Configurando um Personal Access Token do GitHub

Para que o match possa interagir com o repositório do GitHub, você precisará de um 'Personal Access Token'. Veja como criar esse token.

Após criar o token, converta-o para Base64 e copie o resultado da seguinte maneira:

echo -n 'your_github_username:your_personal_access_token' | base64 | pbcopy

Gerando Certificados e Perfis de Provisionamento

Execute os comandos do Match para gerar os certificados:

  • Para desenvolvimento: fastlane match development
  • Para distribuição: fastlane match appstore

Lembre-se de criar uma senha para o keychain quando solicitado e guardá-la em um local seguro.

Recriando Certificados (se necessário): Em algumas circunstâncias, pode ser necessário recriar os certificados e perfis de provisionamento do projeto. Podemos usar o nuke do Fastlane Match para lidar com esse cenário.

Para App Store: fastlane match nuke distribution para revogar os certificados e fastlane match appstore para recriá-los.

Para development: fastlane match nuke development para revogar os certificados e fastlane match development para recriá-los.

Confira no repositório 'secrets' se os certificados foram criados com sucesso.

Configurando Variáveis de Ambiente

Crie um arquivo .env.default dentro da pasta 'fastlane' para armazenar temporariamente as chaves necessárias:

MATCH_PASSWORD="your_match_keychain"
MATCH_GIT_BASIC_AUTHORIZATION="your_GIT_PAT_TOKEN"

MATCH_GIT_BASIC_AUTHORIZATION é o Personal Access Token gerado acima e convertido para Base64, MATCH_PASSWORD é o keychain criado acima. Essas chaves serão posteriormente armazenadas de forma segura nas variáveis de ambiente do GitHub.

4. Configurando o App Store Connect

Para interagir eficientemente com a App Store Connect, é crucial carregar o token da API, que nos permite baixar perfis de provisionamento, enviar binários para o TestFlight, entre outras operações. Vamos configurar isso através de uma 'lane' no 'Fastfile'.

platform :ios do
before_all do
load_asc_api_token
end

desc "Load the App Store Connect API token"
lane :load_asc_api_token do
app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_content: ENV["ASC_KEY_P8"],
is_key_content_base64: true,
in_house: false
)
end
end

Nesse script, utilizamos três variáveis de ambiente: ASC_KEY_ID, ASC_ISSUER_ID e ASC_KEY_P8. Elas serão criadas e armazenadas temporariamente no arquivo de environments.

Gerando as Chaves da API

  • Acesse a sua conta na App Store Connect.
  • Navegue até 'Users and Access'.
  • Selecione a aba 'Keys' e clique em 'Generate API Key'.
  • Nomeie a chave e atribua o papel de 'App Manager', que fornece as permissões necessárias.
  • Após a criação, você receberá a 'Issuer ID' (ASC_ISSUER_ID) e 'Key ID' (ASC_KEY_ID).

Faça o download da chave my_api_key.p8. Converta esta chave para Base64 usando o seguinte comando no terminal:

base64 -i my_api_key.p8 | pbcopy

Isso copiará a chave para o clipboard, e então armazene a chave ASC_KEY_P8 convertida para o arquivo provisório de environments.

ASC_KEY_ID="your_asc_key_id"
ASC_ISSUER_ID="your_asc_issuer_id"
ASC_KEY_P8="your_base64_asc_key_p8"
MATCH_PASSWORD="your_match_keychain"
MATCH_GIT_BASIC_AUTHORIZATION="your_GIT_PAT_TOKEN"

Guarde estas chaves em um local seguro e nunca as exponha publicamente. Posteriormente, estas chaves serão transferidas para as variáveis de ambiente seguras do GitHub para automação através do GitHub Actions.

5. Configurando a Lane de Release no Fastlane

Para preparar a automação do processo de lançamento do nosso aplicativo Flutter para o TestFlight, realizamos algumas configurações no XCode:

  1. Bump da Versão Mínima do iOS: Na aba 'General', ajustamos a versão mínima do iOS para 12.0, garantindo compatibilidade com as versões mais recentes.
  2. Gerenciamento Automático de assinaturas: Na aba 'Signing & Capabilities', desabilitamos a opção 'Automatically manage signing' e selecionamos o perfil de provisionamento 'match AppStore com.fastlaneexample.example'.

A lane de release no Fastfile será configurada para automatizar o lançamento de novos builds. Para fornecer um ponto de referência claro, mostramos em qual commit a release está sendo iniciada.

desc "Release a new build to Apple Store"
lane :release_beta do
commit = last_git_commit
puts "*** Starting iOS release for commit(#{commit[:abbreviated_commit_hash]}) ***"
end

Extraímos a chave da API da App Store Connect para uso posterior.

desc "Release a new build to Apple Store"
lane :release_beta do
...

#read api key from app_store_connect_api_key lane variable
api_key = lane_context[SharedValues::APP_STORE_CONNECT_API_KEY]
end

Utilizamos o Fastlane Match para sincronizar certificados e perfis.

desc "Release a new build to Apple Store"
lane :release_beta do
...

#sync certificates and profiles using match
sync_code_signing(
api_key: api_key,
type: "appstore",
readonly: true,
)
end

Atualizamos automaticamente o número da build, baseando-nos na última versão disponível no TestFlight.

def bump_build_number()
latest_build_number = latest_testflight_build_number(initial_build_number: 0)
return (latest_build_number + 1)
end

desc "Release a new build to Apple Store"
lane :release_beta do
...

build_number = bump_build_number()
end

Definimos o nome da versão, usando uma lógica para lidar com a ausência de versões anteriores.

def get_version_name()
version_name = lane_context[SharedValues::LATEST_TESTFLIGHT_VERSION]

if version_name.empty?
puts "*** Version name is empty, add version 1.0.0 ***"
version_name = "1.0.0"
end

return version_name
end

desc "Release a new build to Apple Store"
lane :release_beta do
...

version_name = get_version_name()
end

Finalizamos a configuração da lane com a compilação do projeto Flutter e o envio da build para o TestFlight.

desc "Release a new build to Apple Store"
lane :release_beta do
...

Dir.chdir "../.." do
puts "*** Build flutter iOS release for version #{version_name}+#{build_number} ***"
sh("flutter", "build", "ipa", "--release", "--build-number=#{build_number}", "--build-name=#{version_name}")
end

puts "*** Build and sign iOS app release ***"
build_app(
skip_build_archive: true,
archive_path: "../build/ios/archive/Runner.xcarchive",
)

puts "*** Upload app to testflight ***"
upload_to_testflight(api_key: api_key)
end

Note que o nome do target no projeto XCode, 'Runner', pode variar; substitua-o pelo nome correto do seu projeto conforme necessário.

Execute fastlane release_beta dentro da pasta app/ios e gere uma nova versão para o TestFlight.

Keychain access

Ao rodar localmente, você será solicitado a adicionar a senha para liberar o acesso à keychain. Insira a senha do seu login no computador e clique em 'Always Allow'.

6. Armazenando as Chaves no GitHub

O armazenamento seguro das chaves e a configuração correta de um workflow são essenciais para automatizar o processo de build e release com o GitHub Actions. Acesse o seu GitHub, clique em 'Settings' e adicione todas as chaves que foram criadas seguindo as imagens a seguir.

7. Configurando um Workflow no GitHub Actions

Na raiz do seu projeto, crie a pasta .github/workflows e adicione um novo arquivo para o workflow, como apple-release.yaml.

├── .github
└── workflows
└── apple-release.yaml
├── app
├── android
├── ios
├── fastlane
├── Appfile
├── Fastfile
└── Matchfile
└── Gemfile
└── pubspec.yaml
├── melos.yaml
└── pubspec.yaml

No arquivo de workflow, defina os passos que o GitHub Actions deve seguir para construir e distribuir seu aplicativo.

name: Apple Release

on:
push:
branches:
- apple-release

jobs:
build-and-deploy-ios:
runs-on: macos-latest
defaults:
run:
working-directory: app
steps:
- name: Set up git and fetch all history for all branches and tags
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "2.7"
- name: Bundle install for iOS Gemfile
timeout-minutes: 5
run: cd ./ios && bundle install
- name: Set up Flutter SDK
uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.2"
channel: "stable"
architecture: x64
cache: true
- name: Set up melos
run: flutter pub global activate melos
- name: Melos Bootstrap
run: melos bs
- name: Build and Deploy to TestFlight
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.GIT_PAT_TOKEN }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
run: |
cd ./ios
bundle exec fastlane release_beta

Ao realizar o push da branch apple-release, o workflow definido anteriormente no GitHub Actions será acionado automaticamente. No entanto, é importante destacar um aspecto crucial relacionado ao acesso à keychain no ambiente de CI/CD.

Automatizando o Acesso à Keychain

Assim como acontece quando executamos a lane release_beta localmente e somos solicitados a inserir a senha do computador para liberar o acesso à keychain, o workflow no GitHub Actions também enfrenta um desafio similar. Em um ambiente de máquina virtual, como o fornecido pelo GitHub Actions, o pedido de senha para acessar a keychain pode interromper o fluxo de trabalho, deixando-o 'travado'.

Para resolver essa questão, é necessário utilizar a action setup_ci do Fastlane. O setup_ci é projetado para configurar o ambiente CI, criando um keychain temporário especificamente para o processo de build, eliminando a necessidade de uma senha. Esse keychain temporário é automaticamente removido ao término do workflow, garantindo segurança e eficiência.

Inclua o setup_ci na sua configuração Fastfile como mostrado abaixo.

platform :ios do
before_all do
setup_ci
load_asc_api_token
end

...
end

Testando o Workflow

Com essas configurações aplicadas, você está pronto para testar o workflow. Faça o push da branch apple-release e observe o GitHub Actions executando o processo de build e release do seu aplicativo.

Agradecimento

Gostaria de expressar a minha gratidão a todos que seguiram este guia até aqui. Um agradecimento especial ao William Cho, cuja contribuição foi fundamental na elaboração deste conteúdo. Espero que as informações compartilhadas aqui possam ajudá-los a simplificar e automatizar o processo de lançamento de aplicativos iOS, tornando o desenvolvimento mais eficiente e agradável. Qualquer dúvida ou sugestão, fiquem à vontade para deixar um comentário. Obrigado por dedicarem seu tempo e esforço, e que vocês tenham sucesso em seus projetos!

--

--