1

Aprendendo a revisar PR como júnior

Sou desenvolvedor Java full stack júnior e quero contar um pouco das práticas que estou utilizando para revisar PRs automáticos em um projeto pessoal meu. Eu utilizo o Dependabot para checar semanalmente atualizações das bibliotecas e pacotes utilizados no meu projeto. A cada semana ele abre cerca de 4 a 7 PRs de forma automática, e decidi utilizar isso a meu favor para aprender um pouco mais sobre como revisar PRs. Vou usar como exemplo um PR de atualização do pacote @eslint/js da versão 9.39.5 para 10.0.1 que gerava um conflito não capturado pelas verificações do meu CI.

Checklist

O checklist que sigo é esse:

  • Identificar primeiramente a finalidade. Se é correção, feature nova, documentação ou atualização de versão.
  • Ver quais arquivos foram alterados.
  • Confirmar se as mudanças afetam o projeto atual (de longe a parte mais chata).
  • Conferir o que os testes cobrem e, se novos testes foram adicionados, confirmar se eles verificam o comportamento afetado na aplicação.

Identificando a finalidade do PR

Claro, o título chore(deps-dev): bump @eslint/js from 9.39.5 to 10.0.1 já era bastante intuitivo. A descrição do PR também confirmava essa atualização, todos os checks estavam verdes e apenas os arquivos package.json e package-lock.json tinham sido alterados. Era só fazer o merge já que tudo estava aparentemente OK, mas o questionamento que resolvi fazer foi:

  • É uma atualização da versão 9 para a versão 10. Será que os testes cobrem o que pode dar errado nessa mudança?

Foi quando eu decidi olhar as notas de release. Na passagem para a versão 10.0.0, havia uma lista com 22 BREAKING CHANGES! Isso já era motivo para olhar com mais atenção.

Alterações do PR e início do problema

Aqui o primeiro passo é não julgar o impacto do commit pela quantidade de arquivos alterados. Mesmo alterando somente os arquivos de dependências, o importante é saber o que essa nova versão introduziu e como ela poderia afetar o seu projeto.

Antes mesmo de abrir os commits citados nas releases, acabei encontrando um possível problema nos arquivos alterados. Tanto o package.json quanto o package-lock.json trocavam a faixa de versões permitidas do @eslint/js de ^9.39.3 para ^10.0.1, porém o ESLint por si só não acompanhava a atualização. Ficou assim:

package.json

Antes:

{
  "devDependencies": {
    "@eslint/js": "^9.39.3",
    "eslint": "^9.39.3",
    "eslint-plugin-react": "^7.37.5"
  }
}

Depois:

{
  "devDependencies": {
    "@eslint/js": "^10.0.1",
    "eslint": "^9.39.3",
    "eslint-plugin-react": "^7.37.5"
  }
}

package-lock.json

Na entrada de @eslint/js, a versão exata mudou de 9.39.5 para 10.0.1. O ESLint continuou registrado como 9.39.5. Além disso, a entrada do pacote atualizado passou a incluir este requisito:

{
  "peerDependencies": {
    "eslint": "^10.0.0"
  },
  "peerDependenciesMeta": {
    "eslint": {
      "optional": true
    }
  }
}

Compare o antes e depois. Os dois arquivos atualizavam o @eslint/js e nenhum dos dois atualizava o ESLint. Mas isso, sozinho, não prova um erro: pacotes com números de versão diferentes podem ser compatíveis.

O problema estava no peerDependencies: o próprio @eslint/js declarava que precisava do ESLint na faixa ^10.0.0. Ou seja, a partir de 10.0.0 e abaixo de 11.0.0, e não qualquer versão superior a 10.

Já o meu projeto pedia ^9.39.3, que não permite chegar à versão 10. O package.json dizia quais versões eram permitidas, e o lockfile registrava a versão exata escolhida: 9.39.5.

Para não confundir os dois pacotes: o eslint executa a análise do código, enquanto o @eslint/js fornece configurações de regras, como a configuração recomendada que eu utilizo.

Para ter certeza de que era um erro, fui reproduzi-lo localmente. Usei uma worktree com a branch desse PR para investigar sem mexer na minha pasta principal. Até então, as verificações do CI tinham passado. Mas, ao simular a resolução dos pacotes com este comando, o erro apareceu:

npm install --package-lock-only --ignore-scripts --dry-run --no-audit --no-fund

O comando não instala os pacotes nem grava as alterações propostas no lockfile. Pode consultar o registro do npm e gerar logs ou cache, mas permite testar a resolução sem aplicar a atualização.

Agora sim apareceu o ERESOLVE, apontando a incompatibilidade. Na comparação local, essa verificação passou antes da mudança e falhou com os arquivos do PR.

Provavelmente você iria se perguntar: “Por que um pacote tão utilizado pela comunidade JS iria cometer um erro desse? Ninguém revisou essa joça de comparar a versão dos dois pacotes antes de colocar dentro da release não?”

Foi aí que precisei separar as coisas. O pacote tinha declarado a compatibilidade dele. A proposta de atualização do meu projeto é que não atendia ao requisito.

E aquele optional: true? Ele declara o ESLint como opcional (só voltar lá no exemplo do código que você vai ver). Para ter o @eslint/js instalado, não é obrigatório ter o ESLint também. Mas, se ele estiver instalado, a versão precisa ser compatível.

Você não é obrigado a ter um controle para jogar um jogo que também aceita teclado, mas precisa ter um controle compatível caso opte por utilizar um.

Logo, o erro já estava claro.

O meu projeto utiliza tanto o @eslint/js quanto o eslint. A versão 10.0.1 do @eslint/js exige ESLint na faixa ^10.0.0 quando ele está instalado, mas o meu projeto continuava na série 9.

Mas por que os testes não capturaram esse erro?

O CI usava npm ci, que instala as versões registradas no lockfile. Naquela execução, a instalação, o lint, os testes e o build passaram. Já a verificação adicional de resolução encontrou o conflito.

Isso não quer dizer que o npm ci nunca encontre incompatibilidades, nem que o npm install saia atualizando tudo. Foi o comportamento observado nesse caso. Quando as versões do lockfile atendem ao package.json, o npm install normalmente as reutiliza; quando não atendem, ele precisa resolver novas versões dentro das faixas permitidas.

No meu projeto, ele não poderia simplesmente escolher ESLint 10 para resolver o problema, porque o ^9.39.3 ainda limitava a escolha à série 9. Foi justamente esse conflito que a simulação apontou.

Decidi então adicionar a verificação ao workflow:

- name: Validate dependency resolution
  run: npm install --package-lock-only --ignore-scripts --dry-run --no-audit --no-fund

Rodei o CI novamente no PR e dessa vez a etapa falhou com o mesmo erro. O problema que antes passava despercebido agora era capturado. Isso não transforma o comando numa garantia contra toda incompatibilidade, mas ele passou a verificar um cenário que estava faltando no meu CI.

De quem foi a culpa?

Pode-se dizer que o erro estava na proposta do Dependabot: uma atualização incompleta. Ele manteve o ESLint na série 9 e criou uma combinação incompatível.

Por outro lado, isso pode ser uma limitação da atualização individual ou da configuração utilizada. Só esse resultado não prova um bug no Dependabot. O que ficou claro é que o PR automático também precisava de revisão.

A solução

Eu tinha duas opções para seguir: manter os dois pacotes na série 9 e fechar esse PR, ou tentar migrar também o ESLint e conferir a compatibilidade dos plugins.

Optei por tentar atualizar o ESLint para a versão 10, mas aí descobri que o eslint-plugin-react 7.37.5 só declarava suporte até a série 9 do ESLint 🤡. Essa já era a versão marcada como latest no npm. Não adiantava simplesmente mandar atualizar tudo.

Acabei mantendo os dois pacotes na série 9 mesmo. Para seguir com a migração, teria que esperar o suporte do plugin ou avaliar uma alternativa, o que já aumentaria o escopo dessa atualização.


Sei que para alguns isso vai ser algo básico, mas acredito que hoje em dia até o básico não é falado por ser superestimado. Infelizmente nunca fui uma pessoa que aprende bem com leitura de artigos e tutoriais dos outros, por isso decidi documentar alguns problemas que encaro no dia a dia para poder fixar melhor.

Carregando publicação patrocinada...