Publicações

GitHub Actions vs GitLab CI. Quais são as principais diferenças?

Oscar Esgalha
Oscar Esgalha Backend Developer

Há alguns anos, ter um projeto de software com uma esteira de integração contínua (CI) era uma tarefa trabalhosa. Sua equipe deveria implantar e dar manutenção para um sistema de CI interno, provavelmente uma instância de Jenkins.

Nos últimos anos, no entanto, surgiram diversas soluções de SaaS para CI, com um custo de adoção bem menor: basta adicionar um arquivo de configuração em um repositório git e sua esteira está pronta para rodar.

Mais conveniente ainda, hoje alguns sistemas de hospedagem de código fonte contam com um sistema de CI embutido. Dessa forma o código fonte de seu projeto fica bem próximo de suas esteiras.

Neste post você vai ler uma análise comparativa entre duas soluções bem populares que se enquadram nessa última categoria: o GitLab CI e o GitHub Actions.

Vai começar um projeto e está na dúvida em qual escolher? Já usa uma das soluções e se pergunta se a outra opção seria melhor? Este artigo é para você.

Maturidade

Para começar, é importante destacar o tempo que essas soluções ficaram disponíveis para usarmos, ou seja, atingiram General Availability (GA).

O GitLab CI do jeito que conhecemos hoje, integrado na plataforma de hospedagem de código, está disponível desde 2015. O GitHub Actions atingiu disponibilidade geral 4 anos depois, em 2019.

Isso tem algumas implicações: com mais tempo de mercado, o GitLab CI possui mais usuários, mais conteúdo disponível (tutoriais, guias e exemplos) e também mais funcionalidades. Por outro lado, por ser mais recente, o GitHub Actions teve que trazer novas ideias para sistemas de CI.

Essa é a primeira grande diferença entre os dois: a maturidade. Como o GitHub Actions surgiu há menos de 2 anos, dependendo da complexidade da esteira que você precisar, vai esbarrar em algumas limitações da solução. Não é muito raro ter que recorrer ao Fórum de Suporte.

Note que a maturidade do GitLab CI aqui não é sinônimo de tecnologia datada. A cada versão nova a plataforma fica melhor, tem uma boa cadência de atualizações e está em evolução constante. É um sistema de CI moderno, completo e maduro.

Popularidade

De acordo com a StackShare, um serviço que permite empresas compartilharem as tecnologias que usam, o GitLab CI e o GitHub Actions são respectivamente a 4a e a 7a plataformas mais usadas. Em primeiro e segundo lugar estão o Jenkins e o Travis CI.

"Hosted internally" is the primary reason developers pick Jenkins over its competitors, while "Github integration" is the reason why Travis CI was chosen.

Vale lembrar que o Jenkins e Travis CI existem desde 2011, e, considerando o tempo em que o Jenkins era conhecido como Hudson, pode-se dizer que o Jenkins é usado desde 2005.


Segundo a pesquisa The State of Developer Ecosystem 2021 da JetBrains, feita com 31.743 pessoas desenvolvedoras de 183 países, o GitHub Actions é o segundo sistema de CI mais usado, enquanto o GitLab CI é o terceiro. O Jenkins aparece novamente em primeiro lugar.

While Jenkins is the most popular CI system for company usage, GitHub Actions is the most frequent choice for personal use.

Apesar de novo, o GitHub Actions já é uma preferência entre devs. Isso deve impactar o uso de sistemas de CI em organizações nos próximos anos.

Semelhanças

Apesar de produtos diferentes, ambos os sistemas atendem o mesmo objetivo: construir e executar esteiras de integração contínua. Portanto esses sistemas compartilham diversas capacidades, para citar algumas:

Jobs

Uma esteira de CI no GitLab CI ou GitHub Actions é composta por uma coleção de jobs. Cada job define um conjunto de passos que são executados em sequência.

Os jobs podem ser organizados para executarem sequencialmente ou em paralelo, podem ter dependências declaradas para que executem em determinada ordem e podem ser executados em máquinas distintas.

No exemplo abaixo é definido um job que imprime "Hello World" no console:

GitLab CI/CDGitHub Actions

job1:
  script:
    - echo "Hello World"

jobs:
  job1:
    steps:
      - run: echo "Hello World"

As chaves usadas no YAML mudam um pouco, mas até agora nada de muito diferente.

Runners

Em ambas as plataformas os jobs são executados por Runners. Num sistema de CI, um Runner é uma aplicação que roda em uma máquina separada. Essa aplicação está conectada ao sitema de CI e fica aguardando jobs para iniciar a sua execução.

A execução da esteira de CI depende da disponibilidade de um runner para iniciá-la. A quantidade de recursos que pode ser usada por processos definidos na sua esteira também estão diretamente ligadas à capacidade desses runners. Se a execução dos testes da sua aplicação exigirem 4GB de memória, por exemplo, não é possível rodar esse job num runner que está numa máquina com 512MB de memória.

Esse aspecto da arquitetura de um sistema de CI é mais relevante quando você é responsável por gerenciar os Runners. A alternativa é usar os runners gerenciados pela plataforma, que lida com escalabilidade para atender as demandas de seu projeto e o provisionamento de um ambiente limpo pra a sua esteira de CI.

As duas plataformas oferecem runners gerenciados e a opção de você gerenciar seus próprios runners. Independente de qual opção você escolher, é importante ter em mente que a esteira de CI será executada num ambiente com um limite bem determinado de recursos.

Para determinar o runner que executa um job, você usa a chave tags no GitLab CI e runs-on no GitHub Actions:

GitLab CI/CDGitHub Actions

windows_job:
  tags:
    - shared-windows
    - windows
    - windows-1809
  script:
    - echo "Hello from Windows!"

jobs:
  windows_job:
    runs-on: windows-latest
    steps:
      - run: echo "Hello from Windows!"

O software usado pelos runners são Open Source:

O consumo de recursos desses runners é desprezível, o dimensionamento das máquinas usadas para sustentar os runners deve ser feito pensando nos jobs que vão executar na máquina.

Para os runners gerenciados a cobrança é feita por minuto de execução dos jobs. Note que o minuto de execução é contabilizado por processamento, não pelo tempo total (wall time) da sua esteira de CI. Uma esteira que roda 5 jobs de 2 minutos em paralelo termina em 2 minutos, mas será cobrada em 10 minutos, por exemplo.

No plano gratuito, para projetos privados o GitLab CI oferece 400 minutos por mês e o GitHub Actions oferece 2000 minutos por mês. Para projetos públicos não há limite de uso no GitHub, enquanto no GitLab há uma cota de 50.000 minutos.

Tanto o GitLab CI quanto o GitHub Actions oferecem runners gerenciados Linux, Windows ou MacOS, no entanto os runners Windows e MacOS ainda estão em beta no GitLab CI.

As máquinas Linux do GitLab CI tem 1 vCPU, 3,75GB de RAM e 25GB de HDD, as máquinas com MacOS tem 4 vCPUs e 10GB de RAM, e as máquinas Windows tem 2 vCPU e 7,5GB de RAM.

As máquinas Linux e Windows do GitHub Actions contam com 2 vCPU, 7GB de RAM e 14GB de SSD, enquanto as máquinas com MacOS tem 3 vCPU, 14GB de RAM e 14GB de SSD.

Docker

As duas opções suportam executar jobs dentro de um container Docker.

Para determinar a imagem, você usa a chave image no GitLab CI e container no GitHub Actions:

GitLab CI/CDGitHub Actions

docker_job:
  image: ruby:3.0.2-slim-bullseye
  script:
    - bundle exec rake spec

jobs:
  docker_job:
    container: ruby:3.0.2-slim-bullseye
    steps:
      - run: bundle exec rake spec

Variáveis de Ambiente e Secrets

Os dois sistemas permitem definir variáveis de ambiente disponíveis para uso na esteira. Algumas variáveis de ambiente já são pré-definidas, são informações específicas do versionamento do projeto e ambiente como o sha do commit, nome da branch, nome da esteira, nome do repositório, etc. Há uma lista completa na documentação:

Variáveis de Ambiente e Secrets no GitHub Actions

Na interface do GitHub é possível definir mais variáveis para usar na esteira. No repositório, acesse o menu Settings » Secrets. Os valores definidos nessa tela, no entanto, não são acessados da mesma forma que uma variável de ambiente. Para usar um secret chamado SECRET_KEY na esteira:

jobs:
  job_with_secret:
    steps:
    - run: SECRET_CONF="${{ secrets.SECRET_KEY }}" example-cli

No exemplo acima a variável de ambiente SECRET_CONF recebe o valor do secret SECRET_KEY definido pela interface nas configurações do repositório, que fica disponível para uso pelo comando example-cli. É possível passar variáveis de ambiente através da chave env em diversos escopos: para a esteira inteira, para um job ou para um passo do job. O exemplo acima poderia ser reescrito assim:

jobs:
  job_with_secret:
    steps:
    - run: example-cli
      env:
        SECRET_CONF: ${{ secrets.SECRET_KEY }}

Variáveis de Ambiente e Secrets no GitLab CI

Também é possível definir varáveis adicionais através da tela do GitLab CI e no arquivo de configuração da esteira. Porém a distinção entre o que é secret e o que não é fica mais sutil. Tudo é chamado de variável e todos os valores estão disponíveis por padrão como variável de ambiente.

Para definir variáveis extras pela interface, na página do repositório acesse o menu Settings » CI/CD » Variables. Para usar na esteira uma variável chamada SECRET_CONF definida nessa tela:

job_with_secret:
  script:
  - example-cli

Para entender o exemplo acima é necessário saber que o comando example-cli usa a variável de ambiente SECRET_CONF. Por outro lado, não é necessário escrever nenhuma configuração a mais para isso.

Esse comportamento também varia de acordo com o ambiente. Se o job rodar em um container é necesário redefinir as variáveis, porque variáveis definidas pela interface não ficam disponíveis por padrão. Exemplo:

job_with_secret:
  image: debian:bullseye
  variables:
    SECRET_CONF: $SECRET_CONF
  script:
  - example-cli

É possível definir mais variáveis de ambiente através da chave variables para a esteira inteira ou para um job:

variables:
  TEST_FAIL_FAST: 1

test_job:
  variables:
    TEST_SKIP_SLOW: 1
  script:
  - test-cmd

No exemplo acima tanto a variável de ambiente TEST_FAIL_FAST quanto a TEST_SKIP_SLOW ficam disponíveis para o comando test-cmd.

Para evitar que o valor de uma variável apareça acidentalmente nos logs do job na interface do GitLab CI ou nos logs do runner é necessário marcar a variável como Masked. Esse comportamento vem habilitado por padrão no GitHub.

Condicionais

Uma boa esteira de CI deve ser rápida. Uma maneira de se fazer isso é evitar rodar jobs que não são necessários. As duas opções analisadas aqui oferecem formas de executar um job apenas se algumas condições forem satisfeitas.

O uso adequado de condicionais é a diferença entre uma esteira de CI que ajuda o time a entregar com velocidade e qualidade; e um pedágio caro que deve ser pago até para subir alterações pequenas.

Condicionais no GitLab CI

A chave rules pode ser usada na definição de jobs para definir condições para execução. Uma chave if pode ser usada para definir condições usando os valores das variáveis de ambiente. Exemplo:

job_with_condition:
  script: echo "I'll only run for MRs to main"
  rules:
    - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH

Esse job roda apenas se um Merge Request tiver como alvo a branch padrão do repositório. Qualquer variável pode ser usada para compor essas condições, há suporte para && (and) e || (or), comparação com regex usando =~ e !~, bem como comparações com == ou !=.

A chave changes pode ser usada para definir um job que executa apenas quando há alterações em um ou alguns arquivos específicos:

ruby_tests:
  image: ruby:3.0.2-slim-bullseye
  script:
    - bundle exec rake spec
  rules:
    changes:
      - "**/*.rb"

No exemplo acima os testes de Ruby rodam apenas quando houver alguma alteração em arquivos com extensão .rb. Uma atualização de arquivos de documentação, alguns arquivos .md por exemplo, não acionaria esse job.

Usando if e changes dentro das rules de um job é possível ter um controle bem preciso dos jobs executados em uma esteira.

Condicionais no GitHub Actions

O GitHub Actions também suporta condições com if, mas a curva de aprendizado é um pouco maior. Ao invés de variáveis de ambiente e operações, são usados objetos e funções. As expressões lembram menos Shell script e mais JavaScript. É necessário consultar a documentação para ver as funções e o formato (ou schema) dos objetos disponíveis para compor suas condições.

Enquanto no GitLab CI uma visita à página com a tabela de variáveis disponíveis é o suficiente para começar a escrever, no GitHub Actions você pode perder um certo tempo navegando entre páginas de documentação para descobrir qual propriedade de qual objeto você precisa para escrever uma condição. Exemplo:

on: pull_request
jobs:
  job_with_condition:
    if: ${{ github.event.pull_request.base.ref == github.event.repository.default_branch }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "I'll only run for PRs to main"

Esse job roda apenas se um Pull Request tiver como alvo a branch padrão do repositório. Note o acesso aos objetos aninhados. Mais um exemplo:

on: pull_request
jobs:
  job_with_condition:
    if: ${{ startsWith(github.event.pull_request.head.ref, 'feat') }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "I'll only run for feature branches"

No exemplo acima foi usado a função startsWith para executar o job apenas para pull requests de branches que começam com "feat".

A condição de arquivos alterados está atrelado ao evento de push ou pull_request e pode ser determinada apenas na raiz da esteira. Ou seja, não há recursos nativos na plataforma para pular um job ou um passo de acordo com os arquivos alterados, apenas a esteira inteira. Exemplo:

on:
  pull_request:
    paths:
      - '**.rb'

jobs:
  job1:
  # (...)
  job2:
  # (...)
  job3:
  # (...)

No exemplo acima todos os jobs definidos são ignorados se não houver alterações em algum arquivo de extensão .rb.

Há basicamente 3 formas de lidar com essa limitação:

  1. Organize sua esteira em múltiplos arquivos de workflow com escopo pequeno
  2. Use um pouco de Shell script e git para construir a condição manualmente
  3. Use uma solução construída pela comunidade

Agendamento de execução

Além dos casos mais comuns em que uma esteira de CI roda quando um novo commit é empurrado para o repositório ou um Merge Request é aberto, também é possível agendar execuções periódicas. Isso pode ser útil para executar testes mais caros apenas 1 vez por semana ao invés de várias vezes por dia, por exemplo.

A maior diferença aqui é a forma de configurar esses agendamentos: enquanto no GitHub Actions isso é feito nos arquivos de configuração, no GitLab CI as execuções são agendadas através da interface.

Como o controle de acesso é mais rígido na interface do que nos arquivos que entram no projeto, a leitura que deve ser feita aqui é a seguinte: no GitHub Actions mais pessoas do time de desenvolvimento tem poder de criar agendamentos, no GitLab CI há um controle mais fino na gestão de agendamentos.

Ordem de execução dos jobs ou arquitetura da esteira

Os dois serviços permitem especificar qual a ordem que os jobs são executados numa esteira e quais as dependências de cada job. O GitLab CI oferece mais opcões de como fazer isso, enquanto o GitHub oferece apenas uma forma, que também está disponível no GitLab CI.

Então o GitLab CI é melhor aqui, certo? Não necessariamente. Apesar de ter mais opções, inclusive uma similar a do GitHub Actions, essa única forma de declarar dependências é a melhor para maximizar o paralelismo dos jobs executados.

Enquanto no GitLab CI você tem mais opções para construir sua esteira, no GitHub Actions é mais fácil você chegar numa esteira rápida.

Ordem dos jobs no GitHub Actions

Para controlar a ordem de execução dos jobs no GitHub Actions você usa a chave needs passando a lista de jobs que são dependências do job. Exemplo:

jobs:
  build_frontend:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building frontend"

  build_backend:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building backend"

  test_frontend:
    runs-on: ubuntu-latest
    needs: [build_frontend]
    steps:
      - run: echo "Test the frontend"

  test_backend:
    runs-on: ubuntu-latest
    needs: [build_backend]
    steps:
      - run: echo "Test the backend"

  test_e2e:
    runs-on: ubuntu-latest
    needs: [build_frontend, build_backend]
    steps:
      - run: echo "Test the complete app"

  deploy:
    runs-on: ubuntu-latest
    needs: [test_frontend, test_backend, test_e2e]
    steps:
      - run: echo "Deploy the validated app"

No exemplo acima, há uma aplicação dividida entre frontend e backend, as builds dessas partes podem rodar em paralelo e os testes das partes individuais também (após cada build), mas o teste da aplicação completa depende da build das duas partes; e o deploy depende do sucesso de todos os testes.

Para um exemplo de esteira pequeno como esse, não é muito difícil descobrir quais jobs podem executar em paralelo e quais devem esperar os outros antes de começar, mas esse trabalho não é necessário. A partir dessas declarações de dependências o GitHub Actions constroi um grafo direcionado acíclico que descreve a esteira de CI e executa todos os jobs possíveis assim que possível.

O exemplo acima aparece assim na interface:

Visualização da esteira de CI no GitHub

Ordem dos jobs no GitLab CI

O GitLab CI oferece 3 formas principais de organizar as esteiras de CI: básica com estágios (stages), com grafos direcionado acíclico (DAG) e esteiras Child/Parent.

O exemplo abaixo replica o exemplo usado no GitHub Actions usando os stages do GitLab CI.

stages:
  - build
  - test
  - deploy

build_frontend:
  stage: build
  script:
    - echo "Building frontend"

build_backend:
  stage: build
  script:
    - echo "Building backend"

test_frontend:
  stage: test
  script:
    - echo "Test the frontend"

test_backend:
  stage: test
  script:
    - echo "Test the backend"

test_e2e:
  stage: test
  script:
    - echo "Test the complete app"

deploy:
  stage: deploy
  script:
    - echo "Deploy the validated app"

O exemplo acima aparece assim na interface:

Visualização da esteira de CI no GitLab

Um problema dessa abordagem é que todos os jobs de cada estágio devem terminar antes de começar os jobs do próximo. Isso pode ter um grande impacto se o tempo dos jobs de um mesmo estágio forem muito diferentes, um job mais lento de um estágio atrasa toda a esteira.

Para resolver esse problema o GitLab CI oferece também a construção de esteiras com DAGs, basta adicionar a chave needs em alguns locais do exemplo acima:

 test_frontend:
   stage: test
+  needs: [build_frontend]
   script:
     - echo "Test the frontend"

 test_backend:
   stage: test
+  needs: [build_backend]
   script:
     - echo "Test the backend"

 test_e2e:
   stage: test
+  needs: [build_frontend, build_backend]
   script:
     - echo "Test the complete app"

 deploy:
   stage: deploy
+  needs: [test_frontend, test_backend, test_e2e]
   script:
     - echo "Deploy the validated app"

Com essa pequena alteração os jobs agora executam assim que possível. E na interface é possível ver as dependências entre os jobs:

Visualização da esteira de CI no GitLab

O GitLab CI também oferece uma visualização alternativa, que é interativa e desenha a sua esteira como uma pipeline mesmo:

Visualização interativa da esteira de CI no GitLab

Uma terceira opção para arquitetar esteiras de CI no GitLab é usar as esteiras Child/Parent. Esse padrão é útil para repositórios que armazenam diversos componentes num mesmo lugar, mas que poderiam ter esteiras de CI distintas.

A esteira Parent do repositório (definida no arquivo .gitlab-ci.yml principal do repositório) pode iniciar outras esteiras Child, definidas em outros arquivos, através da chave trigger. Essa ativação através do trigger pode usar as regras para execução condicional de jobs. Isso permite organizar esteiras de projetos complexos de maneira bem eficiente. Observe o exemplo abaixo:

stages:
  - triggers

trigger_service1:
  stage: triggers
  trigger:
    include: service1/.gitlab-ci.yml
  rules:
    - changes:
      - service1/*

trigger_service2:
  stage: triggers
  trigger:
    include: service2/.gitlab-ci.yml
  rules:
    - changes:
      - service2/*

Neste exemplo, essa esteira Parent (.gitlab-ci.yml na raiz do repositório) orquestra múltiplas esteiras Child de acordo com as alterações. Cada serviço alterado nesse exemplo tem sua própria esteira, que é ativada apenas quando há alterações em seus arquivos. A esteira do service1, definida em service1/.gitlab-ci.yml é executada apenas quando há alguma alteração nos arquivos da pasta service1.

Serviços Extras

Para rodar os testes de integração automatizados de uma aplicação, pode ser necessário subir um banco de dados, um cache, um cofre de segredos ou algum outro serviço extra. As duas opções analisadas neste post permitem subir serviços para usar nas esteira, usando containers Docker.

Exemplo que sobe um PostgreSQL:

GitLab CI/CDGitHub Actions

services:
  - postgres

variables:
  POSTGRES_DB: custom_db
  POSTGRES_USER: custom_user
  POSTGRES_PASSWORD: custom_pass

connect:
  image: postgres
  script:
  - |
    export PGPASSWORD=$POSTGRES_PASSWORD
    psql \
      -h "postgres" \
      -U "$POSTGRES_USER" \
      -d "$POSTGRES_DB" \
      -c "SELECT 'OK' AS status;"

jobs:
  connect:
    runs-on: ubuntu-latest
    container: postgres
    services:
      postgres:
        image: postgres
        env:
          POSTGRES_DB: custom_db
          POSTGRES_USER: custom_user
          POSTGRES_PASSWORD: custom_pass
    steps:
      - name: Connect to PostgreSQL
        run: |
          psql \
            -h "postgres" \
            -U "$POSTGRES_USER" \
            -d "$POSTGRES_DB" \
            -c "SELECT 'OK' AS status;"
        env:
          POSTGRES_DB: custom_db
          POSTGRES_USER: custom_user
          POSTGRES_PASSWORD: custom_pass

No exemplo acima o job também é executado dentro de um container PostgreSQL, que faz apenas um teste de conexão.

Esse provisionamento automático de serviços é feito apenas para containers Docker. Portanto, no GitLab CI está disponível apenas para jobs que executam em runners Docker e no GitHub Actions está disponível apenas para runners Linux com Docker instalado. Não é preciso se preocupar com isso para runners gerenciados.

Para usar um serviço extra que não esteja containerizado, é necessário deixar o serviço pré-configurado num runner self-hosted ou configurá-lo durante o tempo de execução da esteira de CI. No entanto, a natureza efêmera e isolamento dos containers Docker é ideal para subir esses serviços adicionais. Além do atalho na configuração, o gerenciamento dos serviços fica muito mais conveniente.

Cache e Artefatos

Tanto o GitHub Actions quanto o GitLab CI suportam armazenar arquivos como cache ou artefatos entre jobs. Configurar a esteira para cachear as dependências do projeto pode acelerar execuções subsequentes. Subir artefatos pode ser útil para analisar arquivos gerados durante a esteira de CI: screenshots de testes de interface, relatório de cobertura de testes ou arquivos de log, por exemplo.

Cache

O cache é usado dentro da esteira de CI, é muito útil para evitar baixar novamente as dependências da aplicação, por exemplo.

O GitHub Actions não suporta cache em runners self-hosted, apenas nos runners gerenciados pelo próprio GitHub. Arquivos que não foram acessados nos últimos 7 dias são removidos e caso o total de arquivos armazenados ultrapasse 5GB, os mais antigos são removidos.

No GitLab CI a documentação não cita por quanto tempo os runners gerenciados mantém o cache ou quanto pode ser armazenado. Nos runners self-hosted depende da configuração feita no runner.

Para cachear as dependências de um projeto que usa yarn:

GitLab CI/CDGitHub Actions

test_app:
  stage: test
  cache:
    - key:
        files:
          - yarn.lock
      paths:
        - .yarn-cache/
  script:
    - yarn install --cache-folder .yarn-cache
    - yarn test

jobs:
  test_app:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/cache@v2
      with:
        path: .yarn-cache/
        key: yarn-deps-${{ hashFiles('**/yarn.lock') }}
        restore-keys: yarn-deps-
    - run: yarn install --cache-folder .yarn-cache
    - run: yarn test

Artefatos

Os artefatos podem ser baixados fora da esteira de CI.

No GitHub Actions eles são retidos por 90 dias por padrão, em repositórios públicos isso pode ser alterado para algo entre 1 e 90 dias, para repositórios privados pode variar entre 1 e 400 dias. Esse valor é configurado através da chave retention-days. No plano gratuito há uma cota de 500MB por projeto. Essa cota é consumida apenas por artefatos e pacotes armazenados.

No GitLab CI os artefatos são retidos por 30 dias por padrão. Esse valor pode ser alterado para todos os projetos para instâncias privadas do GitLab. Também é possível alterar esse valor para cada job através da chave expire_in. Não há restrição ao tempo de retenção. No plano gratuito o GitLab oferece uma cota de 10GB, porém qualquer informação persistida do projeto contabiliza para essa cota: texto das issues, código fonte, anexos, artefatos, logs, etc.

Para disponibilizar o arquivo coverage/index.html após a execução da esteira de CI:

GitLab CI/CDGitHub Actions

test_with_coverage:
  script: run-tests --coverage-report=coverage/index.html
  artifacts:
    paths:
      - coverage/index.html

jobs:
  test_with_coverage:
    runs-on: ubuntu-latest
    steps:
    - run: run-tests --coverage-report=coverage/index.html
    - uses: actions/upload-artifact@v2
      with:
        name: coverage-report
        path: coverage/index.html

Diferenças

Conforme demonstrado acima, essas duas ferramentas são semelhantes em vários aspectos. Oferecem as mesmas capacidades com algumas variações pequenas nos arquivos de configuração.

Agora será discutido alguns pontos em que o GitHub Actions e o GitLab CI são muito diferentes.

Organização dos arquivos

Ambas as plataformas são configuradas usando arquivos YAML, no entanto o GitHub Actions não suporta algumas funcionalidades da própria especificação desse formato de arquivo, como o separador de documentos --- e anchors &:

Somado com a limitação de definir um workflow por arquivo, se não for tomado um cuidado para usar as Actions adequadamente o resultado pode ser múltiplos arquivos grandes e com bastante repetição.

No GitLab CI a chave include permite importar definição de partes de uma esteira de CI de outros arquivos, que podem estar em outra pasta, outro repositório ou até mesmo numa URL qualquer. Isso dá liberdade para organizar os arquivos da esteira da forma que se desejar.

Retry

No GitLab CI, através da interface, é possível rodar novamente um job clicando em um botão. Pedir para reexecutar um job é algo bem comum numa esteira de CI, uma falha pode ter ocorrido temporariamente e um simples retry resolve o problema.

No GitHub Actions não é possível dar retry em um job individual, você deve dar retry no workflow inteiro. Ao clicar em "Re-run jobs" a esteira inteira vai rodar novamente. Corrigir essa limitação é algo pedido desde o lançamento do GitHub Actions em 2019:

Para evitar problemas é melhor criar vários arquivos de workflows que executam rapidamente. Preferir várias esteiras rasas ao invés de uma esteira profunda.

Marketplace

A maior diferença entre o GitHub Actions e outros sistemas de CI é sem dúvidas o marketplace de Actions. No meio da definição de suas esteiras com um simples uses você economiza um bom tempo: não precisa escrever várias linhas de Shell script no meio de um YAML ou empacotar algo num container Docker.

O código das Actions fica no próprio GitHub, portanto vários vendors e organizações oferecem actions oficiais para incluir suas ferramentas numa esteira de CI. Hoje existe quase 10.000 Actions disponíveis para construir esteiras de CI mais rápido. Seu próprio time de desenvolvimento pode criar Actions para diversos fins.

Além disso, limitações que existem na plataforma podem ser superadas usando Actions desenvolvidas pela comunidade. É uma plataforma aberta para extensões.

Versões e Edições

O modelo de hospdeagem do GitLab é bem mais flexível do que o do GitHub. Existe uma instância pública do SaaS em gitlab.com e a sua organização pode hospedar uma instância privada do GitLab, em diversas edições. Como o GitLab é um software Open Source existe até uma edição que você pode hospedar sem pagar nada para a GitLab. O GitLab CI faz parte do GitLab, portanto a versão e edição usada no GitLab e GitLab CI são a mesma.

Essa flexibilidade de como gerenciar instâncias de GitLab reflete na documentação: você deve ficar atento com os avisos de qual versão do GitLab ganhou uma funcionalidade e se ela está disponível na licença que você usa.

Ao lado do nome de uma funcionalidade tem uma etiqueta identificando a licença necessária para usar: premium ou all tiers, por exemplo. E abaixo do nome da funcionalidade tem um histórico de mudanças significativas e em qual versão do GitLab essa mudança entrou: "Introduced in GitLab 11.4", por exemplo.

Se a sua organização usa a versão SaaS ou mantém o GitLab sempre atualizado, isso não é uma preocupação grande. Basta saber a edição usada (licença) e conferir a documentação.

Qual é o sistema ideal para o seu time?

O GitHub Actions e o GitLab CI são bons sistemas de integração contínua. Você provavelmente vai conseguir construir uma esteira adequada para as necessidades de seus projetos independente da escolha. Mas ter essas diferenças em mente pode ajudar a fazer uma escolha melhor para seu time e seus projetos.

A sintaxe do GitHub Actions lembra menos Shell script e mais JavaScript, incentiva o empacotamento de funcionalidades em repositórios git e tem opções mais limitadas para a organização das esteiras. Essas limitações no entanto, ajudam a evitar decisões ruins.

Para o GitLab CI é útil saber Shell script e se sentir confortável containerizando aplicações. Há muito mais opções de como organizar as esteiras e flexibilidade para definir permissionamento e controle de acesso. Com a opção de gerenciar tanto a instância de GitLab quanto os runners do CI, é uma opção excelente para o cenário enterprise.

Portanto, se a maioria do seu time for composta por devs, o GitHub Actions pode ser a melhor opção. Se algumas pessoas do seu time tem "DevOps" ou "SRE" no título do cargo, o pontencial do GitLab CI pode ser melhor explorado.

Por ser uma solução mais madura, o GitLab CI é ideal para projetos com muitas partes móveis e/ou processo de release mais exigentes: com etapas de aprovação manual, ajuste fino de permissões, múltiplos ambientes, etc.

Tabela Comparativa
Funcionalidade GitLab CI GitHub Actions
CI Nativo ✔️ ✔️
Jobs ✔️ ✔️
Minutos inclusos 400 para repositórios privados, 50.000 para públicos 2.000 para repositórios privados, ilimitado para públicos
Runners self-hosted ✔️ ✔️
Runners Linux Gerenciados 1 vCPU, 3,75GB de RAM e 25GB de HDD 2 vCPU, 7GB de RAM e 14GB de SSD
Runners Windows Gerenciados Beta. 2 vCPU e 7,5GB de RAM 2 vCPU, 7GB de RAM e 14GB de SSD
Runners MacOS Gerenciados Beta. 4 vCPUs e 10GB de RAM 3 vCPU, 14GB de RAM e 14GB de SSD
Docker ✔️ ✔️
Variáveis de Ambiente e Secrets ✔️ ⭐ Usa padrões mais seguros
Condicionais ⭐ Mais fácil de aprender e mais flexível ✔️
Execução Agendada Mais restrito, com maior controle Menos restrito, mais disponível
Arquitetura da esteira Oferece mais opções, tem mais flexibilidade Oferece apenas uma opção, que é performática
Serviços Extras Apenas para runners Docker ✔️
Cache ✔️ Apenas para runners gerenciados
Artefatos 30 dias por padrão, cota compartilhada de 10GB 90 dias por padrão, cota exclusiva de 500MB
Organização dos arquivos Muito flexível Uma esteira por arquivo
Retry ✔️ Apenas para a esteira inteira
Marketplace ✔️

Construindo esteiras de CI em segundos

Este post não cobre todas as funcionalidades que existem nesses dois sistemas de CI, e ainda existem outros. Construir uma esteira de CI eficiente pode ser trabalhoso, exigir horas para ler documentação e testar a esteira. Como o feedback loop do desenvolvimento de uma esteira de CI é lento, bem mais devagar do que o loop do TDD, por exemplo, as primeiras tentativas podem ser bem frustrantes.

Além disso, vários projetos compartilham características similares, que poderiam ser descritas por estágios bem padronizados de esteiras de CI. Com a evolução dos projetos de software e dos próprios sistemas de CI, a esteiras de CI se tornam parte do projeto que também precisam de manutenção e atualização constante.

Pensando nesses problemas, a Instruct começou o desenvolvimento do Pipelinit, uma ferramenta gratuita e Open Source que gera esteiras de CI completas em segundos. Disponível para uso através de um CLI ou um Playground Online. Confira o projeto!

Para saber mais

Referências

Solução de problemas, melhoria de gestão e aumento de produtividade

Com tecnologias renomadas, aplicamos práticas ágeis e capacitamos sua equipe.

Fale com um consultor