GitHub Actions: CI/CD profissional do zero
CI/CD é o sistema que transforma “push no main” em “versão rodando em produção” sem ninguém arrastar arquivo por SSH. Integração contínua (CI) roda testes, lint, build e scanners a cada commit. Entrega contínua (CD) pega o artefato aprovado e publica em staging/prod. GitHub Actions é a implementação nativa do GitHub: workflows em YAML, runners hospedados (ou self-hosted), ecossistema enorme de actions reutilizáveis, e integração profunda com PRs, issues, releases. Para projetos que vivem no GitHub, é o caminho default — barato (2.000 min/mês grátis em repos privados pessoais, ilimitado em públicos) e fácil de começar.
Por que CI/CD existe
Anatomia de um workflow
# .github/workflows/ci.yml — o menor workflow útil
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm test -- --ci
- run: npm run buildJobs em paralelo + needs (o mapa real de um pipeline)
Jobs independentes devem rodar em paralelo. O needs declara dependência — o deploy só começa quando test + lint + build terminam com sucesso.
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
- run: npm test -- --coverage
- uses: actions/upload-artifact@v4
with: { text: coverage, path: coverage/ }
build:
needs: [lint, test] # só roda se lint E test passarem
runs-on: ubuntu-latest
outputs:
image: ${'$'}{{ steps.meta.outputs.image }}
steps:
- uses: actions/checkout@v4
- id: meta
run: echo "image=ghcr.io/${'$'}{{ github.repository }}:${'$'}{{ github.sha }}" >> "$GITHUB_OUTPUT"
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${'$'}{{ github.actor }}
password: ${'$'}{{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
push: true
tags: ${'$'}{{ steps.meta.outputs.image }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-staging:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: staging # ativa gate de aprovação se configurado
steps:
- run: echo "Deploying ${'$'}{{ needs.build.outputs.image }}"Matrix builds — testar em N versões sem duplicar YAML
jobs:
test:
runs-on: ${'$'}{{ matrix.os }}
strategy:
fail-fast: false # não derruba os outros se um falhar
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
include:
- os: ubuntu-latest
node: 20
coverage: true # só essa combinação sobe coverage
exclude:
- os: windows-latest
node: 18 # não suportamos essa combinação
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${'$'}{{ matrix.node }} }
- run: npm ci
- run: npm test
- if: matrix.coverage
uses: codecov/codecov-action@v4Matrix × 3 OS × 3 Node = 9 jobs em paralelo. Em repo público você paga zero. Em privado isso consome minutos rápido — use com cuidado e restrinja a matrix no branch .
Secrets e OIDC — a parte mais crítica de segurança
| Abordagem | Como funciona | Risco |
|---|---|---|
| Static secret | AWS_ACCESS_KEY_ID fixo em Settings → Secrets | Alto — chave permanente, rotação manual, vaza no log se você der echo |
| OIDC Federation | GitHub assina JWT por run; cloud valida trust policy e emite STS temporário | Baixo — credencial vive só durante o run, trust policy amarra a repo/branch/env |
| Reusable workflow + env | Secrets centralizados em um único ponto, herdado via secrets: inherit | Médio — melhor que espalhar, ainda estático |
| HashiCorp Vault / AWS Secrets Manager | Action busca segredo em runtime | Baixo — quando OIDC não existe para o provider |
# OIDC para AWS (zero static keys)
permissions:
id-token: write # <<< necessário pra emitir o JWT OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deployer
role-session-name: gha-${'$'}{{ github.run_id }}
aws-region: us-east-1
- run: aws s3 sync ./dist s3://meu-bucket --deletePara habilitar OIDC na AWS: criar um Identity Provider do tipo OIDC apontando para , criar uma IAM Role com trust policy restrita a . Assim essa role só pode ser assumida por workflows desse repo, nesse branch. Se um fork fizer PR, não consegue deploy.
Nunca ecoe segredo. = segredo em log em texto claro. GitHub faz mask de segredos declarados, mas derivados (base64, JSON encoded) passam batido. Use ou evite qualquer print.
Cache — transforme build de 8min em 2min
actions/cache salva pastas entre runs. A chave (key) determina se há hit. A boa prática: colocar um hash do lockfile na key e definir restore-keys em cascata.
- name: Cache node_modules
uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: ${'$'}{{ runner.os }}-node-${'$'}{{ hashFiles('**/package-lock.json') }}
restore-keys: |
${'$'}{{ runner.os }}-node-
# Melhor ainda: use o cache embutido do setup-node (atrás é actions/cache)
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm' # detecta package-lock.json e cacheia ~/.npmReusable workflows — DRY em escala
Copiar o mesmo workflow em 30 repos é receita para drift. Reusable workflows são workflows chamados por outros workflows com uses: — parametrizáveis com inputs e secrets.
# org/.github/.github/workflows/build-and-push.yml (o "definido")
on:
workflow_call:
inputs:
image-name:
required: true
type: string
platforms:
default: 'linux/amd64'
type: string
secrets:
REGISTRY_TOKEN:
required: true
jobs:
build-push:
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
push: true
tags: ${'$'}{{ inputs.image-name }}:${'$'}{{ github.sha }}
platforms: ${'$'}{{ inputs.platforms }}
cache-from: type=gha
cache-to: type=gha,mode=max# no repo que consome:
jobs:
build:
uses: minha-org/.github/.github/workflows/build-and-push.yml@v1
with:
image-name: ghcr.io/minha-org/minha-api
platforms: 'linux/amd64,linux/arm64'
secrets:
REGISTRY_TOKEN: ${'$'}{{ secrets.GITHUB_TOKEN }}Versione reusable workflows com tag (, ). Mudanças breaking só num major novo. Pin por SHA em repos regulados.
Environments, protection rules e approvals
Environments (Settings → Environments) são grupos lógicos (staging, prod) com regras: required reviewers, wait timer, deployment branches, e secrets próprios. Quando um job tem environment: prod, a execução pausa até reviewers aprovarem.
jobs:
deploy-prod:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://app.meusite.com # aparece na UI
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111:role/prod-deployer
aws-region: us-east-1
- run: ./scripts/deploy.sh prodRelease automatizado com semantic versioning
name: Release
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
id: rp
with:
release-type: node # lê commits convencionais e cria PR de release
outputs:
release_created: ${'$'}{{ steps.rp.outputs.release_created }}
tag_name: ${'$'}{{ steps.rp.outputs.tag_name }}
build-and-publish:
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, registry-url: 'https://registry.npmjs.org' }
- run: npm ci
- run: npm run build
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${'$'}{{ secrets.NPM_TOKEN }}Provenance (): o npm registra no Sigstore que aquele pacote veio desse workflow, desse SHA. Consumidores podem verificar — quebra categoria inteira de ataque de supply chain.
Deploy em Kubernetes — fluxo completo
name: Deploy
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
build:
uses: ./.github/workflows/build-and-push.yml
with:
image-name: ghcr.io/me/api
secrets: inherit
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111:role/eks-deployer
aws-region: us-east-1
- name: Setup kubectl + kubeconfig
run: |
aws eks update-kubeconfig --name prod-cluster --region us-east-1
kubectl version --client
- name: Apply manifests com image atualizada
run: |
cd k8s/
kustomize edit set image api=ghcr.io/me/api:${'$'}{{ github.sha }}
kubectl apply -k .
kubectl rollout status deploy/api --timeout=5m
- name: Smoke test
run: |
ENDPOINT=$(kubectl get ing api -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
curl -fsS "https://$ENDPOINT/health" || exit 1Self-hosted runners — quando usar
| Dimensão | GitHub-hosted | Self-hosted |
|---|---|---|
| Setup | Zero — já existe | Você provisiona VM/K8s + instala agent |
| Custo | Grátis em públicos; minutos pagos em privados | Seus recursos (VM, K8s) + operação |
| Segurança | VM descartada após cada job | Você garante isolamento entre jobs |
| Custom tooling | Instala a cada run (lento) | Imagem pré-configurada — rápido |
| Rede privada | Não acessa sua VPC/on-prem | Acessa — runner está na sua rede |
| Quando usar | Default para 95% dos casos | On-prem, builds GPU, compliance, caches pesados |
Nunca rode self-hosted runner em repo público sem configurar “fork pull request approval”. Fork pode rodar código arbitrário no runner — é o equivalente a dar shell root em sua máquina para qualquer um do mundo.
Supply chain: pinning, least privilege, scanners
permissions:
contents: read # default mínimo
jobs:
release:
permissions:
contents: write # cria tag/release
id-token: write # OIDC
packages: write # publica no GHCR
# nada maisDebugging prático
Decisão: GH Actions vs Jenkins vs Azure DevOps
📋 Startup de 10 devs, tudo no GitHub
Zero setup, integração nativa com PR/issues, OIDC pra cloud. Custo baixo, curva rasa. Migrar depois se crescer de verdade.
Alt: Jenkins —
📋 Empresa enterprise, repos distribuídos em Bitbucket/GitLab/GitHub, compliance apertado
Jenkins é plataforma-agnóstica, roda on-prem, plugin pra qualquer coisa, shared libraries organizam padrão comum. Cobra em sofrimento operacional, mas atende requisitos que GH Actions não cumpre.
Alt: GH Actions Enterprise Server —
📋 Time Microsoft, cloud Azure, integração com Azure AD e Boards
Integração nativa com Azure (service connections, environments, AKS), governança com Azure AD groups, Boards + Repos + Pipelines no mesmo produto.
Alt: GH Actions —
Perguntas típicas
❓ Workflow passou local no act mas falha no GitHub. Por quê?
❓ Como fazer um workflow rodar só quando certas pastas mudam?
❓ Posso compartilhar uma action que escrevi entre repos?
❓ Tem como cancelar runs antigas quando um novo push chega?
❓ Workflows em monorepo com 20 apps ficam lentos. Como otimizar?
Take-aways. (1) Workflow = arquivo YAML, jobs = paralelos, steps = sequenciais no mesmo runner. (2) orquestra; comunica entre jobs. (3) OIDC > static secrets — trust policy amarra a repo/branch/env. (4) Cache via ou com key baseada no lockfile. (5) Reusable workflows para DRY em escala — versione com tag ou SHA. (6) Environments + protection rules = gate de aprovação real. (7) Pin por SHA em actions sensíveis, declare mínimos, use Dependabot. (8) Próximo salto: deploy em K8s com OIDC + kubectl + smoke test pós-rollout.
Próximos passos sugeridos
Discussão
Carregando…