Teste de carga no GitHub Actions: guia prático
7 min de leitura
Para fazer teste de carga no GitHub Actions basta um arquivo de workflow: instalar a ferramenta, rodar um teste curto contra uma URL de staging e deixar o código de saída da ferramenta reprovar o job quando um threshold não for atingido. Envie os resultados como artefato para que cada execução deixe registro. Mantenha o teste pequeno, porque um runner padrão hospedado pelo GitHub é uma única máquina virtual com poucos núcleos de CPU.
Os dados sobre as ferramentas e sobre o GitHub vêm da documentação de cada fornecedor, conferidos em 9 de outubro de 2026, e os workflows seguem esse uso documentado.
| Ferramenta | Instalada por | O que reprova o job | Resultados para guardar |
|---|---|---|---|
| k6 | setup-k6-action da Grafana | Um threshold não atingido | Saída em JSON |
| Artillery | artilleryio/action-cli | Uma verificação ensure que falhou | report.json |
| Locust | pip install locust | Qualquer requisição com falha, ou uma regra sua | Arquivos CSV e HTML |
| JMeter | O arquivo da versão | Nada embutido: adicione um passo de verificação | Arquivo .jtl e relatório HTML |
Do que um workflow de teste de carga precisa?
De um gatilho, um runner, um passo que instala a ferramenta, um passo que roda o teste com thresholds e um passo que envia os resultados. O resto é opcional.
Guarde o script de teste no repositório, por exemplo em tests/load/. Leia a URL de staging de uma variável do repositório pelo contexto vars e defina timeout-minutes no job: o padrão é de 360 minutos (conferido em 9 de outubro de 2026).
Quais eventos devem disparar um teste de carga?
Rode um teste curto nos pull requests e um mais longo por agendamento ou manualmente.
Use pull_request para pegar uma regressão antes do merge, workflow_dispatch para execuções manuais e schedule para execuções cron (em UTC, a menos que você defina um fuso horário). A referência de eventos do GitHub traz alguns limites (conferido em 9 de outubro de 2026): agendamentos rodam só na branch padrão, no máximo a cada cinco minutos, e podem atrasar no começo de cada hora. Pull requests vindos de forks não recebem segredos além do GITHUB_TOKEN.
Como rodar o k6 no GitHub Actions?
Use as duas actions oficiais da Grafana, uma para instalar o k6 e outra para rodar o script. O passo falha sempre que o k6 termina com código de saída diferente de zero, que é o que um threshold não atingido provoca.
Tanto a setup-k6-action quanto a run-k6-action estão na v1 (conferido em 9 de outubro de 2026):
name: load-test
on:
pull_request:
workflow_dispatch:
jobs:
k6:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
BASE_URL: ${{ vars.STAGING_URL }}
steps:
- uses: actions/checkout@v7
- uses: grafana/setup-k6-action@v1
- uses: grafana/run-k6-action@v1
with:
path: tests/load/smoke.js
flags: --out json=results.json
- uses: actions/upload-artifact@v7
if: always()
with:
name: k6-results
path: results.json
retention-days: 14
Os thresholds ficam no script, que lê BASE_URL do ambiente:
import http from "k6/http";
export const options = {
vus: 10,
duration: "1m",
thresholds: {
http_req_failed: ["rate<0.01"],
http_req_duration: ["p(95)<500"],
},
};
export default function () {
http.get(`${__ENV.BASE_URL}/health`);
}
A documentação do k6 deixa claro que os checks não afetam o código de saída, então a decisão de aprovar ou reprovar precisa vir dos thresholds.
Como rodar Artillery, Locust ou JMeter no lugar?
Troque os passos de instalação e execução, mantenha o resto do workflow e use o mecanismo de cada ferramenta para falhar quando uma meta não for atingida.
Artillery tem uma GitHub Action oficial cujo README diz que a @v2 acompanha a versão 2.x mais recente (conferido em 9 de outubro de 2026). O passo é uses: artilleryio/action-cli@v2 com command: run --target ${{ vars.STAGING_URL }} tests/load/api.yml --output report.json, e o plugin ensure faz o Artillery sair com código diferente de zero quando uma meta não é atingida. A seção config do script:
config:
target: "https://staging.example.com"
phases:
- duration: 60
arrivalRate: 5
plugins:
ensure:
thresholds:
- "http.response_time.p95": 500
Locust sai com código 1 quando alguma requisição falhou, e para regras de latência você define environment.process_exit_code num listener do evento quitting, como mostra a mesma página (conferido em 9 de outubro de 2026). Depois de actions/setup-python@v7 com python-version: "3.13":
- run: pip install locust
- run: >
locust -f tests/load/locustfile.py --headless
-u 20 -r 5 -t 2m -H "$BASE_URL"
--csv results --html report.html
JMeter roda sem interface gráfica com -n, e as opções de linha de comando do manual não incluem nenhum threshold de aprovação (conferido em 9 de outubro de 2026). Um passo curto pode ler o arquivo de resultados, em CSV com uma coluna success por padrão, e reprovar o job por conta própria:
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: "21"
- name: Install JMeter 5.6.3
env:
JM_BASE: https://archive.apache.org/dist/jmeter/binaries
run: curl -sSL "$JM_BASE/apache-jmeter-5.6.3.tgz" | tar -xzf -
- name: Run the test plan
run: >
apache-jmeter-5.6.3/bin/jmeter -n -t tests/load/plan.jmx
-l results.jtl -e -o report
- name: Fail above 1% errors
run: |
python3 - <<'EOF'
import csv, sys
rows = list(csv.DictReader(open("results.jtl")))
bad = sum(r["success"] != "true" for r in rows)
print(f"{bad} of {len(rows)} samples failed")
sys.exit(1 if not rows or bad > 0.01 * len(rows) else 0)
EOF
Como fazer o job falhar quando o teste não atinge uma meta?
Escreva as metas no teste como thresholds e deixe o código de saída fazer o resto. O GitHub marca um passo como falho quando o comando dele termina com código diferente de zero, e o job falha junto.
Comece com uma taxa de erro rígida e um percentil de latência folgado, e aperte o limite de latência quando souber o quanto seus números variam de uma execução para outra. O guia passo a passo de teste de carga mostra como escolher essas metas.
Onde devem ficar os resultados de cada execução?
Envie-os como artefato do workflow, com if: always() para que um teste reprovado ainda guarde o relatório. Os artefatos ficam guardados por 90 dias por padrão, e retention-days encurta esse prazo.
O README da upload-artifact documenta os 90 dias padrão (conferido em 9 de outubro de 2026). Defina if-no-files-found: error para que um relatório ausente faça o passo falhar de forma visível.
Quanta carga um runner hospedado pelo GitHub consegue gerar?
O suficiente para um smoke test ou uma pequena checagem de regressão. Um runner Linux padrão tem 4 CPUs e 16 GB de RAM em repositórios públicos, 2 CPUs e 8 GB em privados, e um job para depois de 6 horas.
Esses números vêm da referência de runners e dos limites do Actions do GitHub (conferido em 9 de outubro de 2026), que não trazem nenhum número de largura de banda de rede.
Um job roda numa única máquina, então é um único gerador de carga: se a CPU dele satura, os tempos de resposta incluem o tempo de espera no próprio runner. Toda requisição também sai dessa máquina, então rate limiters que contam por endereço de cliente enxergam um único cliente muito ativo. O GitHub desaconselha colocar em allowlists as faixas de IP do Azure usadas pelos runners Ubuntu; runners maiores (larger runners) nos planos Team e Enterprise Cloud trazem mais CPU, mais memória e endereços IP estáticos.
Quando a carga deve vir de outro lugar?
Quando você precisa de mais carga do que uma máquina produz, de tráfego de várias regiões ou de execuções mais longas que o limite de um job.
Com k6, a run-k6-action roda o teste no Grafana Cloud k6 quando você define cloud-run-locally: false e passa K6_CLOUD_TOKEN, K6_CLOUD_PROJECT_ID e, com o k6 v2, K6_CLOUD_STACK_ID. Uma execução na nuvem ignora por padrão o ambiente do runner, então adicione -e BASE_URL=${{ vars.STAGING_URL }} a flags (conferido em 10 de outubro de 2026). A action do Artillery pode chamar run-fargate ou run-lambda para gerar a carga a partir da sua própria conta AWS (ambos conferidos em 9 de outubro de 2026). O comparativo k6 vs Artillery cobre os dois.
Para JMeter, a LoadFocus documenta uma integração com o GitHub Actions baseada no seu cliente npm. Você salva o teste na sua conta LoadFocus, guarda a chave de API e o ID do time como segredos e chama run-test. As flags abaixo seguem a ajuda do cliente:
- run: npm install -g @loadfocus/loadfocus-api-client
- env:
LF_KEY: ${{ secrets.LOADFOCUS_API_KEY }}
LF_TEAM: ${{ secrets.LOADFOCUS_TEAM_ID }}
run: |
test -n "$LF_KEY"
test -n "$LF_TEAM"
loadfocus-api config set \
--apikey "$LF_KEY" --teamid "$LF_TEAM"
loadfocus-api jmeter run-test --name checkout-api \
--thresholds "avgresponse<=200,errors==0,p95<=250"
O cliente espera até 1.800 segundos por padrão (--wait-timeout muda isso), então dê ao job um timeout-minutes acima de 30, como 40. Ele sai com código 1 quando um threshold não é atingido e com 0 quando falta a chave de API ou o ID do time, por isso as linhas test -n fazem o passo falhar antes quando um segredo está vazio. A carga vem da LoadFocus, que roda scripts JMeter a partir de 25 regiões na nuvem.
FAQ
Quanto tempo deve durar um teste de carga num pull request? Alguns minutos: o bastante para passar da rampa de subida e obter um percentil estável, e pouco o bastante para ninguém ficar esperando.
É permitido fazer teste de carga a partir de runners hospedados pelo GitHub? Os termos do GitHub para o Actions proíbem perturbar qualquer serviço e limitam os runners hospedados ao trabalho no próprio projeto do repositório (conferido em 9 de outubro de 2026). Testar o ambiente de staging desse mesmo projeto, sem sobrecarregar os servidores do GitHub, é o tipo de teste que esses termos descrevem.
Posso usar um runner auto-hospedado no lugar? Sim. Ele roda no seu próprio hardware e na sua rede, e a página de limites do GitHub permite que um job dure até 5 dias nele. Ele continua sendo um único gerador de carga.
Leituras relacionadas
- Critérios de aprovação de um teste de carga: como definir
- Como fazer um teste de carga: guia passo a passo
- k6 vs Artillery: qual ferramenta de teste de carga escolher
As afirmações técnicas deste artigo não foram revisadas de forma independente.