Verificador de Cobertura e Propriedade do CODEOWNERS

De Wikiprompt, a enciclopédia livre de prompts

Fatih Kadir Akın
Contribuído porFatih Kadir AkınFonte

4 de nov. de 2026

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.

Referências

Categorias:coding| prompts.chat| codeowners| github

Discussão

0 comentários