Introdução
A integração contínua, ou CI (continuous integration) é a estratégia de compilar e testar a aplicação toda vez que um desenvolvedor desejar integrar suas modificações ao repositório central. Essa estratégia ajuda a detectar falhas tanto no código enviado, como no encaixe desse novo código ao código já existente, de forma mais precoce dentro do fluxo de desenvolvimento. No CI podem ser executados testes automatizados, analisadores de estilo e de organização, escaneadores de vulnerabilidades e qualquer coisa que possa ser automatizada e ajude o time de desenvolvimento ter uma aplicação mais segura, organizada ou com bom desempenho.
Uma das ferramentas disponíveis para implementar o CI, é o Github Actions, um produto disponibilizado dentro do ecossistema do Github e que se integra perfeitamente dentro da página dos repositórios sem nenhuma fricção de ter que ficar trocando de serviço para acompanhar o andamento das execuções, ou de ter que configurar o acesso de outro serviço aos projetos. Nesse artigo vou explicar como configurar o Github Actions em uma aplicação Rails, executando o RSpec (um framework de testes) e o Rubocop (um analisador de código Ruby).
Criando o workflow
No fim do artigo eu coloquei o arquivo YAML completo para quem quiser apenas copiar e colar e adequar para sua realidade. Aqui eu vou explicar o que significa cada configuração para explicar os porquês de cada linha e suas alternativas.
O primeiro passo é criar uma pasta na raíz do repositório chamada .github e adicionar a pasta workflows dentro dela, pois é nessa pasta que o Github detecta que o projeto tem fluxos a serem executados. Depois crie um arquivo YAML dentro de .github/workflows e nomeie com algo que indique a natureza do fluxo que será executado. Pro caso de testes e análise de código, eu geralmente nomeio como ci.yml ou test_and_code_analysis.yml.
A estrutura básica do YAML que o Github Actions aceita é basicamente essa:
name: CI # Nome do fluxo
on: push # Evento que dispara o fluxo
jobs:
rubocop: # Identificador do job
... # Configurações do job
rspec: # Identificador do job
... # Opçòes de configuração do job
Logo mais vou explicar como configurar cada um dos jobs, mas primeiro, atenção para o on: push
, nele é colocado o evento que fará o fluxo ser disparado. No meu caso, eu coloco push, pois quero que o CI seja executado toda vez que alguém fizer um git push origin nome_do_branch. Mas há a opção de executar apenas quando um pull request for criado, que é a configuração on: pull_request
. Para outras opções de evento, há uma lista bem detalhada na documentação oficial do Github.
Configurando o Rubocop
Deixando claro que essa parte é opcional, pois caso tu não tenha necessidade ou possibilidade de ter uma análise de código no projeto, é só seguir o artigo sem adicionar os códigos referentes ao Rubocop e aproveitar apenas os trechos que mostro como configurar a execução dos testes.
Voltando ao .github/workflows/ci.yml
para definir o fluxo de análise de código:
...
rubocop:
name: Rubocop
runs-on: ubuntu-latest # Informa o SO que será usado nos testes
steps:
# Pega o código do projeto
- name: Checkout code
uses: actions/checkout@v1
# Configura o Ruby e instala as dependências
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
# Exemplo de outras configurações:
# ruby-version: 3.0.0
# bundler: 2.2.8
# working-directory: ./backend
# Executa o Rubocop
- name: Analyze code
run: bundle exec rubocop
...
É um fluxo simples, com 3 passos: Pegar o código, instalar o ruby e as dependências e executar o Rubocop. Antes de seguir, vale a pena me aprofundar nesse passo de instalação do Ruby para explicar melhor o que está acontecendo. Antes, o modo "oficial" de instalar o Ruby num fluxo, era utilizando a action criada pela equipe do Github, a actions/setup-ruby, mas ela foi depreciada recentemente, e agora é sugerido utilizar a ruby/setup-ruby, uma action mantida pelo time do Ruby. Ela tem algumas configurações importantes que podem ser passadas dentro do with:
:
ruby-version
:
Essa opção permite que seja informada a versão do ruby que será instalada. Caso não seja definida, será detectada a versão definida no arquivo .ruby-version
ou no .tool-versions
.
bundler
:
Essa opção serve para definir a versão do bundler que será utilizada para instalar as dependências. Se não estiver definida, a versão será detectada de acordo com o BUNDLED_WITH
do Gemfile.lock
, e se não encontrar, será utilizada a versão mais recente.
working-directory
:
Caso o repositório tenha uma organização de pastas diferenciada, e a aplicação Rails não esteja na raíz, é necessário informar o caminho onde estão os arquivos .ruby-version
, .tool-versions
e Gemfile.lock
, assim as versões do Ruby e do bundler poderão ser detectadas. No caso da aplicação que trabalho, tenho que fazer: working-directory: ./services/catarse
, para o fluxo funcionar corretamente.
bundler-cache
:
Se essa opção estiver como true, a action irá instalar as dependências com um bundle install e fará o cache das gems de forma automática, para não ter que instalar todo o conjunto de dependências a cada nova execução do fluxo.
Agora note que não configurei nada de PostgreSQL, ou NodeJS, porque a análise de código do Rubocop não necessita dessas ferramentas, apenas do Ruby e das gems da família Rubocop.
Configurando o RSpec
Agora o principal, que é a execução dos testes. Dessa vez terá uns passos a mais, mas também nada muito complexo. Primeiro vou deixar a configuração inteira e vou explicando aos poucos parte por parte:
rubocop:
...
rspec:
name: RSpec
needs: rubocop
runs-on: ubuntu-20.04
env:
RAILS_ENV: test
DATABASE_URL: postgres://postgres:example@localhost:5432/db_test
services:
postgres:
image: postgres:latest
ports: ['5432:5432']
env:
POSTGRES_DB: db_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: example
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v1
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Install postgres client dependencies
run: sudo apt-get install libpq-dev
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 12.20.0
- name: Yarn package cache
uses: actions/cache@v2
with:
path: ./node_modules
key: ${{ runner.os }}-yarn-v1-${{ hashFiles('./yarn.lock') }}
- name: Install Yarn packages
run: yarn install --pure-lockfile
- name: Create database
run: |
bundle exec rails db:create
# Se o projeto usar `schema.rb`
bundle exec rails db:schema:load
# Se o projeto usar `structure.sql`
bundle exec rails db:structure:load
# Se o projeto não usar nenhum dos dois
bundle exec rails db:migrate
- name: Run tests
run: bundle exec rspec spec
Começando primeiro por aqui:
rspec:
name: RSpec
needs: rubocop
runs-on: ubuntu-20.04
É bem parecido com o início da definição do fluxo do Rubocop, mas com um parâmetro a mais, o needs
. Essa configuração serve para avisar ao Github que o RSpec só poderá ser executado depois da execução bem sucedida do Rubocop, ou seja, se o código estiver fora do padrão do time, os testes nem serão executados. Mas caso queira que o RSpec e o Rubocop sejam executados paralelamente, só remover essa opção, fica ao critério da equipe. Agora a configuração do banco que dados:
env:
RAILS_ENV: test
DATABASE_URL: postgres://postgres:example@localhost:5432/db_test
services:
postgres:
image: postgres:latest
ports: ['5432:5432']
env:
POSTGRES_DB: db_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: example
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
No env.DATABASE_URL
, eu defini uma URL de conexão com o banco de dados que o Rails utilizará quando for criar a estrutura das tabelas da aplicação. Essa URL é baseada nas configurações que passei na criação do serviço postgres que será necessário para a execução dos testes. Informei a imagem sendo postgres:lastest
que pegará o PostgreSQL mais recente, mas pode ser utilizada qualquer versão, por exemplo: postgres:12. Dentro de services.postgres.options
está alguns comandos para aguardar o servidor de banco de dados subir, senão corre o risco do fluxo seguir e não ser possível se conectar no banco quando for necessário por conta do PostgreSQL não estar pronto.
steps:
- name: Checkout code
uses: actions/checkout@v1
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Install postgres client dependencies
run: sudo apt-get install libpq-dev
Os dois primeiros passos são idênticos ao dois primeiros do fluxo de análise de código, então volta lá na seção anterior caso tenha esquecido do que se trata. No terceiro, é instalado uma dependência de desenvolvimento do PostgreSQL, que é necessária para rodar os comando de banco de dados.
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 12.20.0
- name: Yarn package cache
uses: actions/cache@v2
with:
path: ./node_modules
key: ${{ runner.os }}-yarn-v1-${{ hashFiles('./yarn.lock') }}
- name: Install Yarn packages
run: yarn install --pure-lockfile
Para projetos que utilizam o Webpacker às vezes é necessário instalar as dependências de front-end para que os testes sejam executados. No primeiro passo está sendo instalado o Node 12.20, no passo seguinte é utilizado o actions/cache
que serve tanto para recuperar um cache quanto, se for necessário, para criar um novo cache no final do fluxo. A pasta que está sendo armazenada para ser reaproveitada nas execuções seguintes é a node_modules
que geralmente fica na raíz do projeto, e será o conteúdo do yarn.lock
que o github saberá se será necessário instalar dependências ou apenas reutilizar a node_modules existente. E no fim o yarn install
é utilizado para fazer essa instalação dos pacotes, com a opção —pure-lockfile
para que não seja gerado um yarn.lock novo.
- name: Create database
run: |
bundle exec rails db:create
# Se o projeto usar `schema.rb`
bundle exec rails db:schema:load
# Se o projeto usar `structure.sql`
bundle exec rails db:structure:load
# Se o projeto não usar nenhum dos dois
bundle exec rails db:migrate
- name: Run tests
run: bundle exec rspec spec
Por fim, no "Create database", o banco de testes é criado e a sua estrutura é montada a partir do schema.rb
ou structure.sql
e, caso esses arquivos não existam, as migrações serão executadas. Uma outra opção que substitui esse trecho da criação do banco de dados e sua estrutura é utilizar o comando rails db:test:prepare
.
Após o banco de dados pronto, a execução dos testes é realizada com o bundle exec rspec
spec e esse é o ponto final do fluxo.
Conclusão
Agora com tudo configurado, todo push que for feito ao projeto dispará uma execução do CI no Github Actions, que pode ser acompanhada na aba "Actions" dentro do projeto no Github, e após o fim da execução, será adicionado um "check" verdinho ou um "x" vermelho ao lado do commit, e dentro do pull request para indicar o resultado da execução.
Para projetos de código aberto, o Github Actions é gratuito, já para projetos privados de usuários não pagantes, são disponibilizados 2.000 minutos de execução mensalmente. Não há desculpa para não ter CI no projeto.
TL;DR
name: CI
on: push
jobs:
rubocop:
name: Rubocop
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v1
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Analyze code
run: bundle exec rubocop
rspec:
name: RSpec
needs: rubocop
runs-on: ubuntu-20.04
env:
RAILS_ENV: test
DATABASE_URL: postgres://postgres:example@localhost:5432/db_test
services:
postgres:
image: postgres:latest
ports: ['5432:5432']
env:
POSTGRES_DB: db_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: example
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v1
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Install postgres client dependencies
run: sudo apt-get install libpq-dev
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 12.20.0
- name: Yarn package cache
uses: actions/cache@v2
with:
path: ./node_modules
key: ${{ runner.os }}-yarn-v1-${{ hashFiles('./yarn.lock') }}
- name: Install Yarn packages
run: yarn install --pure-lockfile
- name: Create database
run: |
bundle exec rails db:create
# Se o projeto usar `schema.rb`
bundle exec rails db:schema:load
# Se o projeto usar `structure.sql`
bundle exec rails db:structure:load
# Se o projeto não usar nenhum dos dois
bundle exec rails db:migrate
- name: Run tests
run: bundle exec rspec spec
Top comments (1)
Muito bom. Obrigado!