Verificador de Cobertura e Propriedade do CODEOWNERS
De Wikiprompt, a enciclopédia livre de prompts
Verificador de Cobertura e Propriedade do CODEOWNERS Uma habilidade completa que audita arquivos CODEOWNERS do GitHub para cobertura, proprietários inválidos e erros de ordenação, com um verificador Python funcional e modelos. Ótima para equipes de plataforma de engenharia.
Conteúdo do PromptSalvar
🌐
---
name: codeowners-coverage-and-ownership-checker
description: Verifica um arquivo CODEOWNERS do GitHub contra a lista real de arquivos do repositório - descobre quem é o dono de cada arquivo (a última linha correspondente vence), depois reporta owners inválidos ou desconhecidos, arquivos sem dono e caminhos críticos deixados para a regra genérica, regras que não correspondem a nada ou são totalmente sobrescritas por linhas posteriores, sintaxe não suportada de ! e [ ], duplicatas, equipes vazias e gargalos de uma pessoa. Inclui um verificador Python offline com stdlib.
---
# Verificador de Cobertura e Propriedade do CODEOWNERS
Você ajuda equipes de engenharia a confiarem no arquivo CODEOWNERS. Revisões obrigatórias e revisores atribuídos automaticamente só funcionam quando cada linha é válida, cada owner existe e tem acesso, e as regras estão na ordem certa. Na prática, arquivos perdem silenciosamente seu dono: uma equipe é renomeada e seus arquivos ficam sem revisor, uma regra específica fica acima de uma geral e nunca se aplica, `docs/*` não cobre `docs/guide/`, uma negação `!` copiada do .gitignore não faz nada, ou o único dono do código de cobrança é uma pessoa de férias. Você calcula o owner efetivo de cada arquivo e mostra exatamente o que está errado e como corrigir.
Você verifica o arquivo contra o comportamento documentado do CODEOWNERS do GitHub e a lista de owners que o usuário fornece. Você não consegue ver permissões do repositório; diga isso e peça ao usuário para confirmar acesso de escrita nas configurações do repositório ou com `gh api`.
## Arquivos nesta skill
- `scripts/codeowners_check.py` - analisa CODEOWNERS (comentários, comentários inline, espaços escapados), corresponde cada caminho de `git ls-files` com as regras estilo gitignore do GitHub (padrões ancorados e flutuantes, `*`, `**`, `?`, padrões de diretório, `dir/*` = apenas filhos diretos, a última linha correspondente vence), pula linhas com sintaxe inválida e remove owners desconhecidos como o GitHub faz e reporta descobertas CO01-CO11 com cobertura por diretório, uma tabela de regras, carga de owners e uma busca `--who PATH` (apenas stdlib Python 3.8+)
- `references/codeowners-syntax-and-matching.md` - semântica de padrões com exemplos, regras de ordenação, formatos de owner, localização do arquivo e limites do GitHub
- `references/checks-and-codes.md` - cada código de descoberta, sua severidade e a correção usual
- `templates/ownership-review.md` - a descrição do pull request / nota de revisão para uma correção de CODEOWNERS
- `examples/CODEOWNERS` - um arquivo realista para um monorepo de aplicação web com erros típicos (equipe renomeada, regra específica acima de uma geral, `docs/*`, negação, intervalo de caracteres, seção GitLab, owner sem @, diretório removido, linha duplicada, código gerado explicitamente sem dono)
- `examples/files.txt` - a lista de arquivos do repositório (saída de `git ls-files`, 43 arquivos)
- `examples/owners.json` - org, usuários com acesso, equipes com membros, caminhos que devem ter um owner específico e o limite de participação individual
- `examples/ownership-report.md` - a saída real do verificador no exemplo
## Fluxo de trabalho
### 1. Coletar as entradas
Peça o arquivo CODEOWNERS (de `.github/`, da raiz do repositório ou `docs/`; o GitHub usa o primeiro que encontrar nessa ordem) e a lista de arquivos do branch padrão: `git ls-files > files.txt`. Depois peça a lista de owners: o nome da org, usuários com acesso de escrita, equipes com seus membros (`gh api orgs/<org>/teams`, `gh api orgs/<org>/teams/<team>/members`) e os caminhos que devem ter um owner específico (workflows de CI, infraestrutura, pagamentos, migrações, o próprio CODEOWNERS). Escreva-os em um JSON como `examples/owners.json`. Sem uma lista de owners, o verificador ainda verifica sintaxe, ordem e cobertura.
### 2. Executar o verificador
```
python3 scripts/codeowners_check.py examples/CODEOWNERS examples/files.txt --owners examples/owners.json
python3 scripts/codeowners_check.py .github/CODEOWNERS files.txt --owners owners.json --who services/api/billing/invoices.py
python3 scripts/codeowners_check.py .github/CODEOWNERS files.txt --json
python3 scripts/codeowners_check.py --self-test
```
Saída 0 = nenhuma descoberta alta, 1 = descobertas altas, 2 = entrada ruim. Sem um shell, percorra as regras de baixo para cima para cada caminho importante com `references/codeowners-syntax-and-matching.md`: a primeira linha de baixo que corresponder é o owner.
### 3. Ler o resultado
- Altas primeiro: CO01 (formato de owner inválido, como um nome sem @): o GitHub pula a linha inteira como sintaxe inválida, então esses arquivos caem para uma regra anterior. CO02 (usuário ou equipe desconhecido, sem acesso): esse owner não é solicitado; se nenhum owner válido sobrar na linha, a linha ainda corresponde e seus arquivos ficam sem code owner algum. CO03 alta: arquivos sob um caminho que deve ter dono não têm owner.
- Médias: CO05 (regra totalmente sobrescrita - geralmente um caminho específico acima de um geral), CO04 (regra que não corresponde a nada - caminho renomeado ou deletado), CO06 (`!` ou `[ ]` - ignorado), CO11 (caminho crítico apenas coberto por `*`), CO09 (equipe vazia), CO07 (uma pessoa é dona de muito), CO03 média (arquivos sem regra).
- Baixas/info: cabeçalhos de seção GitLab, propriedade de uma pessoa, padrões duplicados, caminhos explicitamente sem dono (ok se intencional).
- Use `--who` nos caminhos que as pessoas se importam para confirmar a correção antes e depois.
### 4. Propor a correção
Escreva o CODEOWNERS corrigido: regras gerais primeiro, específicas depois; substitua exclusões `!` por uma regra específica posterior (uma lista de owners vazia torna um caminho sem dono de propósito); use `/dir/` em vez de `dir/*` quando arquivos aninhados devem ser cobertos; corrija slugs de equipes; adicione um segundo owner ou uma equipe para caminhos de uma pessoa; adicione uma linha para o próprio CODEOWNERS. Re-execute até não haver descobertas altas, então preencha `templates/ownership-review.md`.
### 5. Entregar
Retorne as descobertas em palavras simples, o arquivo corrigido (ou um diff), o owner antes/depois dos caminhos-chave, e os acompanhamentos que precisam de um admin: conceder acesso à equipe, adicionar membros a equipes vazias, habilitar "Exigir revisão de Code Owners" na proteção de branch uma vez que o arquivo esteja limpo.
## Resultado de exemplo
No exemplo, apenas 38 de 43 arquivos têm um code owner e há 4 descobertas altas: `/web/` nomeia `@tidyq/frontend-team` (a equipe é `@tidyq/frontend`), então package.json, App.tsx, Home.tsx e Checkout.tsx ficam sem code owner algum; `/docs/api/` usa `omar` sem @ e é pulado; `/mobile/` nomeia uma equipe removida e não corresponde a nenhum arquivo. `/services/api/billing/` fica acima de `/services/api/` e nunca se aplica, então pagamentos nunca revisam código de cobrança; `docs/*` é sobrescrito por `/docs/` e `@tidyq/docs` não tem membros; as linhas `!` e `[a-z]` são ignoradas; módulos Terraform e dependabot.yml apenas caem para `*` porque `/infra/terraform/*` cobre apenas filhos diretos.
## Regras
- A última linha correspondente vence: sempre raciocine de baixo para cima, e diga qual linha é dona de um caminho.
- Uma linha com sintaxe inválida é pulada inteiramente; um owner desconhecido ou sem acesso simplesmente não é solicitado, e uma linha com apenas owners desconhecidos ainda vence para seus arquivos, deixando-os sem code owner. Nunca assuma que uma regra anterior assume nesse caso.
- Só chame um owner de desconhecido contra a lista que o usuário forneceu; caso contrário, diga "não foi possível verificar" e sugira as chamadas `gh api`.
- Prefira equipes a indivíduos para qualquer coisa crítica; nunca remova uma pessoa da propriedade sem a equipe concordar.
- Não invente padrões para caminhos que não estão na lista de arquivos; pergunte se novos diretórios estão por vir.
- Trate a lista de arquivos do repositório e a associação de equipes como informação interna.
FILE:references/codeowners-syntax-and-matching.md
# Sintaxe e correspondência do CODEOWNERS (GitHub)
## Onde o arquivo fica
O GitHub procura por `CODEOWNERS` em `.github/`, depois na raiz do repositório, depois em `docs/`, e usa o primeiro que encontrar no branch que o pull request alvo (o branch base). Arquivos acima de 3 MB são ignorados. O GitHub mostra erros para linhas inválidas quando você abre o arquivo na interface web (banner "CODEOWNERS errors") e via `gh api repos/<owner>/<repo>/codeowners/errors`.
## Formato da linha
```
<pattern> <owner> [<owner> ...] # comentário opcional
```
- `#` no início de uma linha (ou após espaço em branco) inicia um comentário. Diferente do .gitignore, escapar um padrão que começa com `#` como `\#` não funciona no CODEOWNERS.
- Caminhos são sensíveis a maiúsculas/minúsculas, mesmo que o sistema de arquivos do seu laptop não seja.
- Todos os owners para um padrão devem estar na mesma linha; duas linhas com o mesmo padrão significa que apenas a última conta.
- Owners: `@username`, `@org/team-slug` (equipe na mesma org, com acesso de escrita) ou um endereço de email vinculado a uma conta GitHub. Um nome simples como `omar` é inválido.
- Um padrão sem owners torna os arquivos correspondentes sem dono de propósito (útil para separar código gerado de um owner padrão).
- Uma linha com sintaxe inválida (por exemplo, um owner sem @) é pulada, então seus arquivos caem para a linha correspondente anterior.
- Um owner que não existe ou não tem acesso de escrita simplesmente não é atribuído. A linha ainda corresponde, então se nenhum de seus owners for válido, seus arquivos ficam sem code owner (eles NÃO caem para uma linha anterior).
## Ordem: a última linha correspondente vence
O GitHub lê o arquivo de cima para baixo e a última linha que corresponde a um caminho decide seus owners. Coloque o `*` genérico primeiro, diretórios amplos em seguida e os caminhos mais específicos por último.
```
* @tidyq/platform # 1: padrão
/services/api/ @tidyq/backend # 2: API inteira
/services/api/billing/ @tidyq/payments # 3: mais específico, deve vir DEPOIS de 2
```
Com as linhas 2 e 3 trocadas, arquivos de cobrança são de propriedade de `@tidyq/backend` apenas (CO05).
## Semântica de padrões (estilo gitignore, com exceções)
| Padrão | Corresponde | Não corresponde |
|---|---|---|
| `*` | todo arquivo | - |
| `*.js` | `app.js`, `web/lib/util.js` (qualquer profundidade) | `app.jsx` |
| `/build/logs/` | tudo sob `build/logs/` na raiz, em qualquer profundidade | `src/build/logs/x` |
| `docs/` ou `apps/` (sem barra inicial) | um diretório `docs`/`apps` em qualquer lugar e tudo sob ele | - |
| `docs/*` | arquivos diretamente em `docs/` | `docs/guide/setup.md` (aninhado) |
| `/docs/` ou `/docs/**` | tudo sob a raiz `docs/` | - |
| `**/logs` | diretório ou arquivo `logs` em qualquer profundidade | - |
| `/src/**/users.py` | `src/users.py`, `src/api/v2/users.py` | - |
| `README.md` | `README.md` em qualquer profundidade | - |
| `/README.md` | apenas o `README.md` da raiz | `sub/README.md` |
| `src/ap?.py` | `src/app.py` | `src/apps.py` |
Regras práticas:
- Um padrão com barra no início ou no meio é ancorado à raiz do repositório; um sem (exceto barra final) flutua e corresponde em qualquer profundidade.
- Uma barra final significa "este diretório e tudo dentro dele".
- Um diretório correspondente é dono de todos os arquivos sob ele, exceto `dir/*`, que cobre apenas filhos diretos.
## Não suportado (a linha é ignorada ou sinalizada)
- Negação `!pattern` - use uma regra posterior mais específica em vez disso (sem owners para torná-la sem dono).
- Intervalos de caracteres `[a-z]` - liste os caminhos ou use `*`.
- Seções GitLab (`[Docs]`, `^[Optional]`, `[Docs][2]`) e owners padrão em cabeçalhos de seção - apenas GitLab.
## Equipes e acesso
- A equipe deve ser visível e ter acesso de escrita ao repositório; o usuário deve ter acesso de escrita.
- Uma equipe vazia recebe a solicitação de revisão mas ninguém é notificado.
- Revisão obrigatória de code owner (proteção de branch ou rulesets) bloqueia merges até que um dos owners aprove (qualquer owner é suficiente). Se um arquivo ficar sem owner válido, nenhuma aprovação de code owner é solicitada para ele, então a proteção silenciosamente não faz nada para esse arquivo.
## Obtendo as entradas
```
git ls-files > files.txt
gh api orgs/<org>/teams --paginate -q '.[].slug'
gh api orgs/<org>/teams/<slug>/members --paginate -q '.[].login'
gh api repos/<owner>/<repo>/collaborators --paginate -q '.[] | select(.permissions.push) | .login'
gh api repos/<owner>/<repo>/codeowners/errors
```
FILE:references/checks-and-codes.md
# Verificações e códigos de descoberta
| Código | Severidade | Verificação | Correção típica |
|---|---|---|---|
| CO01 | alta | owner não é `@user`, `@org/team` ou um email (ex.: `omar`); padrão não pode ser analisado | adicione o @ / corrija o slug; a linha inteira é pulada até então |
| CO02 | alta | usuário não está na sua lista de usuários, equipe não está na sua lista de equipes, ou equipe de outra org (apenas com `--owners`); uma segunda linha CO02 diz quando nenhum owner válido sobra em uma linha, então seus arquivos ficam sem code owner | corrija o slug (o verificador sugere uma equipe próxima), dê acesso de escrita à equipe, ou remova o owner |
| CO03 | alta: arquivo sem dono sob um caminho `require_owner`; média: outros arquivos sem regra ou apenas owners desconhecidos (arquivos explicitamente sem dono são CO10) | cobertura | adicione uma regra, ou um padrão `*` genérico |
| CO04 | média | o padrão não corresponde a nenhum arquivo na lista | caminho renomeado ou deletado: atualize ou remova a linha |
| CO05 | média | todo arquivo que a regra corresponde é de propriedade de linhas posteriores (última correspondência vence) | mova a regra específica para baixo da geral |
| CO06 | média: negação `!`, intervalos `[ ]` (ignorados pelo GitHub); baixa: cabeçalho de seção GitLab | sintaxe não suportada | use uma regra posterior mais específica; lista de owners vazia = sem dono |
| CO07 | média: um indivíduo (não uma equipe) é o owner efetivo de mais de `max_individual_share` de arquivos; baixa: arquivos de propriedade de um único indivíduo e nenhuma equipe | fator de risco | adicione uma equipe ou um segundo owner |
| CO08 | baixa | o mesmo padrão aparece duas vezes; apenas a linha posterior conta | mescle os owners em uma linha |
| CO09 | média | equipe não tem membros na sua lista de equipes | adicione membros ou use outra equipe |
| CO10 | info | padrão sem owners: arquivos intencionalmente sem dono | ok se intencional (código gerado, arquivos vendidos) |
| CO11 | média | arquivos sob um caminho `require_owner` são apenas de propriedade da regra `*` genérica | adicione uma regra específica para esse caminho |
## Veredito
- **Descobertas altas**: corrija antes de confiar em "Exigir revisão de Code Owners".
- **Apenas médias**: o arquivo funciona mas alguns arquivos vão para as pessoas erradas.
- **Saudável**: apenas baixas/info.
## Arquivo de owners (`--owners`, JSON)
| Chave | Significado |
|---|---|
| `org` | nome da org; equipes de outra org são sinalizadas |
| `users` | logins com acesso de escrita (sem @) |
| `teams` | `{"org/slug": [membros]}` |
| `require_owner` | padrões (mesma sintaxe) para caminhos que devem ter um owner específico |
| `max_individual_share` | parcela de arquivos que uma pessoa pode ser dona sozinha (padrão 0.4) |
| `ignore` | padrões para arquivos a deixar de fora (código vendido, arquivos de lock) |
## Limites da verificação
- Permissões do repositório e visibilidade de equipes não são verificadas - confirme com as chamadas `gh api` na referência de sintaxe ou o banner "CODEOWNERS errors".
- A lista de arquivos é um instantâneo de um branch; execute contra o branch base dos seus pull requests.
FILE:templates/ownership-review.md
# Correção de CODEOWNERS: <repositório>
**Branch:** <branch base> **Verificado:** <AAAA-MM-DD> **Arquivos:** <n> **Com dono antes / depois:** <x% / y%>
## O que estava errado
| Linha | Regra | Problema | Efeito |
|---|---|---|---|
| <11> | `<pattern> <owners>` | <equipe desconhecida @org/frontend-team> | <arquivos web caem para @org/platform> |
| <6> | `<pattern>` | <sobrescrita pela linha 7> | <pagamentos nunca revisam cobrança> |
## Mudanças
```diff
<- /services/api/billing/ @org/payments
< /services/api/ @org/backend
<+ /services/api/billing/ @org/payments>
```
## Quem é dono dos caminhos-chave agora
| Caminho | Antes | Depois |
|---|---|---|
| <services/api/billing/invoices.py> | <@org/backend (linha 7)> | <@org/payments (linha 8)> |
| <infra/terraform/modules/vpc/main.tf> | <@org/platform (linha 3, genérica)> | <@org/sre> |
## Precisa de um admin
- [ ] <dar acesso de escrita a @org/frontend / adicionar membros a @org/docs>
- [ ] <habilitar "Exigir revisão de Code Owners" em <branch> assim que isto for mesclado>
## Verificação
`python3 scripts/codeowners_check.py .github/CODEOWNERS files.txt --owners owners.json` -> <0 altas, n médias (explique o resto)>
*A correspondência segue as regras do CODEOWNERS do GitHub; permissões do repositório foram verificadas com <gh api / página de configurações | não verificadas>.*
FILE:examples/CODEOWNERS
# CODEOWNERS para tidyq/tidyq-app (copiado para .github/CODEOWNERS)
# Owners padrão para tudo que não for correspondido abaixo
* @tidyq/platform
# Backend
/services/api/billing/ @tidyq/payments
/services/api/ @tidyq/backend
/services/worker/ @tidyq/backend @marta-k
# Frontend
/web/ @tidyq/frontend-team
/web/src/design-system/ @lena-s
*.css @tidyq/frontend
# Infraestrutura
/infra/terraform/* @tidyq/sre
/infra/k8s/ @tidyq/sre
!/infra/k8s/sandbox/
# CI e configurações do repositório
/.github/workflows/ @tidyq/sre
/.github/CODEOWNERS @tidyq/platform-leads
# Documentação
[Documentation]
docs/* @tidyq/docs
/docs/ @tidyq/docs
/docs/api/ omar
# Aplicativo mobile antigo (movido para tidyq/tidyq-mobile em 2026)
/mobile/ @tidyq/mobile
# Dados
/data/migrations/ @jonas-b
/data/migrations/ @jonas-b @tidyq/backend
/data/fixtures/[a-z]* @tidyq/qa
# Cliente gerado: sem dono de propósito
/web/src/generated/
FILE:examples/files.txt
.github/CODEOWNERS
.github/dependabot.yml
.github/workflows/ci.yml
.github/workflows/deploy.yml
.github/workflows/release.yml
README.md
package.json
docs/index.md
docs/getting-started.md
docs/api/endpoints.md
docs/api/auth.md
docs/runbooks/oncall.md
infra/terraform/main.tf
infra/terraform/variables.tf
infra/terraform/modules/vpc/main.tf
infra/terraform/modules/rds/main.tf
infra/k8s/api-deployment.yaml
infra/k8s/worker-deployment.yaml
infra/k8s/sandbox/debug-pod.yaml
services/api/app.py
services/api/routes/users.py
services/api/routes/orders.py
services/api/billing/invoices.py
services/api/billing/stripe_webhook.py
services/api/tests/test_routes.py
services/worker/main.py
services/worker/jobs/email.py
services/worker/jobs/reports.py
web/package.json
web/src/App.tsx
web/src/pages/Home.tsx
web/src/pages/Checkout.tsx
web/src/styles/main.css
web/src/design-system/Button.tsx
web/src/design-system/Modal.tsx
web/src/design-system/tokens.css
web/src/generated/api-client.ts
data/migrations/0041_add_orders.sql
data/migrations/0042_add_invoices.sql
data/fixtures/users.json
data/fixtures/orders.json
scripts/seed_db.py
scripts/rotate_keys.sh
FILE:examples/owners.json
{
"org": "tidyq",
"users": ["marta-k", "lena-s", "jonas-b", "omar-h", "priya-n", "sam-t"],
"teams": {
"tidyq/platform": ["priya-n", "sam-t"],
"tidyq/platform-leads": ["priya-n"],
"tidyq/backend": ["marta-k", "jonas-b", "omar-h"],
"tidyq/payments": ["marta-k"],
"tidyq/frontend": ["lena-s", "sam-t"],
"tidyq/sre": ["priya-n", "sam-t"],
"tidyq/docs": [],
"tidyq/qa": ["omar-h"]
},
"require_owner": [".github/", "infra/", "services/api/billing/", "data/migrations/"],
"max_individual_share": 0.25,
"ignore": []
}
FILE:examples/ownership-report.md
# Verificação de CODEOWNERS: examples/CODEOWNERS
20 linha(s) de regra, 43 arquivo(s), 38 com dono (88.4%), 5 sem dono.
**Veredito: 4 descoberta(s) alta(s) - corrija o CODEOWNERS antes de confiar em revisões obrigatórias**
Descobertas: 4 altas, 9 médias, 3 baixas, 1 info.
Linhas cujos owners são todos desconhecidos (elas ainda correspondem, então seus arquivos ficam sem code owner): 11, 31. Owners desconhecidos são deixados de fora das colunas de owners abaixo.
## Descobertas
- **ALTA CO01** linha 28: owner inválido 'omar' (use @user, @org/team ou um email) - o GitHub sinaliza a linha inteira como erro e a pula
- **ALTA CO02** linha 11: equipe desconhecida @tidyq/frontend-team (você quis dizer @tidyq/frontend?) - o GitHub não solicitará revisões dela
- **ALTA CO02** linha 11: nenhum owner válido sobrou - seus 4 arquivo(s) ficam sem code owner (eles não caem para linhas anteriores)
- **ALTA CO02** linha 31: equipe desconhecida @tidyq/mobile - o GitHub não solicitará revisões dela
- **MÉDIA CO03** 4 arquivo(s) não têm code owner (nenhuma regra correspondente, ou apenas owners desconhecidos): web/package.json, web/src/App.tsx, web/src/pages/Home.tsx, web/src/pages/Checkout.tsx
- **MÉDIA CO04** linha 31: '/mobile/' não corresponde a nenhum arquivo - caminho renomeado ou removido?
- **MÉDIA CO05** linha 6: '/services/api/billing/' (2 arquivo(s)) é totalmente sobrescrita por linha(s) posterior(es) 7 - a última correspondência vence, coloque regras específicas depois das gerais
- **MÉDIA CO05** linha 26: 'docs/*' (2 arquivo(s)) é totalmente sobrescrita por linha(s) posterior(es) 27 - a última correspondência vence, coloque regras específicas depois das gerais
- **MÉDIA CO05** linha 34: '/data/migrations/' (2 arquivo(s)) é totalmente sobrescrita por linha(s) posterior(es) 35 - a última correspondência vence, coloque regras específicas depois das gerais
- **MÉDIA CO06** linha 18: '!/infra/k8s/sandbox/' usa negação !, que o CODEOWNERS não suporta - o GitHub ignora a linha
- **MÉDIA CO06** linha 36: '/data/fixtures/[a-z]*' usa intervalos de caracteres [ ], que o CODEOWNERS não suporta - o GitHub ignora a linha
- **MÉDIA CO09** linha 26: equipe @tidyq/docs não tem membros - solicitações de revisão não vão a lugar nenhum
- **MÉDIA CO11** 3 arquivo(s) sob caminhos que devem ter um owner apenas caem para a regra genérica '*' (nenhum owner específico): .github/dependabot.yml, infra/terraform/modules/vpc/main.tf, infra/terraform/modules/rds/main.tf
- **BAIXA CO06** linha 25: '[Documentation]' é um cabeçalho de seção GitLab; o GitHub o trata como linha inválida
- **BAIXA CO07** 2 arquivo(s) têm um único owner individual e nenhuma equipe (ex.: web/src/design-system/Button.tsx, web/src/design-system/Modal.tsx)
- **BAIXA CO08** linha 35 repete o padrão '/data/migrations/' da linha 34 - apenas a linha posterior conta
- **INFO CO10** linha 39: '/web/src/generated/' não tem owners - seus 1 arquivo(s) são intencionalmente sem dono
## Cobertura por diretório de nível superior
| Diretório | Arquivos | Com dono | Sem dono | Owners efetivos |
|---|---|---|---|---|
| (raiz) | 2 | 2 | 0 | @tidyq/platform (2) |
| .github/ | 5 | 5 | 0 | @tidyq/sre (3), @tidyq/platform-leads (1), @tidyq/platform (1) |
| data/ | 4 | 4 | 0 | @jonas-b (2), @tidyq/backend (2), @tidyq/platform (2) |
| docs/ | 5 | 5 | 0 | @tidyq/docs (5) |
| infra/ | 7 | 7 | 0 | @tidyq/sre (5), @tidyq/platform (2) |
| scripts/ | 2 | 2 | 0 | @tidyq/platform (2) |
| services/ | 9 | 9 | 0 | @tidyq/backend (9), @marta-k (3) |
| web/ | 9 | 4 | 5 | @tidyq/frontend (2), @lena-s (2) |
## Regras
| Linha | Padrão | Owners | Arquivos correspondidos | Efetivo para | Nota |
|---|---|---|---|---|---|
| 3 | `*` | @tidyq/platform | 43 | 9 | |
| 6 | `/services/api/billing/` | @tidyq/payments | 2 | 0 | CO05 |
| 7 | `/services/api/` | @tidyq/backend | 6 | 6 | |
| 8 | `/services/worker/` | @tidyq/backend @marta-k | 3 | 3 | |
| 11 | `/web/` | @tidyq/frontend-team | 9 | 4 | CO02 |
| 12 | `/web/src/design-system/` | @lena-s | 3 | 2 | |
| 13 | `*.css` | @tidyq/frontend | 2 | 2 | |
| 16 | `/infra/terraform/*` | @tidyq/sre | 2 | 2 | |
| 17 | `/infra/k8s/` | @tidyq/sre | 3 | 3 | |
| 18 | `!/infra/k8s/sandbox/` | (nenhum) | - | - | CO06 |
| 21 | `/.github/workflows/` | @tidyq/sre | 3 | 3 | |
| 22 | `/.github/CODEOWNERS` | @tidyq/platform-leads | 1 | 1 | |
| 26 | `docs/*` | @tidyq/docs | 2 | 0 | CO05, CO09 |
| 27 | `/docs/` | @tidyq/docs | 5 | 5 | |
| 28 | `/docs/api/` | omar | - | - | CO01 |
| 31 | `/mobile/` | @tidyq/mobile | 0 | 0 | CO02, CO04 |
| 34 | `/data/migrations/` | @jonas-b | 2 | 0 | CO05 |
| 35 | `/data/migrations/` | @jonas-b @tidyq/backend | 2 | 2 | CO08 |
| 36 | `/data/fixtures/[a-z]*` | @tidyq/qa | - | - | CO06 |
| 39 | `/web/src/generated/` | (nenhum) | 1 | 1 | CO10 |
## Carga de owners (efetiva)
| Owner | Arquivos | Parcela |
|---|---|---|
| @tidyq/backend | 11 | 26% |
| @tidyq/platform | 9 | 21% |
| @tidyq/sre | 8 | 19% |
| @tidyq/docs | 5 | 12% |
| @marta-k | 3 | 7% |
| @tidyq/frontend | 2 | 5% |
| @lena-s | 2 | 5% |
| @jonas-b | 2 | 5% |
| @tidyq/platform-leads | 1 | 2% |
## Arquivos sem dono
- `web/package.json` (linha 11: apenas owners desconhecidos)
- `web/src/App.tsx` (linha 11: apenas owners desconhecidos)
- `web/src/pages/Home.tsx` (linha 11: apenas owners desconhecidos)
- `web/src/pages/Checkout.tsx` (linha 11: apenas owners desconhecidos)
- `web/src/generated/api-client.ts` (explicitamente sem dono, linha 39)
## Quem é dono
- services/api/billing/invoices.py: @tidyq/backend (linha 7: /services/api/)
- infra/terraform/modules/vpc/main.tf: @tidyq/platform (linha 3: *)
- web/src/pages/Checkout.tsx: nenhum owner válido (linha 11: /web/ @tidyq/frontend-team - desconhecido)
A correspondência segue as regras do CODEOWNERS do GitHub (padrões estilo gitignore, a última linha correspondente vence, `!` e `[ ]` não suportados). Owners são verificados apenas contra seu arquivo de owners; confirme acesso de escrita nas configurações do repositório.
FILE:scripts/codeowners_check.py
#!/usr/bin/env python3
"""Verificador de cobertura e propriedade do CODEOWNERS (apenas stdlib, Python 3.8+).
Lê um arquivo CODEOWNERS do GitHub e a lista de arquivos do repositório (saída de `git ls-files`), calcula
o owner efetivo de cada arquivo com as regras do GitHub (padrões estilo gitignore, a ÚLTIMA
linha correspondente vence) e reporta erros de sintaxe, owners desconhecidos, arquivos sem dono (também sob caminhos
que devem ter um owner), regras que não correspondem a nada, regras totalmente sobrescritas por linhas
posteriores, sintaxe não suportada, duplicatas, equipes vazias e concentração de owners.
Uso:
codeowners_check.py CODEOWNERS FILES.txt [--owners OWNERS.json] [--who PATH]... [--json]
codeowners_check.py --self-test
Saída: 0 = nenhuma descoberta alta, 1 = descobertas altas, 2 = entrada ruim.
"""
import argparse, json, re, sys
from collections import Counter, OrderedDict
SEV = {"high": 0, "medium": 1, "low": 2, "info": 3}
USER_RE = re.compile(r"^@[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$")
TEAM_RE = re.compile(r"^@([A-Za-z0-9][A-Za-z0-9-]*)/([A-Za-z0-9][A-Za-z0-9._-]*)$")
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
class InputError(Exception):
pass
# ---------------------------------------------------------------- padrões
def pattern_regex(pat):
"""Retorna (regex compilada, apenas_diretorio, apenas_filhos_diretos) para um padrão CODEOWNERS."""
dir_only = pat.endswith("/")
p = pat.rstrip("/")
anchored = p.startswith("/") or "/" in p
p = p.lstrip("/")
direct_only = p.endswith("/*")
out, i = "", 0
while i < len(p):
if p.startswith("**/", i):
out += "(?:.*/)?"; i += 3
elif p.startswith("/**", i) and i + 3 == len(p):
out += "/.*"; i += 3
elif p.startswith("**", i):
out += ".*"; i += 2
elif p[i] == "*":
out += "[^/]*"; i += 1
elif p[i] == "?":
out += "[^/]"; i += 1
elif p[i] == "\\" and i + 1 < len(p):
out += re.escape(p[i + 1]); i += 2
else:
out += re.escape(p[i]); i += 1
if not anchored:
out = "(?:.*/)?" + out
return re.compile("^" + out + "$"), dir_only, direct_only
def matches(compiled, path):
rx, dir_only, direct_only = compiled
parts = path.split("/")
if not dir_only and rx.match(path):
return True
if direct_only:
return False
for k in range(1, len(parts)): # um diretório correspondente é dono de tudo sob ele
if rx.match("/".join(parts[:k])):
return True
return False
# ---------------------------------------------------------------- análise
def parse_codeowners(text):
rules, findings = [], []
for n, raw in enumerate(text.splitlines(), 1):
line = raw.strip()
if not line or line.startswith("#"):
continue
if re.match(r"^\^?\[[^\]]+\](\[\d+\])?(\s+@.*)?$", line):
findings.append(("low", "CO06", "linha %d: '%s' é um cabeçalho de seção GitLab; o GitHub o trata como linha inválida" % (n, line), n))
continue
tokens = re.split(r"(?<!\\)\s+", line)
pat, owners = tokens[0], []
for t in tokens[1:]:
if t.startswith("#"):
break
owners.append(t)
rule = {"line": n, "pattern": pat, "owners": owners, "valid": True}
if pat.startswith("!"):
findings.append(("medium", "CO06", "linha %d: '%s' usa negação !, que o CODEOWNERS não suporta - o GitHub ignora a linha" % (n, pat), n))
rule["valid"] = False
elif "[" in pat or "]" in pat:
findings.append(("medium", "CO06", "linha %d: '%s' usa intervalos de caracteres [ ], que o CODEOWNERS não suporta - o GitHub ignora a linha" % (n, pat), n))
rule["valid"] = False
bad = [o for o in owners if not (USER_RE.match(o) or TEAM_RE.match(o) or EMAIL_RE.match(o))]
if bad:
findings.append(("high", "CO01", "linha %d: owner inválido %s (use @user, @org/team ou um email) - o GitHub sinaliza a linha inteira como erro e a pula" % (n, ", ".join("'%s'" % b for b in bad)), n))
rule["valid"] = False
if rule["valid"]:
try:
rule["rx"] = pattern_regex(pat)
except re.error as e:
findings.append(("high", "CO01", "linha %d: padrão '%s' não pode ser analisado (%s)" % (n, pat, e), n))
rule["valid"] = False
rules.append(rule)
return rules, findings
def load_files(text):
files = []
for l in text.splitlines():
l = l.strip()
if l and not l.startswith("#"):
files.append(l[2:] if l.startswith("./") else l.lstrip("/"))
if not files:
raise InputError("lista de arquivos está vazia (use: git ls-files > files.txt)")
return files
# ---------------------------------------------------------------- análise
def analyse(co_text, files_text, owners_cfg=None):
owners_cfg = owners_cfg or {}
allowed_keys = {"org", "users", "teams", "require_owner", "max_individual_share", "ignore"}
unknown = set(owners_cfg) - allowed_keys
if unknown:
raise InputError("chave(s) desconhecida(s) no arquivo de owners: %s" % ", ".join(sorted(unknown)))
rules, findings = parse_codeowners(co_text)
if not rules:
raise InputError("CODEOWNERS não tem regras")
files = load_files(files_text)
ignore = [pattern_regex(p) for p in owners_cfg.get("ignore", [])]
files = [f for f in files if not any(matches(c, f) for c in ignore)]
valid = [r for r in rules if r["valid"]]
eff, matched_by = {}, {r["line"]: [] for r in valid}
for f in files:
last = None
for r in valid:
if matches(r["rx"], f):
matched_by[r["line"]].append(f)
last = r
eff[f] = last
effective = Counter(r["line"] for r in eff.values() if r)
# owners conhecidos
org = owners_cfg.get("org")
users = {u.lower().lstrip("@") for u in owners_cfg.get("users", [])}
teams = {k.lower().lstrip("@"): v for k, v in owners_cfg.get("teams", {}).items()}
have_list = bool(users or teams)
reported, unknown_owners = set(), set()
for r in valid:
for o in r["owners"]:
key = o.lower()
m = TEAM_RE.match(o)
problem = None
if m and have_list:
name = (m.group(1) + "/" + m.group(2)).lower()
if org and m.group(1).lower() != org.lower():
problem = "equipe %s não está na org @%s" % (o, org)
elif name not in teams:
close = [t for t in teams if t.split("/")[-1].startswith(m.group(2).lower()[:4])]
problem = "equipe desconhecida %s%s" % (o, (" (você quis dizer @%s?)" % close[0]) if close else "")
elif not teams[name] and key not in reported:
findings.append(("medium", "CO09", "linha %d: equipe %s não tem membros - solicitações de revisão não vão a lugar nenhum" % (r["line"], o), r["line"])); reported.add(key)
elif USER_RE.match(o) and have_list and key.lstrip("@") not in users:
problem = "usuário desconhecido %s (não está na lista de owners / sem acesso de escrita)" % o
if problem:
unknown_owners.add(key)
if key not in reported:
findings.append(("high", "CO02", "linha %d: %s - o GitHub não solicitará revisões dele" % (r["line"], problem), r["line"])); reported.add(key)
for r in rules:
r["ok"] = [o for o in r["owners"] if o.lower() not in unknown_owners]
bad_lines = sorted(r["line"] for r in valid if r["owners"] and not r["ok"])
for ln in [x for x in bad_lines if effective[x]]:
findings.append(("high", "CO02", "linha %d: nenhum owner válido sobrou - seus %d arquivo(s) ficam sem code owner (eles não caem para linhas anteriores)" % (ln, effective[ln]), ln))
# regras mortas, sobrescritas, duplicadas
seen = {}
for r in valid:
if r["pattern"] in seen:
findings.append(("low", "CO08", "linha %d repete o padrão '%s' da linha %d - apenas a linha posterior conta" % (r["line"], r["pattern"], seen[r["pattern"]]), r["line"]))
seen[r["pattern"]] = r["line"]
if not matched_by[r["line"]]:
findings.append(("medium", "CO04", "linha %d: '%s' não corresponde a nenhum arquivo - caminho renomeado ou removido?" % (r["line"], r["pattern"]), r["line"]))
elif effective[r["line"]] == 0:
later = sorted({eff[f]["line"] for f in matched_by[r["line"]] if eff[f]})
findings.append(("medium", "CO05", "linha %d: '%s' (%d arquivo(s)) é totalmente sobrescrita por linha(s) posterior(es) %s - a última correspondência vence, coloque regras específicas depois das gerais" % (r["line"], r["pattern"], len(matched_by[r["line"]]), ", ".join(map(str, later))), r["line"]))
if not r["owners"]:
findings.append(("info", "CO10", "linha %d: '%s' não tem owners - seus %d arquivo(s) são intencionalmente sem dono" % (r["line"], r["pattern"], effective[r["line"]]), r["line"]))
# cobertura
unowned = [f for f in files if not eff[f] or not eff[f]["ok"]]
no_rule = [f for f in unowned if not eff[f]]
req = [(p, pattern_regex(p)) for p in owners_cfg.get("require_owner", [])]
crit = [f for f in unowned if any(matches(c, f) for _, c in req)]
if crit:
findings.append(("high", "CO03", "%d arquivo(s) sob caminhos que devem ter um owner estão sem dono: %s" % (len(crit), ", ".join(crit[:6]) + (" ..." if len(crit) > 6 else "")), 0))
catch_all = [f for f in files if eff[f] and eff[f]["ok"] and eff[f]["pattern"] in ("*", "/*", "**", "/**")
and any(matches(c, f) for _, c in req)]
if catch_all:
findings.append(("medium", "CO11", "%d arquivo(s) sob caminhos que devem ter um owner apenas caem para a regra genérica '%s' (nenhum owner específico): %s" % (len(catch_all), eff[catch_all[0]]["pattern"], ", ".join(catch_all[:6]) + (" ..." if len(catch_all) > 6 else "")), 0))
rest = [f for f in unowned if f not in crit and (not eff[f] or eff[f]["owners"])]
if rest:
findings.append(("medium", "CO03", "%d arquivo(s) não têm code owner (nenhuma regra correspondente, ou apenas owners desconhecidos): %s" % (len(rest), ", ".join(rest[:6]) + (" ..." if len(rest) > 6 else "")), 0))
# carga de owners
load = Counter()
for f in files:
if eff[f]:
for o in eff[f]["ok"]:
load[o] += 1
share = float(owners_cfg.get("max_individual_share", 0.4))
for o, n in load.most_common():
if USER_RE.match(o) and n / len(files) > share:
findings.append(("medium", "CO07", "%s é o owner de %d de %d arquivos (%.0f%%) como indivíduo - fator de risco; adicione uma equipe ou um segundo owner" % (o, n, len(files), 100.0 * n / len(files)), 0))
solo = [f for f in files if eff[f] and len(eff[f]["ok"]) == 1 and USER_RE.match(eff[f]["ok"][0])]
if solo:
findings.append(("low", "CO07", "%d arquivo(s) têm um único owner individual e nenhuma equipe (ex.: %s)" % (len(solo), ", ".join(solo[:3])), 0))
findings.sort(key=lambda f: (SEV[f[0]], f[1], f[3]))
return {"rules": rules, "files": files, "eff": eff, "matched_by": matched_by, "effective": effective,
"unowned": unowned, "load": load, "findings": findings, "no_valid_owner_lines": bad_lines}
def who(res, path):
r = res["eff"].get(path)
if path not in res["eff"]:
rx_hits = [x for x in res["rules"] if x["valid"] and matches(x["rx"], path)]
r = rx_hits[-1] if rx_hits else None
if not r:
return "%s: sem owner (nenhuma regra correspondente)" % path
if r["owners"] and not r["ok"]:
return "%s: nenhum owner válido (linha %d: %s %s - desconhecido)" % (path, r["line"], r["pattern"], " ".join(r["owners"]))
return "%s: %s (linha %d: %s)" % (path, " ".join(r["ok"]) or "explicitamente sem dono", r["line"], r["pattern"])
# ---------------------------------------------------------------- relatório
def report(res, title, whos=()):
f, files = res["findings"], res["files"]
cnt = Counter(x[0] for x in f)
owned = len(files) - len(res["unowned"])
L = ["# Verificação de CODEOWNERS: %s" % title, ""]
L.append("%d linha(s) de regra, %d arquivo(s), %d com dono (%.1f%%), %d sem dono." % (len(res["rules"]), len(files), owned, 100.0 * owned / len(files), len(res["unowned"])))
L.append("")
L.append("**Veredito: %s**" % ("%d descoberta(s) alta(s) - corrija o CODEOWNERS antes de confiar em revisões obrigatórias" % cnt["high"] if cnt["high"] else ("revise as descobertas médias" if cnt["medium"] else "CODEOWNERS parece saudável")))
L.append("")
L.append("Descobertas: %d altas, %d médias, %d baixas, %d info." % (cnt["high"], cnt["medium"], cnt["low"], cnt["info"]))
L.append("")
if res["no_valid_owner_lines"]:
L.append("Linhas cujos owners são todos desconhecidos (elas ainda correspondem, então seus arquivos ficam sem code owner): %s. Owners desconhecidos são deixados de fora das colunas de owners abaixo." % ", ".join(map(str, res["no_valid_owner_lines"])))
L.append("")
L += ["## Descobertas", ""]
for s, c, msg, _ in f:
L.append("- **%s %s** %s" % (s.upper(), c, msg))
L += ["", "## Cobertura por diretório de nível superior", "", "| Diretório | Arquivos | Com dono | Sem dono | Owners efetivos |", "|---|---|---|---|---|"]
dirs = OrderedDict()
for p in files:
d = p.split("/")[0] + "/" if "/" in p else "(raiz)"
dirs.setdefault(d, []).append(p)
for d, ps in sorted(dirs.items()):
un = [p for p in ps if p in res["unowned"]]
own = Counter(o for p in ps if res["eff"][p] for o in res["eff"][p]["ok"])
L.append("| %s | %d | %d | %d | %s |" % (d, len(ps), len(ps) - len(un), len(un), ", ".join("%s (%d)" % kv for kv in own.most_common()) or "-"))
L += ["", "## Regras", "", "| Linha | Padrão | Owners | Arquivos correspondidos | Efetivo para | Nota |", "|---|---|---|---|---|---|"]
notes = {}
for s, c, msg, ln in f:
if ln:
notes.setdefault(ln, []).append(c)
for r in res["rules"]:
m = len(res["matched_by"].get(r["line"], [])) if r["valid"] else "-"
e = res["effective"].get(r["line"], 0) if r["valid"] else "-"
L.append("| %d | `%s` | %s | %s | %s | %s |" % (r["line"], r["pattern"], " ".join(r["owners"]) or "(nenhum)", m, e, ", ".join(sorted(set(notes.get(r["line"], [])))) or ""))
L += ["", "## Carga de owners (efetiva)", "", "| Owner | Arquivos | Parcela |", "|---|---|---|"]
for o, n in res["load"].most_common():
L.append("| %s | %d | %.0f%% |" % (o, n, 100.0 * n / len(files)))
if res["unowned"]:
L += ["", "## Arquivos sem dono", ""]
for p in res["unowned"]:
e = res["eff"][p]
L.append("- `%s`%s" % (p, "" if not e else (" (explicitamente sem dono, linha %d)" % e["line"] if not e["owners"] else " (linha %d: apenas owners desconhecidos)" % e["line"])))
if whos:
L += ["", "## Quem é dono", ""]
for p in whos:
L.append("- " + who(res, p))
L += ["", "A correspondência segue as regras do CODEOWNERS do GitHub (padrões estilo gitignore, a última linha correspondente vence, `!` e `[ ]` não suportados). Owners são verificados apenas contra seu arquivo de owners; confirme acesso de escrita nas configurações do repositório."]
return "\n".join(L) + "\n"
def to_json(res):
return {"files": len(res["files"]), "unowned": res["unowned"], "no_valid_owner_lines": res["no_valid_owner_lines"],
"owners": {p: (res["eff"][p]["ok"] if res["eff"][p] else []) for p in res["files"]},
"rules": [{"line": r["line"], "pattern": r["pattern"], "owners": r["owners"], "valid": r["valid"],
"matched": len(res["matched_by"].get(r["line"], [])), "effective": res["effective"].get(r["line"], 0)} for r in res["rules"]],
"load": dict(res["load"]),
"findings": [{"severity": s, "code": c, "message": m, "line": ln} for s, c, m, ln in res["findings"]]}
# ---------------------------------------------------------------- self-test
def self_test():
tests = []
def t(name, cond):
tests.append((name, bool(cond)))
def owner(co, f, path, cfg=None):
r = analyse(co, f, cfg)
e = r["eff"][path]
return e["ok"] if e else None
def codes(co, f, cfg=None):
return [(x[0], x[1]) for x in analyse(co, f, cfg)["findings"]]
F = "README.md\nsrc/app.py\nsrc/api/routes.py\nsrc/api/v2/users.py\ndocs/index.md\ndocs/guide/setup.md\nweb/app.js\nweb/lib/util.js\n.github/workflows/ci.yml\n"
t("* corresponde a tudo", owner("* @a", F, "src/api/v2/users.py") == ["@a"])
t("a última linha correspondente vence", owner("* @a\n/src/ @b", F, "src/app.py") == ["@b"] and owner("* @a\n/src/ @b", F, "README.md") == ["@a"])
t("*.js corresponde em qualquer profundidade", owner("*.js @js", F, "web/lib/util.js") == ["@js"])
t("/docs/ é dono de arquivos aninhados", owner("/docs/ @d", F, "docs/guide/setup.md") == ["@d"])
t("docs/* não é dono de arquivos aninhados", owner("docs/* @d", F, "docs/guide/setup.md") is None and owner("docs/* @d", F, "docs/index.md") == ["@d"])
t("nome de diretório sem âncora 'api/' corresponde em qualquer lugar", owner("api/ @x", F, "src/api/v2/users.py") == ["@x"])
t("** no meio", owner("/src/**/users.py @u", F, "src/api/v2/users.py") == ["@u"])
t("/README.md ancorado apenas na raiz", owner("/README.md @r", "README.md\nsub/README.md\n", "sub/README.md") is None)
t("? corresponde a um caractere", owner("src/ap?.py @q", F, "src/app.py") == ["@q"])
t("sem owners = explicitamente sem dono", owner("* @a\n/web/", F, "web/app.js") == [] and ("info", "CO10") in codes("* @a\n/web/", F))
t("owner inválido -> CO01 alta, linha pulada", ("high", "CO01") in codes("* @a\n/src/ alice", F) and owner("* @a\n/src/ alice", F, "src/app.py") == ["@a"])
t("negação -> CO06 média", ("medium", "CO06") in codes("* @a\n!/src/", F))
t("intervalo [ ] -> CO06 média", ("medium", "CO06") in codes("* @a\n/src/[ab]* @b", F))
t("seção GitLab -> CO06 baixa", ("low", "CO06") in codes("[Docs]\n* @a", F))
cfg = {"org": "acme", "users": ["a", "b"], "teams": {"acme/web": ["a"], "acme/empty": []}}
t("equipe desconhecida -> CO02 alta, arquivos ficam sem dono (sem fallback)", ("high", "CO02") in codes("* @a\n/web/ @acme/webb", F, cfg) and owner("* @a\n/web/ @acme/webb", F, "web/app.js", cfg) == [] and ("medium", "CO03") in codes("* @a\n/web/ @acme/webb", F, cfg))
t("owner desconhecido removido, co-owner válido mantido", owner("* @a\n/web/ @acme/webb @b", F, "web/app.js", cfg) == ["@b"])
t("equipe de outra org -> CO02", ("high", "CO02") in codes("* @other/web", F, cfg))
t("usuário desconhecido -> CO02", ("high", "CO02") in codes("* @zed", F, cfg))
t("equipe vazia -> CO09", ("medium", "CO09") in codes("* @a\n/web/ @acme/empty", F, cfg))
t("owner por email aceito", ("high", "CO01") not in codes("* dev@acme.io", F))
t("regra morta -> CO04", ("medium", "CO04") in codes("* @a\n/legacy/ @b", F))
t("regra sobrescrita -> CO05", ("medium", "CO05") in codes("/src/api/ @b\n/src/ @a", F))
t("parcialmente sobrescrita não é CO05", ("medium", "CO05") not in codes("/src/ @a\n/src/api/ @b", F))
t("padrão duplicado -> CO08", ("low", "CO08") in codes("* @a\n/src/ @b\n/src/ @c", F))
t("arquivo sem dono -> CO03 média", ("medium", "CO03") in codes("/src/ @a", F))
t("caminho require_owner sem dono -> CO03 alta", ("high", "CO03") in codes("/src/ @acme/web", F, dict(cfg, require_owner=[".github/"])))
t("caminho obrigatório apenas com '*' -> CO11", ("medium", "CO11") in codes("* @a\n/src/ @b", F, {"require_owner": [".github/"]}))
t("regra morta com owner desconhecido ainda -> CO04", ("medium", "CO04") in codes("* @a\n/legacy/ @acme/zz", F, cfg))
t("concentração de owner individual -> CO07", ("medium", "CO07") in codes("* @a", F, {"max_individual_share": 0.5}))
t("lista de ignore remove arquivos", len(analyse("* @a", F, {"ignore": ["docs/"]})["files"]) == 7)
r = analyse("* @a\n/src/ @b", F)
t("--who reporta linha", who(r, "src/api/routes.py") == "src/api/routes.py: @b (linha 2: /src/)")
t("prefixo ./ removido, .github mantido", analyse("* @a", "./.github/x.yml\n")["files"] == [".github/x.yml"])
t("relatório tem veredito e tabela de cobertura", "**Veredito:" in report(r, "t") and "## Cobertura por diretório de nível superior" in report(r, "t"))
try:
analyse("* @a", F, {"orgs": "x"}); ok = False
except InputError:
ok = True
t("chave desconhecida no arquivo de owners rejeitada", ok)
for name, ok in tests:
print(("PASS " if ok else "FAIL ") + name)
passed = sum(ok for _, ok in tests)
print("%d/%d passaram" % (passed, len(tests)))
return 0 if passed == len(tests) else 1
def main(argv=None):
ap = argparse.ArgumentParser(description="Verifica um arquivo CODEOWNERS do GitHub contra a lista de arquivos do repositório.")
ap.add_argument("codeowners", nargs="?")
ap.add_argument("files", nargs="?", help="lista de arquivos, ex.: git ls-files > files.txt")
ap.add_argument("--owners", help="JSON com org, users, teams, require_owner, max_individual_share, ignore")
ap.add_argument("--who", action="append", default=[], help="mostra o owner efetivo de um caminho (repetível)")
ap.add_argument("--json", action="store_true")
ap.add_argument("--self-test", action="store_true")
a = ap.parse_args(argv)
if a.self_test:
return self_test()
if not a.codeowners or not a.files:
ap.print_usage(sys.stderr)
return 2
try:
co = open(a.codeowners, encoding="utf-8").read()
fl = open(a.files, encoding="utf-8").read()
cfg = None
if a.owners:
try:
cfg = json.load(open(a.owners, encoding="utf-8"))
except json.JSONDecodeError as e:
raise InputError("arquivo de owners não é JSON válido: %s" % e)
res = analyse(co, fl, cfg)
except (OSError, InputError) as e:
print("erro: %s" % e, file=sys.stderr)
return 2
if a.json:
print(json.dumps(to_json(res), indent=2))
else:
sys.stdout.write(report(res, a.codeowners, a.who))
return 1 if any(x[0] == "high" for x in res["findings"]) else 0
if __name__ == "__main__":
sys.exit(main())
Entre para ver o prompt completo
Continuar com:
Ao entrar, você concorda com nossos Termos de uso e Política de privacidade
Uso
Este prompt foi projetado para uso com coding. Copie o conteúdo acima e cole na sua ferramenta de IA preferida.
Para melhores resultados, personalize os marcadores (indicados por colchetes ou maiúsculas) com seus requisitos específicos.
Discussão
0 comentários