Conventional Comments: 9 rótulos, 1 trava merge
No capítulo 7 do meu livro em PT Revisão de Código com Harness Engineering eu propus duas coisas: adotar Conventional Comments como vocabulário de review, e colocar um workflow do GitHub Actions chamado review-gate.yml para travar o merge enquanto houver comentário issue: sem resolução. O capítulo apresenta a lista de rótulos como se fossem seis, e o workflow usa um filtro !c.line em cima da REST API para decidir se o comentário está resolvido.
Fui conferir essas duas afirmações contra a documentação oficial, antes de recomendar o padrão sem reservas em outros lugares. A spec real define nove rótulos, não seis. E o filtro !c.line que eu publiquei no YAML não detecta resolve — a REST API não devolve esse estado, e o campo line não desaparece quando alguém marca o thread como resolvido na UI.
Este post é a correção pública dessas duas peças, com link para as fontes.

A spec tem nove rótulos, não seis
conventionalcomments.org foi publicado em 2020 por Paul Slaughter (engenheiro na GitLab) como uma convenção leve para dar prefixo a comentários de code review. A lista oficial, hoje, tem nove rótulos principais:
- praise — destacar algo positivo (a spec sugere pelo menos um por review).
- nitpick — preferência trivial, marcada como não-bloqueante por natureza.
- suggestion — proposta de melhoria com a razão explícita.
- issue — problema concreto (bug, segurança, correção obrigatória).
- todo — mudança pequena que precisa ser feita antes do merge.
- question — dúvida que exige resposta, nem sempre mudança.
- thought — ideia aberta, útil para mentoria e discussão.
- chore — tarefa de processo (atualizar CHANGELOG, assinar CLA).
- note — informação relevante; sempre não-bloqueante.
Mais três variantes expressivas opcionais: typo (como todo, só para erro de digitação), polish (como suggestion, para melhoria sem corrigir erro) e quibble (como nitpick, com outro tom).
No meu capítulo 7 eu listei seis — praise / issue / suggestion / nitpick / question / thought — e falei que “nada além de issue é blocking”. A simplificação não é totalmente errada: na prática, times que adotam Conventional Comments costumam escolher um subset. O que eu deveria ter escrito, no entanto, é que a spec tem nove, e que o subset é escolha de time, não da convenção. O leitor que foi para conventionalcomments.org depois de ler meu capítulo encontrou três rótulos a mais (todo, chore, note) que o capítulo não cita. É pouca coisa, mas é o tipo de simplificação que vira dívida: todo em particular carrega semântica de “obrigatório antes de merge” que issue não cobre bem (um todo: adicionar teste de regressão não é bug, mas é bloqueante).
Outra coisa que o capítulo omite são as decorações da spec: (blocking), (non-blocking) e (if-minor). Elas podem ser anexadas a qualquer rótulo:
suggestion (blocking): este endpoint precisa validar o input antes de persistir.
nitpick (if-minor): este nome de variável poderia ser mais curto.
question (non-blocking): essa escolha de ORM é por preferência pessoal?
A existência de (blocking) como decoração explícita significa que “travar merge” não é propriedade intrínseca de issue. É uma convenção que o time instaura, usando o rótulo e/ou a decoração. Qualquer gate que associe hard-coded “só issue: bloqueia” está pré-filtrando as outras oito combinações válidas. É uma escolha legítima — mas precisa ser documentada como escolha do time, não como regra do padrão.
O merge-gate do capítulo 7
Agora o pedaço mais embaraçoso. O capítulo 7 propõe um workflow do GitHub Actions para transformar o issue: em portão automático:
# .github/workflows/review-gate.yml (versão do capítulo 7 — com defeito)
name: Review Gate
on:
pull_request:
types: [review_requested, synchronize]
jobs:
check-unresolved-issues:
runs-on: ubuntu-latest
steps:
- name: Check unresolved CodeRabbit issues
uses: actions/github-script@v7
with:
script: |
const comments = await github.rest.pulls.listReviewComments({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
});
const unresolvedIssues = comments.data.filter(c =>
c.body.startsWith('issue:') && !c.line // resolved comments lose line
);
A ideia é: pega os review comments do PR via REST (pulls.listReviewComments), filtra os que começam com issue:, e descarta os que foram “resolvidos”. O comentário na linha do filtro — resolved comments lose line — é a premissa que o workflow depende. Se ela cai, o gate inteiro vira teatro: ele sempre vê zero comentário resolvido, logo nunca falha, logo nunca bloqueia merge.
Fui conferir essa premissa.
O que a REST API de review comments realmente devolve
O endpoint que o workflow chama é GET /repos/{owner}/{repo}/pulls/{pull_number}/comments, documentado em docs.github.com/en/rest/pulls/comments. O schema de resposta de cada comentário tem os campos url, pull_request_review_id, id, node_id, diff_hunk, path, position, original_position, commit_id, original_commit_id, in_reply_to_id, user, body, created_at, updated_at, html_url, pull_request_url, author_association, _links, start_line, original_start_line, start_side, line, original_line, side, subject_type, reactions, body_html, body_text.
Nenhum campo que indica resolução. Nem resolved, nem isResolved, nem state. O line existe, aponta para a linha do diff, e não é anulado quando o thread é marcado como resolvido na UI. A resolução é uma propriedade do review thread, não do comment, e os dois endpoints estão em superfícies diferentes da API.
Para confirmar que isso é limitação conhecida e não algo que eu interpretei mal, procurei a discussão oficial. Em community/discussions/9175, o título é literalmente “[Github API] List comments for a pull request review github api — lack of status”. O autor abre pedindo exatamente o que meu workflow assume: “there is no way to find out which conversation is resolved/unresolved though the github api” via REST. A resposta mais votada aponta GraphQL como o único caminho atual, e o tópico segue aberto há anos como pedido de feature (sem resposta oficial marcada como “accepted”). Em community/discussions/24854 a pergunta é a mesma, com título “GraphQL resolved conversations”, e a solução é a mesma: reviewThreads { nodes { isResolved } }.
Na prática: o filtro c.body.startsWith('issue:') && !c.line sempre vai manter todos os comentários issue: como “não resolvidos”, porque c.line nunca é falsy por conta de resolução. O gate nunca vai bloquear merge. É um gate pintado: dá sensação de proteção, mas o fluxo passa por baixo dele.
O caminho correto: GraphQL isResolved
A API que de fato expõe resolução é o GraphQL, no tipo PullRequestReviewThread, com o campo isResolved: Boolean!. A query equivalente ao que o workflow queria fazer é:
query ($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100) {
nodes {
isResolved
comments(first: 1) {
nodes { body }
}
}
}
}
}
}
A estrutura muda: você sai do nível de comment e sobe para thread. Um thread tem isResolved: true | false e uma lista de comments. Para replicar a intenção do workflow — “existe algum thread aberto cujo primeiro comentário começa com issue:?” — a lógica fica assim, em JavaScript dentro de actions/github-script:
# .github/workflows/review-gate.yml (versão corrigida)
name: Review Gate
on:
pull_request:
types: [opened, synchronize, review_requested]
jobs:
check-unresolved-issues:
runs-on: ubuntu-latest
steps:
- name: Check unresolved issue: threads via GraphQL
uses: actions/github-script@v7
with:
script: |
const query = `
query ($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100) {
nodes {
isResolved
comments(first: 1) {
nodes { body }
}
}
}
}
}
}
`;
const data = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo,
pr: context.payload.pull_request.number,
});
const threads = data.repository.pullRequest.reviewThreads.nodes;
const unresolved = threads.filter(t =>
!t.isResolved &&
t.comments.nodes[0]?.body.trimStart().startsWith('issue:')
);
if (unresolved.length > 0) {
core.setFailed(
`${unresolved.length} thread(s) com "issue:" ainda não resolvido(s)`
);
}
Três diferenças em relação à versão do capítulo:
github.graphqlem vez degithub.rest.pulls.listReviewComments. É a mudança de superfície que resolve o problema.- Nível de agregação sobe para thread. O filtro agora é “thread com
isResolved === false”, não “comentário semline”. A primeira é uma propriedade real; a segunda era imaginação minha. trimStart()antes dostartsWith. Em testes rápidos, muitas respostas do CodeRabbit vêm com espaço ou quebra de linha no começo do body. Sem otrim, o gate perde comentários que começam comissue:ou\nissue:.
Também mudei o trigger de [review_requested, synchronize] para [opened, synchronize, review_requested], porque o padrão do capítulo deixava a primeira abertura do PR sem gate até alguém pedir review explicitamente. Pequeno detalhe que não tem a ver com o bug principal, mas aproveitei a correção.
Por que a versão quebrada parecia funcionar
Tem uma armadilha interessante aqui: um gate que “nunca falha” pode ficar meses no repositório sem que ninguém note. Diferente de um lint que deixa passar código ruim (visível no PR seguinte), um merge-gate falso-positivo passa de forma silenciosa. O time acredita que está protegido, só vê CI verde, e nunca descobre que o gate está vazio.
A única maneira de pegar esse tipo de bug é escrever um teste que espera o gate falhar — abrir um PR deliberado com um comentário issue: não resolvido e verificar que o workflow bloqueia. Esse teste nunca estava no capítulo 7. Era obrigação do leitor, e eu nem avisei que era obrigação do leitor. É o tipo de omissão que atrapalha mais do que a configuração em si: a configuração, uma vez corrigida, para de errar; a ausência do teste significa que a próxima configuração vai errar igual, só com outro filtro.
Esse padrão — gate pintado que passa silenciosamente — é a mesma classe do problema que descrevi em Escrevi ‘testes antes do PR’ no AGENTS.md por 3 meses. Só 12% seguiram. Regra sem portão não vira comportamento; portão sem teste de regressão não vira defesa.
O que isso muda no fluxo que o capítulo propõe
A arquitetura do capítulo 7 continua defensável: adotar Conventional Comments como vocabulário comum, forçar o uso via .coderabbit.yaml, e deixar que um gate automático traduza issue: em “merge bloqueado”. O que precisa ser trocado é só a implementação do gate:
- O
.coderabbit.yamlcomreviews.path_instructionsdo capítulo continua correto. Esse schema está documentado hoje em docs.coderabbit.ai e tempath(glob) +instructions(string multi-linha), exatamente como no livro. - Os templates de Conventional Comments em
AGENTS.mdcontinuam úteis — só precisam incluirtodo,choreenotecomo opções da spec, e mencionar as decorações(blocking)/(non-blocking)/(if-minor). - O workflow precisa ser trocado pela versão GraphQL. A versão REST do capítulo é um gate decorativo.
A versão PT do livro na Amazon está em B0H2DB9YXD com data de 2026-05-20. Vou subir uma errata no próximo update do manuscrito corrigindo o capítulo 7, e deixar este post como referência pública enquanto isso.
O que eu tiro desse exercício
Duas coisas práticas:
1. Comentário em código de livro técnico é afirmação, não decoração. A linha // resolved comments lose line era uma afirmação sobre como a REST API da GitHub se comporta. Eu nunca verifiquei. O código compilava, o YAML passava no CI do próprio projeto do livro (que não tinha issue: nenhum para testar), e o comentário parecia plausível. Nenhum desses sinais exige que a afirmação seja verdadeira.
2. “Resolve de review” é propriedade de thread, não de comment, em todas as APIs modernas da GitHub. Essa é a lição transferível para outros gates que alguém queira montar: se o seu filtro depende de “este comentário foi resolvido”, você precisa subir um nível para reviewThreads em GraphQL. REST só devolve comentários individuais, sem o contexto de resolução.
A proposta do capítulo não muda — gate automático que trata issue: como “obrigatório antes de merge” ainda é um padrão útil. O que muda é a tubulação. E, bem honestamente, o fato de eu ter publicado a tubulação quebrada e só ter percebido agora é um lembrete de que verificar a doc depois de o YAML parecer bonito já é tarde.
Referências
- Spec de Conventional Comments: conventionalcomments.org (lista de 9 rótulos + decorações
blocking/non-blocking/if-minor). - GitHub REST: docs.github.com/en/rest/pulls/comments — schema de
listReviewComments(sem campo de resolução). - Discussão oficial confirmando a ausência:
community/discussions/9175. - GraphQL
PullRequestReviewThread.isResolved:community/discussions/24854. - CodeRabbit
reviews.path_instructions: docs.coderabbit.ai/configuration/path-instructions. - Capítulo 7 do livro Revisão de Código com Harness Engineering (PT): Kindle Brasil (BRL 24,99).
ken imoto · WebRTC & Voice AI engineer · kenimoto.dev · TabNews
Livro relacionado Revisão de Código com Harness Engineering Revisão de código em três camadas | hooks + IA + humano · AGENTS.md · CodeRabbit · GitHub Actions Ver a página do livro → Este artigo foi útil?