Configurando o Rails, RSpec e Rubocop no Github Actions
Tags: rails, ruby, github, ci
Published: 2022-03-25 23:19:45

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

```yml
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](https://docs.github.com/en/actions/reference/events-that-trigger-workflows).

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

```yml
...
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](https://github.com/actions/setup-ruby),  mas ela foi depreciada recentemente, e agora é sugerido utilizar a [ruby/setup-ruby](https://github.com/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:

```yml
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:

```yml
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:

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

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

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

```yml
- 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

```yml
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
```