O problema real
Você tem um repositório GitHub, faz commit, e dois desenvolvedores testam localmente. Um passa, outro falha — "funciona no meu laptop". Código vai pra produção, quebra. Ou você roda testes manualmente antes de fazer deploy, e tá aí 30 minutos esperando para liberar uma mudança simples de uma linha.
A realidade: sem CI/CD, cada desenvolvedor testa de forma diferente. Sem CD (continuous deployment), ninguém sabe quando tá seguro deploy. GitHub Actions resolve: toda mudança que entra é testada automaticamente, e se passar, pode ir direto pra produção sem click manual.
Anatomia de um workflow: on, jobs, steps, runs-on
Um workflow é arquivo YAML em .github/workflows/. Estrutura básica:
name: CI/CD Pipeline # nome visual
on: # quando disparar?
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs: # conjunto de tarefas
test: # primeira tarefa
runs-on: ubuntu-latest # qual máquina?
steps:
- name: Checkout code # descarga repo
uses: actions/checkout@v4
- name: Setup Node # instala Node
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Run tests # executa testes
run: npm test
Registrador: GitHub mostra cada step, com tempo e output. Se um falha, marca tudo em vermelho e avisa via email/Slack.
Pipeline completo: lint → testes → build → deploy
name: Full CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
lint:
name: Lint & Format Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Check formatting with Prettier
run: npm run format:check
test:
name: Unit Tests
runs-on: ubuntu-latest
needs: lint # roda SÓ se lint passar
strategy:
matrix:
node-version: [16, 18, 20] # testa 3 versões do Node
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm test -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
build:
name: Build Docker Image
runs-on: ubuntu-latest
needs: test # só se testes passarem
if: github.event_name == 'push' # só em push, não em PR
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=semver,pattern={{version}}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/develop' # só branch develop
steps:
- name: Deploy to Staging
run: |
curl -X POST ${{ secrets.DEPLOY_WEBHOOK }} \
-H "Content-Type: application/json" \
-d '{"image":"${{ needs.build.outputs.image }}"}'
deploy-prod:
name: Deploy to Production
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' # só branch main
environment:
name: production
url: https://app.example.com
steps:
- name: Manual approval required
run: echo "Waiting for approval..."
- name: Deploy to Production
run: |
curl -X POST ${{ secrets.PROD_DEPLOY_WEBHOOK }} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" \
-d '{"environment":"production"}'
- name: Notify deployment
if: success()
run: |
echo "✓ Deployed to production"
O que acontece:
on pushdispara todo o pipelinelintroda em paralelo (runs-on: ubuntu-latest= máquina Ubuntu)testroda SÓ se lint passou (needs: lint)testroda em 3 versões de Node simultaneamente (matrix.node-version)buildroda SÓ se testes passaramdeployedeploy-prodsão condicionais por branch- Cache economiza minutos (veja seção abaixo)
Cache de dependências e quanto economiza
Cache reutiliza node_modules entre rodadas:
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.npm # cache local do npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
Teste real:
- Sem cache:
npm ci= 45 segundos - Com cache, hit:
npm ci= 3 segundos (lê cache) - Com cache, miss:
npm ci= 45 segundos (regenera cache)
Por 10 runs/dia: (45-3) * 10 = 420 segundos = 7 minutos economizados/dia. Em um mês: 3,5 horas.
Matriz de versões
Testar em múltiplas versões de linguagem com strategy.matrix:
strategy:
matrix:
node-version: [16, 18, 20]
os: [ubuntu-latest, macos-latest, windows-latest]
Isso cria 9 combinações de jobs. Cada um roda isolado. Seu código roda em Node 16, 18 e 20 em Linux, Mac e Windows. Número real: encontra incompatibilidades antes do usuário.
Secrets e por que nunca ir para o log
❌ NUNCA hardcode secrets:
# ERRADO: mesmo que você apague depois, GitHub guarda história
- name: Deploy
run: curl https://api.com -H "Authorization: Bearer abc123def456"
GitHub procura por padrões de secrets no commit e avisa. Mas já vaza pra log publicamente.
✓ USE secrets:
- name: Deploy
run: |
curl https://api.com \
-H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}"
Secrets são:
- Definidos em Settings > Secrets and variables > Actions
- Não aparecem em logs (GitHub substitui por
***) - Somente acessíveis em workflows
- Criptografados em repouso
Real:
# Log publicamente visível
Run: curl -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}"
# Resultado: Authorization: Bearer ***
Ambientes e aprovação manual
Para produção, aprove manualmente antes de deploy:
deploy-prod:
name: Deploy to Production
runs-on: ubuntu-latest
environment:
name: production
url: https://app.example.com
steps:
- name: Deploy
run: ./deploy.sh
GitHub aguarda aprovação. Clica na aba "Deployments", vê a request, aprova ou rejeita. Até 30 dias de espera.
Jobs condicionais por branch
Roda apenas em branches específicas:
deploy:
if: github.ref == 'refs/heads/develop' # só develop
runs-on: ubuntu-latest
steps:
- name: Deploy to staging
run: ./deploy-staging.sh
deploy-prod:
if: github.ref == 'refs/heads/main' # só main
runs-on: ubuntu-latest
steps:
- name: Deploy to production
run: ./deploy-prod.sh
Ou por evento (push vs PR):
if: github.event_name == 'push' # só em push
if: github.event_name == 'pull_request' # só em PR
Armadilhas comuns
1. Workflow que roda em todo push e estoura minuto mensal gratuito:
GitHub Actions: 2.000 minutos/mês grátis (privado). Um workflow de 45 minutos por push em 50 pushes/dia = 2.250 minutos. Paga.
Solução:
on:
push:
branches: [main, develop] # só branches importantes
paths-ignore:
- 'docs/**' # não rode se muda só docs
- '*.md'
2. Segredo exposto em PR de fork:
Fork externo faz PR, workflow roda, e não tem acesso a secrets da repo principal (por padrão, segurança). Mas push da main tem. Risco: alguém injeta step que faz log de secret.
Proteção:
permissions:
contents: read # read-only
pull-requests: read
3. Deploy sem rollback:
Se deploy quebra produção, ninguém volta. Sempre tenha reversão automática.
- name: Deploy
run: ./deploy.sh
- name: Health check
run: curl -f https://app.example.com/health || ./rollback.sh
Quando NÃO usar GitHub Actions
Projeto com 50+ deploy/dia: Actions fica caro (paga minuto adicional). Use Jenkins ou GitLab CI (mais barato em scale).
Build que demora 2 horas: máquinas Actions são ubuntu-latest com 4 CPUs. Compile localmente, cacheia, ou use runner dedicado (pago).
Segredos sensíveis demais: se precisa de vaults externos (HashiCorp Vault, AWS Secrets Manager), integre, mas complexidade cresce. Considere sistema dedicado.
Próximos passos
Empacotar imagens Docker durante CI/CD com Docker: imagens menores com multi-stage — otimize build time.
Escalável e com infra em código: Terraform na prática: sua primeira infra versionada.
Crie um repositório novo no GitHub, coloque o workflow acima, faça commit, e vê rodar em tempo real na aba Actions. Clique em cada step e veja logs. Quebrar uma coisa propositalmente (remova um import do código, vê test falhar). Esse feedback visual é ouro para aprender.