CI/CD com GitHub Actions + Fastlane
Introdução
Estou considerando que o leitor terá conhecimento prévio do fastlane, do git flow, da pirâmide de testes, e de commits convencionais. Qualquer dúvida, podem deixar nos comentários!
Se você já teve que fazer o deployment de alguma versão de software, entende que o processo é complexo e passível a falhas. Consideramos, para nossos fins, que a caminhada começa desde o momento em que a equipe de desenvolvimento recebe uma demanda:
- Cria um feature branch (a partir da
develop
, no padrão do Git Flow). - Desenvolve a funcionalidade.
- Abre a PR e espera algum colega fazer a homologação e code review.
- Caso tenha algo para corrigir, você corrige e volta para o passo 3. Caso não, segue pro 5.
- Descobre que sua branch está várias semanas atrás da
develop
. - Gasta horas resolvendo conflitos do merge e testando que nada quebrou.
- Observa que seu colega está no mesmo barco com a tarefa dele.
- Sincronizam para que no final, todas as tarefas pendentes estejam juntas na
develop
, funcionando com deveriam. - Faz o version-bump e escreve um changelog.
- Gera sua versão.
- Distribui por algum mecanismo (npm, fastlane, etc).
Observe que essas etapas podem ser consideradas uma espécie de “pipeline”. Esse termo é bastante comum no mundo de CI/CD.
CI/CD nada mais é do que um conjunto de processos e ferramentas cujo objetivo é automatizar o maior número de etapas dessa pipeline possível.
A economia de tempo e de esforço, bem como a segurança de automatizar processos ameaçados por erro humano, é bastante tentadora. Vamos entender como isso funciona e dar uma olhada nos prós e contras.
GitHub Actions
O serviço de CI/CD que recomendo é o GitHub Actions. Se você já usa os serviços de hosting do GitHub, começar a usar o GitHub Actions é literalmente uma questão de commitar e subir o arquivo com um workflow (recomendo commitar direto na branch main/master
).
Crie uma pasta .github
, e dentro dela crie outra pasta chamada workflows
, e defina dentro dessa última pasta seu arquivo YML. O GitHub irá detectar esse arquivo automaticamente e... só isso. Sua pipeline foi criada.
Vamos passar rapidinho pela sintaxe do GitHub Actions, para facilitar o entendimento depois.
O nome desse script é My First Github Action
. Esse nome é arbitário e opcional.
Usando a keyword on
, definimos o seguinte: esse script deve ser executado sempre que alguém abrir uma PR apontando para as branches main
, master
, ou develop
. Nesse caso, será possível observar o resultado desse script dentro da própria PR.
Esse script define um único job chamado linting
. Esse job tem dois steps: checkout repo
e lint code
. Esses nomes são arbitrários; você pode defini-los como quiser.
Cada job é executado em uma dada máquina virtual. Hoje, o GitHub Actions oferece seis máquinas diferentes, todas com acesso aos mesmos recursos de hardware. Cada máquina já vem com uma gama de softwares preinstalados, então dê uma olhada antes de instalar algo manualmente.
Esse script, por exemplo, roda no ubunto-latest
, ou seja, a versão mais recente do Ubunto. Se você definir mais de um job, eles vão ser executados em paralelo por padrão e cada um pode rodar em uma máquina diferente.
Os steps dentro de um job serão executados sequencialmente. Porém! Cada step é executado em um processo separado, então sempre considere que o ambiente sofre um “reset” no início de cada step.
Reconhecemos um step pelo traço inicial. Ele pode ter várias propriedades: id, name, uses, needs, if, env, run
. Nenhuma propriedade é obrigatória, mas existem agrupamentos válidos e inválidos de propriedades. Prefiro definir sempre a prop name
, a título de documentação.
A propriedade runs
significa que você gostaria de definir um script a ser rodado. A propriedade uses
significa que você está usando uma Action criada e disponibilizada no GitHub Marketplace por outro usuário.
O básico é isso. Vamos ao que interessa.
Continuous Integration
É comum que entre uma versão e outra de um software, seu repositório encha de branches em desenvolvimento por várias pessoas que terão que se juntar em algum momento. Gastamos horas com problemas de merge, conflitos, e a quebra repentina de funcionalidades pós-merge.
Quanto maior a equipe, maior será o esforço de sincronização e mais passível de erro será a resolução de conflitos. De forma similar, quanto mais complexa a tarefa, mais tempo ela levará pra ser desenvolvida. Assim, a branch feature ficará cada vez mais distante da branch base e mais passível de erro será a resolução de conflitos.
Enfim, o problema não é simples, mas a solução talvez seja.
Uma pipeline que inclui integração contínua prega que mudanças ao repositório devem ser commitadas e integradas ao repositório remoto, numa branch de integração, com frequência.
Mas… como fazer isso?
Integrando com frequência
Tem algumas maneiras de alcançar a integração contínua de alterações ao código.
- Para equipes pequenas, talvez a maneira mais simples de atingir esse objetivo é simplesmente trabalhar na mesma branch. Para duplas com boa comunicação, como foi o caso da minha equipe atual por bastante tempo, isso funciona muito bem.
- Para equipes maiores, usar uma branch de integração talvez seja a melhor opção. No script a seguir, estou assumindo que a branch de integração é a
develop
, mas isso não é necessário. Essa branch pode ser amaster
. Também pode haver uma branch de integração antes dedevelop
, para permitir code reviews e homologações manuais.
Essa ação integra todo commit feito a uma branch feature
direto na develop
, sem precisar do envolvimento dos desenvolvedores. Caso os commits sejam frequentes e atômicos (como deveriam ser 👀), raramente ocorrerá conflitos. Caso ocorra, a ação falhará e só assim o desenvolvedor terá que agir.
Perceba que ainda não é possível automatizar por completo esse processo. Fica com o desenvolvedor a responsabilidade de fazer commits pequenos e subi-los ao repositório remoto com frequência.
Mas mesmo que não haja conflitos, fazer merges direto na develop
sem homologação ou code review é complicado. Como saber se uma linha que você alterou não quebrou outra funcionalidade? Como saber se o código escrito é de boa qualidade?
Automação de testes
A automação de testes não faz parte da definição oficial de integração contínua, mas é uma etapa bastante comum, por motivos já apontados. Vamos ver um script com essa automação:
O Github Actions executa os passos sequencialmente e se um passo retornar erro, a sequência inteira é abortada. Ou seja, se os testes falharem, por exemplo, o script não chega a executar o merge.
Perceba que a pipeline não é capaz de automatizar a escrita de testes. A equipe de desenvolvimento fica com essa responsabilidade. E não devemos nos limitar a testes unitários! Cada tipo de teste funciona em um nível diferente. Para tornar a automação de testes realmente útil, é necessário passar pela pirâmide inteira: testes unitários, de integração, de componente, e E2E.
Adaptando para o Git Flow
A integração contínua não é compatível com o Git Flow, que sugere uma divisão às vezes complexa de branches e também sugere fazer merges entre branches somente no final de uma etapa de desenvolvimento. Porém, ainda é possível agilizar etapas do Git Flow.
Por exemplo, podemos fazer nossas verificações (linting, testes, etc.) não em cima de cada commit, mas em cima de cada PR:
Retirar o passo de merge automático permitirá que parte do processo de code review + homologação + merge ainda seja feita manualmente. O resultado de cada passo dessa ação aparecerá automaticamente na PR em questão. Nas configurações do repositório, é possível impedir que a PR seja aceita até que todos os passos finalizem sem erro.
Isso agiliza o processo, mas não automatiza-o.
Husky vs. CI
O Husky é uma ferramenta que facilita o uso de git hooks. Isso nos permite criar e executar scripts em certos momentos, como antes de um commit.
Essa configuração, por exemplo, faz… basicamente a mesma coisa que o script do GitHub Actions faz. Então por que fazer a transição para o GitHub Actions ou outro ambiente de CI/CD se usar o Husky é grátis e tão comum?
Considerações
- CI: o Husky nos permite fazer linting, rodar testes, etc., mas não nos permite fazer integração contínua. Não automatiza merges ou faz comentários automáticos na PR.
- Espaço: o Husky é um pacote pequeno, mas ainda assim, é instalado como devDependency do seu app. Muitos tutoriais ainda recomendam a instalação do lint-staged, que faz o lint somente de arquivos que serão commitados.
- Tempo: o Husky roda localmente, e para projetos com suítes de testes bem-desenvolvidas, a execução do script de pre-commit pode demorar vários minutos.
Teoricamente, o Husky também pode rodar testes E2E. Porém, testes E2E rodam em um simulador ou emulador, e é difícil configurar a execução dos testes de tal modo que sirva para vários ambientes de desenvolvimento diferentes.
O GitHub Actions oferece um único ambiente para rodar os testes, e nos permite fazer o commit e continuar desenvolvendo enquanto as verificações rodam no servidor do GitHub.
- Financeiro: o Husky é grátis e o GitHub Actions é grátis somente para projetos open-source. Para projetos particulares, o Actions oferece certo tempo de uso grátis por mês. A maioria dos provedores de CI/CD seguem o mesmo padrão.
Caso executemos nossa pipeline somente sobre cada PR, usaremos uma quantia limitada de tempo. Caso executemos nossa pipeline sobre cada commit, nosso uso será bem mais elevado. Isso terá implicações financeiras!
Conclusão: se for necessário escolher um desses métodos, recomendo o GitHub Actions por ser mais flexível e o Husky por ser definitivamente grátis. 👀 Cabe a cada equipe decidir, mas nada te impede de usar os dois, caso ache necessário.
Continuous Delivery vs. Continuous Deployment
O ‘CD’ na sigla CI/CD pode significar duas coisas: continuous delivery ou continuous deployment. São coisas bastante parecidas, mas não idênticas.
A entrega contínua (continuous delivery) sugere um ciclo de release bem curto, onde funcionalidades novas entram na branch principal frequentemente em incrementos pequenos. Nesse caso, o software não é lançado: o importante é manter a branch principal num estado tal que você possa lançar uma versão nova, manualmente, a qualquer momento.
O lançamento contínuo (continuous deployment) prega a mesma coisa, só que também automatiza o processo de lançamento. Cada commit ou pull request à branch main
, por exemplo, desencadeará a geração automática de uma nova versão.
Adaptando para o Git Flow
Para empresas que estão acostumadas com o Git Flow, às vezes nem o continuous delivery nem o continuous deployment parecem servir bem. Ou talvez nossos clientes preferem versões discriminadas, com features completas. Nesse caso, podem passar meses entre uma versão e outra.
Isso não significa que não podemos automatizar nosso processo de releases. Considere então que estamos usando o fastlane para gerar e distribuir versões. Podemos executar o fastlane diretamente da máquina virtual, sempre que houver um GIT PUSH na branch main
, por exemplo.
Antes disso, porém, temos algumas questões a resolver.
Changelog e Bump-Version
Existe um plugin do fastlane capaz de fazer o bump-version e outro capaz de gerar um changelog automático. Tentamos usar o primeiro plugin, mas sua configuração foi tão complicada que acabamos desistindo. O segundo gera o changelog a partir dos últimos 10 commits, o que é bastante limitado.
Existe uma alternativa que não passa pelo fastlane. Projetos que convencionam seus commits conseguem automatizar o bump-version e a geração de changelog através de uma ferramenta chamada standard-version
.
Fastlane
O seguinte script faz:
- o bump-version
- gera o CHANGELOG.md
- commita e sobe essas mudanças
- roda um lane do fastlane
Considerações Finais
Implementar um pipeline de CI/CD não é complicado, mas requer mudança de processo e de mentalidade, e o custo disso varia de equipe em equipe. Outra coisa a levar em consideração ao planejar sua transição: o serviço de hosting do repositório é separado do serviço de CI/CD, o que tem consequências monetárias e de praticidade.
Em termos de praticidade: às vezes, a mesma empresa oferece ambos os serviços (GitHub + GitHub Actions, GitLab + GitLab CI/CD); às vezes você pode escolher empresas diferentes. Sempre há certa facilidade em usar a mesma empresa.
Em termos monetários: para projetos open-source, a transição para uma pipeline geralmente é grátis. Para projetos privados, porém, é comum que uma parcela de uso dos serviços de CI/CD seja grátis e o resto, pago.