Coloquei o escopo num YAML versionado e um check no CI: 4 PRs barrados em seis semanas
Fui procurar no repositório por que a data de uma entrega tinha andado duas vezes. O código estava todo lá: commit, autor, revisor, data, diff. A decisão que moveu a data não estava em lugar nenhum. Ela tinha acontecido em chamada, em três mensagens de chat e num print de reunião que ninguém sabe mais onde foi parar.
Dei uma varrida na thread e contei umas onze mensagens começando com "dá pra incluir" ou "isso aqui é só um ajuste". Onze mudanças de escopo, zero commits por causa delas. O escopo é a única parte do projeto que muda sem deixar diff, e é justamente a parte que decide a data.
Em obra, isso já tem solução chata e velha: mudança vira aditivo, com medição, número e assinatura. Resolvi ver se dava para fazer a mesma coisa dentro do repositório, sem inventar processo novo, usando o que o git e o CI já sabem fazer. Rodei no meu próprio projeto, um sistema de gestão que eu escrevo fora do horário comercial, então o custo de errar era só meu.
O escopo virou arquivo
A primeira parte é boba: uma pasta scope/ com um arquivo por entrega. Nada de ferramenta nova, nada de integração com board.
# scope/checkout-v2.yml
entrega: checkout-v2
prazo_atual: 2026-09-18
prazo_original: 2026-09-04
itens:
- id: CK-01
titulo: cálculo de desconto por faixa
estado: acordado
- id: CK-03
titulo: relatório de conciliação diária
estado: acordado
- id: CK-07
titulo: split de pagamento por vendedor
estado: aditivo
entrou_em: 2026-08-11
esforco: 3d
move_prazo: true
O campo que faz o trabalho todo é estado. Item que nasceu com a entrega é acordado. Item que chegou depois é aditivo, e aditivo carrega quando entrou, quanto custa e se mexe na data. Quem quiser saber por que a entrega andou duas semanas roda git log -p scope/checkout-v2.yml e lê a história inteira, com autor e data, sem depender da memória de ninguém.
O check que barra o PR
A segunda parte é a que dói, e é a que faz o arquivo não virar enfeite. Um script compara o arquivo antes e depois do PR.
#!/usr/bin/env bash
# scripts/check-scope.sh
set -euo pipefail
base="${1:-origin/main}"
file="scope/checkout-v2.yml"
changed=$(git diff --name-only "$base"...HEAD)
if ! echo "$changed" | grep -q "^${file}$"; then
echo "escopo intacto neste PR"
exit 0
fi
if ! echo "$changed" | grep -q '^scope/CHANGELOG.md$'; then
echo "FALHA: $file mudou e o CHANGELOG não"
exit 1
fi
antes=$(git show "$base:$file" | yq '.prazo_atual')
depois=$(yq '.prazo_atual' "$file")
novos=$(yq '[.itens[] | select(.estado == "aditivo" and .move_prazo == true)] | length' "$file")
antigos=$(git show "$base:$file" | yq '[.itens[] | select(.estado == "aditivo" and .move_prazo == true)] | length')
echo "aditivos que movem prazo: $antigos -> $novos"
echo "prazo: $antes -> $depois"
if [ "$novos" -gt "$antigos" ] && [ "$antes" = "$depois" ]; then
echo "FALHA: entrou item que move prazo e a data continua a mesma"
exit 1
fi
A regra é uma só, e ela cabe numa frase: item novo que consome dia de trabalho não entra sem a data se mexer. Se você acha que aquele item não move a data, escreve move_prazo: false no arquivo, com o seu usuário no commit. Aí não é mais opinião de reunião, é linha assinada.
No GitHub Actions são cinco linhas:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: sudo snap install yq
- run: ./scripts/check-scope.sh origin/${{ github.base_ref }}
A saída de um PR barrado fica assim:
$ ./scripts/check-scope.sh origin/main
aditivos que movem prazo: 2 -> 3
prazo: 2026-09-04 -> 2026-09-04
FALHA: entrou item que move prazo e a data continua a mesma
Error: Process completed with exit code 1
O que aconteceu em seis semanas
Rodando isso por perto de seis semanas em duas entregas do mesmo repositório: 4 PRs barrados pelo check, 3 deles voltaram com data nova e entrada no CHANGELOG, e 1 terminou com o item saindo do escopo em vez de entrar. Esse último foi o mais interessante. Quando o custo aparece na mesma tela do pedido, metade dos pedidos morre sozinha.
O efeito colateral que eu não esperava foi na revisão. Antes, o revisor olhava só o diff de código. Agora, quando o scope/ aparece na lista de arquivos alterados, ele lê aquilo primeiro, porque é o arquivo que explica o resto do PR.
E tem um ganho de arqueologia. Daqui a seis meses, quando alguém perguntar por que a entrega levou o dobro, a resposta é um comando, não uma reconstrução de memória:
$ git log --format='%ad %an' --date=short -p scope/checkout-v2.yml | grep -c 'estado: aditivo'
7
Onde isso não serve
O check é fácil de contornar. Quem quiser passar por cima muda o prazo_atual em um dia e devolve no outro, e nenhum script pega isso. Ele não impede má fé, só tira a mudança do modo silencioso.
Em produto novo, ainda procurando cliente, eu não usaria. Ali o escopo muda toda semana de propósito, e cada alteração viraria um PR de cerimônia sem nada do outro lado. O custo do processo passaria o custo do retrabalho que ele evita.
Também não sei dizer se isso aguenta um repositório com dez times mexendo no mesmo arquivo de escopo. No meu caso são poucas mãos, e o scope/ quase não dá conflito. Com muita gente, desconfio que o arquivo por entrega vira arquivo por time, e aí já é outro desenho.
Como vocês registram mudança de escopo hoje? Alguém já colocou esse tipo de regra no CI e conseguiu manter viva depois do segundo mês, ou isso sempre acaba com um label de exceção que todo mundo usa?
· · ·
Publicado originalmente no blog da Revin: https://revin.com.br/pt/blog/software-atrasa-obra-entrega-contrato