Pular para o conteúdo
Quiz
← Início

Integração contínua

100 questões

Certas
0
Erradas
0
Não respondidas
100
Dificuldade
Número de questões

100 questões neste filtro

Resumo teórico

Imagine uma turma de dez crianças montando juntas um castelo gigante de Lego. Se cada criança trabalha sozinha por um mês inteiro e todo mundo tenta juntar as peças no último dia, nada se encaixa e todo mundo fica triste. Se, em vez disso, cada criança encaixa a sua peça no castelo aos poucos, e um amigo confere na hora se o castelo continua de pé, os problemas são pequenos e resolvidos em minutos. A integração contínua, chamada de CI, é exatamente essa ideia aplicada ao software: juntar pequenos pedaços de trabalho com frequência e deixar uma máquina conferir cada pedaço automaticamente.

A máquina segue uma receita escrita chamada . Ela compila o programa, confere o estilo do código, roda os testes e diz 'verde, tudo certo' ou 'vermelho, algo quebrou'. A mesma receita pode ir mais longe e entregar o programa aos usuários. Isso é o CD, sigla de entrega contínua ou implantação contínua. Você encontra CI e CD todos os dias: todo app do seu celular que recebe atualizações várias vezes por ano foi compilado, testado e entregue por um pipeline.

Neste resumo você vai aprender: o que são CI e CD e as regras que fazem tudo funcionar, como ler um workflow do GitHub Actions (a ferramenta de pipeline usada por este próprio repositório), como rodar jobs em vários sistemas ao mesmo tempo, como economizar tempo com caches e passar arquivos com artefatos, como manter senhas seguras, como compartilhar pedaços de pipeline, como construir portões que barram mudanças ruins, como lançar novas versões sem assustar os usuários, como funcionam números de versão e changelogs, e como proteger o pipeline de atacantes. No final você estará pronto para o quiz.

CI e CD: o panorama geral

Voltando ao castelo de Lego. A forma antiga de trabalhar se chama integração big-bang: todo mundo constrói sozinho por semanas e junta tudo no final. A (em inglês, mainline) é o lugar onde todo o trabalho se encontra. Na integração contínua, cada desenvolvedor junta seu trabalho na linha principal com frequência, pelo menos uma vez por dia, e cada junção dispara um automático mais uma rodada de testes. Os problemas aparecem minutos depois de criados, enquanto a pessoa ainda lembra o que fez.

A CI é um hábito do time, não uma máquina. Um servidor que compila todo branch, enquanto os branches ficam separados por duas semanas, não é CI de verdade, porque a junção real continua acontecendo tarde. O servidor só ajuda o hábito.

As regras que fazem a CI funcionar

  • Junte pequeno e com frequência, pelo menos uma vez por dia, para que cada junção seja pequena e os conflitos sejam raros.
  • Mantenha a linha principal verde. Se o build quebrar, consertá-lo ou desfazer a mudança (um revert) vem antes de qualquer trabalho novo, e ninguém acrescenta mais mudanças em cima de um build vermelho.
  • Falhe rápido. Rode primeiro as verificações baratas e rápidas, e depois as lentas e caras. Uma rodada de testes unitários de dois minutos deve vir antes de uma rodada de testes de ponta a ponta de quarenta minutos, porque a maioria dos erros é pega pela rápida.
  • Mantenha tudo no controle de versão: código, testes, scripts de build e de deploy, a configuração de cada ambiente e o próprio pipeline. Senhas são a única exceção: o repositório apenas faz referência a elas, não as guarda.
  • Use um único processo de deploy para todos os ambientes. Staging e produção devem ser implantados pelo mesmo script automático, e só a configuração muda. Um deploy de produção feito à mão a partir de uma página de wiki depende da memória, e um passo esquecido quebra tudo na pior hora.

Da CI ao CD

Cada degrau precisa do anterior. Implantar sem CI seria só entregar erros mais rápido.
PráticaO que significaQuem aperta o botão
Integração contínuaToda mudança é juntada na linha principal e verificada por um build e testes automáticosNinguém, é automático
Entrega contínua (continuous delivery)O software está sempre pronto para ser lançado, porque toda mudança passa por todo o pipelineUma pessoa decide quando lançar (uma decisão de negócio)
Implantação contínua (continuous deployment)Toda mudança que passa no pipeline é lançada aos usuários na horaNinguém, não há portão humano

O pipeline de implantação

O livro Continuous Delivery (Humble e Farley) chama a receita de deployment pipeline (pipeline de implantação): um caminho automático que leva uma mudança do controle de versão até os usuários. O caminho tem estágios que custam mais e dão mais confiança conforme você avança. Uma mudança ou falha cedo, com feedback, ou vira um release candidate (candidata a lançamento).

commit  -->  testes de   -->  outras    -->  lançamento
stage        aceitação        verificações
(minutos)    (mais lento)     (opcional)

  falhou aqui = o dev é avisado em minutos
  passou = o mesmo artefato vai para o próximo estágio
Estágios de um pipeline de implantação

O primeiro estágio é o commit stage. Ele compila o código, roda os testes unitários e a análise rápida do código, e produz o artefato (o programa empacotado). Precisa ser rápido, alguns minutos, para que os desenvolvedores possam esperar a resposta antes de seguir em frente. Depois vem uma regra de ouro: construa os binários uma vez só, guarde o artefato e leve esse mesmo artefato por todos os estágios seguintes. Um programa reconstruído é um programa diferente, porque versões de ferramentas e configurações podem mudar entre builds. As diferenças entre ambientes são resolvidas com configuração, nunca construindo de novo.

E se uma funcionalidade está pela metade, mas você precisa juntar todo dia? No desenvolvimento baseado em trunk (trunk-based development) todo mundo trabalha na linha principal, então o trabalho inacabado precisa ser seguro para ir ao ar. Uma feature flag (também chamada de feature toggle) é um interruptor no código: o código novo é juntado e até implantado, mas fica desligado para os usuários. Remova a flag quando a funcionalidade for lançada, ou as flags se acumulam como dívida técnica.

↑ Voltar ao sumário

GitHub Actions: workflows, eventos, jobs e steps

O GitHub Actions é a ferramenta que roda o pipeline deste repositório. Pense numa cozinha. A receita é um workflow. A receita começa quando algo acontece, como um cliente fazendo um pedido: isso é um evento. A receita é dividida em jobs, cada um feito por um cozinheiro diferente numa cozinha diferente, e cada job é uma lista de steps (passos) feitos um depois do outro.

Um workflow é um arquivo guardado na pasta .github/workflows/ do repositório. Como ele mora no repositório, qualquer mudança no pipeline é revisada e guardada no histórico como o resto do código.

Eventos: o que inicia um workflow

A chave on lista os eventos. Os mais comuns são push (alguém enviou commits), pull_request (um pedido para juntar um branch foi aberto ou atualizado), workflow_dispatch (uma pessoa inicia à mão pela aba Actions, e pode preencher um formulário de inputs tipados) e schedule (um cronômetro). Um filtro como branches, tags ou paths pertence ao evento sob o qual está escrito.

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:
  schedule:
    - cron: '30 5 * * 1'
Quatro gatilhos. Sem 'types', pull_request usa por padrão opened, synchronize e reopened

No exemplo, o filtro do push vale só para o push, então um pull request em qualquer branch ainda inicia o workflow. Um pull request sem types reage a três atividades: opened, synchronize (novos commits foram enviados para ele) e reopened.

O evento schedule usa a sintaxe de : minuto, hora, dia do mês, mês, dia da semana. Os horários são em UTC, e a execução usa o último commit do branch padrão. Então 30 5 * * 1 significa segunda-feira às 05:30 UTC. Execuções agendadas podem atrasar quando o GitHub está ocupado, então não são exatas ao minuto.

Jobs e steps

  • Um job roda na sua própria máquina nova. Todo job comum precisa de runs-on para dizer que tipo de máquina ele quer.
  • Jobs rodam em paralelo por padrão. needs cria uma ordem: um job espera os jobs que lista, e é pulado se algum deles falhar (a menos que o seu if diga outra coisa).
  • Um step é uma tarefa dentro de um job. Os steps do mesmo job rodam em ordem, na mesma máquina, compartilhando os mesmos arquivos.
  • Jobs são isolados uns dos outros. Eles trocam dados só de propósito, com outputs (saídas) ou artefatos.
jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]
  test:
    runs-on: ubuntu-latest
    steps: [...]
  deploy:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps: [...]
lint e test rodam juntos; deploy só começa depois dos dois

Um step faz uma de duas coisas. Com uses ele roda uma action pronta, uma peça reutilizável como actions/checkout, e você dá configurações a ela com with. Com run ele roda os seus próprios comandos de shell, como bun test. Um runner novo começa com uma pasta vazia, então a maioria dos jobs começa com actions/checkout, que baixa o commit que está sendo compilado (por padrão só aquele commit).

Contextos, condições e variáveis

Um texto escrito como ${{ ... }} é uma expressão. Ela pode ler contextos, que são sacolas de informação. O contexto github descreve o evento. Em um push, github.ref é algo como refs/heads/main. Em um pull request é refs/pull/<n>/merge, e github.head_ref e github.base_ref guardam o branch de origem e o de destino. Assim, if: github.ref == 'refs/heads/main' roda um step só depois de uma junção na main, e ele é pulado num pull request.

  • env define variáveis de ambiente para o workflow inteiro, para um job ou para um step. O nível mais específico vence para um dado nome: o step vence o job, e o job vence o workflow.
  • Sem um if, um step só roda se todos os steps anteriores tiveram sucesso (o padrão escondido é success()). failure() roda só depois de uma falha, e always() roda aconteça o que acontecer, o que é bom para enviar relatórios ou limpar recursos.
  • concurrency deixa só uma execução de um grupo andar por vez. Com cancel-in-progress: true, uma execução nova cancela a antiga do mesmo grupo. Agrupar por github.ref economiza tempo em branches que recebem muitos pushes.

Passando valores entre steps e jobs

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.meta.outputs.tag }}
    steps:
      - id: meta
        run: echo "tag=v1.2.3" >> "$GITHUB_OUTPUT"
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "${{ needs.build.outputs.tag }}"
Dois saltos: saída do step, depois saída do job, depois needs

Um step escreve uma linha nome=valor no arquivo cujo caminho está em $GITHUB_OUTPUT. Os steps seguintes do mesmo job leem como steps.<id>.outputs.<nome>, então o step que produz precisa de um id. Para chegar a outro job, o job precisa mapear o valor nas suas próprias outputs, e o outro job lê needs.<job>.outputs.<nome>. Se esquecer o mapeamento, você recebe uma string vazia.

↑ Voltar ao sumário

Runners e builds em matriz

Um runner é o computador que faz o trabalho de um job. Os runners hospedados pelo GitHub são feitas a partir de uma imagem mantida (Ubuntu, Windows ou macOS). Eles são efêmeros: uma máquina novinha é criada para um job e jogada fora quando o job termina. Isso deixa os builds limpos e repetíveis, mas também significa que todo job precisa instalar suas ferramentas de novo, e é por isso que o cache importa.

Rótulos como ubuntu-latest apontam para a imagem mais nova que o GitHub considera estável, e esse alvo muda com o tempo. Um rótulo com versão, como ubuntu-24.04, fica fixo até ser aposentado. Fixar troca atualizações automáticas por reprodutibilidade, e por isso este repositório escreve ubuntu-24.04.

Runners self-hosted

Você também pode usar a sua própria máquina como um runner self-hosted. Ele tem rótulos padrão (self-hosted, o seu sistema operacional e a sua arquitetura) mais rótulos personalizados. runs-on: [self-hosted, linux, gpu] é uma lista de rótulos exigidos: só um runner que tenha os três pode pegar o job.

A matriz

E se você quer testar seu programa em três sistemas operacionais e três versões do Node.js? Escrever nove jobs à mão é chato. Uma matriz (matrix) sob strategy lista variáveis e seus valores, e o GitHub cria um job para cada combinação (o produto cartesiano). Cada job lê seus próprios valores com o contexto matrix, até no runs-on.

test:
  strategy:
    matrix:
      os: [ubuntu-latest, macos-latest, windows-latest]
  runs-on: ${{ matrix.os }}
  steps:
    - uses: actions/checkout@v5
    - run: bun test

Duas variáveis com 2 e 3 valores dão 2 x 3 = 6 jobs. Uma única execução de workflow pode criar no máximo 256 jobs de matriz. Duas chaves extras mudam a lista:

  • exclude remove as combinações que ele casa.
  • include acrescenta chaves extras a uma combinação que casa. Se ele sobrescrevesse um dos valores originais, vira uma combinação nova própria.
  • A ordem é: primeiro o produto, depois as remoções do exclude, e por último as entradas do include. Contar jobs é seguir essa ordem.
os: [ubuntu, windows]  x  node: [20, 22]  = 4 jobs

 ubuntu-20   ubuntu-22   windows-20   windows-22

exclude windows+20  -->  3 jobs
include ubuntu+24   -->  4 jobs (24 não é um valor
                         original, então é uma nova combinação)
Contando uma matriz, passo a passo

Quando um job falha

Por padrão fail-fast é true: quando um job da matriz falha, os jobs que ainda esperam ou rodam são cancelados. Isso economiza tempo de runner quando a falha provavelmente se repetirá em todo lugar. Defina fail-fast: false para deixar cada combinação terminar, assim você vê exatamente quais plataformas falham.

Para uma versão experimental, como a próxima versão do Node.js, use continue-on-error: ${{ matrix.experimental }} no job. As falhas dessas combinações continuam visíveis mas não reprovam a execução, enquanto as falhas das combinações estáveis ainda reprovam.

↑ Voltar ao sumário

Cache e artefatos

Como todo runner hospedado começa vazio, cada execução baixaria os mesmos pacotes de novo e de novo. Duas ferramentas guardam arquivos fora do runner, por dois motivos diferentes.

CaracterísticaCacheArtefato
ObjetivoVelocidade: evitar repetir trabalho lentoLevar resultados: para jobs posteriores, para pessoas, para um lançamento
Reutilizado porMuitas execuções, achado por uma chaveUma execução do workflow
Se for perdidoSó custa tempo, o job recalculaO resultado se foi
Conteúdo típicoPacotes baixadosSaída do build, relatórios de teste, cobertura

Como o cache funciona

O actions/cache salva uma pasta sob uma chave (key) e a restaura numa execução posterior quando a chave casa. A chave deve dizer de que os arquivos dependem. A função hashFiles() transforma o conteúdo de um lockfile em texto, então a chave muda exatamente quando as dependências mudam. Incluir runner.os mantém separados os caches de sistemas diferentes.

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: deps-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      deps-${{ runner.os }}-
Um acerto exato exige o mesmo SO e o mesmo conteúdo do lockfile
  • Acerto exato (exact hit): existe um cache com exatamente essa key. Ele é restaurado, e nada novo é salvo.
  • Acerto parcial (partial hit): a chave exata falhou, então a action tenta os prefixos de restore-keys em ordem e restaura um cache mais antigo mas parecido. Isso costuma ser bem mais rápido que começar do zero. Como a chave exata falhou, um cache novo é salvo no final, e a próxima execução tem um acerto exato.
  • Falha (miss): nada casa. O job faz o trabalho lento e salva um cache novo.

Uma entrada de cache é imutável: depois que uma chave é salva, ela nunca muda. Então uma chave constante como deps-${{ runner.os }} é uma armadilha. A primeira execução a salva, todas as seguintes acertam nela, e o cache fica velho em silêncio. A chave precisa mudar sempre que o conteúdo do cache deveria mudar.

O acesso ao cache é limitado por branch. Uma execução pode restaurar caches do seu próprio branch e do branch padrão (para pull requests, também do branch base). Um cache salvo na main ajuda todos os branches, enquanto um cache salvo num branch de funcionalidade ajuda só aquele branch. As actions de configuração de linguagem podem fazer tudo isso por você: cache: npm no actions/setup-node escolhe a pasta e monta a chave a partir do lockfile.

Como os artefatos funcionam

Um job envia arquivos com actions/upload-artifact e outro job os baixa com actions/download-artifact. Essa é a forma certa de passar uma pasta dist/ de um job build para um job deploy, e também guarda saídas que as pessoas podem baixar. É a ideia de construir uma vez em ação: os arquivos implantados são os arquivos que foram construídos e testados.

- uses: actions/upload-artifact@v4
  with:
    name: coverage-${{ matrix.os }}
    path: coverage/
    retention-days: 1
Um nome único por job da matriz, e retenção curta
  • Desde a versão 4, os artefatos são imutáveis e seus nomes precisam ser únicos dentro de uma execução. Jobs de matriz que enviam devem pôr valores da matriz no nome. Um job posterior pode baixá-los por padrão e juntá-los.
  • Artefatos e logs são guardados por 90 dias por padrão. Administradores podem mudar isso, e um envio pode pedir menos dias com retention-days. Retenção menor economiza armazenamento, que conta na sua cota.
↑ Voltar ao sumário

Segredos, ambientes e permissões

Um pipeline precisa de senhas e chaves: para publicar um pacote, para entrar num servidor. Isso são segredos (secrets). Se vazarem, qualquer um pode se passar por você. Por isso o pipeline segue a ideia do menor privilégio: cada job recebe só os poderes de que realmente precisa, e nada além.

Segredos e variáveis

Segredos ficam guardados criptografados nas configurações de um repositório, de uma organização ou de um ambiente, e são lidos com ${{ secrets.NOME }}. O padrão usual é mapear um deles para env só no step que precisa. O GitHub esconde o valor exato de um segredo nos logs, trocando-o por ***. Mas esse esconder compara o texto exato, então uma forma alterada (codificada, quebrada, embrulhada em JSON) ainda pode vazar. Você pode pedir mais mascaramento com ::add-mask::, mas o melhor plano é nunca imprimir segredos.

Nem tudo é segredo. O nome de uma região da nuvem é só configuração. Coloque-o numa variável e leia como ${{ vars.NOME }}. Segredos e variáveis podem ser definidos por ambiente, então o mesmo workflow pega valores diferentes para staging e produção.

O GITHUB_TOKEN e as permissões

Para cada job o GitHub cria uma chave temporária, o GITHUB_TOKEN. Ele vale só para o repositório atual e expira quando o job termina, então é mais seguro que um token de acesso pessoal. Seus poderes vêm da chave permissions. Assim que você lista um escopo, tudo o que você não listou vira none.

permissions: {}
jobs:
  test:
    runs-on: ubuntu-latest
    steps: [...]
  release:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps: [...]
Nenhum poder no topo, poder extra só onde é preciso

Aqui o job test não recebe permissão nenhuma, então nem um step sequestrado consegue escrever no repositório. Um bloco permissions em um job substitui o do nível do workflow para aquele job. O workflow deste repositório começa com contents: read.

Mais um detalhe: coisas feitas com o GITHUB_TOKEN (enviar um commit, abrir um pull request, criar uma tag) não iniciam novas execuções de workflow. Isso evita laços infinitos em que workflows disparam uns aos outros. Se você precisa encadear, use workflow_dispatch, workflow_run ou um token de um GitHub App.

Ambientes

Um ambiente (environment, como production) é um lugar nomeado para implantar, com suas próprias regras, segredos e variáveis. Um job entra nele com environment: production. Então:

  • Revisores obrigatórios (required reviewers): uma pessoa precisa aprovar antes de o job começar. É a forma usual de acrescentar uma decisão manual de lançamento à entrega contínua.
  • Regras de branch: só branches ou tags escolhidos podem implantar ali. O GitHub verifica isso por conta própria, então vale mesmo que alguém edite o workflow num branch de funcionalidade.
  • Segredos do ambiente: liberados só a jobs que citam o ambiente, depois que as regras dele são cumpridas. Um job sem a chave environment: lê um valor vazio para eles.

Pull requests vindos de forks

Um envia código de fora da fronteira de confiança. Por isso, para o evento pull_request, os workflows de forks rodam sem segredos e com um token somente leitura. Isso é de propósito.

OIDC: segredos sem segredos

Para implantar numa nuvem, você não precisa guardar uma chave de longa duração. Com o OpenID Connect (OIDC) o job pede ao GitHub um token de identidade curto e o mostra à nuvem, que só confia nele se afirmações (claims) como o repositório e o ambiente baterem com a sua política. O job precisa de permissions: id-token: write para pedir esse token. As credenciais são de curta duração, então não há chave para vazar nem para rotacionar.

↑ Voltar ao sumário

Actions próprias e workflows reutilizáveis

Quando você copia as mesmas dez linhas para vinte workflows, um dia vai esquecer de consertar uma delas. O GitHub oferece duas formas de escrever algo uma vez e usar muitas vezes, e elas funcionam em tamanhos diferentes.

Actions próprias

Uma action é uma unidade reutilizável de trabalho que um step chama com uses. Ela mora numa pasta com um arquivo de metadados chamado action.yml (ou action.yaml). Esse arquivo declara as entradas (inputs) que ela aceita (com descrição, valor padrão e se é obrigatória), as saídas (outputs) que ela produz, e a seção runs que diz como ela executa. Existem três tipos:

TipoComo rodaOnde rodaBom para
JavaScriptCódigo Node.jsLinux, Windows e macOSRápida e portátil
Contêiner DockerDentro de uma imagem que você constróiSó em runners LinuxControle total do ambiente, mas demora mais para iniciar
Composta (composite)Uma lista de steps, que podem chamar outras actionsOnde seus steps puderem rodarJuntar alguns steps num pacote

Uma action composta roda dentro do job de quem a chama, no runner de quem a chama. Ela tem duas regras famosas. Primeiro, cada step run precisa declarar o seu shell explicitamente, porque steps compostos não têm shell padrão. Segundo, uma action composta não entrega um valor por mágica: os steps de dentro têm saídas privadas, então o action.yml precisa mapear uma saída com value: apontando para a saída de um step interno.

outputs:
  version:
    description: Computed version
    value: ${{ steps.calc.outputs.version }}
runs:
  using: composite
  steps:
    - id: calc
      shell: bash
      run: echo "version=1.4.0" >> "$GITHUB_OUTPUT"
O mapeamento da saída e o shell explícito

Workflows reutilizáveis

Um workflow reutilizável (reusable workflow) é um arquivo de workflow normal que tem workflow_call no seu on. Ele declara inputs tipados e os secrets de que precisa. Quem chama o executa como um job com uses: no nível do job, apontando para dono/repo/.github/workflows/<arquivo>@<ref> (ou um caminho local). As entradas vão em with. Os segredos vão um a um em secrets, ou todos de uma vez com secrets: inherit dentro da mesma organização. Um job chamador que usa um workflow reutilizável não pode ter steps próprios.

jobs:
  ci:
    uses: my-org/shared/.github/workflows/ci.yml@v1
    with:
      node-version: 22
    secrets: inherit

Um workflow reutilizável é isolado de quem o chama. Só entram os inputs e secrets declarados, e suas saídas voltam. O env de quem chama não é repassado, então uma variável definida no topo do chamador aparece vazia dentro do workflow chamado.

Qual escolher?

CaracterísticaAction compostaWorkflow reutilizável
O que empacotaAlguns stepsJobs inteiros
Onde rodaDentro do job e do runner de quem chamaEm jobs próprios, com runners próprios
Pode terSó stepsMatrizes, ambientes, segredos, vários jobs
Melhor paraUma receita pequena que se repeteUm pipeline padrão compartilhado por muitos repositórios

Versões de actions

Actions são versionadas com refs do Git. Um hábito comum é publicar tags exatas como v4.2.1 e também uma tag de major móvel v4 que o autor vai movendo para a frente. Quem usa pode escolher @v4 (atualizações compatíveis automáticas), @v4.2.1 (um lançamento exato) ou um SHA de commit completo (imutável). Cada escolha troca conveniência por controle.

↑ Voltar ao sumário

Portões de qualidade

No aeroporto você passa pela conferência do bilhete, pela segurança e pelo portão de embarque. Se qualquer uma disser não, você para. Um portão de qualidade (quality gate) é uma verificação no pipeline que uma mudança precisa passar para seguir. Os portões baratos e rápidos vêm primeiro (formato, lint, checagem de tipos, testes unitários), então a maioria das mudanças ruins é barrada em minutos. Um portão precisa ser automático e binário, passa ou falha. Se as pessoas podem negociar com ele, deixou de ser um portão.

Um portão é só um código de saída

Como o GitHub Actions sabe que um comando passou? Pelo contrato do Unix: código de saída 0 significa sucesso, qualquer outro número significa falha. Linters, verificadores de tipos e executores de testes seguem isso, então um pipeline consegue encadeá-los. Um script que acha um problema, imprime e mesmo assim sai com 0 é um portão que nunca fecha.

- name: Documentation index is up to date
  run: bun run docs:index --check
Um modo de verificação: não escreve nada e falha se o arquivo commitado for diferente

Esse exemplo é usado por este repositório. Alguns arquivos são gerados a partir de outros e também ficam commitados, e podem se afastar da sua origem. Com --check, o script refaz o resultado na memória, compara com o arquivo commitado e sai com código diferente de zero em qualquer diferença. A CI nunca altera o repositório: ela avisa, e o desenvolvedor aplica a correção localmente. Formatadores funcionam do mesmo jeito (biome ci).

Tornando um portão obrigatório no GitHub

Um workflow que falha é só uma marca vermelha até você proteger o branch. As regras de proteção de branch (ou rulesets) transformam checks escolhidos em required status checks (checks de status obrigatórios). Então o GitHub se recusa a juntar um pull request enquanto um check obrigatório estiver falhando, pendente ou ausente.

  • Atualizado antes de juntar (checks estritos): o resultado só vale se o branch do pull request contém a base mais recente, então o código testado é igual ao código juntado. Duas mudanças podem passar cada uma sozinha e quebrar juntas. O preço são mais builds, porque cada junção deixa os outros pull requests abertos desatualizados.
  • Merge queue (fila de merge): paga esse preço automaticamente. Ela monta um branch temporário com a base mais recente mais os pull requests da fila, e roda os checks obrigatórios nessa combinação. O GitHub anuncia isso com o evento merge_group, então todo workflow que reporta um check obrigatório precisa de on: merge_group ao lado de pull_request. Se faltar, o pull request na fila espera um check que nunca chega.

Checks pulados: um detalhe delicado

Como o check é puladoO que o GitHub reportaEfeito no merge
O workflow não começou (filtro de branch ou de caminho)O check fica pendente, nunca é reportadoO pull request fica bloqueado
Um job foi pulado pela condição ifO job conta como sucessoO pull request pode ser juntado, embora nada tenha sido testado

Então um portão obrigatório não deve poder ser pulado por algo que o autor controla, como adicionar ou tirar um rótulo.

Limites, como a cobertura

Um commit stage pode falhar por limites mensuráveis: avisos do compilador, violações de estilo, duplicação, cobertura de código (a fatia das linhas que rodam durante os testes). Um piso de cobertura como 80 por cento é útil, já que código que nunca roda em um teste certamente não foi verificado. Mas é um teto fraco: rodar uma linha não é o mesmo que conferir o comportamento dela, e quando o número vira meta, as pessoas escrevem testes só para subi-lo.

↑ Voltar ao sumário

Estratégias de deploy

Sua nova versão passou por todos os portões. Agora você precisa colocá-la na frente dos usuários de verdade sem estragar o dia deles. Primeiro, um par de palavras importante do Continuous Delivery. Implantação (deployment) é técnica: colocar uma versão num ambiente e iniciá-la. Lançamento (release) é um ato de negócio: deixar os usuários chegarem a essa versão ou funcionalidade. Mantê-los separados reduz o risco dos dois, porque você pode implantar e conferir a nova versão em produção antes de alguém vê-la. Todas as estratégias abaixo são formas de lançar algo que já foi implantado.

Blue-green

Mantenha dois ambientes de produção tão parecidos quanto possível, chamados blue (azul) e green (verde). Só um recebe o tráfego dos usuários. Você instala a nova versão no ocioso e a testa lá. Lançar significa mudar o roteador ou o balanceador de carga para apontar para ela, e leva segundos.

          +--> blue  (v1)  ativo
usuários --> roteador
          +--> green (v2)  ocioso, testado (smoke)

troca o roteador:
          +--> blue  (v1)  ocioso, pronto p/ rollback
usuários --> roteador
          +--> green (v2)  ativo
Blue-green: a versão antiga fica pronta

O rollback (voltar atrás) também é uma troca: mande o tráfego de volta ao ambiente antigo. Leva segundos e não precisa de rebuild.

Canary

Os antigos mineiros de carvão levavam um canário para a mina, porque ele adoecia primeiro se o ar estivesse ruim. Um canary release roda a nova versão ao lado da antiga, dando a ela uma pequena parte do tráfego real. Se os números dela forem tão bons quanto os da antiga, a parte cresce até 100 por cento. Se forem piores, o tráfego volta e poucos usuários foram afetados.

Um canary é um experimento com um grupo de controle: a mesma métrica, medida ao mesmo tempo, na antiga e na nova. Compare taxas, nunca contagens brutas, porque os grupos têm tamanhos muito diferentes. E decida a regra antes do lançamento, para que uma ferramenta, e não uma pessoa esperançosa, decida continuar ou parar.

Taxa de erro = erros divididos por requisições. Compare as duas taxas com a regra que você escreveu
VersãoRequisiçõesErrosTaxa de erro
Estável49.0009898 / 49.000 = 0,2%
Canary1.0003131 / 1.000 = 3,1%

Atualização gradual (rolling)

Uma rolling update substitui as instâncias de um serviço em lotes, algumas por vez. Precisa de pouco hardware extra e não causa indisponibilidade, mas durante todo o lançamento duas versões estão vivas juntas. Então a nova versão precisa ser retrocompatível com a antiga em tudo que elas compartilham: esquema do banco, formatos de mensagem, sessões e a API vista pelos clientes.

Ela também reduz a capacidade enquanto roda. A capacidade que sobra é (instâncias menos o lote) vezes a capacidade de uma instância. Com 5 instâncias de 100 requisições por segundo cada, um lote de 2 deixa 3 x 100 = 300 requisições por segundo. Se o pico de carga é maior que isso, o lote é grande demais. O tamanho do lote é uma troca entre a velocidade do lançamento e a margem de segurança, e acrescentar instâncias temporárias extras (um surge) elimina a troca ao preço de hardware.

Smoke tests

Uma implantação pode falhar mesmo com o código perfeito: uma string de conexão errada, uma variável de ambiente faltando, uma porta de firewall fechada. Um smoke test é uma verificação automática curta feita logo depois da implantação, antes de o tráfego dos usuários chegar. Ele confere que a aplicação em execução inicia e alcança seu banco de dados, suas filas e outros serviços. Se falhar, a implantação sofre rollback ou nunca é lançada, o que não custa nada quando os usuários foram mantidos longe.

A parte difícil: os dados

O código pode sofrer rollback girando um roteador. Os dados não. Se a versão 2 muda o banco de um jeito que a versão 1 não consegue ler, o caminho de rollback do blue-green, do canary e da atualização gradual desaparece. O mesmo vale para linhas, arquivos, caches e mensagens de fila que a versão 2 escreveu. O plano seguro é dividir a mudança em etapas, chamado expandir e contrair (expand and contract):

  1. Expandir: mude o esquema de forma compatível, por exemplo adicione a nova coluna full_name e mantenha a antiga name.
  2. Implante código que lê as duas, depois lance a nova versão.
  3. Contrair: em uma implantação posterior, quando nenhum rollback para o código antigo é esperado, remova a coluna antiga.
↑ Voltar ao sumário

SemVer, Conventional Commits e changelog

Quando um amigo diz 'versão 2.4.7', isso te diz alguma coisa? Com o Semantic Versioning 2.0.0 (SemVer) diz. Uma versão tem a forma MAJOR.MINOR.PATCH, e cada parte promete algo às pessoas que usam o software.

Quando um número cresce, todos os números à direita dele voltam a 0
ParteCresce quandoExemplo a partir de 2.4.7
PATCHUma correção de bug retrocompatível2.4.8
MINORNova funcionalidade retrocompatível2.5.0
MAJORUma mudança incompatível na API pública3.0.0

Atenção: com a versão major acima de zero, qualquer mudança incompatível na API pública exige um novo major. (Versões 0.y.z são para o desenvolvimento inicial, e este repositório, que está antes da 1.0.0, usa um aumento de minor para um novo mini-projeto e um aumento de patch para correções.)

Pré-lançamentos e metadados de build

Um pré-lançamento (pre-release) é escrito com um hífen e identificadores separados por pontos, como 1.0.0-beta.2. Ele sempre ordena antes da versão normal que prepara. Dois pré-lançamentos são comparados identificador por identificador: números como números, palavras em ordem ASCII (do alfabeto), e um número é menor que uma palavra. Comparar versões como texto simples dá respostas erradas (10 seria menor que 9), então as ferramentas usam uma biblioteca de SemVer.

1.0.0-alpha  <  1.0.0-alpha.1  <  1.0.0-beta
  <  1.0.0-beta.2  <  1.0.0-beta.11  <  1.0.0-rc.1
  <  1.0.0

1.0.0+build.5  e  1.0.0+build.9  --> mesma precedência
Ordem de precedência, da menor para a maior

Os metadados de build vêm depois de um mais, como 1.0.0+20261010.sha.5114f85. Eles dão nome a um build e são ignorados na comparação: duas versões que diferem só neles têm a mesma precedência. Então nunca ponha nos metadados de build informação que importa para atualizar.

Conventional Commits

Uma mensagem de commit é mais fácil para máquinas lerem quando tem um formato fixo. O Conventional Commits 1.0.0 diz que a primeira linha é tipo(escopo): descrição. O tipo diz que espécie de mudança é (fix, feat, e outros como docs, chore ou refactor), e o escopo opcional diz onde.

CommitAumento no SemVer
fix(api): return 404 for unknown idPATCH
feat(cli): add --json flagMINOR
Qualquer commit com uma mudança que quebra compatibilidadeMAJOR
docs, chore, refactor... sem quebra de compatibilidadeNenhum aumento por si só

Uma mudança que quebra compatibilidade (breaking change) é marcada de duas formas, que podem ser combinadas: um ponto de exclamação logo antes dos dois-pontos (refactor(auth)!: ...), ou um rodapé que começa com BREAKING CHANGE: em letras maiúsculas. Ela não depende do tipo: um refactor, um fix ou um chore podem quebrar a API. Um lançamento cobre muitos commits mas recebe um aumento, o mais alto que qualquer um deles pede. Se o último lançamento foi 1.3.2 e os commits novos incluem um fix, um feat e um docs, o lançamento é 1.4.0. Se um deles quebra compatibilidade, é 2.0.0.

Keep a Changelog

Um changelog conta às pessoas que usam o software o que é diferente em cada versão. Não é uma cópia do git log: as mensagens de commit documentam passos para quem constrói o código, enquanto um changelog é escrito para quem o usa. O Keep a Changelog 1.1.0 pede uma entrada por versão, a mais recente primeiro, com a data como AAAA-MM-DD, e uma seção Unreleased no topo para o que está por vir. As mudanças são agrupadas sob seis títulos:

TítuloUse para
AddedFuncionalidades novas
ChangedMudanças em comportamento existente
DeprecatedFuncionalidades que serão removidas em breve (o aviso)
RemovedFuncionalidades removidas agora (a remoção que foi avisada)
FixedCorreções de bugs
SecurityCorreções de vulnerabilidades
↑ Voltar ao sumário

Segurança da cadeia de suprimentos no CI

Seu pipeline usa código de outras pessoas: actions, pacotes, imagens base. Esse código faz parte da sua cadeia de suprimentos (supply chain), a longa estrada que vai do trabalho dos outros até o programa nas mãos dos seus usuários. Se um atacante entra em uma parte da cadeia, seu pipeline e seus segredos correm perigo. Três ideias simples te protegem.

1. Fixe as actions em um SHA de commit completo

Uma action roda dentro do seu job, com acesso ao seu código, ao seu GITHUB_TOKEN e aos segredos que você dá a ela. Uma tag como v3, ou um branch, é um ponteiro móvel: dono/action@v3 significa o que o dono decidir amanhã. Um SHA de commit completo é a única referência imutável. O custo é que as atualizações deixam de ser automáticas, então os times combinam a fixação com um bot que abre pull requests com SHAs novos.

ReferênciaPode mudar debaixo de você?
@main (um branch)Sim, a cada push
@v3 (uma tag)Sim, o dono pode movê-la
@<SHA de 40 caracteres>Não, é imutável

2. Nunca cole texto não confiável num script

Uma expressão ${{ }} dentro de um step run é trocada pelo seu valor quando o GitHub escreve o arquivo do script, antes de qualquer shell começar. Se o valor vem de um usuário (o título de um pull request, o nome de um branch, o corpo de uma issue, uma mensagem de commit), o usuário está escrevendo parte do seu script. Isso é injeção de script (script injection).

# Arriscado: o título vira parte do script
- run: echo "Checking ${{ github.event.pull_request.title }}"

# Mais seguro: o texto viaja como dado numa variável
- run: echo "Checking $TITLE"
  env:
    TITLE: ${{ github.event.pull_request.title }}
Use uma variável de ambiente intermediária

3. Cuidado com o pull_request_target

Como você viu, o pull_request vindo de um fork é seguro por desenho: sem segredos e com token somente leitura. O pull_request_target é o oposto. Ele roda o workflow do repositório base e dá a ele os segredos, supondo que só código confiável vai rodar. Se o workflow faz checkout e executa a cabeça (head) do pull request, ele roda o código de um desconhecido com os seus segredos, e a suposição se quebra.

  • Compile e teste código não confiável sob pull_request, sem privilégios.
  • Faça o trabalho privilegiado (como comentar ou rotular) em um workflow separado que nunca executa esse código.

SLSA e proveniência

Quando você baixa um pacote, como sabe que ele foi construído a partir do código-fonte público e não alterado no caminho? O SLSA (Supply-chain Levels for Software Artifacts, que se lê 'salsa') responde com a proveniência de build (build provenance): uma declaração assinada que liga um artefato (pelo seu digest, uma impressão digital do conteúdo) ao construtor, ao processo de build e ao commit de origem.

Nível de buildO que você tem
L1A proveniência existe
L2A proveniência é assinada por uma plataforma de build hospedada
L3A plataforma é endurecida de modo que o próprio build não consegue forjar a proveniência
↑ Voltar ao sumário

Juntando tudo: o pipeline deste repositório

Tudo o que você leu é usado no arquivo .github/workflows/ci.yml deste repositório. Vamos lê-lo como um mapa.

Conceitos deste resumo encontrados no pipeline real
IdeiaOnde aparece no ci.yml
EventosRoda em push na main, em todo pull_request e à mão com workflow_dispatch
Menor privilégioUm permissions: contents: read no topo
ConcorrênciaUm grupo feito do nome do evento e da ref, com cancel-in-progress: true, para que um push não cancele uma execução completa manual
Runner fixadoruns-on: ubuntu-24.04, não ubuntu-latest
Jobs em paralelotypescript, formatters, quiz e list-projects rodam ao mesmo tempo
Job dependentemini-projects tem needs: list-projects
Matrizformatters tem uma matriz de cinco linguagens com fail-fast: false, e mini-projects tem um job por projeto
Matriz dinâmicalist-projects escreve JSON em $GITHUB_OUTPUT, e a matriz o lê com fromJson
Portões de qualidadebiome ci, checagem de tipos, testes unitários, validação do quiz e docs:index --check, todos por código de saída
Uso seguro de entradaO comando de formatação viaja numa variável de ambiente, não colado no script
push / pull_request
        |
        +--> typescript   (lint, tipos, testes, docs)
        +--> formatters   (matriz: 5 linguagens)
        +--> quiz         (unitário + ponta a ponta)
        +--> list-projects --> mini-projects (matriz, 1 cada)

Todos rodam em paralelo. Qualquer job vermelho bloqueia o
merge quando é um required status check.
A forma do pipeline

Erros comuns para evitar

  • Chamar de CI algo em que os branches vivem por semanas. CI de verdade são junções pequenas na linha principal todo dia.
  • Pôr as verificações mais lentas primeiro. Falhe rápido, com as baratas na frente.
  • Uma chave de cache constante, que cria um cache velho. A chave do cache precisa mudar com o conteúdo.
  • Enviar artefatos com o mesmo nome de todos os jobs da matriz.
  • Dar permissions amplas por padrão, ou dar segredos a código de forks.
  • Tornar um check obrigatório pulável (if com um rótulo) ou filtrado por paths.
  • Fazer rollback do código esquecendo que o banco ou a fila já mudaram.
  • Fixar actions em tags móveis, ou colar texto de ${{ github.event... }} direto no run.
↑ Voltar ao sumário