GitHub Actions: Dominando Workflows, Jobs, Steps e Actions Reutilizáveis
GitHub Actions é a solução nativa do GitHub para automação e integração contínua (CI/CD). Diferentemente de ferramentas externas, ela integra-se perfeitamente ao seu repositório, elimina a complexidade de configurações em servidores separados e permite que você automatize praticamente qualquer fluxo de trabalho com apenas alguns arquivos YAML. Neste artigo, vamos entender desde o conceito fundamental até a criação de actions reutilizáveis que você pode compartilhar entre projetos.
O que é GitHub Actions?
GitHub Actions funciona com base em eventos. Quando algo acontece no seu repositório — como um push, um pull request ou até uma publicação de release — uma "workflow" é acionada. Essa workflow contém a lógica de execução: testes, builds, deployments, notificações e muito mais. O grande diferencial é que tudo roda em máquinas virtuais fornecidas pelo GitHub, sem necessidade de infraestrutura própria.
Estrutura Fundamental: Workflows, Jobs e Steps
Entendendo a Hierarquia
Uma workflow é um arquivo YAML armazenado em .github/workflows/ que define o fluxo de automação. Dentro de uma workflow, você tem jobs — unidades independentes de trabalho que podem rodar em paralelo ou sequencial. Dentro de cada job, existem steps — as ações individuais que executam comandos ou rodam ações pré-construídas.
A hierarquia é clara:
Workflow (arquivo .yaml)
├── Job 1
│ ├── Step 1
│ ├── Step 2
│ └── Step 3
└── Job 2
├── Step 1
└── Step 2
Exemplo Prático: Workflow Básica
Vamos criar uma workflow que executa testes automaticamente quando há um push na branch main. Crie o arquivo .github/workflows/ci.yaml:
name: CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
if: always()
Neste exemplo, o job test é executado em uma máquina Ubuntu. Os steps seguem uma sequência: checkout do código, setup do Node.js, instalação de dependências, execução de lint e testes. O if: always() garante que a cobertura seja enviada mesmo se os testes falharem.
Executando Jobs em Paralelo e Sequência
Por padrão, jobs executam em paralelo. Se você precisar que um job dependa de outro, use a palavra-chave needs. Veja este exemplo onde o job de deploy depende do job de build:
name: Build and Deploy
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: build
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: build
- name: Deploy to production
run: |
echo "Deploying application..."
# Seu script de deploy aqui
Aqui, o job deploy só começa após o sucesso do job build — isso é essencial para garantir que você só faça deploy de uma versão que foi testada.
Actions Reutilizáveis: Criando Componentes Modulares
Por Que Actions Reutilizáveis Importam
Conforme seus projetos crescem, você notará que certos passos são repetidos em múltiplas workflows. Ao invés de copiar e colar o mesmo código YAML, você pode criar uma action reutilizável — um componente que encapsula lógica e pode ser utilizado em qualquer workflow, até em repositórios diferentes.
Actions reutilizáveis funcionam como funções: recebem inputs, executam lógica, produzem outputs e podem ser compartilhadas.
Estrutura de uma Action Reutilizável
Uma action reutilizável é definida em um arquivo action.yaml e pode conter scripts em shell, JavaScript ou usar imagens Docker. Vamos criar uma action que valida a versão de um projeto:
name: 'Validate Version'
description: 'Valida se a versão no package.json segue semântica correta'
inputs:
version-path:
description: 'Caminho para o arquivo com a versão'
required: false
default: 'package.json'
outputs:
current-version:
description: 'Versão extraída'
value: ${{ steps.extract.outputs.version }}
is-valid:
description: 'Se a versão é válida (true/false)'
value: ${{ steps.validate.outputs.valid }}
runs:
using: 'composite'
steps:
- name: Extract version
id: extract
shell: bash
run: |
VERSION=$(cat ${{ inputs.version-path }} | grep '"version"' | head -1 | sed 's/.*"version": "\([^"]*\)".*/\1/')
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Validate semantic versioning
id: validate
shell: bash
run: |
VERSION="${{ steps.extract.outputs.version }}"
if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "valid=true" >> $GITHUB_OUTPUT
echo "✓ Versão $VERSION é válida"
else
echo "valid=false" >> $GITHUB_OUTPUT
echo "✗ Versão $VERSION não segue semântica semver"
exit 1
fi
Salve este arquivo como .github/actions/validate-version/action.yaml. Agora você pode usar essa action em qualquer workflow:
name: Validate Release
on:
push:
tags: [ 'v*' ]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate project version
id: version
uses: ./.github/actions/validate-version
with:
version-path: 'package.json'
- name: Show results
run: |
echo "Versão atual: ${{ steps.version.outputs.current-version }}"
echo "É válida: ${{ steps.version.outputs.is-valid }}"
Usando Secrets e Variáveis em Actions
Actions reutilizáveis frequentemente precisam de dados sensíveis, como tokens ou senhas. O GitHub fornece uma forma segura de usar esses dados através de secrets. Vamos criar uma action que faz deploy usando um token:
name: 'Deploy Application'
description: 'Faz deploy da aplicação para o servidor'
inputs:
environment:
description: 'Ambiente de deploy (staging ou production)'
required: true
default: 'staging'
outputs:
deployment-url:
description: 'URL da aplicação deployada'
value: ${{ steps.deploy.outputs.url }}
runs:
using: 'composite'
steps:
- name: Deploy
id: deploy
shell: bash
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
ENV: ${{ inputs.environment }}
run: |
# Valida se token existe
if [ -z "$DEPLOY_TOKEN" ]; then
echo "Erro: DEPLOY_TOKEN não configurado"
exit 1
fi
# Faz deploy
echo "Deploying para $ENV..."
# Seu script de deploy aqui
# Define output
if [ "$ENV" = "production" ]; then
echo "url=https://app.example.com" >> $GITHUB_OUTPUT
else
echo "url=https://staging.example.com" >> $GITHUB_OUTPUT
fi
Use assim em sua workflow:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to production
uses: ./.github/actions/deploy
with:
environment: 'production'
secrets:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Padrões Avançados e Melhores Práticas
Reutilizando Workflows Inteiras
Você também pode reutilizar workflows completas! Isso é útil quando múltiplos projetos precisam do mesmo pipeline. Crie .github/workflows/reusable-test.yaml:
name: Reusable Test Workflow
on:
workflow_call:
inputs:
node-version:
type: string
required: false
default: '18'
secrets:
npm-token:
required: false
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Install dependencies
run: npm ci
env:
NPM_TOKEN: ${{ secrets.npm-token }}
- name: Run tests
run: npm test
Em outro repositório, você chama assim:
name: CI
on: [ push, pull_request ]
jobs:
test:
uses: seu-usuario/seu-repo/.github/workflows/reusable-test.yaml@main
with:
node-version: '20'
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
Tratamento de Erros e Condicionais
GitHub Actions oferece várias funções para controlar o fluxo baseado em resultados:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check syntax
id: syntax
continue-on-error: true
run: |
npm run lint
- name: Notify on failure
if: failure()
run: |
echo "Pipeline falhou"
exit 1
- name: Report success
if: success()
run: echo "Tudo passou!"
- name: Always run
if: always()
run: echo "Isso sempre executa, sucesso ou falha"
O if: failure() executa apenas se algum step anterior falhar. O if: success() executa apenas se todos forem bem-sucedidos. O if: always() ignora completamente o status anterior.
Cacheando Dependências para Performance
Um padrão essencial é cachear dependências para evitar re-downloads:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
- name: Cache npm 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: Build
run: npm run build
O cache usa um hash do package-lock.json como chave. Se o arquivo não mudou, as dependências em cache são reutilizadas, economizando tempo e largura de banda.
Conclusão
GitHub Actions transforma a forma como você automatiza seu fluxo de desenvolvimento. Os três pontos principais que você deve levar adiante são:
-
Workflows, Jobs e Steps são blocos de construção hierárquicos: comece simples, entenda como eles se relacionam, e escale conforme necessário. Use
needspara criar dependências entre jobs quando o pipeline exigir sequência. -
Actions reutilizáveis reduzem duplicação e aumentam manutenibilidade: encapsule lógica comum em actions e compartilhe-as. Isso não apenas economiza linhas de código, mas torna sua pipeline mais profissional e fácil de manter.
-
Performance e segurança são responsabilidades suas: implemente caching estrategicamente, nunca hardcode secrets (use a gestão de secrets nativa), e sempre teste suas workflows antes de mergear em produção.