GitHub Actions: CI/CD profissional do zero
- ⬜🐳 Docker Completo: do zero ao production-ready(DevOps & Containers)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
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: { name: 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@v4fail-fast com cuidado e restrinja a matrix no branch main.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 --deletetoken.actions.githubusercontent.com, criar uma IAM Role com trust policy restrita a repo:me/meu-repo:ref:refs/heads/main. Assim essa role só pode ser assumida por workflows desse repo, nesse branch. Se um fork fizer PR, não consegue deploy.run: echo $SECRET = segredo em log em texto claro. GitHub faz mask de segredos declarados, mas derivados (base64, JSON encoded) passam batido. Use ::add-mask:: 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 }}@v1, @v2). 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 |
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 — overkill e custo operacional alto.
📋 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 — se o norte é consolidar tudo no GitHub.
📋 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 — Microsoft empurra pra lá em projetos novos — migração eventual.
Perguntas típicas
❓ Workflow passou local no act mas falha no GitHub. Por quê?
GITHUB_WORKSPACE, secrets. Use act pra iterar rápido, mas valide sempre no runner real antes de declarar pronto.❓ Como fazer um workflow rodar só quando certas pastas mudam?
on.push.paths e on.pull_request.paths aceitam glob. Ex.: paths: ['apps/api/**', '.github/workflows/ci-api.yml']. Útil em monorepos pra não rodar CI da API quando só o frontend muda.❓ Posso compartilhar uma action que escrevi entre repos?
❓ Tem como cancelar runs antigas quando um novo push chega?
concurrency: { group: ci-${{ github.ref }}, cancel-in-progress: true }. Cada branch tem grupo próprio; novo push no mesmo branch cancela o run anterior. Economiza minutos e evita deploy de versão já desatualizada.❓ Workflows em monorepo com 20 apps ficam lentos. Como otimizar?
needs orquestra; outputscomunica entre jobs. (3) OIDC > static secrets — trust policy amarra a repo/branch/env. (4) Cache via setup-* ou actions/cache 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 permissions mínimos, use Dependabot. (8) Próximo salto: deploy em K8s com OIDC + kubectl + smoke test pós-rollout.Quiz rápido
4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito