Como hospedar um agente de revisão de PR em uma VPS
Execute um revisor de PR com runner self-hosted, prompts só para o diff, filtros de caminho e tamanho, comentários inline e custo por PR.
O que faz um agente de revisão de PR autoalojado
Um agente de revisão de PR autoalojado é um programa pequeno executado num servidor que controla. Lê o diff de um pull request (PR) e envia apenas as linhas alteradas para um modelo. O resultado é publicado como comentários inline na revisão. Nunca faz checkout do seu branch e nunca lê um ficheiro que o pull request não tenha alterado. As únicas credenciais que possui são uma chave de API (application programming interface) de um modelo e um token que pode adicionar comentários, sem qualquer outra permissão.
Um modelo consegue ler um diff. Essa parte está resolvida. O que importa é para onde o diff vai e quem controla a chave. Um bot de revisão alojado significa que todos os diffs de todos os repositórios privados saem da sua rede, ficam nos logs de terceiros e são abrangidos pela política de retenção desse fornecedor. Num VPS (virtual private server) que controla, o diff vai do GitHub para o seu servidor e daí para a API do modelo. Também pode ler as quarenta linhas de código que determinam o que é enviado.
O que é necessário antes de começar
- Uma VPS a executar Ubuntu 24.04, com um runner self-hosted do GitHub Actions já registado no repositório. Atribua-lhe o rótulo adicional
pr-reviewdurante o registo, porque o workflow abaixo seleciona esse rótulo. - Uma chave de API da Anthropic obtida na Claude Console.
- Um repositório onde possa controlar quem pode abrir um pull request. Um repositório privado é o caso mais simples. A secção sobre forks abaixo aborda o caso público, mas a resposta é menos confortável.
Instale o reviewer na VPS
O serviço runner é executado com a conta sem privilégios que criou ao executar ./svc.sh install. Instale o reviewer nessa mesma conta para que o job possa executá-lo sem sudo. Substitua runner abaixo pelo nome da sua conta.
sudo apt update && sudo apt install -y gh python3-venv
sudo install -d -m 755 -o runner -g runner /opt/pr-review
sudo -u runner python3 -m venv /opt/pr-review/venv
sudo -u runner /opt/pr-review/venv/bin/pip install anthropic
gh --versiongh --version imprime gh version 2.45.0 no Ubuntu 24.04 em agosto de 2026. Qualquer versão a partir da 2.20 inclui a flag --input usada abaixo. Uma mensagem Command 'gh' not found significa que o componente universe não está ativado. Nesse caso, execute sudo add-apt-repository universe e tente novamente.
Onde ficam a chave e o token
Dois segredos, dois períodos de validade diferentes. Nenhum dos dois deve ser guardado no repositório.
ANTHROPIC_API_KEY é um segredo do repositório, definido em Settings, depois Secrets and variables e, por fim, Actions. O GitHub cifra-o e injeta-o no ambiente do passo durante a execução. Nunca é um ficheiro no disco e nunca fica no histórico do git.
GITHUB_TOKEN funciona de forma diferente. O Actions cria um token novo para cada job e destrói-o quando o job termina. As ações que esse token pode executar são definidas pelo bloco permissions: no workflow. É aqui que se aplica efetivamente o princípio do menor privilégio:
permissions:
contents: read
pull-requests: writeEsse token pode publicar uma revisão. Não pode fazer push de um commit, fazer merge de uma branch, editar um ficheiro de workflow nem aceder a outro repositório. Um agente que pode comentar é um revisor. Um agente que pode fazer push é um committer, e ninguém autorizou isso. Trate a chave do modelo com o mesmo cuidado, porque ela gasta dinheiro da sua conta. Há mais informações sobre este tipo de problema em manter os segredos fora do alcance de um agente de IA.
O Actions substitui a cadeia exata do segredo por *** nos logs do job. A correspondência é feita apenas com a cadeia exata. Por isso, uma chave codificada em base64, dividida em duas linhas ou impressa um caractere de cada vez aparece em texto simples. Não adicione um passo de depuração que descarregue o ambiente.
Por que um pull request de um fork nunca vê a sua chave de API
A regra do GitHub é simples: com exceção de GITHUB_TOKEN, os secrets não são enviados para o runner quando um workflow é acionado a partir de um repositório fork. Assim, uma execução de pull_request iniciada a partir de um fork executa o seu script sem ANTHROPIC_API_KEY, e a primeira chamada à API falha com invalid x-api-key.
A correção tentadora é mudar o acionador para pull_request_target, que executa no contexto do repositório base e recebe os secrets. Não faça isso neste caso. A própria documentação de segurança do GitHub afirma que esses workflows "são privilegiados, o que significa que partilham a mesma cache da branch principal com outros acionadores de workflows privilegiados e podem ter acesso de escrita ao repositório e acesso aos secrets referenciados" e que o resultado "pode ser explorado para assumir o controlo de um repositório".
A mesma documentação é clara sobre o runner: "Os self-hosted runners quase nunca devem ser usados para repositórios públicos no GitHub, porque qualquer utilizador pode abrir pull requests para o repositório e comprometer o ambiente."
Isso determina duas escolhas de desenho. O job inclui uma condição para ser executado apenas em branches enviadas para o seu próprio repositório. E o workflow não tem qualquer passo actions/checkout. O agente nunca tem a branch no disco, portanto um pull request hostil é apenas texto enviado para um modelo. Ele não pode executar um script de build no seu VPS, porque nada no seu VPS chega a executá-lo. No entanto, texto não é sinónimo de entrada inofensiva: um diff escrito por um desconhecido é uma entrada não confiável recebida por um modelo, na mesma fronteira de confiança encontrada quando você dá a um agente acesso à pesquisa na Web, e a única contenção neste caso é que esse agente não pode fazer nada além de publicar um comentário.
Obtenha o diff, não o repositório
Uma única requisição obtém todo o diff como texto simples.
export GH_TOKEN=your_token # in the workflow this comes from secrets.GITHUB_TOKEN
gh api /repos/OWNER/REPO/pulls/42 -H "Accept: application/vnd.github.diff"O tipo de media Accept: application/vnd.github.diff transforma a resposta de um objeto JSON que descreve o pull request no próprio diff unificado, e gh api imprime esse corpo sem alterações. A primeira linha apresentada deve começar com diff --git a/. Um gh: Not Found (HTTP 404) significa que o token não consegue ver o repositório. Num token pessoal com permissões granulares, isto quase sempre significa que a permissão Pull requests não foi ativada.
Filtre antes de gastar um token
Esta secção distingue um bot que as pessoas leem de um bot que as pessoas silenciam. Cada filtro abaixo é executado antes de o modelo receber um único byte.
- Filtros de caminhos. Ignore ficheiros de bloqueio, diretórios de dependências incorporadas, bundles minificados e código gerado. Um comentário do modelo sobre
package-lock.jsoné puro ruído, e esses ficheiros costumam representar a maior parte dos bytes de um diff. - Um limite de tamanho. Acima desse limite, ignore a revisão e termine com estado de sucesso. Uma refatoração com 4,000 linhas recebe uma única mensagem honesta a indicar que era demasiado grande para ser revista automaticamente, em vez de sessenta suposições.
- Um limiar de severidade e um limite de comentários. Comunique os problemas de severidade alta e média, até um máximo de dez, começando pelos de maior severidade. Ninguém lê o comentário onze.
O script
Guarde isto como /opt/pr-review/review.py. O script lê a configuração a partir do ambiente, por isso o workflow pode mudar de modelo sem alterar o código.
#!/usr/bin/env python3
"""Review only the changed lines of one pull request."""
import json
import os
import subprocess
import sys
import anthropic
REPO = os.environ["GITHUB_REPOSITORY"]
PR = os.environ["PR_NUMBER"]
MODEL = os.environ.get("REVIEW_MODEL", "claude-haiku-4-5-20251001")
MAX_DIFF_BYTES = int(os.environ.get("MAX_DIFF_BYTES", "120000"))
MIN_SEVERITY = os.environ.get("MIN_SEVERITY", "medium")
MAX_COMMENTS = 10
RANK = {"low": 0, "medium": 1, "high": 2}
SKIP = ("package-lock.json", "poetry.lock", "/vendor/", "/node_modules/", ".min.js")
raw_diff = subprocess.run(
["gh", "api", f"/repos/{REPO}/pulls/{PR}",
"-H", "Accept: application/vnd.github.diff"],
check=True, capture_output=True, text=True,
).stdoutSeparar o diff por ficheiro é o que permite filtrar por caminho. Numerar cada linha é o que permite posicionar os comentários da revisão. O GitHub só aceita um comentário inline numa linha que faça parte do diff, por isso o modelo tem de indicar um número de linha real. Fornecer-lhe os números permite copiar um número em vez de inventar um.
def per_file(diff_text):
"""Split a unified diff into one string per file."""
sections, current = [], []
for line in diff_text.splitlines():
if line.startswith("diff --git ") and current:
sections.append("\n".join(current))
current = []
current.append(line)
if current:
sections.append("\n".join(current))
return sections
def annotate(section):
"""Prefix every line that exists in the new file with its line number."""
out, n, in_hunk = [], 0, False
for line in section.splitlines():
if line.startswith("@@"):
n = int(line.split("+")[1].split(",")[0].split(" ")[0])
in_hunk = True
out.append(line)
elif not in_hunk or line.startswith(("-", "\\")):
out.append(line)
else:
out.append(f"{n}\t{line}")
n += 1
return "\n".join(out)
kept = [s for s in per_file(raw_diff)
if not any(p in s.split("\n", 1)[0] for p in SKIP)]
payload = "\n".join(annotate(s) for s in kept)
if not payload.strip():
print("every changed file was filtered out")
raise SystemExit(0)
if len(payload) > MAX_DIFF_BYTES:
print(f"diff is {len(payload)} bytes, over the {MAX_DIFF_BYTES} cap")
raise SystemExit(0)O cabeçalho do hunk contém a numeração. @@ -12,7 +12,9 @@ indica que o hunk do ficheiro novo começa na linha 12, por isso o contador começa aí e avança apenas nas linhas adicionadas e inalteradas. As linhas removidas passam sem numeração, porque não existem no ficheiro novo. A condição que verifica linhas começadas por uma barra invertida ignora o marcador de ausência de nova linha que o git escreve no fim de um ficheiro. Sem essa condição, todos os números seguintes ficariam deslocados uma posição.
Ambas as saídas usam o status 0, e não 1. Um pull request filtrado ou demasiado grande deve apresentar uma verificação verde. Uma verificação vermelha sobre a qual uma pessoa não pode agir é ignorada. Depois de uma verificação ser ignorada, todas são.
SYSTEM = (
"You review one pull request diff. Every line that exists in the new file is "
"prefixed with its line number and a tab character. "
"Report only defects you can see in the lines shown: a crash, a resource leak, "
"a security mistake, a wrong boundary condition, a broken contract with code "
"that is visible in this diff. Do not comment on style, naming or formatting. "
"Do not guess about code you cannot see. Leave out anything you are not "
"certain about. An empty findings list is a normal and common answer. "
'Reply with JSON only, in this shape: {"findings": [{"path": "src/app.py", '
'"line": 42, "severity": "high", "comment": "what is wrong, then why"}]} '
"Every line number must be one you can see in the left column of that file."
)
client = anthropic.Anthropic()
message = client.messages.create(
model=MODEL,
max_tokens=2000,
system=SYSTEM,
messages=[{"role": "user", "content": payload}],
)
print(f"stop={message.stop_reason} in={message.usage.input_tokens} "
f"out={message.usage.output_tokens}", file=sys.stderr)
text = message.content[0].text
findings = json.loads(text[text.find("{"):text.rfind("}") + 1])["findings"]
findings = [f for f in findings if RANK.get(f["severity"], 0) >= RANK[MIN_SEVERITY]]
findings.sort(key=lambda f: -RANK.get(f["severity"], 0))
del findings[MAX_COMMENTS:]
if not findings:
print("nothing above the severity threshold; posting no comment")
raise SystemExit(0)
review = {
"event": "COMMENT",
"body": f"Automated review of the changed lines. {len(findings)} finding(s).",
"comments": [
{"path": f["path"].removeprefix("b/"), "line": f["line"], "side": "RIGHT",
"body": f"**{f['severity']}** {f['comment']}"}
for f in findings
],
}
subprocess.run(
["gh", "api", "-X", "POST", f"/repos/{REPO}/pulls/{PR}/reviews", "--input", "-"],
input=json.dumps(review), text=True, check=True,
)Três detalhes nesse bloco são essenciais. O JSON é extraído entre o primeiro { e o último } porque, por vezes, o modelo envolve a resposta num code fence, e json.loads não processa esse fence. O path tem o b/ inicial removido, porque esse prefixo vem do cabeçalho do diff e o GitHub espera um caminho relativo ao repositório. E --input - envia a revisão inteira numa única chamada à API, para que dez problemas cheguem como uma notificação em vez de dez.
Quando não há nada a reportar, o script não publica nada. Um bot que escreva "não foram encontrados problemas" em todos os pull requests ensina as pessoas a passar os olhos por essa mensagem. Depois, passam os olhos também pela mensagem que era importante.
Integrar no fluxo de trabalho
Guarde isto como .github/workflows/pr-review.yml:
name: pr-review
on:
pull_request:
types: [opened, synchronize, reopened]
paths-ignore:
- '**.md'
- 'docs/**'
permissions:
contents: read
pull-requests: write
concurrency:
group: pr-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review:
if: github.event.pull_request.head.repo.full_name == github.repository && !contains(github.event.pull_request.labels.*.name, 'no-ai-review')
runs-on: [self-hosted, linux, pr-review]
steps:
- name: Review the changed lines
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REVIEW_MODEL: claude-haiku-4-5-20251001
MIN_SEVERITY: medium
run: /opt/pr-review/venv/bin/python /opt/pr-review/review.pyGITHUB_REPOSITORY não está nesse bloco env: porque o Actions já o define para todos os jobs. O grupo concurrency é importante para a faturação: sem ele, o envio de três correções rápidas para um branch inicia três revisões completas e paga pelas três; com ele, apenas a última é executada.
A linha if: cumpre duas funções. A primeira metade ignora pull requests de forks, que falhariam de qualquer forma sem uma chave. A segunda metade fornece à equipa um interruptor de desativação: adicione o rótulo no-ai-review a um pull request para impedir a execução do job.
Abra um pull request e observe o que acontece:
gh run list --workflow=pr-review.yml --limit 3
gh run view --log
gh pr view 42 --commentsUma execução que termina em poucos segundos com nothing above the severity threshold; posting no comment no log está a funcionar corretamente. Esse é o resultado esperado num pull request pequeno e sem alterações problemáticas.
Quanto custa uma revisão automatizada de pull request?
O diff representa quase toda a entrada, portanto o tamanho do diff define o preço. Abaixo está um diff medido de 500 linhas, além do prompt do sistema, contabilizado com o endpoint de contagem de tokens, e não por estimativa.
The data behind this chart
[
{
"label": "Haiku 4.5",
"input_tokens": "8,000",
"output_tokens": "1,200"
},
{
"label": "Sonnet 5",
"input_tokens": "10,400",
"output_tokens": "1,560"
},
{
"label": "Opus 5",
"input_tokens": "10,400",
"output_tokens": "1,560"
}
]Esse diff correspondeu a 8,000 tokens de entrada no Haiku 4.5 e a 10,400 no Sonnet 5. O texto é o mesmo, mas a contagem é diferente. Os modelos Claude a partir do 4.7 usam um tokenizador mais recente, que produz aproximadamente 30% mais tokens para a mesma entrada, conforme documentado pela Anthropic na sua página de preços. Tenha isso em conta ao comparar o preço por milhão de tokens de um modelo mais recente com o de um modelo mais antigo.
Preços de tabela em agosto de 2026: o Haiku 4.5 custa $1 por milhão de tokens de entrada e $5 por milhão de tokens de saída. O Sonnet 5 custa $2 e $10 na modalidade de preço introdutório, válida até 31 August 2026; depois, passa a custar $3 e $15. O Opus 5 custa $5 e $25.
The data behind this chart
[
{
"label": "Haiku 4.5",
"cost_per_pr_cents": 1.4,
"cost_200_prs_usd": "2.80"
},
{
"label": "Sonnet 5",
"cost_per_pr_cents": 3.64,
"cost_200_prs_usd": "7.28"
},
{
"label": "Opus 5",
"cost_per_pr_cents": 9.1,
"cost_200_prs_usd": "18.20"
}
]Isso corresponde a 1.4 cêntimos por pull request no Haiku 4.5 e a 9.1 cêntimos no Opus 5. Uma equipa que faça merge de 200 pull requests por mês paga aproximadamente $2.80 no Haiku 4.5, $7.28 no Sonnet 5 ou $18.20 no Opus 5. A partir de 1 September 2026, multiplique o valor do Sonnet 5 por 1.5.
Dois fatores fazem com que a fatura real seja superior a essa estimativa. O acionador synchronize inicia uma revisão a cada push, portanto uma branch ativa com oito pushes custa oito revisões, e a regra de concorrência só ajuda quando os pushes ocorrem com pouca diferença de tempo. Os valores também pressupõem que os filtros de caminho estão a funcionar: um único ficheiro de lock sem filtro pode duplicar a entrada.
O prompt caching não ajuda neste caso. O prefixo em cache tem de ser byte a byte idêntico entre chamadas, e o diff muda a cada chamada. O prompt do sistema é a única parte estável, mas fica muito abaixo do comprimento mínimo necessário para o cache. Para consultar a regra geral, veja quando o prompt caching compensa, e, para escolher entre os três modelos acima, consulte qual modelo Claude usar em cada tarefa.
Meça os seus próprios diffs antes de ativar esta funcionalidade
Adicione esta linha depois de payload ser criado e execute o script manualmente com algumas pull requests do mês passado:
print(client.messages.count_tokens(
model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)O endpoint de contagem não executa o modelo, portanto não consome tokens de entrada nem de saída, e usa o tokenizador correspondente ao modelo indicado. Execute-o sobre dez pull requests reais do seu próprio repositório e use a mediana em vez da média, para que uma migração muito grande não distorça a estimativa.
Por que os bots de revisão são silenciados e como evitar isso
Dois comportamentos destroem a confiança nesses bots, e ambos têm uma correção no código acima.
Rever tudo de uma vez. Um bot que deixa quarenta comentários não tem nenhum deles lido. O limite de severidade e o limite de dez comentários não são uma questão de cortesia. São o que mantém os problemas reais visíveis. Ordenar por severidade antes de truncar faz com que o limite remova os problemas menos importantes, em vez de remover dez problemas aleatórios.
Comentar com confiança sobre algo que não consegue verificar. Este é o comportamento que faz os engenheiros desativarem o bot permanentemente. Um modelo ao qual são mostradas 200 linhas de uma base de código com 40,000 linhas ainda escreverá "isto quebra a invalidação da cache em redis_client.py" sobre um ficheiro que nunca viu. O prompt do sistema é explícito: reporte apenas os defeitos visíveis nas linhas apresentadas e omita tudo aquilo de que não tenha certeza. Identificar diretamente o tipo de falha funciona melhor do que pedir precisão de forma genérica. Informar o modelo de que um resultado vazio é normal impede-o de inventar algo para dizer sobre uma alteração de duas linhas.
Publique a revisão como COMMENT, nunca como REQUEST_CHANGES. A opinião de um modelo não deve poder bloquear um merge. Quando isso acontece, alguém com um prazo apertado remove todo o workflow em vez de discutir com o bot.
Modos de falha e as mensagens apresentadas
HTTP 422 ao publicar a revisão. gh apresenta gh: Unprocessable Entity (HTTP 422) e o corpo da resposta identifica o campo: Pull request review thread line must be part of the diff. O GitHub não consegue ancorar esse comentário. As causas mais comuns são um número de linha inventado pelo modelo, um path que ainda contém o prefixo b/ ou um comentário numa linha removida. Nesse caso, é necessário definir side como LEFT, em vez de RIGHT. Apresente o JSON da revisão antes de o publicar e confirme manualmente um comentário em relação ao diff.
invalid x-api-key da API do modelo. O passo falha na primeira chamada messages.create. O segredo ANTHROPIC_API_KEY não está definido no repositório ou o pull request veio de um fork, pelo que o Actions não recebeu segredos. A proteção contra forks na linha if: deveria ter ignorado esse passo. Por isso, comece por verificar essa linha.
gh: Resource not accessible by integration (HTTP 403). O token do job não pode escrever em pull requests. Adicione pull-requests: write ao bloco permissions:. Se já estiver presente, abra Settings, depois Actions e General. Uma política da organização pode limitar o que qualquer token de workflow pode solicitar.
json.decoder.JSONDecodeError. O modelo não devolveu JSON analisável. A causa comum é uma resposta que atingiu o limite de tokens e terminou a meio de um objeto. A linha de log apresenta stop_reason precisamente para este caso: um valor de max_tokens significa que deve aumentar max_tokens ou reduzir MAX_COMMENTS.
O workflow nunca é executado. gh run list não apresenta nada para o pull request. Confirme que paths-ignore não filtrou todos os ficheiros alterados. Depois, verifique a proteção contra forks, a proteção por label e se o runner está ativo no VPS, usando sudo systemctl status 'actions.runner.*'. Um runner offline deixa o job na fila sem qualquer mensagem de erro no pull request.
Todas as revisões regressam vazias. Defina MIN_SEVERITY como low numa execução. Se surgirem resultados, o limiar está a funcionar. Se nada surgir, apresente payload e confirme que os filtros não removeram todo o diff.
Executá-lo junto com os outros agentes
O revisor é pequeno, por isso pode parecer tentador instalá-lo no servidor que já executa tudo o resto. Mantenha-o separado se o repositório for importante. Este processo tem um token que pode comentar o seu código e uma chave que pode gastar o seu dinheiro. Um self-hosted runner é, por definição, um local onde o código dos workflows é executado. Uma conta dedicada sem privilégios, sem direitos de sudo, num host que não execute mais nada, é a configuração de base. Se também executar agentes interativos que fazem checkout do código, uma VM descartável por agente é o padrão mais robusto. Executar um coding agent numa VPS apresenta a configuração geral. Se a Anthropic API for nova para si, uma primeira aplicação da Claude API numa VPS é um ponto de partida mais simples do que este.
FAQ
Um agente de revisão de PR com IA precisa de acesso de escrita ao meu repositório?
Não. Precisa de pull-requests: write para publicar uma revisão e de contents: read para obter o diff. Essa é a lista completa, e pode defini-la no bloco permissions: do workflow, que limita o que o GITHUB_TOKEN de cada job pode fazer. Com estas duas linhas, o agente pode comentar um pull request, mas não pode fazer push de um commit nem fazer merge de uma branch. Publique revisões com event: COMMENT em vez de REQUEST_CHANGES, para que o agente também não possa bloquear um merge.
Porque é que o meu comentário de revisão falha com HTTP 422?
O GitHub aceita um comentário de revisão inline apenas numa linha que faça parte do diff do pull request e devolve Pull request review thread line must be part of the diff quando isso não acontece. Confirme que path é relativo ao repositório e não inclui o prefixo b/ do cabeçalho do diff, e que o número da linha aparece dentro de um hunk desse ficheiro. side tem de ser RIGHT para uma linha adicionada ou inalterada e LEFT para uma linha removida. Adicionar o número da linha do ficheiro novo como prefixo a todas as linhas do diff antes de o enviar para o modelo impede que o modelo invente números logo à partida.
Posso executar isto num repositório público com pull requests de forks?
Não com este desenho. O GitHub não fornece secrets a um workflow acionado a partir de um fork, por isso a chave do modelo fica em falta e a execução falha. O GitHub também afirma que os self-hosted runners "should almost never be used for public repositories", porque qualquer pessoa pode abrir um pull request que faça o código ser executado na sua máquina. Num projeto público, restrinja o revisor a branches enviadas para o próprio repositório, que é o que a proteção if: faz, ou mova a etapa de revisão para um runner alojado pelo GitHub e aceite que o diff sai da sua infraestrutura.
Que modelo devo usar para a revisão de pull requests?
Comece com Haiku 4.5. Ler um diff limitado e compará-lo com uma lista fixa de tipos de defeito não é um problema de raciocínio difícil, e o modelo mais barato mantém a fatura mensal num valor que ninguém contesta. Passe para Sonnet 5 se verificar que o modelo não deteta bugs reais na sua linguagem ou framework, e meça esse resultado em vez de partir do princípio de que isso acontece. Opus 5 é, por uma margem ampla, o mais caro dos três por pull request, o que é mais fácil de justificar numa branch de release do que em cada push para todas as branches de funcionalidades.