Quando as pessoas dizem “CI/CD” como se fosse uma palavra só, na maioria das vezes estão na verdade descrevendo apenas CI com um passo de deploy colado no final. Eu cometi esse erro por muito tempo também.

Em um dos projetos em que estou trabalhando na Bitboundaire, eu precisei separar as duas coisas na prática. No momento em que comecei a tratar testes E2E como uma barreira de deployment em vez de “só mais um job no CI”, a diferença entre CI e CD ficou muito clara.

Repositórios e camadas diferentes se movem de forma independente: frontend, backend, auth, IaC etc. Cada parte evolui separadamente, e isso cria um risco real: uma pequena mudança local pode quebrar silenciosamente um fluxo global do usuário. Eu queria uma configuração em que qualquer mudança nessas partes pudesse ser validada pela perspectiva da jornada do usuário antes de chegar à produção, e de um jeito que os engenheiros se sentissem responsáveis pela qualidade da plataforma de ponta a ponta.

Na Bitboundaire não temos um time de Teste dedicado. As mesmas pessoas que escrevem features são as que escrevem e atualizam testes, assumem falhas e corrigem bugs. Isso molda bastante como desenhamos nossos pipelines: qualidade não é algo que jogamos por cima de um muro, é algo que incorporamos no processo de release.

O incidente que nos levou para E2E distribuído

Um dia, nesse projeto, fizemos uma mudança de infraestrutura “simples” que deu errado.

Uma pequena atualização no nosso IaC afetou indiretamente o Cognito e bloqueou novos usuários de fazerem signup. Todo o CI estava verde. Testes unitários e de integração estavam ok. O problema só apareceu quando todo o ecossistema foi ligado junto. Não detectamos isso nos pipelines. Detectamos depois que as pessoas começaram a reclamar no suporte.

Nenhum teste unitário ou de integração local teria pego isso antes de produção. Não era um “bug em um único serviço”, era um efeito colateral do ecossistema.

Foi exatamente por isso que migramos para uma abordagem de E2E distribuído.

Neste projeto, agora garantimos que:

  • Os fluxos fim a fim sejam validados em todo o ecossistema, não só por repositório.
  • Mudanças em uma área (como IaC ou auth) sejam testadas junto com o resto da plataforma.
  • Não dependamos de um time de Teste dedicado ou de ciclos de regressão manuais para proteger jornadas centrais.

O resultado não é apenas “mais testes”. O resultado são menos surpresas: quando algo quebra, tende a quebrar primeiro em um ambiente controlado, e não na frente de uma pessoa nova tentando se cadastrar. Isso está muito alinhado com a forma como pensamos na Bitboundaire: se dizemos que realmente nos importamos, isso precisa aparecer na forma como fazemos releases de software, não em um slide de apresentação.

Como eu separo CI e CD na minha cabeça

Meu modelo mental é:

CI (Continuous Integration) é onde eu tento quebrar código do jeito mais rápido e barato possível.

CD (Continuous Delivery/Deployment) é onde eu tento quebrar releases antes que as pessoas usuárias vejam.

Neste projeto:

  • CI vive dentro de cada repositório: frontend, backend, auth, infra.
  • CD é cross-repo e é orquestrado por um repositório de E2E dedicado, usando GitHub Actions e repository_dispatch.

Em todo PR e merge, o CI roda o stack usual dentro de cada repositório: lint, type checks, testes unitários, testes de integração. Se qualquer coisa disso ficar vermelha, a mudança nem encosta em staging.

O CD só começa depois que uma mudança já foi mesclada e deployada em staging. É aí que entra o E2E distribuído, em um repositório separado, validando a plataforma inteira de fora para dentro.

Essa separação é importante porque muda o comportamento do time:

  • Falhas em CI parecem “eu quebrei meu próprio código”.
  • Falhas em CD / E2E parecem “eu quebrei a experiência da plataforma”.

O mesmo engenheiro, mas em um nível diferente de responsabilidade.

O que mantemos em CI vs o que movemos para CD

Para evitar repetir as mesmas coisas em dez seções, vou resumir as camadas e depois falar sobre como elas aparecem no processo.

Testes unitários no CI

Testes unitários vivem totalmente no CI:

  • Validam pequenas partes de lógica em isolamento.
  • São baratos e rápidos, então rodamos o tempo todo.
  • Quando falham, o sinal é bem direto: você sabe exatamente onde olhar.

Eles são a primeira linha de defesa. Se estamos quebrando lógica básica e deixando isso chegar em integração ou E2E, algo está errado na forma como escrevemos código.

Testes de integração no CI

Testes de integração também vivem no CI:

  • Cobrem interações entre partes dentro de um repositório: controller + service + banco, ou uma árvore React + API mockada etc.
  • São mais lentos, mas ainda aceitáveis para PRs.
  • Quando falham, você ainda tem um raio de impacto relativamente localizado.

Eles nos dão garantias em nível de serviço. Para um único repositório, queremos saber: “internamente, essa coisa está consistente”.

Testes E2E no CD (staging)

Testes E2E vivem no CD e rodam em staging:

  • Cobrem jornadas reais de usuários através de múltiplos componentes: login, onboarding, fluxos principais, billing etc.
  • São mais lentos e mais caros de manter, mas refletem a realidade do ponto de vista da pessoa usuária.
  • Quando falham, normalmente significam: “alguém acabou de quebrar algo importante que uma pessoa usuária real vai sentir”.

A parte importante é quando cada camada roda:

  • Unit + integration: barreira para fazer merge (CI).
  • E2E: barreira para liberar release (CD).

Então, se algo escapa e o E2E pega, isso vira feedback para melhorar as camadas inferiores dentro dos repositórios. Como o mesmo engenheiro é responsável por feature + testes + bugfix, esse ciclo de feedback é de fato curto e prático.

A arquitetura: repositório de E2E separado + repository_dispatch

Agora a parte divertida: como isso funciona ligado no GitHub Actions.

Temos múltiplos repositórios:

  • frontend
  • backend
  • auth
  • infra / IaC
  • e2e-tests (este é especial)

Todo “repositório de feature” (frontend, backend etc.) tem um formato parecido:

1. Pipeline de CI no PR:

  • lint
  • type checking
  • testes unitários
  • testes de integração

2. Merge → deploy para staging:

  • depois que o CI fica verde, fazemos deploy da nova versão em staging.

3. Após o deploy em staging ser concluído, o workflow dispara um evento repository_dispatch direcionado para o repositório e2e-tests.

O repositório de E2E tem seu próprio workflow:

  • Escuta repository_dispatch.
  • Lê o payload (qual repo, SHA, informações de ambiente etc.).
  • Roda a suíte de E2E com Playwright contra o ambiente de staging.
  • Reporta o status de volta (via GitHub checks / statuses), que é usado pelo pipeline original para decidir se o deploy em produção está permitido.

Isso nos dá algumas propriedades interessantes:

  • A lógica de E2E é centralizada em um único repositório, não duplicada em cada serviço.
  • Quando mudamos a definição de “plataforma está saudável”, fazemos isso uma vez só.
  • Podemos evoluir a suíte de E2E e sua infraestrutura sem tocar nos repositórios de feature.

E, de novo, não existe um departamento de QA “dono” disso. Os mesmos engenheiros que enviam features contribuem para o repositório de E2E quando adicionam novos fluxos ou mudam comportamentos existentes. A responsabilidade não é terceirizada.

Por que um repositório de E2E dedicado em vez de misturar tudo

Vejo muitas configurações em que os testes E2E vivem dentro do repo de frontend ou de backend. Eu fazia isso também. Funciona no começo, mas não escala bem em um monolito modular com múltiplos repositórios ao redor.

Com um repositório e2e-tests dedicado, algumas coisas ficam mais fáceis:

1. Visão clara em nível de sistema

Os testes são escritos da perspectiva de “a plataforma”, não de “o projeto de frontend”. O modelo mental é a jornada do usuário: “sign up”, “reset password”, “upgrade plan” etc.

2. Contrato explícito do que significa “estar saudável”

O repositório de E2E se torna o contrato que descreve o que precisa ser verdade antes de permitirmos um deploy em produção. Quando mudamos um comportamento de forma intencional, mudamos os testes junto.

3. Modelo de ownership alinhado com responsabilidade

Como os engenheiros são donos da qualidade, a regra é simples: mexeu em fluxo crítico, atualize ou adicione o E2E que protege esse fluxo. A estrutura do repositório deixa isso óbvio e visível.

Playwright como ferramenta principal de E2E

Para a camada de E2E escolhemos Playwright. As razões são bem pragmáticas:

  • Suporte forte a automação de browser.
  • Boa paralelização e estabilidade.
  • Boa DX com TypeScript, que encaixa com o resto da nossa stack.
  • Tracing, screenshots e vídeos, que ajudam muito quando alguém precisa debugar uma execução que falhou.

O repositório é estruturado mais ou menos por domínios do produto, não por camadas técnicas. Então, em vez de “/login-page” e “/user-service”, temos algo como:

tests/
  auth/
    login.spec.ts
    password-reset.spec.ts
  onboarding/
    signup.spec.ts
    first-session.spec.ts
  billing/
    checkout.spec.ts
    change-plan.spec.ts

Cada teste é escrito com uma história em mente:

  • “Usuário cria uma conta, confirma o e-mail e cai no dashboard.”
  • “Usuário existente faz upgrade de plano e continua vendo todos os seus dados.”
  • “Usuário no tier pago vê as funcionalidades X, os demais não.”

Da perspectiva do engenheiro, isso facilita o raciocínio: quando um requisito de produto muda, ele vai até o teste que descreve aquela jornada e adapta. Mesma pessoa, ciclo completo: código → testes → deploy → E2E → fix.

Staging precisa ser próximo de produção ou E2E vira teatro

Tudo isso só funciona porque staging não é um sandbox aleatório. Tratamos staging como “prod com menos audiência”:

  • Mesma topologia de infra e estilo de configuração.
  • Mesmos caminhos de autenticação (SSO, tokens etc.).
  • Mesmo modelo de feature flags, talvez com um pouco mais liberado para testes internos.

Se staging diverge demais de prod, E2E passando não significa nada. Então forçamos bastante para mantê-los alinhados. Como engenheiros são donos tanto do código quanto da qualidade, esse alinhamento é algo que eles sentem no dia a dia, não um checklist.

O que de fato trocamos: menos deploys, mais confiança

Uma consequência dessa configuração é visível no papel: deploys em produção ficaram um pouco mais lentos.

Antes disso, conseguíamos deployar mais vezes por dia. Depois de adicionar a barreira de E2E completa, adicionamos cerca de 30 minutos no caminho de “merge” até “prod”, porque rodamos a suíte de Playwright em staging.

Se você olha só para “deploys por dia”, parece uma desaceleração.

Mas menos deploys em um dia não significa que ficamos mais lentos como time. Na prática, existe um efeito de velocidade que importa muito mais:

  • Gastamos muito menos tempo caçando bugs em produção.
  • Fazemos rollback e hotfix com menos frequência.
  • Evitamos muitos context switches dolorosos causados por incidentes.

Então sim, adicionamos checks automáticos de E2E no caminho de release. Mas, na prática, aceleramos o processo como um todo, porque não estamos o tempo todo pagando o imposto de enviar coisas em que não confiamos. Fazemos releases com mais confiança e menos apagar incêndio, o que significa mais entrega real.

Isso combina com a forma como pensamos engenharia na Bitboundaire: enviar rápido é bom, mas enviar algo em que as pessoas podem confiar é inegociável. A estrutura do pipeline é só o reflexo dessa crença.

Engenheiros sendo donos da qualidade de ponta a ponta

Um último ponto que considero importante: essa configuração só funciona porque os engenheiros são donos da qualidade.

Não existe etapa de “jogar para o QA”. Os princípios principais são:

  • Se você envia uma feature, você é responsável pelos testes unitários e de integração necessários para ela.
  • Se sua mudança afeta um fluxo crítico, você também é responsável pelas atualizações de E2E correspondentes.
  • Se o pipeline está vermelho, você não fica olhando em volta esperando alguém do “time de QA” arrumar. Você arruma.

O design de CI/CD reforça isso: falhas são muito visíveis e bloqueiam merges ou releases. Mas o processo não está ali para punir ninguém; está ali para apoiar o tipo de engenheiros que queremos ser: aquela que se importa com o que acontece depois do git push.

E, para este projeto, a combinação de um CI forte dentro de cada repositório com uma barreira de E2E distribuído em staging nos deu um bom equilíbrio entre velocidade e segurança. Permite que nos movamos de forma um pouco mais deliberada, mas com a sensação de que o que chega em produção é algo que conseguimos defender.

Na Bitboundaire, “we truly care” significa tratar disponibilidade e confiança das pessoas usuárias como requisitos de produto, não como um pensamento tardio. Este projeto é um exemplo concreto disso: quando peças independentes começaram a criar riscos invisíveis, respondemos fortalecendo nossas garantias fim a fim em vez de aceitar que “o suporte vai segurar”.