Discusión

Verificador de Cobertura y Propiedad de CODEOWNERS

De Wikiprompt, la enciclopedia libre de prompts

Fatih Kadir Akın

4 nov 2026

Verificador de Cobertura y Propiedad de CODEOWNERS Una habilidad completa que audita archivos CODEOWNERS de GitHub para verificar cobertura, propietarios inválidos y errores de orden, con un verificador de Python funcional y plantillas. Ideal para equipos de plataforma de ingeniería.

Contenido del PromptGuardar

🌐
--- name: codeowners-coverage-and-ownership-checker description: Comprueba un archivo CODEOWNERS de GitHub contra la lista real de archivos del repositorio - calcula quién es dueño de cada archivo (la última línea que coincide gana), luego informa propietarios inválidos o desconocidos, archivos sin propietario y rutas críticas dejadas a la regla general, reglas que no coinciden con nada o que están completamente anuladas por líneas posteriores, sintaxis no soportada ! y [ ], duplicados, equipos vacíos y cuellos de botella de una sola persona. Incluye un verificador Python offline de la biblioteca estándar. --- # Verificador de Cobertura y Propiedad de CODEOWNERS Ayudas a los equipos de ingeniería a confiar en su archivo CODEOWNERS. Las revisiones requeridas y los revisores autoasignados solo funcionan cuando cada línea es válida, cada propietario existe y tiene acceso, y las reglas están en el orden correcto. En la práctica, los archivos pierden silenciosamente a su propietario: un equipo se renombra y sus archivos silenciosamente no reciben revisor, una regla específica está por encima de una general y nunca se aplica, `docs/*` no cubre `docs/guide/`, una negación `!` copiada de .gitignore no hace nada, o el único propietario del código de facturación es una persona de vacaciones. Calculas el propietario efectivo de cada archivo y muestras exactamente qué está mal y cómo solucionarlo. Compruebas el archivo contra el comportamiento documentado de CODEOWNERS de GitHub y la lista de propietarios que el usuario te da. No puedes ver los permisos del repositorio; dilo y pide al usuario que confirme el acceso de escritura en la configuración del repositorio o con `gh api`. ## Archivos en esta habilidad - `scripts/codeowners_check.py` - analiza CODEOWNERS (comentarios, comentarios en línea, espacios escapados), coincide cada ruta de `git ls-files` con las reglas de estilo gitignore de GitHub (patrones anclados y flotantes, `*`, `**`, `?`, patrones de directorio, `dir/*` = solo hijos directos, la última línea que coincide gana), omite líneas con sintaxis inválida y descarta propietarios desconocidos como hace GitHub e informa hallazgos CO01-CO11 con cobertura por directorio, una tabla de reglas, carga de propietarios y una búsqueda `--who RUTA` (solo biblioteca estándar de Python 3.8+) - `references/codeowners-syntax-and-matching.md` - semántica de patrones con ejemplos, reglas de orden, formatos de propietario, ubicación del archivo y límites de GitHub - `references/checks-and-codes.md` - cada código de hallazgo, su severidad y la solución habitual - `templates/ownership-review.md` - la descripción de la solicitud de extracción / nota de revisión para una corrección de CODEOWNERS - `examples/CODEOWNERS` - un archivo realista para un monorepo de aplicación web con errores típicos (equipo renombrado, regla específica por encima de una general, `docs/*`, negación, rango de caracteres, sección de GitLab, propietario sin @, directorio eliminado, línea duplicada, código generado explícitamente sin propietario) - `examples/files.txt` - la lista de archivos del repositorio (salida de `git ls-files`, 43 archivos) - `examples/owners.json` - org, usuarios con acceso, equipos con miembros, rutas que deben tener un propietario específico y el límite de participación individual - `examples/ownership-report.md` - la salida real del verificador en el ejemplo ## Flujo de trabajo ### 1. Recopilar las entradas Pide el archivo CODEOWNERS (de `.github/`, la raíz del repositorio o `docs/`; GitHub usa el primero que encuentra en ese orden) y la lista de archivos de la rama predeterminada: `git ls-files > files.txt`. Luego pide la lista de propietarios: el nombre de la org, usuarios con acceso de escritura, equipos con sus miembros (`gh api orgs/<org>/teams`, `gh api orgs/<org>/teams/<team>/members`) y las rutas que deben tener un propietario específico (flujos de CI, infraestructura, pagos, migraciones, el propio CODEOWNERS). Escríbelos en un JSON como `examples/owners.json`. Sin una lista de propietarios, el verificador aún comprueba sintaxis, orden y cobertura. ### 2. Ejecutar el 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 ``` Salida 0 = sin hallazgos altos, 1 = hallazgos altos, 2 = entrada incorrecta. Sin un shell, recorre las reglas de abajo hacia arriba para cada ruta importante con `references/codeowners-syntax-and-matching.md`: la primera línea desde abajo que coincide es el propietario. ### 3. Leer el resultado - Altos primero: CO01 (formato de propietario inválido como un nombre sin @): GitHub omite toda la línea como sintaxis inválida, por lo que esos archivos caen a una regla anterior. CO02 (usuario o equipo desconocido, sin acceso): ese propietario no se solicita; si no queda ningún propietario válido en la línea, la línea aún coincide y sus archivos no obtienen ningún propietario de código. CO03 alto: archivos bajo una ruta de propiedad obligatoria no tienen propietario. - Medios: CO05 (regla completamente anulada - usualmente una ruta específica por encima de una general), CO04 (regla que no coincide con nada - ruta renombrada o eliminada), CO06 (`!` o `[ ]` - ignorado), CO11 (ruta crítica solo cubierta por `*`), CO09 (equipo vacío), CO07 (una persona posee demasiado), CO03 medio (archivos sin regla). - Bajos/info: encabezados de sección de GitLab, propiedad de una sola persona, patrones duplicados, rutas explícitamente sin propietario (bien si es intencional). - Usa `--who` en las rutas que le importan a la gente para confirmar la corrección antes y después. ### 4. Proponer la corrección Escribe el CODEOWNERS corregido: reglas generales primero, específicas después; reemplaza exclusiones `!` con una regla posterior más específica (una lista de propietarios vacía hace que una ruta no tenga propietario a propósito); usa `/dir/` en lugar de `dir/*` cuando los archivos anidados deben estar cubiertos; corrige los slugs de equipos; agrega un segundo propietario o un equipo para rutas de una sola persona; agrega una línea para el propio CODEOWNERS. Vuelve a ejecutar hasta que no haya hallazgos altos, luego completa `templates/ownership-review.md`. ### 5. Entregar Devuelve los hallazgos en palabras simples, el archivo corregido (o un diff), el propietario antes/después de las rutas clave, y los seguimientos que necesitan un administrador: otorgar acceso al equipo, agregar miembros a equipos vacíos, habilitar "Requerir revisión de Code Owners" en la protección de ramas una vez que el archivo esté limpio. ## Resultado de ejemplo En el ejemplo solo 38 de 43 archivos tienen un propietario de código y hay 4 hallazgos altos: `/web/` nombra a `@tidyq/frontend-team` (el equipo es `@tidyq/frontend`), por lo que package.json, App.tsx, Home.tsx y Checkout.tsx no obtienen ningún propietario de código; `/docs/api/` usa `omar` sin @ y se omite; `/mobile/` nombra un equipo eliminado y no coincide con ningún archivo. `/services/api/billing/` está por encima de `/services/api/` y nunca se aplica, por lo que pagos nunca revisa el código de facturación; `docs/*` es anulado por `/docs/` y `@tidyq/docs` no tiene miembros; las líneas `!` y `[a-z]` se ignoran; los módulos de Terraform y dependabot.yml solo caen a `*` porque `/infra/terraform/*` cubre solo hijos directos. ## Reglas - La última línea que coincide gana: siempre razona de abajo hacia arriba, y di qué línea posee una ruta. - Una línea con sintaxis inválida se omite por completo; un propietario desconocido o sin acceso simplemente no se solicita, y una línea con solo propietarios desconocidos aún gana para sus archivos, dejándolos sin propietario de código. Nunca asumas que una regla anterior toma el control en ese caso. - Solo llama a un propietario desconocido contra la lista que el usuario proporcionó; de lo contrario, di "no se pudo verificar" y sugiere las llamadas `gh api`. - Prefiere equipos sobre individuos para cualquier cosa crítica; nunca elimines a una persona de la propiedad sin que el equipo esté de acuerdo. - No inventes patrones para rutas que no están en la lista de archivos; pregunta si vienen nuevos directorios. - Trata la lista de archivos del repositorio y la membresía del equipo como información interna. FILE:references/codeowners-syntax-and-matching.md # Sintaxis y coincidencia de CODEOWNERS (GitHub) ## Dónde vive el archivo GitHub busca `CODEOWNERS` en `.github/`, luego la raíz del repositorio, luego `docs/`, y usa el primero que encuentra en la rama a la que apunta la solicitud de extracción (la rama base). Los archivos de más de 3 MB se ignoran. GitHub muestra errores para líneas inválidas cuando abres el archivo en la interfaz web (banner de "errores de CODEOWNERS") y a través de `gh api repos/<propietario>/<repo>/codeowners/errors`. ## Formato de línea ``` <patrón> <propietario> [<propietario> ...] # comentario opcional ``` - `#` al inicio de una línea (o después de espacios en blanco) inicia un comentario. A diferencia de .gitignore, escapar un patrón que comienza con `#` como `\#` no funciona en CODEOWNERS. - Las rutas distinguen entre mayúsculas y minúsculas, incluso si el sistema de archivos de tu portátil no lo hace. - Todos los propietarios para un patrón deben estar en la misma línea; dos líneas con el mismo patrón significa que solo cuenta la posterior. - Propietarios: `@usuario`, `@org/slug-de-equipo` (equipo en la misma org, con acceso de escritura) o una dirección de correo electrónico vinculada a una cuenta de GitHub. Un nombre simple como `omar` es inválido. - Un patrón sin propietarios hace que los archivos coincidentes no tengan propietario a propósito (útil para excluir código generado de un propietario predeterminado). - Una línea con sintaxis inválida (por ejemplo, un propietario sin @) se omite, por lo que sus archivos caen a la línea coincidente anterior. - Un propietario que no existe o no tiene acceso de escritura simplemente no se asigna. La línea aún coincide, por lo que si ninguno de sus propietarios es válido, sus archivos terminan sin propietario de código (NO caen a una línea anterior). ## Orden: la última línea que coincide gana GitHub lee el archivo de arriba a abajo y la última línea que coincide con una ruta decide sus propietarios. Pon el comodín `*` primero, los directorios amplios después y las rutas más específicas al final. ``` * @tidyq/platform # 1: predeterminado /services/api/ @tidyq/backend # 2: toda la API /services/api/billing/ @tidyq/payments # 3: más específico, debe venir DESPUÉS de 2 ``` Con las líneas 2 y 3 intercambiadas, los archivos de facturación son propiedad de `@tidyq/backend` solamente (CO05). ## Semántica de patrones (estilo gitignore, con excepciones) | Patrón | Coincide | No coincide | |---|---|---| | `*` | cada archivo | - | | `*.js` | `app.js`, `web/lib/util.js` (cualquier profundidad) | `app.jsx` | | `/build/logs/` | todo bajo `build/logs/` en la raíz, a cualquier profundidad | `src/build/logs/x` | | `docs/` o `apps/` (sin barra inicial) | un directorio `docs`/`apps` en cualquier lugar y todo lo que contiene | - | | `docs/*` | archivos directamente en `docs/` | `docs/guide/setup.md` (anidado) | | `/docs/` o `/docs/**` | todo bajo la `docs/` raíz | - | | `**/logs` | directorio o archivo `logs` a cualquier profundidad | - | | `/src/**/users.py` | `src/users.py`, `src/api/v2/users.py` | - | | `README.md` | `README.md` a cualquier profundidad | - | | `/README.md` | solo el `README.md` raíz | `sub/README.md` | | `src/ap?.py` | `src/app.py` | `src/apps.py` | Reglas prácticas: - Un patrón con una barra al inicio o en el medio está anclado a la raíz del repositorio; uno sin (aparte de una barra final) flota y coincide a cualquier profundidad. - Una barra final significa "este directorio y todo lo que contiene". - Un directorio coincidente posee todos los archivos bajo él, excepto `dir/*`, que cubre solo hijos directos. ## No soportado (la línea se ignora o se marca) - Negación `!patrón` - usa una regla posterior más específica en su lugar (sin propietarios para que no tenga propietario). - Rangos de caracteres `[a-z]` - enumera las rutas o usa `*`. - Secciones de GitLab (`[Docs]`, `^[Opcional]`, `[Docs][2]`) y propietarios predeterminados en encabezados de sección - solo GitLab. ## Equipos y acceso - El equipo debe ser visible y tener acceso de escritura al repositorio; el usuario debe tener acceso de escritura. - Un equipo vacío recibe la solicitud de revisión pero nadie es notificado. - La revisión requerida de propietarios de código (protección de ramas o reglas) bloquea las fusiones hasta que uno de los propietarios apruebe (cualquier propietario es suficiente). Si un archivo termina sin propietario válido, no se solicita aprobación de propietario de código para él, por lo que la protección silenciosamente no hace nada para ese archivo. ## Obtener las 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/<propietario>/<repo>/collaborators --paginate -q '.[] | select(.permissions.push) | .login' gh api repos/<propietario>/<repo>/codeowners/errors ``` FILE:references/checks-and-codes.md # Comprobaciones y códigos de hallazgo | Código | Severidad | Comprobación | Corrección típica | |---|---|---|---| | CO01 | alto | el propietario no es `@usuario`, `@org/equipo` o un correo electrónico (ej. `omar`); el patrón no se puede analizar | agrega el @ / corrige el slug; toda la línea se omite hasta entonces | | CO02 | alto | usuario no en tu lista de usuarios, equipo no en tu lista de equipos, o equipo de otra org (solo con `--owners`); una segunda línea CO02 dice cuando no queda ningún propietario válido en una línea, por lo que sus archivos no obtienen propietario de código | corrige el slug (el verificador sugiere un equipo cercano), da acceso de escritura al equipo, o elimina el propietario | | CO03 | alto: archivo sin propietario bajo una ruta `require_owner`; medio: otros archivos sin regla o solo propietarios desconocidos (archivos explícitamente sin propietario son CO10) | cobertura | agrega una regla, o un predeterminado `*` | | CO04 | medio | el patrón no coincide con ningún archivo en la lista | ruta renombrada o eliminada: actualiza o elimina la línea | | CO05 | medio | cada archivo que la regla coincide es propiedad de líneas posteriores (la última coincidencia gana) | mueve la regla específica debajo de la general | | CO06 | medio: negación `!`, rangos `[ ]` (ignorados por GitHub); bajo: encabezado de sección de GitLab | sintaxis no soportada | usa una regla posterior más específica; lista de propietarios vacía = sin propietario | | CO07 | medio: un individuo (no un equipo) es el propietario efectivo de más de `max_individual_share` de archivos; bajo: archivos propiedad de un solo individuo y ningún equipo | factor de bus | agrega un equipo o un segundo propietario | | CO08 | bajo | el mismo patrón aparece dos veces; solo cuenta la línea posterior | fusiona los propietarios en una línea | | CO09 | medio | el equipo no tiene miembros en tu lista de equipos | agrega miembros o usa otro equipo | | CO10 | info | patrón sin propietarios: archivos intencionalmente sin propietario | bien si es intencional (código generado, archivos vendidos) | | CO11 | medio | archivos bajo una ruta `require_owner` solo son propiedad de la regla comodín `*` | agrega una regla específica para esa ruta | ## Veredicto - **Hallazgos altos**: corrige antes de confiar en "Requerir revisión de Code Owners". - **Solo medios**: el archivo funciona pero algunos archivos van a las personas equivocadas. - **Saludable**: solo bajos/info. ## Archivo de propietarios (`--owners`, JSON) | Clave | Significado | |---|---| | `org` | nombre de la org; los equipos de otra org se marcan | | `users` | inicios de sesión con acceso de escritura (sin @) | | `teams` | `{"org/slug": [miembros]}` | | `require_owner` | patrones (misma sintaxis) para rutas que deben tener un propietario específico | | `max_individual_share` | proporción de archivos que una persona puede poseer sola (predeterminado 0.4) | | `ignore` | patrones para archivos a excluir (código vendido, archivos de bloqueo) | ## Límites de la comprobación - Los permisos del repositorio y la visibilidad del equipo no se comprueban - confirma con las llamadas `gh api` en la referencia de sintaxis o el banner de "errores de CODEOWNERS". - La lista de archivos es una instantánea de una rama; ejecútala contra la rama base de tus solicitudes de extracción. FILE:templates/ownership-review.md # Corrección de CODEOWNERS: <repositorio> **Rama:** <rama base> **Comprobado:** <AAAA-MM-DD> **Archivos:** <n> **Propiedad antes / después:** <x% / y%> ## Qué estaba mal | Línea | Regla | Problema | Efecto | |---|---|---|---| | <11> | `<patrón> <propietarios>` | <equipo desconocido @org/frontend-team> | <los archivos web caen a @org/platform> | | <6> | `<patrón>` | <anulada por la línea 7> | <pagos nunca revisa facturación> | ## Cambios ```diff <- /services/api/billing/ @org/payments < /services/api/ @org/backend <+ /services/api/billing/ @org/payments> ``` ## Quién posee las rutas clave ahora | Ruta | Antes | Después | |---|---|---| | <services/api/billing/invoices.py> | <@org/backend (línea 7)> | <@org/payments (línea 8)> | | <infra/terraform/modules/vpc/main.tf> | <@org/platform (línea 3, comodín)> | <@org/sre> | ## Necesita un administrador - [ ] <dar a @org/frontend acceso de escritura / agregar miembros a @org/docs> - [ ] <habilitar "Requerir revisión de Code Owners" en <rama> una vez que esto se fusione> ## Comprobación `python3 scripts/codeowners_check.py .github/CODEOWNERS files.txt --owners owners.json` -> <0 altos, n medios (explica el resto)> *La coincidencia sigue las reglas de CODEOWNERS de GitHub; los permisos del repositorio se comprobaron con <gh api / página de configuración | no comprobados>.* FILE:examples/CODEOWNERS # CODEOWNERS para tidyq/tidyq-app (copiado en .github/CODEOWNERS) # Propietarios predeterminados para todo lo no coincidente abajo * @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 # Infraestructura /infra/terraform/* @tidyq/sre /infra/k8s/ @tidyq/sre !/infra/k8s/sandbox/ # CI y configuración del repositorio /.github/workflows/ @tidyq/sre /.github/CODEOWNERS @tidyq/platform-leads # Docs [Documentation] docs/* @tidyq/docs /docs/ @tidyq/docs /docs/api/ omar # Aplicación móvil antigua (movida a tidyq/tidyq-mobile en 2026) /mobile/ @tidyq/mobile # Datos /data/migrations/ @jonas-b /data/migrations/ @jonas-b @tidyq/backend /data/fixtures/[a-z]* @tidyq/qa # Cliente generado: sin propietario a 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 # Comprobación de CODEOWNERS: examples/CODEOWNERS 20 línea(s) de regla, 43 archivo(s), 38 con propietario (88.4%), 5 sin propietario. **Veredicto: 4 hallazgo(s) alto(s) - corrige CODEOWNERS antes de confiar en revisiones requeridas** Hallazgos: 4 altos, 9 medios, 3 bajos, 1 info. Líneas cuyos propietarios son todos desconocidos (aún coinciden, por lo que sus archivos no obtienen propietario de código): 11, 31. Los propietarios desconocidos se excluyen de las columnas de propietarios a continuación. ## Hallazgos - **ALTO CO01** línea 28: propietario inválido 'omar' (usa @usuario, @org/equipo o un correo electrónico) - GitHub marca toda la línea como un error y la omite - **ALTO CO02** línea 11: equipo desconocido @tidyq/frontend-team (¿quisiste decir @tidyq/frontend?) - GitHub no solicitará revisiones de él - **ALTO CO02** línea 11: no queda ningún propietario válido - sus 4 archivo(s) no obtienen propietario de código (no caen a líneas anteriores) - **ALTO CO02** línea 31: equipo desconocido @tidyq/mobile - GitHub no solicitará revisiones de él - **MEDIO CO03** 4 archivo(s) no tienen propietario de código (sin regla coincidente, o solo propietarios desconocidos): web/package.json, web/src/App.tsx, web/src/pages/Home.tsx, web/src/pages/Checkout.tsx - **MEDIO CO04** línea 31: '/mobile/' no coincide con ningún archivo - ¿ruta renombrada o eliminada? - **MEDIO CO05** línea 6: '/services/api/billing/' (2 archivo(s)) está completamente anulada por línea(s) posterior(es) 7 - la última coincidencia gana, pon reglas específicas después de las generales - **MEDIO CO05** línea 26: 'docs/*' (2 archivo(s)) está completamente anulada por línea(s) posterior(es) 27 - la última coincidencia gana, pon reglas específicas después de las generales - **MEDIO CO05** línea 34: '/data/migrations/' (2 archivo(s)) está completamente anulada por línea(s) posterior(es) 35 - la última coincidencia gana, pon reglas específicas después de las generales - **MEDIO CO06** línea 18: '!/infra/k8s/sandbox/' usa negación !, que CODEOWNERS no soporta - GitHub ignora la línea - **MEDIO CO06** línea 36: '/data/fixtures/[a-z]*' usa rangos de caracteres [ ], que CODEOWNERS no soporta - GitHub ignora la línea - **MEDIO CO09** línea 26: el equipo @tidyq/docs no tiene miembros - las solicitudes de revisión no van a ninguna parte - **MEDIO CO11** 3 archivo(s) bajo rutas que deben tener un propietario solo caen a la regla comodín '*' (sin propietario específico): .github/dependabot.yml, infra/terraform/modules/vpc/main.tf, infra/terraform/modules/rds/main.tf - **BAJO CO06** línea 25: '[Documentation]' es un encabezado de sección de GitLab; GitHub lo trata como una línea inválida - **BAJO CO07** 2 archivo(s) tienen un solo propietario individual y ningún equipo (ej. web/src/design-system/Button.tsx, web/src/design-system/Modal.tsx) - **BAJO CO08** línea 35 repite el patrón '/data/migrations/' de la línea 34 - solo cuenta la línea posterior - **INFO CO10** línea 39: '/web/src/generated/' no tiene propietarios - sus 1 archivo(s) están intencionalmente sin propietario ## Cobertura por directorio de nivel superior | Directorio | Archivos | Con propietario | Sin propietario | Propietarios efectivos | |---|---|---|---|---| | (raíz) | 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) | ## Reglas | Línea | Patrón | Propietarios | Archivos coincidentes | Efectivo 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/` | (ninguno) | - | - | 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/` | (ninguno) | 1 | 1 | CO10 | ## Carga de propietarios (efectiva) | Propietario | Archivos | Proporción | |---|---|---| | @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% | ## Archivos sin propietario - `web/package.json` (línea 11: solo propietarios desconocidos) - `web/src/App.tsx` (línea 11: solo propietarios desconocidos) - `web/src/pages/Home.tsx` (línea 11: solo propietarios desconocidos) - `web/src/pages/Checkout.tsx` (línea 11: solo propietarios desconocidos) - `web/src/generated/api-client.ts` (explícitamente sin propietario, línea 39) ## Quién posee - services/api/billing/invoices.py: @tidyq/backend (línea 7: /services/api/) - infra/terraform/modules/vpc/main.tf: @tidyq/platform (línea 3: *) - web/src/pages/Checkout.tsx: sin propietario válido (línea 11: /web/ @tidyq/frontend-team - desconocido) La coincidencia sigue las reglas de CODEOWNERS de GitHub (patrones de estilo gitignore, la última línea que coincide gana, `!` y `[ ]` no soportados). Los propietarios se comprueban solo contra tu archivo de propietarios; confirma el acceso de escritura en la configuración del repositorio. FILE:scripts/codeowners_check.py #!/usr/bin/env python3 """Verificador de cobertura y propiedad de CODEOWNERS (solo biblioteca estándar, Python 3.8+). Lee un archivo CODEOWNERS de GitHub y la lista de archivos del repositorio (salida de `git ls-files`), calcula el propietario efectivo de cada archivo con las reglas de GitHub (patrones de estilo gitignore, la ÚLTIMA línea que coincide gana) e informa errores de sintaxis, propietarios desconocidos, archivos sin propietario (también bajo rutas que deben tener un propietario), reglas que no coinciden con nada, reglas que están completamente anuladas por líneas posteriores, sintaxis no soportada, duplicados, equipos vacíos y concentración de propietarios. Uso: codeowners_check.py CODEOWNERS FILES.txt [--owners OWNERS.json] [--who RUTA]... [--json] codeowners_check.py --self-test Salida: 0 = sin hallazgos altos, 1 = hallazgos altos, 2 = entrada incorrecta. """ 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 # ---------------------------------------------------------------- patrones def pattern_regex(pat): """Devuelve (regex compilada, solo_directorio, solo_hijos_directos) para un patrón de 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)): # un directorio coincidente posee todo lo que contiene if rx.match("/".join(parts[:k])): return True return False # ---------------------------------------------------------------- análisis 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", "línea %d: '%s' es un encabezado de sección de GitLab; GitHub lo trata como una línea 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", "línea %d: '%s' usa negación !, que CODEOWNERS no soporta - GitHub ignora la línea" % (n, pat), n)) rule["valid"] = False elif "[" in pat or "]" in pat: findings.append(("medium", "CO06", "línea %d: '%s' usa rangos de caracteres [ ], que CODEOWNERS no soporta - GitHub ignora la línea" % (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", "línea %d: propietario inválido %s (usa @usuario, @org/equipo o un correo electrónico) - GitHub marca toda la línea como un error y la omite" % (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", "línea %d: el patrón '%s' no se puede analizar (%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("la lista de archivos está vacía (usa: git ls-files > files.txt)") return files # ---------------------------------------------------------------- análisis 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("clave(s) desconocida(s) en el archivo de propietarios: %s" % ", ".join(sorted(unknown))) rules, findings = parse_codeowners(co_text) if not rules: raise InputError("CODEOWNERS no tiene reglas") 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) # propietarios conocidos 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 = "el equipo %s no está en la 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 = "equipo desconocido %s%s" % (o, (" (¿quisiste decir @%s?)" % close[0]) if close else "") elif not teams[name] and key not in reported: findings.append(("medium", "CO09", "línea %d: el equipo %s no tiene miembros - las solicitudes de revisión no van a ninguna parte" % (r["line"], o), r["line"])); reported.add(key) elif USER_RE.match(o) and have_list and key.lstrip("@") not in users: problem = "usuario desconocido %s (no en la lista de propietarios / sin acceso de escritura)" % o if problem: unknown_owners.add(key) if key not in reported: findings.append(("high", "CO02", "línea %d: %s - GitHub no solicitará revisiones de él" % (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", "línea %d: no queda ningún propietario válido - sus %d archivo(s) no obtienen propietario de código (no caen a líneas anteriores)" % (ln, effective[ln]), ln)) # reglas muertas, anuladas, duplicadas seen = {} for r in valid: if r["pattern"] in seen: findings.append(("low", "CO08", "línea %d repite el patrón '%s' de la línea %d - solo cuenta la línea posterior" % (r["line"], r["pattern"], seen[r["pattern"]]), r["line"])) seen[r["pattern"]] = r["line"] if not matched_by[r["line"]]: findings.append(("medium", "CO04", "línea %d: '%s' no coincide con ningún archivo - ¿ruta renombrada o eliminada?" % (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", "línea %d: '%s' (%d archivo(s)) está completamente anulada por línea(s) posterior(es) %s - la última coincidencia gana, pon reglas específicas después de las generales" % (r["line"], r["pattern"], len(matched_by[r["line"]]), ", ".join(map(str, later))), r["line"])) if not r["owners"]: findings.append(("info", "CO10", "línea %d: '%s' no tiene propietarios - sus %d archivo(s) están intencionalmente sin propietario" % (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 archivo(s) bajo rutas que deben tener un propietario no tienen propietario: %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 archivo(s) bajo rutas que deben tener un propietario solo caen a la regla comodín '%s' (sin propietario 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 archivo(s) no tienen propietario de código (sin regla coincidente, o solo propietarios desconocidos): %s" % (len(rest), ", ".join(rest[:6]) + (" ..." if len(rest) > 6 else "")), 0)) # carga de propietarios 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 es el propietario de %d de %d archivos (%.0f%%) como individuo - factor de bus; agrega un equipo o un segundo propietario" % (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 archivo(s) tienen un solo propietario individual y ningún equipo (ej. %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: sin propietario (sin regla coincidente)" % path if r["owners"] and not r["ok"]: return "%s: sin propietario válido (línea %d: %s %s - desconocido)" % (path, r["line"], r["pattern"], " ".join(r["owners"])) return "%s: %s (línea %d: %s)" % (path, " ".join(r["ok"]) or "explícitamente sin propietario", r["line"], r["pattern"]) # ---------------------------------------------------------------- informe 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 = ["# Comprobación de CODEOWNERS: %s" % title, ""] L.append("%d línea(s) de regla, %d archivo(s), %d con propietario (%.1f%%), %d sin propietario." % (len(res["rules"]), len(files), owned, 100.0 * owned / len(files), len(res["unowned"]))) L.append("") L.append("**Veredicto: %s**" % ("%d hallazgo(s) alto(s) - corrige CODEOWNERS antes de confiar en revisiones requeridas" % cnt["high"] if cnt["high"] else ("revisa los hallazgos medios" if cnt["medium"] else "CODEOWNERS se ve saludable"))) L.append("") L.append("Hallazgos: %d altos, %d medios, %d bajos, %d info." % (cnt["high"], cnt["medium"], cnt["low"], cnt["info"])) L.append("") if res["no_valid_owner_lines"]: L.append("Líneas cuyos propietarios son todos desconocidos (aún coinciden, por lo que sus archivos no obtienen propietario de código): %s. Los propietarios desconocidos se excluyen de las columnas de propietarios a continuación." % ", ".join(map(str, res["no_valid_owner_lines"]))) L.append("") L += ["## Hallazgos", ""] for s, c, msg, _ in f: L.append("- **%s %s** %s" % (s.upper(), c, msg)) L += ["", "## Cobertura por directorio de nivel superior", "", "| Directorio | Archivos | Con propietario | Sin propietario | Propietarios efectivos |", "|---|---|---|---|---|"] dirs = OrderedDict() for p in files: d = p.split("/")[0] + "/" if "/" in p else "(raíz)" 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 += ["", "## Reglas", "", "| Línea | Patrón | Propietarios | Archivos coincidentes | Efectivo 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 "(ninguno)", m, e, ", ".join(sorted(set(notes.get(r["line"], [])))) or "")) L += ["", "## Carga de propietarios (efectiva)", "", "| Propietario | Archivos | Proporción |", "|---|---|---|"] for o, n in res["load"].most_common(): L.append("| %s | %d | %.0f%% |" % (o, n, 100.0 * n / len(files))) if res["unowned"]: L += ["", "## Archivos sin propietario", ""] for p in res["unowned"]: e = res["eff"][p] L.append("- `%s`%s" % (p, "" if not e else (" (explícitamente sin propietario, línea %d)" % e["line"] if not e["owners"] else " (línea %d: solo propietarios desconocidos)" % e["line"]))) if whos: L += ["", "## Quién posee", ""] for p in whos: L.append("- " + who(res, p)) L += ["", "La coincidencia sigue las reglas de CODEOWNERS de GitHub (patrones de estilo gitignore, la última línea que coincide gana, `!` y `[ ]` no soportados). Los propietarios se comprueban solo contra tu archivo de propietarios; confirma el acceso de escritura en la configuración del repositorio."] 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"]]} # ---------------------------------------------------------------- auto-prueba 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("* coincide con todo", owner("* @a", F, "src/api/v2/users.py") == ["@a"]) t("la última línea que coincide gana", owner("* @a\n/src/ @b", F, "src/app.py") == ["@b"] and owner("* @a\n/src/ @b", F, "README.md") == ["@a"]) t("*.js coincide a cualquier profundidad", owner("*.js @js", F, "web/lib/util.js") == ["@js"]) t("/docs/ posee archivos anidados", owner("/docs/ @d", F, "docs/guide/setup.md") == ["@d"]) t("docs/* no posee archivos anidados", owner("docs/* @d", F, "docs/guide/setup.md") is None and owner("docs/* @d", F, "docs/index.md") == ["@d"]) t("nombre de directorio no anclado 'api/' coincide en cualquier lugar", owner("api/ @x", F, "src/api/v2/users.py") == ["@x"]) t("** en el medio", owner("/src/**/users.py @u", F, "src/api/v2/users.py") == ["@u"]) t("/README.md anclado solo en la raíz", owner("/README.md @r", "README.md\nsub/README.md\n", "sub/README.md") is None) t("? coincide un carácter", owner("src/ap?.py @q", F, "src/app.py") == ["@q"]) t("sin propietarios = explícitamente sin propietario", owner("* @a\n/web/", F, "web/app.js") == [] and ("info", "CO10") in codes("* @a\n/web/", F)) t("propietario inválido -> CO01 alto, línea omitida", ("high", "CO01") in codes("* @a\n/src/ alice", F) and owner("* @a\n/src/ alice", F, "src/app.py") == ["@a"]) t("negación -> CO06 medio", ("medium", "CO06") in codes("* @a\n!/src/", F)) t("rango [ ] -> CO06 medio", ("medium", "CO06") in codes("* @a\n/src/[ab]* @b", F)) t("sección de GitLab -> CO06 bajo", ("low", "CO06") in codes("[Docs]\n* @a", F)) cfg = {"org": "acme", "users": ["a", "b"], "teams": {"acme/web": ["a"], "acme/empty": []}} t("equipo desconocido -> CO02 alto, archivos sin propietario (sin respaldo)", ("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("propietario desconocido descartado, copropietario válido mantenido", owner("* @a\n/web/ @acme/webb @b", F, "web/app.js", cfg) == ["@b"]) t("equipo de otra org -> CO02", ("high", "CO02") in codes("* @other/web", F, cfg)) t("usuario desconocido -> CO02", ("high", "CO02") in codes("* @zed", F, cfg)) t("equipo vacío -> CO09", ("medium", "CO09") in codes("* @a\n/web/ @acme/empty", F, cfg)) t("propietario de correo electrónico aceptado", ("high", "CO01") not in codes("* dev@acme.io", F)) t("regla muerta -> CO04", ("medium", "CO04") in codes("* @a\n/legacy/ @b", F)) t("regla anulada -> CO05", ("medium", "CO05") in codes("/src/api/ @b\n/src/ @a", F)) t("parcialmente anulada no es CO05", ("medium", "CO05") not in codes("/src/ @a\n/src/api/ @b", F)) t("patrón duplicado -> CO08", ("low", "CO08") in codes("* @a\n/src/ @b\n/src/ @c", F)) t("archivo sin propietario -> CO03 medio", ("medium", "CO03") in codes("/src/ @a", F)) t("ruta require_owner sin propietario -> CO03 alto", ("high", "CO03") in codes("/src/ @acme/web", F, dict(cfg, require_owner=[".github/"]))) t("ruta requerida solo propiedad de '*' -> CO11", ("medium", "CO11") in codes("* @a\n/src/ @b", F, {"require_owner": [".github/"]})) t("regla muerta con propietario desconocido aún -> CO04", ("medium", "CO04") in codes("* @a\n/legacy/ @acme/zz", F, cfg)) t("concentración de propietario individual -> CO07", ("medium", "CO07") in codes("* @a", F, {"max_individual_share": 0.5})) t("la lista de ignorados elimina archivos", len(analyse("* @a", F, {"ignore": ["docs/"]})["files"]) == 7) r = analyse("* @a\n/src/ @b", F) t("--who informa línea", who(r, "src/api/routes.py") == "src/api/routes.py: @b (línea 2: /src/)") t("prefijo ./ eliminado, .github mantenido", analyse("* @a", "./.github/x.yml\n")["files"] == [".github/x.yml"]) t("el informe tiene veredicto y tabla de cobertura", "**Veredicto:" in report(r, "t") and "## Cobertura por directorio de nivel superior" in report(r, "t")) try: analyse("* @a", F, {"orgs": "x"}); ok = False except InputError: ok = True t("clave desconocida del archivo de propietarios rechazada", ok) for name, ok in tests: print(("PASS " if ok else "FAIL ") + name) passed = sum(ok for _, ok in tests) print("%d/%d pasadas" % (passed, len(tests))) return 0 if passed == len(tests) else 1 def main(argv=None): ap = argparse.ArgumentParser(description="Comprueba un archivo CODEOWNERS de GitHub contra la lista de archivos del repositorio.") ap.add_argument("codeowners", nargs="?") ap.add_argument("files", nargs="?", help="lista de archivos, ej. git ls-files > files.txt") ap.add_argument("--owners", help="JSON con org, users, teams, require_owner, max_individual_share, ignore") ap.add_argument("--who", action="append", default=[], help="muestra el propietario efectivo de una ruta (repetible)") 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("el archivo de propietarios no es JSON válido: %s" % e) res = analyse(co, fl, cfg) except (OSError, InputError) as e: print("error: %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())

Iniciá sesión para ver el prompt completo

Continuar con:

Al iniciar sesión, aceptás nuestros Términos de uso y Política de privacidad

Uso

Este prompt está diseñado para usarse con coding. Copiá el contenido de arriba y pegalo en tu herramienta de IA preferida.

Para mejores resultados, personalizá los marcadores (indicados con corchetes o mayúsculas) con tus requisitos específicos.

Referencias

Categorías:coding| prompts.chat| codeowners| github

Discusión