Explicar o que são integração contínua e entrega contínua, e que problema real cada uma resolve em um time (mesmo um time de duas pessoas).
Ler e escrever um workflow do GitHub Actions em YAML, distinguindo workflow, evento, job, step, runner e action.
Montar um workflow de CI que instala com npm ci, roda lint e testes a cada push e pull request, com cache de dependências e um banco de verdade em contêiner de serviço.
Guardar credenciais em secrets e variáveis do repositório, e usá-las sem vazá-las nos logs.
Publicar automaticamente um site estático no GitHub Pages (ou no Netlify) a partir do main.
Construir e enviar a imagem Docker para o GHCR e atualizar o VPS por SSH, com tag rastreável e rollback em um comando.
Proteger a branch principal exigindo que a esteira passe, e exibir o badge do resultado no README.md.
[ ] unieventos-api com npm test funcionando localmente (Aula 13 do Nível 3: vitest + supertest) e Dockerfile do Capítulo 07.
[ ] Banco na nuvem configurado por DATABASE_URL (Capítulo 08) — é o que torna o deploy automático seguro.
[ ] Repositório no GitHub com a branch main (Capítulo 02) e a imagem já publicada uma vez no GHCR na mão (Capítulo 07).
[ ] VPS acessível por SSH com o usuário deploy, docker compose instalado e compose.prod.yaml funcionando (Capítulos 06 e 07).
[ ] unieventos-web (ou o site-evento do Nível 1) gerando dist/ com npm run build.
No Capítulo 07 você construiu a imagem na sua máquina e a subiu no GHCR digitando os comandos; no Capítulo 08 o banco saiu do servidor e a unieventos-api ficou sem estado — uma imagem Docker que pode ser destruída e recriada sem perder nada. Só que o caminho até o ar ainda é manual: você constrói a imagem no notebook, faz docker push, abre o SSH, roda docker compose pull e torce. São seis comandos em três máquinas, na ordem certa, sempre que muda uma linha — e alguém vai esquecer um deles em uma sexta-feira à noite. Hoje esse trabalho passa a ser feito por um robô a cada git push: instalar dependências, rodar lint e testes, publicar o site estático, construir a imagem, enviar para o GHCR e atualizar o VPS por SSH — e a branch main ganha um porteiro, que recusa o código que não passar pela esteira. No Capítulo 10 essa mesma esteira ganha medidas de qualidade — cobertura, Lighthouse e monitoramento de erros em produção.
CI — integração contínua. Cada mudança é integrada ao código principal várias vezes por dia, e uma máquina neutra verifica se ela continua funcionando. A palavra importante é "neutra": o teste que roda no seu computador prova que funciona no seu computador, com as suas variáveis de ambiente, as suas dependências instaladas há três meses e aquele arquivo que você esqueceu de commitar. O runner do GitHub começa do zero, clona o repositório e só tem o que está versionado. É por isso que a CI pega o clássico "esqueci de commitar o package-lock.json".
CD — entrega/implantação contínua. O que passou na CI vai para produção sem intervenção manual. O ganho não é preguiça: é que o deploy deixa de ser um evento raro e assustador (e por isso arriscado) e vira uma rotina de dois minutos que acontece dez vezes por semana. Quanto menor a mudança que vai ao ar, mais fácil descobrir qual delas quebrou.
A esteira que você vai montar:
Texto
git push
│
├─► CI ......... npm ci → lint → testes (todo push e todo PR)
│ │
│ └─ falhou? o PR fica vermelho e não pode ser mesclado
│
└─► main ─┬─► site estático: build → GitHub Pages
│
└─► API: build da imagem → GHCR → ssh no VPS → compose pull/up → /health
Três princípios que valem mais que qualquer YAML:
A esteira é a fonte da verdade. "Na minha máquina passa" não conta. Se o teste falha na CI, o código está errado — ou o teste depende de algo que não está versionado.
Rápido ou ninguém usa. Uma CI de 15 minutos é ignorada; as pessoas mesclam sem esperar. Cache, jobs paralelos e testes que não dependem de rede mantêm o ciclo abaixo de 3 minutos.
Falha barulhenta, sucesso silencioso. O sucesso é o esperado. O que precisa gritar é a falha — no PR, no badge e, se você quiser, no e-mail.
🧠 Você sabia?
A ideia de integrar o trabalho de todo mundo continuamente é bem mais velha que a nuvem: nos anos 1990 o daily build da Microsoft era uma regra quase religiosa, e quem quebrasse a compilação do dia herdava um chapéu ridículo ou o dever de vigiar o próximo build. O ritual era feio, mas o objetivo é exatamente o mesmo de um workflow de CI: tornar visível, no mesmo dia, quem quebrou o quê — em vez de descobrir na integração final, quando já é impossível saber qual das duzentas mudanças foi a culpada.
🔬 Investigue
Abra um projeto popular no GitHub (por exemplo vuejs/core ou expressjs/express), entre na aba Actions e escolha uma execução recente. Responda: quantos jobs rodam em paralelo? Quanto tempo levou o mais lento? Em qual sistema operacional cada um roda? Depois clique em um step e veja o log linha a linha. Você está lendo exatamente o mesmo tipo de arquivo YAML que vai escrever nos próximos 40 minutos — a diferença é a quantidade de jobs, não a complexidade de cada um.
Um workflow é um arquivo YAML dentro de .github/workflows/ no seu repositório. O GitHub lê a pasta a cada push e obedece ao que estiver lá. O vocabulário:
Termo
O que é
workflow
um arquivo .yml; tem um nome, uma lista de eventos e um ou mais jobs
evento (on:)
o que dispara: push, pull_request, schedule, workflow_dispatch (botão manual)
job
um conjunto de steps que roda em uma máquina; jobs rodam em paralelo por padrão
runner
a máquina temporária (ubuntu-latest) criada para o job e destruída no fim
step
um passo: ou um run: (comando de shell) ou um uses: (uma action pronta)
action
um pedaço reutilizável, referenciado por dono/nome@versão
secret
valor cifrado do repositório, disponível como ${{ secrets.NOME }}
O menor workflow útil que existe:
YAML
# .github/workflows/exemplo.ymlname:Exemploon:[push]jobs:dizer-oi:runs-on:ubuntu-lateststeps:-name:Falar com o mundorun:echo "rodando no commit ${{ github.sha }}"
Cinco regras de YAML que resolvem 90% dos erros de sintaxe:
Indentação é com espaços, nunca com tabulação. Dois espaços por nível é a convenção.
Lista é - no começo da linha, no mesmo nível dos irmãos.
chave: valor precisa do espaço depois dos dois-pontos.
Valor com : dentro precisa de aspas: run: "echo a: b".
Bloco de várias linhas usa | (preserva as quebras) — é como se escrevem scripts inteiros dentro de um run:.
💡 Dica
O VS Code entende workflows do GitHub Actions se você instalar a extensão oficial GitHub Actions: ela valida o YAML enquanto você digita, completa nomes de actions e avisa quando uma chave não existe. Vale mais do que descobrir o erro dois minutos depois, no log da execução.
Cada job começa em uma máquina virtual limpa, com Ubuntu, Git, Node, Docker e mais uma centena de ferramentas pré-instaladas. Duas consequências que confundem todo mundo no começo:
Jobs não compartilham disco. O que o job A baixou não existe no job B. Para passar arquivos entre jobs, use artefatos (actions/upload-artifact) ou reconstrua.
Cada step compartilha o disco, mas não o shell. Um cd pasta em um step não vale no próximo (use working-directory:), e uma variável exportada com export some (escreva em $GITHUB_ENV).
Este é o workflow que você vai usar todos os dias. Ele roda a cada push no main e a cada pull request:
YAML
# unieventos-api/.github/workflows/ci.ymlname:CIon:push:branches:[main]pull_request:branches:[main]# Princípio do menor privilégio: este workflow só precisa LER o repositório.permissions:contents:read# Se você empurrar dois commits seguidos, cancela a execução antiga e roda só a nova.concurrency:group:ci-${{ github.ref }}cancel-in-progress:truejobs:verificar:name:Lint e testesruns-on:ubuntu-latesttimeout-minutes:10# O config.js valida o ambiente com zod (Aula 13): sem estas variáveis o processo# nem sobe. No runner não existe .env, então elas entram aqui. Os testes usam# repositórios falsos, então a DATABASE_URL só precisa ter forma válida.env:NODE_ENV:testDATABASE_URL:postgresql://postgres:teste@localhost:5432/unieventos_testeFIREBASE_PROJECT_ID:projeto-de-testeCORS_ORIGEM_PERMITIDA:http://localhost:5173steps:-name:Baixar o códigouses:actions/checkout@v4-name:Preparar o Node 22 com cache do npmuses:actions/setup-node@v4with:node-version:'22'cache:npm-name:Instalar dependências do lockfilerun:npm ci-name:Conferir o estilo do códigorun:npm run lint --if-present-name:Rodar os testesrun:npm test
Passo a passo do que acontece:
actions/checkout@v4 clona o repositório dentro do runner. Sem ele, a máquina está vazia — é o step que todo workflow começa.
actions/setup-node@v4 com cache: npm instala o Node 22 e restaura o cache do npm a partir do package-lock.json. Na primeira execução ele guarda; nas seguintes, o npm ci cai de ~40 s para ~8 s.
npm ci (e não npm install) instala exatamente as versões travadas no lockfile e falha se o package.json e o package-lock.json estiverem dessincronizados. Em CI é sempre ci.
--if-present faz o npm ignorar um script que ainda não existe. Assim o workflow já funciona antes de você montar o ESLint (Capítulo 10).
timeout-minutes evita que um teste travado consuma minutos de execução até o limite padrão de seis horas.
⚠️ Atençãonpm ci apaga a pasta node_modules e reinstala do zero. Se o seu package-lock.json não estiver commitado, o step falha com npm ci can only install packages when your package.json and package-lock.json … are in sync. Rode npm install localmente, commite o lockfile e empurre de novo — é o erro número um de quem monta a primeira CI.
Os testes da Aula 13 usam repositórios falsos e não precisam de banco. Mas em algum momento você vai querer testar a consulta SQL de verdade. O GitHub sobe contêineres auxiliares para o job com a chave services: — é o docker compose do Capítulo 07, embutido na CI:
YAML
# unieventos-api/.github/workflows/ci.yml — segundo job, no mesmo arquivointegracao:name:Testes de integração com Postgresruns-on:ubuntu-latesttimeout-minutes:15services:postgres:image:postgres:17-alpineenv:POSTGRES_USER:postgresPOSTGRES_PASSWORD:testePOSTGRES_DB:unieventos_testeports:-5432:5432# Sem healthcheck o job começa antes de o banco aceitar conexão.options:>---health-cmd "pg_isready -U postgres"--health-interval 10s--health-timeout 5s--health-retries 5env:NODE_ENV:testDATABASE_URL:postgresql://postgres:teste@localhost:5432/unieventos_testeFIREBASE_PROJECT_ID:projeto-de-testeCORS_ORIGEM_PERMITIDA:http://localhost:5173steps:-uses:actions/checkout@v4-uses:actions/setup-node@v4with:node-version:'22'cache:npm-run:npm ci-name:Criar o schema no banco do jobrun:npm run migrar-name:Rodar a suíte inteirarun:npm test
O contêiner de serviço fica acessível em localhost:5432 porque a porta foi mapeada. Falta um detalhe: o pool do Capítulo 08 exige TLS com certificado, e esse Postgres local não tem TLS nenhum. Torne a decisão explícita no código, em vez de manter dois arquivos:
JavaScript
// src/db/pool.js — trecho: TLS só quando o banco é remotoconstehBancoLocal=/@(localhost|127\.0\.0\.1)[:/]/.test(config.DATABASE_URL)exportconstpool=newpg.Pool({connectionString:config.DATABASE_URL,ssl:ehBancoLocal?false:{ca:certificadoDaAutoridade,rejectUnauthorized:true},max:10,})
🔎 Por baixo do capô
Contêiner de serviço não é a mesma coisa que docker compose: o GitHub cria uma rede própria para o job e sobe cada serviço nela antes do primeiro step. Se os steps rodassem dentro de um contêiner (chave container:), o endereço do banco seria o nome do serviço (postgres:5432), como no compose; como os nossos steps rodam direto no runner, o endereço é localhost na porta que você mapeou. Trocar um pelo outro é a causa de metade dos ECONNREFUSED em CI.
Se o seu projeto precisa funcionar em mais de uma versão do Node, a matriz cria um job por combinação, todos em paralelo:
YAML
compatibilidade:runs-on:ubuntu-lateststrategy:fail-fast:false# não cancela as outras quando uma falhamatrix:node:['22','24']name:Testes no Node ${{ matrix.node }}steps:-uses:actions/checkout@v4-uses:actions/setup-node@v4with:node-version:${{ matrix.node }}cache:npm-run:npm ci-run:npm testenv:NODE_ENV:testDATABASE_URL:postgresql://postgres:teste@localhost:5432/unieventos_testeFIREBASE_PROJECT_ID:projeto-de-testeCORS_ORIGEM_PERMITIDA:http://localhost:5173
A chave SSH do VPS, o token do Netlify e a senha do banco não podem entrar no repositório. O GitHub guarda esses valores cifrados em Settings → Secrets and variables → Actions:
Secrets — cifrados, nunca exibidos de novo depois de salvos, e mascarados nos logs (aparecem como ***). Use para senhas, tokens e chaves.
Variables — texto simples, visível no painel e no log. Use para coisas não sensíveis, como a URL pública da API.
YAML
-name:Usar um segredo sem vazá-lorun:|# CERTO: o valor vai para a variável de ambiente do processocurl -fsS -H "Authorization: Bearer $TOKEN" https://api.exemplo.com/statusenv:TOKEN:${{ secrets.TOKEN_DA_API }}
Três regras sobre segredos:
Nunca imprima um segredo.echo "${{ secrets.X }}" é mascarado pelo GitHub, mas qualquer transformação (base64, uma quebra em pedaços) escapa da máscara e vira log público para sempre.
Um segredo por finalidade. Uma chave SSH só para o deploy, revogável sem afetar o resto.
Segredos não existem em PRs de forks. Quem abre um pull request a partir de um fork não recebe os seus segredos — por segurança óbvia. Por isso o job de deploy roda só em push no main, nunca em pull_request.
Além dos seus, o GitHub injeta em todo workflow um segredo automático, o GITHUB_TOKEN: um token temporário, válido só durante a execução, cujas permissões você declara no bloco permissions:. É com ele que o workflow publica no GHCR (§6) sem você criar token nenhum.
⚠️ Atençãopermissions: sem declaração herda o padrão da organização, que pode ser de escrita em tudo. Declare sempre o mínimo: contents: read para CI, mais packages: write para publicar imagem, mais pages: write e id-token: write para o Pages. Um workflow comprometido com permissão de escrita pode reescrever o seu repositório.
No Capítulo 03 você publicou o site-evento no GitHub Pages arrastando arquivos e escolhendo uma branch. Agora o Pages passa a ser alimentado pelo próprio workflow — o que resolve o caso de um site que precisa ser construído antes (Vite, do Nível 3).
YAML
# unieventos-web/.github/workflows/publicar-site.ymlname:Publicar siteon:push:branches:[main]workflow_dispatch:# botão "Run workflow" na aba Actionspermissions:contents:readpages:write# publicar no Pagesid-token:write# provar ao Pages que a publicação veio deste workflow# Um deploy por vez, sem cancelar o que já começou a publicar.concurrency:group:pagescancel-in-progress:falsejobs:construir:name:Construir o siteruns-on:ubuntu-lateststeps:-uses:actions/checkout@v4-name:Preparar o Pagesid:pagesuses:actions/configure-pages@v5-uses:actions/setup-node@v4with:node-version:'22'cache:npm-run:npm ci# base_path resolve o subcaminho /nome-do-repositorio/ do Pages (Capítulo 03).-name:Gerar o dist/run:npm run build -- --base="${{ steps.pages.outputs.base_path }}"env:VITE_API_URL:${{ vars.VITE_API_URL }}-name:Empacotar o dist/ como artefato do Pagesuses:actions/upload-pages-artifact@v3with:path:distpublicar:name:Publicar no Pagesneeds:construir# só roda se o build terminou bemruns-on:ubuntu-latestenvironment:name:github-pagesurl:${{ steps.publicacao.outputs.page_url }}steps:-name:Publicarid:publicacaouses:actions/deploy-pages@v4
Antes do primeiro push, ligue a chave: Settings → Pages → Build and deployment → Source: GitHub Actions. Sem isso o job falha com Get Pages site failed.
Repare em duas coisas:
vars.VITE_API_URL é uma variável (não segredo): a URL pública da API vai ser embutida no JavaScript e ficaria visível de qualquer jeito. Como você viu no Capítulo 07, tudo que começa com VITE_ é resolvido em tempo de build — mudar a variável exige rodar o workflow de novo.
environment: cria um ambiente nomeado no GitHub, com histórico de implantações e a URL clicável no fim da execução. É também onde se configura aprovação manual (§8).
O token sai de User settings → Applications → Personal access tokens no Netlify, e o NETLIFY_SITE_ID do painel do site (Site configuration → General). Em produção, fixe a versão principal da CLI (netlify-cli@<versão>) para não ser surpreendido por uma mudança de comportamento.
Este é o workflow que fecha a esteira: constrói a imagem do Capítulo 07, publica no GHCR e manda o VPS baixar a nova versão.
YAML
# unieventos-api/.github/workflows/deploy.ymlname:Publicar imagem e implantaron:push:branches:[main]workflow_dispatch:permissions:contents:readpackages:write# publicar no GitHub Container Registryconcurrency:group:deploy-producaocancel-in-progress:false# nunca cancele um deploy pela metadejobs:imagem:name:Construir e publicar a imagemruns-on:ubuntu-latesttimeout-minutes:20steps:-uses:actions/checkout@v4-name:Preparar o Buildxuses:docker/setup-buildx-action@v3-name:Autenticar no GHCRuses:docker/login-action@v3with:registry:ghcr.iousername:${{ github.actor }}password:${{ secrets.GITHUB_TOKEN }}-name:Calcular nome e tags da imagemid:metadadosuses:docker/metadata-action@v5with:images:ghcr.io/${{ github.repository }}# prefix= (vazio) remove o prefixo padrão "sha-": a tag fica sendo# exatamente o SHA do commit, o mesmo valor usado no deploy abaixo.tags:|type=sha,format=long,prefix=type=raw,value=latest-name:Construir e enviaruses:docker/build-push-action@v6with:context:.platforms:linux/amd64push:truetags:${{ steps.metadados.outputs.tags }}labels:${{ steps.metadados.outputs.labels }}cache-from:type=ghacache-to:type=gha,mode=maximplantar:name:Implantar no VPSneeds:imagemruns-on:ubuntu-latesttimeout-minutes:10environment:producaosteps:-name:Atualizar os contêineres por SSHuses:appleboy/ssh-action@v1with:host:${{ secrets.VPS_HOST }}username:${{ secrets.VPS_USUARIO }}key:${{ secrets.VPS_CHAVE_SSH }}script:|set -ecd /srv/unieventos-apiexport TAG_IMAGEM="${{ github.sha }}"docker compose -f compose.prod.yaml pulldocker compose -f compose.prod.yaml up -ddocker image prune -ffor tentativa in 1 2 3 4 5 6 7 8 9 10; doif curl -fsS http://127.0.0.1:3000/health > /dev/null; thenecho "no ar na versão $TAG_IMAGEM"exit 0fisleep 3doneecho "a API não respondeu ao /health em 30 s"exit 1
docker/metadata-action@v5 calcula as tags da imagem a partir do evento. Com a configuração acima, cada push no main publica duas tags para a mesma imagem: latest e o SHA completo do commit (ghcr.io/usuario/unieventos-api:9f2c1a…). O prefix= vazio importa: sem ele, type=sha gera sha-9f2c1a…, e o script de deploy — que usa ${{ github.sha }} puro — pediria uma tag que não existe, falhando com manifest unknown.
Para o compose.prod.yaml usar essa tag, troque a versão fixa por uma variável:
${TAG_IMAGEM:-latest} significa "use a variável TAG_IMAGEM; se ela não existir, use latest". Como o script do deploy exporta TAG_IMAGEM com o SHA do commit, o servidor sobe exatamente a imagem daquele commit — e o rollback vira um comando só, com o SHA do commit anterior:
💡 Dica
Publicar só latest parece mais simples e é uma armadilha: latest não identifica nada, dois deploys diferentes têm a mesma tag e não existe para onde voltar. Tag por commit é o que transforma "deu ruim, e agora?" em trinta segundos de rollback.
Não use a sua chave pessoal. Gere um par dedicado, sem senha (o robô não tem como digitá-la), e autorize só ele:
Terminal
# na SUA máquina
ssh-keygen-ted25519-C"github actions unieventos"-fchave-deploy-N""# envia a chave PÚBLICA para o VPS
ssh-copy-id-ichave-deploy.pubmeuvps
# mostra a chave PRIVADA para copiar (inteira, com as linhas BEGIN e END)
catchave-deploy
No GitHub, crie três secrets: VPS_HOST (o IP ou domínio), VPS_USUARIO (deploy) e VPS_CHAVE_SSH (o conteúdo completo de chave-deploy, incluindo as linhas -----BEGIN OPENSSH PRIVATE KEY----- e -----END OPENSSH PRIVATE KEY-----). Depois apague o arquivo privado da sua máquina: shred -u chave-deploy.
⚠️ Atenção
Quem tem essa chave entra no seu servidor. Limite o estrago antes de precisar: no ~/.ssh/authorized_keys do VPS, prefixe a linha da chave com from="140.82.0.0/16",no-agent-forwarding,no-port-forwarding para restringir a origem, ou crie um usuário robo que só pode rodar os comandos do deploy. E, se algum dia o repositório for exposto, revogue a chave apagando a linha do authorized_keys — é mais rápido do que trocar tudo.
Proteção da branch — em Settings → Branches → Add branch protection rule (ou em Rules → Rulesets), para main:
[ ] Exigir pull request antes de mesclar (com pelo menos uma aprovação, se você trabalha em dupla).
[ ] Exigir que verificações de status passem: marque o job Lint e testes.
[ ] Exigir que a branch esteja atualizada com o main antes de mesclar.
[ ] Bloquear force push e exclusão da branch.
A partir daí, o botão "Merge pull request" fica cinza enquanto a CI estiver vermelha. É a regra do Capítulo 02 (trabalhar por pull request) ganhando um porteiro que não esquece e não faz exceção para ninguém — nem para você, se marcar "Include administrators".
🧠 Você sabia?
O GitHub Actions é gratuito e ilimitado para repositórios públicos. Para repositórios privados existe uma cota mensal de minutos, e o consumo depende do sistema: um minuto de runner Linux conta como um minuto; Windows conta como dois; macOS conta como dez. É por isso que praticamente toda CI de projeto JavaScript roda em ubuntu-latest — e mais um motivo para deixar o repositório do seu projeto autoral público, com o .env de fora, claro.
🚀 Passo a passo — do git push ao ar, sem tocar no servidor¶
Ao final destes passos, empurrar um commit no main da unieventos-api vai rodar os testes, construir a imagem, publicá-la no GHCR e atualizar o VPS sozinho — e um pull request que quebra os testes não vai conseguir ser mesclado.
Está no Nível 2? Aplique o mesmo passo na cafe-cerrado-api: troque o nome do repositório e o da imagem no GHCR, e o resto dos workflows — ci.yml, imagem.yml e deploy.yml — vale linha por linha.
Crie .github/workflows/ci.yml com o conteúdo da §3 (só o job verificar, por enquanto). Commite em uma branch, não no main:
Terminal
gitswitch-cci-inicial
gitadd.github/workflows/ci.yml
gitcommit-m"ci: rodar lint e testes a cada push e pull request"
gitpush-uoriginci-inicial
ghprcreate--fill
Abra o pull request no navegador: em segundos aparece a verificação "CI / Lint e testes" rodando. Espere ficar verde.
Com a CI verde no main, aplique a proteção de branch da §7 exigindo o status Lint e testes. Teste: crie uma branch com um teste quebrado, abra o PR e confirme que o botão de merge fica bloqueado.
Crie .github/workflows/deploy.yml com o conteúdo da §6, abra o PR, espere a CI e mescle. Assim que o merge entra no main, abra a aba Actions: o job imagem leva uns 2 minutos (na primeira vez; depois o cache derruba para menos de 1) e o implantar termina em segundos.
No unieventos-web, crie .github/workflows/publicar-site.yml (§5), ligue Settings → Pages → Source: GitHub Actions e cadastre a variável VITE_API_URL com a URL pública da API. Empurre e acompanhe.
Altere uma mensagem visível da API (por exemplo, o texto de erro 404), commite em uma branch, abra o PR, espere o verde, mescle — e não faça mais nada. Em três minutos, curl https://api.seudominio.dev/rota-que-nao-existe mostra o texto novo.
A1. Explique a diferença entre on: push e on: pull_request e diga por que o workflow de deploy usa só o primeiro. O que aconteceria se ele também rodasse em pull requests vindos de forks?
A2. Preveja a saída. Um workflow tem dois jobs sem needs: entre eles. Eles rodam em sequência ou em paralelo? O job B enxerga um arquivo que o job A criou com touch relatorio.txt? Justifique.
A3. Por que npm ci e não npm install na CI? Cite dois comportamentos diferentes entre os dois comandos.
A4. Este step está errado: - run: cd unieventos-api seguido de - run: npm test. Explique por que o segundo step não roda dentro da pasta e escreva a correção.
A5. Você precisa guardar a URL pública da API (https://api.seudominio.dev) e o token do Netlify. Qual dos dois vai em secret e qual em variable? Justifique com uma frase.
A6. O que o bloco permissions: contents: read, packages: write autoriza e o que ele impede? Por que declarar isso é melhor do que deixar o padrão?
B1. Adicione ao ci.yml um step que roda npm audit --audit-level=high e faz o job falhar se houver vulnerabilidade alta ou crítica nas dependências de produção.
Resultado esperado: um PR com uma dependência vulnerável fica vermelho; o log mostra o pacote, a severidade e a versão corrigida.
Dica
npm audit --omit=dev --audit-level=high limita a checagem ao que vai para produção. Se hoje o seu projeto já tem um aviso conhecido e sem correção, continue-on-error: true no step deixa o aviso visível sem bloquear a esteira — mas isso é dívida, anote no README.
B2. Faça a CI só rodar quando o código muda. Use paths-ignore para ignorar alterações em README.md, docs/** e .gitignore, e prove que funciona com dois commits.
Resultado esperado: um commit que muda só o README.md não dispara execução nenhuma na aba Actions; um commit em src/ dispara.
Dica
paths-ignore fica dentro do push: e do pull_request:, no mesmo nível de branches:. Cuidado: se a proteção de branch exige o status "Lint e testes" e o PR não dispara a CI, o merge trava esperando um status que nunca virá — teste esse cenário e descreva a solução.
B3. Publique um relatório como artefato. Faça o job de testes gerar a saída em arquivo e anexá-la à execução com actions/upload-artifact@v4, com retenção de 7 dias.
Resultado esperado: na página da execução, uma seção Artifacts com um arquivo baixável contendo a saída dos testes.
Dica
npm test -- --reporter=junit --outputFile=relatorio.xml (Vitest) gera o arquivo. Use if: always() no step de upload, senão ele não roda justamente quando você mais precisa: quando o teste falhou.
B4. Meça o ganho do cache. Rode o workflow uma vez com cache: npm no setup-node e outra sem, e compare a duração do step npm ci nos logs.
Resultado esperado: uma tabela no README.md com os dois tempos e o percentual de redução.
Dica
O cache só existe a partir da segunda execução da mesma chave (que vem do package-lock.json). Compare a terceira execução com cache contra uma execução em que você removeu a linha cache: npm — e repare que o step passa a se chamar "Post Setup Node" quando o cache é salvo.
C1. Faça o deploy acontecer apenas quando você criar uma tag de versão (v1.2.0), e não a cada push no main. A imagem precisa ser publicada com a tag da versão (1.2.0), com 1.2 e com latest, e o VPS precisa subir exatamente a versão criada.
Dica
No on:, troque branches: [main] por tags: ['v*']. No docker/metadata-action@v5, type=semver,pattern={{version}} e type=semver,pattern={{major}}.{{minor}} geram as tags a partir do nome da tag do Git. No script SSH, ${{ github.ref_name }} traz v1.2.0 — decida se a imagem usa o v ou não e mantenha a decisão nos dois lados.
Uma CI que nunca ficou vermelha não provou nada — talvez ela não esteja rodando o que você pensa. Comprove que a sua esteira reprova de verdade, nas três formas de errar que mais acontecem, e documente o que cada uma mostra no log.
Critérios de pronto
Três pull requests, cada um com uma falha diferente e proposital: um teste quebrado, um erro de lint e um package-lock.json dessincronizado do package.json.
Os três aparecem vermelhos e com o merge bloqueado pela proteção de branch.
Uma tabela no README.md com a mensagem de erro literal de cada caso e a correção.
Os três PRs são fechados sem mesclar, e o main continua verde.
Pistas
Para dessincronizar o lockfile, edite a versão de uma dependência no package.json sem rodar npm install.
Se o erro de lint não reprova, veja se o step do lint está mesmo rodando: npm run lint --if-present não falha quando o script não existe.
A mensagem literal está no log do step que falhou; a aba Annotations da execução traz o resumo.
Ao fechar um PR sem mesclar, apague também a branch remota — gh pr close --delete-branch.
Revisar um PR lendo o diff é uma coisa; abrir o site do PR no navegador é outra. Faça com que todo pull request do unieventos-web publique uma versão temporária e comente no próprio PR o endereço onde ela pode ser vista.
Critérios de pronto
Um workflow disparado por pull_request constrói o site e publica uma pré-visualização (deploy preview do Netlify, do Cloudflare Pages ou uma pasta por PR no seu VPS).
Um comentário automático no PR traz o link, atualizado a cada novo commit em vez de repetido dez vezes.
A pré-visualização é apagada quando o PR é fechado ou mesclado.
O workflow não expõe segredo nenhum em PR vindo de fork — descreva no README como você tratou esse caso.
Pistas
netlify deploy sem --prod já devolve uma URL de pré-visualização; a saída do comando traz o endereço, que você pode capturar para $GITHUB_OUTPUT.
Para comentar, gh pr comment ${{ github.event.pull_request.number }} --body "..." usando o GITHUB_TOKEN com pull-requests: write.
Para não repetir comentários, procure por um marcador oculto no corpo do comentário existente e edite-o em vez de criar outro.
O evento pull_request com types: [closed] é o gancho para a limpeza.
Testes com repositório falso não pegam erro de SQL. Faça a CI validar as consultas contra um Postgres real, aplicando as migrations do zero a cada execução — e, de quebra, provar que toda migration nova também desfaz o que fez.
Critérios de pronto
Um job de integração sobe o banco (contêiner de serviço ou uma branch efêmera do Neon), roda npm run migrar em um banco vazio e executa a suíte contra ele.
Pelo menos três testes exercitam SQL de verdade: listagem com filtro, inserção com restrição única violada e exclusão em cascata.
Cada migration tem um par .desfazer.sql, e o job aplica todas, desfaz todas e aplica de novo — terminando com o schema idêntico.
O job roda em menos de 3 minutos e não depende do banco de produção em nenhum momento.
Pistas
O contêiner de serviço da §3.1 já dá o banco; o --health-cmd é o que evita a corrida entre o job e o Postgres.
Para comparar schemas antes e depois, pg_dump --schema-only nos dois momentos e diff entre os arquivos.
Se optar pelo Neon, uma branch do banco nasce em segundos com uma cópia dos dados; crie-a no início do job e apague no fim, mesmo quando a suíte falha (if: always()).
Teste em cascata é onde aparecem os erros interessantes: apague um evento com inscrições e confira o que sobrou.
🔥
🔥 Boss — A esteira completa, com rollback automático¶
ci-cddockerdeploynginxseguranca
Você tem CI, imagem publicada e deploy por SSH. Falta o que separa um pipeline de aula de um pipeline de produção: saber que o deploy deu errado e desfazê-lo sozinho. Monte a esteira completa do UniEventos (front e back), em que um push no main entrega tudo no ar — e uma versão quebrada volta atrás sem ninguém acordar.
Critérios de pronto
Um push no main da API dispara, em ordem: testes → build da imagem com tag por commit → publicação no GHCR → deploy no VPS → verificação de saúde.
A verificação de saúde consulta https://api.seudominio.dev/healthde fora do servidor (não só 127.0.0.1) por até 60 segundos.
Se a verificação falhar, o próprio workflow refaz o deploy da versão anterior e termina em vermelho, com a versão anterior no ar e funcionando.
Um push no main do front publica o site e invalida o cache, e o site publicado consome a API publicada (sem erro de CORS e sem conteúdo misto, Capítulo 04).
O main é protegido: PR obrigatório, CI verde obrigatória, sem force push.
O README.md traz um diagrama em texto da esteira, a lista de secrets necessários (nomes, nunca valores) e o procedimento de rollback manual em 3 linhas.
Nenhum segredo aparece em log algum; o workflow de deploy não roda em PR de fork.
Pistas
A versão anterior está a um comando de distância: guarde o SHA que estava no ar antes (docker inspect no contêiner atual, ou a saída de git rev-parse HEAD~1) em uma variável antes de trocar a imagem.
if: failure() em um step faz dele um step de compensação, que só roda quando algo antes falhou. É o gancho natural do rollback.
Para a verificação de fora, um step com curl --fail --retry 10 --retry-delay 6 --retry-all-errors no próprio runner testa o caminho inteiro: DNS, nginx, HTTPS e API.
Deploy com zero interrupção é outro nível: suba o contêiner novo em outra porta, confira a saúde dele, troque o proxy_pass do nginx e só então derrube o antigo. Se for tentar, faça o nginx -t antes de cada reload.
O front e o back estão em repositórios diferentes: workflow_run ou repository_dispatch permitem que um dispare o outro, se você quiser encadeá-los.
Crie .github/workflows/ci.yml rodando npm ci, lint e testes a cada push e pull request, com cache: npm, permissions: contents: read, concurrency e timeout-minutes.
Proteja o main: pull request obrigatório e a verificação da CI como status obrigatório.
Adicione o badge da CI na primeira linha do README.md.
Crie o segundo workflow: publicação automática do front (Pages ou Netlify) ou da imagem no GHCR — o que fizer sentido para o seu projeto.
Abra um PR com um erro proposital, mostre que ele foi bloqueado, corrija no mesmo PR e mescle.
Critério de pronto: a aba Actions tem pelo menos quatro execuções (duas vermelhas e duas verdes), o badge está verde, o main não aceita push direto e nenhum segredo aparece em log ou no código.
Guarde no seu repositório: commit + push, junto com o link de uma execução vermelha e de uma verde.
Netlify CLI — deploy, --prod e variáveis de autenticação para uso em CI.
No próximo capítulo a esteira ganha exigência: ESLint e Prettier padronizando o código, cobertura de testes, Lighthouse medindo o site publicado antes e depois, logs estruturados, monitoramento de erros em produção e um aviso quando o site sair do ar.