🧠FFVAcademy
🐙

GitHub Actions: CI/CD profissional do zero

20 min de leitura·+90 XP
Pré-requisitos (0/1)0%

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

Feedback rápidoTeste quebra em 3 min, não em 3 dias quando QA abre o tíquete. Bug barato = bug corrigido rápido.
ReprodutibilidadeBuild roda igual no runner limpo — sem "na minha máquina funciona".
Integração contínuaBranches longos morrem. Todo dia junta no main, com merge pequeno e revisado.
Deploy sem dramaRelease passa a ser "PR merged → pipeline empurra", não "sexta 18h Fernando precisa ficar".
AuditoriaCada deploy tem commit, timestamp, autor, logs. Compliance e rollback ficam simples.

Anatomia de um workflow

📂.github/workflows/*.ymlarquivo
Um ou mais YAMLs. Nome do arquivo vira nome do workflow na UI.
evento dispara
on: [push, pull_request, schedule, ...]trigger
push, PR, cron, manual (workflow_dispatch), release, issue etc.
agenda jobs
🧱jobsparalelo
Unidades de trabalho. Rodam em paralelo por default; use needs para ordenar.
aloca runner
🖥️runs-on: ubuntu-latestrunner
VM efêmera do GitHub (Linux/Mac/Win) ou self-hosted seu.
executa
🔢stepssequencial
Comandos (run:) ou actions (uses:) executados em ordem no mesmo runner.
yaml
# .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 build

Jobs 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.

yaml
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

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@v4
💡
Matrix × 3 OS × 3 Node = 9 jobs em paralelo. Em repo público você paga zero. Em privado isso consome minutos rápido — use fail-fast com cuidado e restrinja a matrix no branch main.

Secrets e OIDC — a parte mais crítica de segurança

AbordagemComo funcionaRisco
Static secretAWS_ACCESS_KEY_ID fixo em Settings → SecretsAlto — chave permanente, rotação manual, vaza no log se você der echo
OIDC FederationGitHub assina JWT por run; cloud valida trust policy e emite STS temporárioBaixo — credencial vive só durante o run, trust policy amarra a repo/branch/env
Reusable workflow + envSecrets centralizados em um único ponto, herdado via secrets: inheritMédio — melhor que espalhar, ainda estático
HashiCorp Vault / AWS Secrets ManagerAction busca segredo em runtimeBaixo — quando OIDC não existe para o provider
yaml
# 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 --delete
Para habilitar OIDC na AWS: criar um Identity Provider do tipo OIDC apontando para token.actions.githubusercontent.com, criar uma IAM Role com trust policy restrita a repo:me/meu-repo:ref:refs/heads/main. Assim essa role pode ser assumida por workflows desse repo, nesse branch. Se um fork fizer PR, não consegue deploy.
🚨
Nunca ecoe segredo. 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.

yaml
- 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 ~/.npm
Cache de imagem DockerUse type=gha no buildx (cache-from: type=gha, cache-to: type=gha,mode=max). Cache de layers direto nos artifacts do GH.
Cache de dependêncianpm → ~/.npm, pnpm → ~/.pnpm-store, pip → ~/.cache/pip, cargo → ~/.cargo + target/, gradle → ~/.gradle.
InvalidaçãoKey com hash do lockfile invalida quando deps mudam. restore-keys pega cache de commit anterior se key exata não existe.

Reusable 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.

yaml
# 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
yaml
# 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 (@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.

yaml
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 prod

Release automatizado com semantic versioning

yaml
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 (--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

yaml
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 1

Self-hosted runners — quando usar

DimensãoGitHub-hostedSelf-hosted
SetupZero — já existeVocê provisiona VM/K8s + instala agent
CustoGrátis em públicos; minutos pagos em privadosSeus recursos (VM, K8s) + operação
SegurançaVM descartada após cada jobVocê garante isolamento entre jobs
Custom toolingInstala a cada run (lento)Imagem pré-configurada — rápido
Rede privadaNão acessa sua VPC/on-premAcessa — runner está na sua rede
Quando usarDefault para 95% dos casosOn-prem, builds GPU, compliance, caches pesados
⚠️
Nuncarode 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

Pin por SHAuses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 — tag é móvel, SHA não é.
Dependabot para actions.github/dependabot.yml com package-ecosystem: "github-actions" — Dependabot atualiza pins automaticamente.
permissions: restritivasComeça com permissions: {} no topo do workflow e vai adicionando só o necessário (contents: read, packages: write).
GITHUB_TOKEN granularToken default tem menos escopo se permissions é declarado. Workflow sem declaração herda permissões antigas (amplas) — perigoso.
CodeQL / Trivy / SnykSAST e scan de dependência no próprio workflow. CodeQL é nativo do GitHub (Security tab).
Secret scanningPush protection bloqueia commit com segredo conhecido. Habilite em Settings → Security.
yaml
permissions:
  contents: read     # default mínimo

jobs:
  release:
    permissions:
      contents: write         # cria tag/release
      id-token: write         # OIDC
      packages: write         # publica no GHCR
      # nada mais

Debugging prático

Re-run com debugUI: Re-run jobs → Enable debug logging. Adiciona ACTIONS_RUNNER_DEBUG e ACTIONS_STEP_DEBUG.
tmate — shell remotoStep com mxschmitt/action-tmate@v3 abre shell SSH num runner vivo — útil para debugar Windows ou passos misteriosos.
act — rodar localnektos/act simula workflows no Docker. Não 100% fiel, mas salva muito tempo.
Logs por stepCada step tem artifacts de log; upload-artifact pra guardar coverage/screenshots do Playwright.

Decisão: GH Actions vs Jenkins vs Azure DevOps

📋 Startup de 10 devs, tudo no GitHub

GitHub Actions

Zero setup, integração nativa com PR/issues, OIDC pra cloud. Custo baixo, curva rasa. Migrar depois se crescer de verdade.

Alt: Jenkinsoverkill e custo operacional alto.

📋 Empresa enterprise, repos distribuídos em Bitbucket/GitLab/GitHub, compliance apertado

Jenkins (ou GitLab CI se tudo é GitLab)

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 Serverse o norte é consolidar tudo no GitHub.

📋 Time Microsoft, cloud Azure, integração com Azure AD e Boards

Azure DevOps Pipelines

Integração nativa com Azure (service connections, environments, AKS), governança com Azure AD groups, Boards + Repos + Pipelines no mesmo produto.

Alt: GH ActionsMicrosoft empurra pra lá em projetos novos — migração eventual.

Perguntas típicas

Workflow passou local no act mas falha no GitHub. Por quê?

act usa imagens Docker que não batem 100% com runners reais. Coisas que divergem: ferramentas pré-instaladas, versão de sudo, montagem de 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?

Sim. Crie repo próprio com action.yml na raiz. Outros repos consomem com uses: me/minha-action@v1. Use composite actions pra empacotar vários steps YAML, JavaScript actions pra lógica complexa, Docker actions pra quando precisa de ambiente específico.

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?

Três técnicas: (1) paths filter por app; (2) reusable workflow por linguagem/tipo de app; (3) detector de mudanças (dorny/paths-filter) que dispara jobs dinâmicos. Em escala real, nx/turbo/bazel + cache remoto resolvem mais do que YAML tuning.
Take-aways. (1) Workflow = arquivo YAML, jobs = paralelos, steps = sequenciais no mesmo runner. (2) 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

Continue lendo