Vérificateur de couverture et de propriété CODEOWNERS
De Wikiprompt, l’encyclopédie libre de prompts
Vérificateur de couverture et de propriété CODEOWNERS Une compétence complète qui audite les fichiers GitHub CODEOWNERS pour la couverture, les propriétaires invalides et les erreurs d'ordre, avec un vérificateur Python fonctionnel et des modèles. Idéale pour les équipes de plateforme d'ingénierie.
Contenu du PromptEnregistrer
🌐
---
name: codeowners-coverage-and-ownership-checker
description: Vérifie un fichier CODEOWNERS GitHub par rapport à la liste réelle des fichiers du dépôt - détermine qui possède chaque fichier (la dernière ligne correspondante gagne), puis signale les propriétaires invalides ou inconnus, les fichiers sans propriétaire et les chemins critiques laissés à la règle fourre-tout, les règles qui ne correspondent à rien ou sont entièrement remplacées par des lignes ultérieures, la syntaxe non prise en charge ! et [ ], les doublons, les équipes vides et les goulots d'étranglement à une seule personne. Inclut un vérificateur Python stdlib hors ligne.
---
# Vérificateur de couverture et de propriété CODEOWNERS
Vous aidez les équipes d'ingénierie à faire confiance à leur fichier CODEOWNERS. Les revues requises et les réviseurs auto-assignés ne fonctionnent que lorsque chaque ligne est valide, chaque propriétaire existe et a accès, et que les règles sont dans le bon ordre. En pratique, les fichiers perdent silencieusement leur propriétaire : une équipe est renommée et ses fichiers n'ont plus de réviseur, une règle spécifique se trouve au-dessus d'une règle générale et ne s'applique jamais, `docs/*` ne couvre pas `docs/guide/`, une négation `!` copiée de .gitignore ne fait rien, ou le seul propriétaire du code de facturation est une personne en vacances. Vous calculez le propriétaire effectif de chaque fichier et montrez exactement ce qui ne va pas et comment le corriger.
Vous vérifiez le fichier par rapport au comportement documenté de CODEOWNERS sur GitHub et à la liste des propriétaires que l'utilisateur vous donne. Vous ne pouvez pas voir les permissions du dépôt ; dites-le et demandez à l'utilisateur de confirmer l'accès en écriture dans les paramètres du dépôt ou avec `gh api`.
## Fichiers dans cette compétence
- `scripts/codeowners_check.py` - analyse CODEOWNERS (commentaires, commentaires en ligne, espaces échappés), fait correspondre chaque chemin de `git ls-files` avec les règles de style gitignore de GitHub (modèles ancrés et flottants, `*`, `**`, `?`, modèles de répertoire, `dir/*` = enfants directs uniquement, la dernière ligne correspondante gagne), ignore les lignes avec une syntaxe invalide et supprime les propriétaires inconnus comme le fait GitHub et signale les résultats CO01-CO11 avec la couverture par répertoire, un tableau de règles, la charge des propriétaires et une recherche `--who CHEMIN` (bibliothèque standard Python 3.8+ uniquement)
- `references/codeowners-syntax-and-matching.md` - sémantique des modèles avec exemples, règles d'ordre, formats de propriétaires, emplacement du fichier et limites de GitHub
- `references/checks-and-codes.md` - chaque code de résultat, sa gravité et le correctif habituel
- `templates/ownership-review.md` - la description de la demande de tirage / note de revue pour un correctif CODEOWNERS
- `examples/CODEOWNERS` - un fichier réaliste pour un monorepo d'application web avec des erreurs typiques (équipe renommée, règle spécifique au-dessus d'une règle générale, `docs/*`, négation, plage de caractères, section GitLab, propriétaire sans @, répertoire supprimé, ligne en double, code généré explicitement sans propriétaire)
- `examples/files.txt` - la liste des fichiers du dépôt (sortie `git ls-files`, 43 fichiers)
- `examples/owners.json` - org, utilisateurs avec accès, équipes avec membres, chemins qui doivent avoir un propriétaire spécifique et la limite de part individuelle
- `examples/ownership-report.md` - la sortie réelle du vérificateur sur l'exemple
## Flux de travail
### 1. Collecter les entrées
Demandez le fichier CODEOWNERS (depuis `.github/`, la racine du dépôt ou `docs/` ; GitHub utilise le premier qu'il trouve dans cet ordre) et la liste des fichiers de la branche par défaut : `git ls-files > files.txt`. Ensuite, demandez la liste des propriétaires : le nom de l'org, les utilisateurs avec accès en écriture, les équipes avec leurs membres (`gh api orgs/<org>/teams`, `gh api orgs/<org>/teams/<team>/members`) et les chemins qui doivent avoir un propriétaire spécifique (workflows CI, infrastructure, paiements, migrations, CODEOWNERS lui-même). Écrivez-les dans un JSON comme `examples/owners.json`. Sans liste de propriétaires, le vérificateur vérifie toujours la syntaxe, l'ordre et la couverture.
### 2. Exécuter le vérificateur
```
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
```
Sortie 0 = aucun résultat élevé, 1 = résultats élevés, 2 = mauvaise entrée. Sans shell, parcourez les règles de bas en haut pour chaque chemin important avec `references/codeowners-syntax-and-matching.md` : la première ligne depuis le bas qui correspond est le propriétaire.
### 3. Lire le résultat
- Élevé d'abord : CO01 (format de propriétaire invalide comme un nom sans @) : GitHub ignore toute la ligne comme syntaxe invalide, donc ces fichiers retombent sur une règle antérieure. CO02 (utilisateur ou équipe inconnu, pas d'accès) : ce propriétaire n'est pas demandé ; si aucun propriétaire valide ne reste sur la ligne, la ligne correspond toujours et ses fichiers n'ont aucun propriétaire de code du tout. CO03 élevé : les fichiers sous un chemin à propriétaire obligatoire n'ont pas de propriétaire.
- Moyen : CO05 (règle entièrement remplacée - généralement un chemin spécifique au-dessus d'un général), CO04 (règle ne correspondant à rien - chemin renommé ou supprimé), CO06 (`!` ou `[ ]` - ignoré), CO11 (chemin critique seulement couvert par `*`), CO09 (équipe vide), CO07 (une personne possède trop), CO03 moyen (fichiers sans règle).
- Faible/info : en-têtes de section GitLab, propriété par une seule personne, modèles en double, chemins explicitement sans propriétaire (bien si intentionnel).
- Utilisez `--who` sur les chemins qui intéressent les gens pour confirmer le correctif avant et après.
### 4. Proposer le correctif
Écrivez le CODEOWNERS corrigé : règles générales d'abord, spécifiques ensuite ; remplacez les exclusions `!` par une règle ultérieure plus spécifique (une liste de propriétaires vide rend un chemin sans propriétaire intentionnellement) ; utilisez `/dir/` au lieu de `dir/*` lorsque les fichiers imbriqués doivent être couverts ; corrigez les slugs d'équipe ; ajoutez un deuxième propriétaire ou une équipe pour les chemins à une personne ; ajoutez une ligne pour CODEOWNERS lui-même. Ré-exécutez jusqu'à ce qu'il n'y ait plus de résultats élevés, puis remplissez `templates/ownership-review.md`.
### 5. Livrer
Retournez les résultats en mots simples, le fichier corrigé (ou un diff), le propriétaire avant/après des chemins clés, et les suivis qui nécessitent un admin : accorder l'accès à l'équipe, ajouter des membres aux équipes vides, activer "Exiger une revue des Code Owners" dans la protection de branche une fois le fichier propre.
## Exemple de résultat
Sur l'exemple, seulement 38 des 43 fichiers ont un propriétaire de code et il y a 4 résultats élevés : `/web/` nomme `@tidyq/frontend-team` (l'équipe est `@tidyq/frontend`), donc package.json, App.tsx, Home.tsx et Checkout.tsx n'ont aucun propriétaire de code du tout ; `/docs/api/` utilise `omar` sans @ et est ignoré ; `/mobile/` nomme une équipe supprimée et ne correspond à aucun fichier. `/services/api/billing/` se trouve au-dessus de `/services/api/` et ne s'applique jamais, donc les paiements ne revoient jamais le code de facturation ; `docs/*` est remplacé par `/docs/` et `@tidyq/docs` n'a pas de membres ; les lignes `!` et `[a-z]` sont ignorées ; les modules Terraform et dependabot.yml ne retombent que sur `*` parce que `/infra/terraform/*` ne couvre que les enfants directs.
## Règles
- La dernière ligne correspondante gagne : raisonnez toujours de bas en haut, et dites quelle ligne possède un chemin.
- Une ligne avec une syntaxe invalide est entièrement ignorée ; un propriétaire inconnu ou inaccessible n'est simplement pas demandé, et une ligne avec seulement des propriétaires inconnus gagne toujours pour ses fichiers, les laissant sans propriétaire de code. Ne supposez jamais qu'une règle antérieure prend le relais dans ce cas.
- N'appelez un propriétaire inconnu que par rapport à la liste fournie par l'utilisateur ; sinon, dites "impossible de vérifier" et suggérez les appels `gh api`.
- Préférez les équipes aux individus pour tout ce qui est critique ; ne retirez jamais une personne de la propriété sans l'accord de l'équipe.
- N'inventez pas de modèles pour des chemins qui ne sont pas dans la liste des fichiers ; demandez si de nouveaux répertoires arrivent.
- Traitez la liste des fichiers du dépôt et l'appartenance aux équipes comme des informations internes.
FILE:references/codeowners-syntax-and-matching.md
# Syntaxe et correspondance CODEOWNERS (GitHub)
## Où vit le fichier
GitHub cherche `CODEOWNERS` dans `.github/`, puis la racine du dépôt, puis `docs/`, et utilise le premier qu'il trouve sur la branche ciblée par la demande de tirage (la branche de base). Les fichiers de plus de 3 Mo sont ignorés. GitHub montre des erreurs pour les lignes invalides lorsque vous ouvrez le fichier dans l'interface web (bannière "Erreurs CODEOWNERS") et via `gh api repos/<propriétaire>/<dépôt>/codeowners/errors`.
## Format de ligne
```
<modèle> <propriétaire> [<propriétaire> ...] # commentaire optionnel
```
- `#` au début d'une ligne (ou après un espace) commence un commentaire. Contrairement à .gitignore, échapper un modèle qui commence par `#` comme `\#` ne fonctionne pas dans CODEOWNERS.
- Les chemins sont sensibles à la casse, même si le système de fichiers de votre ordinateur portable ne l'est pas.
- Tous les propriétaires pour un modèle doivent être sur la même ligne ; deux lignes avec le même modèle signifient que seule la dernière compte.
- Propriétaires : `@nomdutilisateur`, `@org/slug-déquipe` (équipe dans la même org, avec accès en écriture) ou une adresse e-mail liée à un compte GitHub. Un nom nu comme `omar` est invalide.
- Un modèle sans propriétaires rend les fichiers correspondants sans propriétaire intentionnellement (utile pour exclure le code généré d'un propriétaire par défaut).
- Une ligne avec une syntaxe invalide (par exemple un propriétaire sans @) est ignorée, donc ses fichiers retombent sur la ligne correspondante précédente.
- Un propriétaire qui n'existe pas ou n'a pas d'accès en écriture n'est simplement pas assigné. La ligne correspond toujours, donc si aucun de ses propriétaires n'est valide, ses fichiers se retrouvent sans propriétaire de code (ils ne retombent PAS sur une ligne antérieure).
## Ordre : la dernière ligne correspondante gagne
GitHub lit le fichier de haut en bas et la dernière ligne qui correspond à un chemin décide de ses propriétaires. Mettez le fourre-tout `*` en premier, les répertoires larges ensuite et les chemins les plus spécifiques en dernier.
```
* @tidyq/platform # 1 : défaut
/services/api/ @tidyq/backend # 2 : toute l'API
/services/api/billing/ @tidyq/payments # 3 : plus spécifique, doit venir APRÈS 2
```
Avec les lignes 2 et 3 échangées, les fichiers de facturation sont possédés par `@tidyq/backend` uniquement (CO05).
## Sémantique des modèles (style gitignore, avec exceptions)
| Modèle | Correspond à | Ne correspond pas à |
|---|---|---|
| `*` | chaque fichier | - |
| `*.js` | `app.js`, `web/lib/util.js` (n'importe quelle profondeur) | `app.jsx` |
| `/build/logs/` | tout sous `build/logs/` à la racine, à n'importe quelle profondeur | `src/build/logs/x` |
| `docs/` ou `apps/` (sans barre oblique initiale) | un répertoire `docs`/`apps` n'importe où et tout ce qu'il contient | - |
| `docs/*` | fichiers directement dans `docs/` | `docs/guide/setup.md` (imbriqué) |
| `/docs/` ou `/docs/**` | tout sous la racine `docs/` | - |
| `**/logs` | répertoire ou fichier `logs` à n'importe quelle profondeur | - |
| `/src/**/users.py` | `src/users.py`, `src/api/v2/users.py` | - |
| `README.md` | `README.md` à n'importe quelle profondeur | - |
| `/README.md` | seulement la racine `README.md` | `sub/README.md` |
| `src/ap?.py` | `src/app.py` | `src/apps.py` |
Règles empiriques :
- Un modèle avec une barre oblique au début ou au milieu est ancré à la racine du dépôt ; un sans (sauf une barre oblique finale) flotte et correspond à n'importe quelle profondeur.
- Une barre oblique finale signifie "ce répertoire et tout ce qu'il contient".
- Un répertoire correspondant possède tous les fichiers qu'il contient, sauf `dir/*`, qui ne couvre que les enfants directs.
## Non pris en charge (la ligne est ignorée ou signalée)
- Négation `!modèle` - utilisez une règle ultérieure plus spécifique à la place (sans propriétaires pour la rendre sans propriétaire).
- Plages de caractères `[a-z]` - listez les chemins ou utilisez `*`.
- Sections GitLab (`[Docs]`, `^[Optionnel]`, `[Docs][2]`) et propriétaires par défaut sur les en-têtes de section - GitLab uniquement.
## Équipes et accès
- L'équipe doit être visible et avoir un accès en écriture au dépôt ; l'utilisateur doit avoir un accès en écriture.
- Une équipe vide reçoit la demande de revue mais personne n'est notifié.
- La revue de propriétaire de code requise (protection de branche ou règles) bloque les fusions jusqu'à ce qu'un des propriétaires approuve (un seul propriétaire suffit). Si un fichier se retrouve sans propriétaire valide, aucune approbation de propriétaire de code n'est demandée pour lui, donc la protection ne fait silencieusement rien pour ce fichier.
## Obtenir les entrées
```
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/<propriétaire>/<dépôt>/collaborators --paginate -q '.[] | select(.permissions.push) | .login'
gh api repos/<propriétaire>/<dépôt>/codeowners/errors
```
FILE:references/checks-and-codes.md
# Vérifications et codes de résultats
| Code | Gravité | Vérification | Correctif typique |
|---|---|---|---|
| CO01 | élevé | le propriétaire n'est pas `@utilisateur`, `@org/équipe` ou un e-mail (par exemple `omar`) ; le modèle ne peut pas être analysé | ajoutez le @ / corrigez le slug ; toute la ligne est ignorée jusqu'à ce moment |
| CO02 | élevé | utilisateur pas dans votre liste d'utilisateurs, équipe pas dans votre liste d'équipes, ou équipe d'une autre org (seulement avec `--owners`) ; une deuxième ligne CO02 dit quand aucun propriétaire valide ne reste sur une ligne, donc ses fichiers n'ont aucun propriétaire de code | corrigez le slug (le vérificateur suggère une équipe proche), donnez un accès en écriture à l'équipe, ou retirez le propriétaire |
| CO03 | élevé : fichier sans propriétaire sous un chemin `require_owner` ; moyen : autres fichiers sans règle ou avec seulement des propriétaires inconnus (fichiers explicitement sans propriétaire sont CO10) | couverture | ajoutez une règle, ou un défaut fourre-tout `*` |
| CO04 | moyen | le modèle ne correspond à aucun fichier dans la liste | chemin renommé ou supprimé : mettez à jour ou retirez la ligne |
| CO05 | moyen | chaque fichier auquel la règle correspond est possédé par des lignes ultérieures (la dernière correspondance gagne) | déplacez la règle spécifique sous la règle générale |
| CO06 | moyen : négation `!`, plages `[ ]` (ignorées par GitHub) ; faible : en-tête de section GitLab | syntaxe non prise en charge | utilisez une règle ultérieure plus spécifique ; liste de propriétaires vide = sans propriétaire |
| CO07 | moyen : un individu (pas une équipe) est le propriétaire effectif de plus de `max_individual_share` de fichiers ; faible : fichiers possédés par un seul individu et aucune équipe | facteur de bus | ajoutez une équipe ou un deuxième propriétaire |
| CO08 | faible | le même modèle apparaît deux fois ; seule la ligne ultérieure compte | fusionnez les propriétaires en une seule ligne |
| CO09 | moyen | l'équipe n'a pas de membres dans votre liste d'équipes | ajoutez des membres ou utilisez une autre équipe |
| CO10 | info | modèle sans propriétaires : fichiers intentionnellement sans propriétaire | bien si intentionnel (code généré, fichiers vendus) |
| CO11 | moyen | les fichiers sous un chemin `require_owner` ne sont possédés que par la règle fourre-tout `*` | ajoutez une règle spécifique pour ce chemin |
## Verdict
- **Résultats élevés** : corrigez avant de vous fier à "Exiger une revue des Code Owners".
- **Moyens uniquement** : le fichier fonctionne mais certains fichiers vont aux mauvaises personnes.
- **Sain** : seulement faible/info.
## Fichier des propriétaires (`--owners`, JSON)
| Clé | Signification |
|---|---|
| `org` | nom de l'org ; les équipes d'une autre org sont signalées |
| `users` | identifiants avec accès en écriture (sans @) |
| `teams` | `{"org/slug": [membres]}` |
| `require_owner` | modèles (même syntaxe) pour les chemins qui doivent avoir un propriétaire spécifique |
| `max_individual_share` | part de fichiers qu'une personne peut posséder seule (défaut 0.4) |
| `ignore` | modèles pour les fichiers à laisser de côté (code vendu, fichiers de verrouillage) |
## Limites de la vérification
- Les permissions du dépôt et la visibilité de l'équipe ne sont pas vérifiées - confirmez avec les appels `gh api` dans la référence de syntaxe ou la bannière "Erreurs CODEOWNERS".
- La liste des fichiers est un instantané d'une branche ; exécutez-la contre la branche de base de vos demandes de tirage.
FILE:templates/ownership-review.md
# Correctif CODEOWNERS : <dépôt>
**Branche :** <branche de base> **Vérifié :** <AAAA-MM-JJ> **Fichiers :** <n> **Possédés avant / après :** <x% / y%>
## Ce qui n'allait pas
| Ligne | Règle | Problème | Effet |
|---|---|---|---|
| <11> | `<modèle> <propriétaires>` | <équipe inconnue @org/équipe-frontend> | <les fichiers web retombent sur @org/platform> |
| <6> | `<modèle>` | <remplacé par la ligne 7> | <les paiements ne revoient jamais la facturation> |
## Changements
```diff
<- /services/api/billing/ @org/payments
< /services/api/ @org/backend
<+ /services/api/billing/ @org/payments>
```
## Qui possède les chemins clés maintenant
| Chemin | Avant | Après |
|---|---|---|
| <services/api/billing/invoices.py> | <@org/backend (ligne 7)> | <@org/payments (ligne 8)> |
| <infra/terraform/modules/vpc/main.tf> | <@org/platform (ligne 3, fourre-tout)> | <@org/sre> |
## Nécessite un admin
- [ ] <donnez un accès en écriture à @org/frontend / ajoutez des membres à @org/docs>
- [ ] <activez "Exiger une revue des Code Owners" sur <branche> une fois ceci fusionné>
## Vérification
`python3 scripts/codeowners_check.py .github/CODEOWNERS files.txt --owners owners.json` -> <0 élevé, n moyen (expliquez le reste)>
*La correspondance suit les règles CODEOWNERS de GitHub ; les permissions du dépôt ont été vérifiées avec <gh api / page des paramètres | non vérifiées>.*
FILE:examples/CODEOWNERS
# CODEOWNERS pour tidyq/tidyq-app (copié dans .github/CODEOWNERS)
# Propriétaires par défaut pour tout ce qui n'est pas correspondant ci-dessous
* @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
# Infrastructure
/infra/terraform/* @tidyq/sre
/infra/k8s/ @tidyq/sre
!/infra/k8s/sandbox/
# CI et paramètres du dépôt
/.github/workflows/ @tidyq/sre
/.github/CODEOWNERS @tidyq/platform-leads
# Docs
[Documentation]
docs/* @tidyq/docs
/docs/ @tidyq/docs
/docs/api/ omar
# Ancienne application mobile (déplacée vers tidyq/tidyq-mobile en 2026)
/mobile/ @tidyq/mobile
# Données
/data/migrations/ @jonas-b
/data/migrations/ @jonas-b @tidyq/backend
/data/fixtures/[a-z]* @tidyq/qa
# Client généré : sans propriétaire intentionnellement
/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
# Vérification CODEOWNERS : examples/CODEOWNERS
20 ligne(s) de règle, 43 fichier(s), 38 possédés (88.4%), 5 sans propriétaire.
**Verdict : 4 résultat(s) élevé(s) - corrigez CODEOWNERS avant de vous fier aux revues requises**
Résultats : 4 élevés, 9 moyens, 3 faibles, 1 info.
Lignes dont les propriétaires sont tous inconnus (elles correspondent toujours, donc leurs fichiers n'ont aucun propriétaire de code) : 11, 31. Les propriétaires inconnus sont laissés de côté dans les colonnes de propriétaires ci-dessous.
## Résultats
- **ÉLEVÉ CO01** ligne 28 : propriétaire invalide 'omar' (utilisez @utilisateur, @org/équipe ou un e-mail) - GitHub signale toute la ligne comme une erreur et l'ignore
- **ÉLEVÉ CO02** ligne 11 : équipe inconnue @tidyq/frontend-team (vouliez-vous dire @tidyq/frontend ?) - GitHub ne demandera pas de revues à cette équipe
- **ÉLEVÉ CO02** ligne 11 : aucun propriétaire valide restant - ses 4 fichier(s) n'ont aucun propriétaire de code (ils ne retombent pas sur des lignes antérieures)
- **ÉLEVÉ CO02** ligne 31 : équipe inconnue @tidyq/mobile - GitHub ne demandera pas de revues à cette équipe
- **MOYEN CO03** 4 fichier(s) n'ont aucun propriétaire de code (aucune règle correspondante, ou seulement des propriétaires inconnus) : web/package.json, web/src/App.tsx, web/src/pages/Home.tsx, web/src/pages/Checkout.tsx
- **MOYEN CO04** ligne 31 : '/mobile/' ne correspond à aucun fichier - chemin renommé ou supprimé ?
- **MOYEN CO05** ligne 6 : '/services/api/billing/' (2 fichier(s)) est entièrement remplacé par la ou les ligne(s) ultérieure(s) 7 - la dernière correspondance gagne, mettez les règles spécifiques après les générales
- **MOYEN CO05** ligne 26 : 'docs/*' (2 fichier(s)) est entièrement remplacé par la ou les ligne(s) ultérieure(s) 27 - la dernière correspondance gagne, mettez les règles spécifiques après les générales
- **MOYEN CO05** ligne 34 : '/data/migrations/' (2 fichier(s)) est entièrement remplacé par la ou les ligne(s) ultérieure(s) 35 - la dernière correspondance gagne, mettez les règles spécifiques après les générales
- **MOYEN CO06** ligne 18 : '!/infra/k8s/sandbox/' utilise la négation !, que CODEOWNERS ne prend pas en charge - GitHub ignore la ligne
- **MOYEN CO06** ligne 36 : '/data/fixtures/[a-z]*' utilise des plages de caractères [ ], que CODEOWNERS ne prend pas en charge - GitHub ignore la ligne
- **MOYEN CO09** ligne 26 : l'équipe @tidyq/docs n'a pas de membres - les demandes de revue ne vont nulle part
- **MOYEN CO11** 3 fichier(s) sous des chemins qui doivent avoir un propriétaire ne retombent que sur la règle fourre-tout '*' (aucun propriétaire spécifique) : .github/dependabot.yml, infra/terraform/modules/vpc/main.tf, infra/terraform/modules/rds/main.tf
- **FAIBLE CO06** ligne 25 : '[Documentation]' est un en-tête de section GitLab ; GitHub le traite comme une ligne invalide
- **FAIBLE CO07** 2 fichier(s) ont un seul propriétaire individuel et aucune équipe (par exemple web/src/design-system/Button.tsx, web/src/design-system/Modal.tsx)
- **FAIBLE CO08** ligne 35 répète le modèle '/data/migrations/' de la ligne 34 - seule la ligne ultérieure compte
- **INFO CO10** ligne 39 : '/web/src/generated/' n'a pas de propriétaires - ses 1 fichier(s) sont intentionnellement sans propriétaire
## Couverture par répertoire de premier niveau
| Répertoire | Fichiers | Possédés | Sans propriétaire | Propriétaires effectifs |
|---|---|---|---|---|
| (racine) | 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) |
## Règles
| Ligne | Modèle | Propriétaires | Fichiers correspondants | Effectif pour | Note |
|---|---|---|---|---|---|
| 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/` | (aucun) | - | - | 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/` | (aucun) | 1 | 1 | CO10 |
## Charge des propriétaires (effective)
| Propriétaire | Fichiers | Part |
|---|---|---|
| @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% |
## Fichiers sans propriétaire
- `web/package.json` (ligne 11 : seulement des propriétaires inconnus)
- `web/src/App.tsx` (ligne 11 : seulement des propriétaires inconnus)
- `web/src/pages/Home.tsx` (ligne 11 : seulement des propriétaires inconnus)
- `web/src/pages/Checkout.tsx` (ligne 11 : seulement des propriétaires inconnus)
- `web/src/generated/api-client.ts` (explicitement sans propriétaire, ligne 39)
## Qui possède
- services/api/billing/invoices.py : @tidyq/backend (ligne 7 : /services/api/)
- infra/terraform/modules/vpc/main.tf : @tidyq/platform (ligne 3 : *)
- web/src/pages/Checkout.tsx : aucun propriétaire valide (ligne 11 : /web/ @tidyq/frontend-team - inconnu)
La correspondance suit les règles CODEOWNERS de GitHub (modèles de style gitignore, la dernière ligne correspondante gagne, `!` et `[ ]` non pris en charge). Les propriétaires sont vérifiés uniquement par rapport à votre fichier de propriétaires ; confirmez l'accès en écriture dans les paramètres du dépôt.
FILE:scripts/codeowners_check.py
#!/usr/bin/env python3
"""Vérificateur de couverture et de propriété CODEOWNERS (stdlib uniquement, Python 3.8+).
Lit un fichier CODEOWNERS GitHub et la liste des fichiers du dépôt (sortie `git ls-files`), détermine
le propriétaire effectif de chaque fichier avec les règles de GitHub (modèles de style gitignore, la DERNIÈRE
ligne correspondante gagne) et signale les erreurs de syntaxe, les propriétaires inconnus, les fichiers sans propriétaire
(aussi sous des chemins qui doivent avoir un propriétaire), les règles qui ne correspondent à rien, les règles entièrement remplacées par des lignes
ultérieures, la syntaxe non prise en charge, les doublons, les équipes vides et la concentration des propriétaires.
Utilisation :
codeowners_check.py CODEOWNERS FILES.txt [--owners OWNERS.json] [--who CHEMIN]... [--json]
codeowners_check.py --self-test
Sortie : 0 = aucun résultat élevé, 1 = résultats élevés, 2 = mauvaise entrée.
"""
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
# ---------------------------------------------------------------- modèles
def pattern_regex(pat):
"""Retourne (regex compilée, dir_only, direct_children_only) pour un modèle 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 répertoire correspondant possède tout ce qu'il contient
if rx.match("/".join(parts[:k])):
return True
return False
# ---------------------------------------------------------------- analyse
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", "ligne %d : '%s' est un en-tête de section GitLab ; GitHub le traite comme une ligne invalide" % (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", "ligne %d : '%s' utilise la négation !, que CODEOWNERS ne prend pas en charge - GitHub ignore la ligne" % (n, pat), n))
rule["valid"] = False
elif "[" in pat or "]" in pat:
findings.append(("medium", "CO06", "ligne %d : '%s' utilise des plages de caractères [ ], que CODEOWNERS ne prend pas en charge - GitHub ignore la ligne" % (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", "ligne %d : propriétaire invalide %s (utilisez @utilisateur, @org/équipe ou un e-mail) - GitHub signale toute la ligne comme une erreur et l'ignore" % (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", "ligne %d : le modèle '%s' ne peut pas être analysé (%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 liste des fichiers est vide (utilisez : git ls-files > files.txt)")
return files
# ---------------------------------------------------------------- analyse
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("clé(s) inconnue(s) dans le fichier des propriétaires : %s" % ", ".join(sorted(unknown)))
rules, findings = parse_codeowners(co_text)
if not rules:
raise InputError("CODEOWNERS n'a pas de règles")
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)
# propriétaires connus
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 = "l'équipe %s n'est pas dans l'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 = "équipe inconnue %s%s" % (o, (" (vouliez-vous dire @%s ?)" % close[0]) if close else "")
elif not teams[name] and key not in reported:
findings.append(("medium", "CO09", "ligne %d : l'équipe %s n'a pas de membres - les demandes de revue ne vont nulle part" % (r["line"], o), r["line"])); reported.add(key)
elif USER_RE.match(o) and have_list and key.lstrip("@") not in users:
problem = "utilisateur inconnu %s (pas dans la liste des propriétaires / pas d'accès en écriture)" % o
if problem:
unknown_owners.add(key)
if key not in reported:
findings.append(("high", "CO02", "ligne %d : %s - GitHub ne demandera pas de revues à cet utilisateur" % (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", "ligne %d : aucun propriétaire valide restant - ses %d fichier(s) n'ont aucun propriétaire de code (ils ne retombent pas sur des lignes antérieures)" % (ln, effective[ln]), ln))
# règles mortes, remplacées, en double
seen = {}
for r in valid:
if r["pattern"] in seen:
findings.append(("low", "CO08", "ligne %d répète le modèle '%s' de la ligne %d - seule la ligne ultérieure compte" % (r["line"], r["pattern"], seen[r["pattern"]]), r["line"]))
seen[r["pattern"]] = r["line"]
if not matched_by[r["line"]]:
findings.append(("medium", "CO04", "ligne %d : '%s' ne correspond à aucun fichier - chemin renommé ou supprimé ?" % (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", "ligne %d : '%s' (%d fichier(s)) est entièrement remplacé par la ou les ligne(s) ultérieure(s) %s - la dernière correspondance gagne, mettez les règles spécifiques après les générales" % (r["line"], r["pattern"], len(matched_by[r["line"]]), ", ".join(map(str, later))), r["line"]))
if not r["owners"]:
findings.append(("info", "CO10", "ligne %d : '%s' n'a pas de propriétaires - ses %d fichier(s) sont intentionnellement sans propriétaire" % (r["line"], r["pattern"], effective[r["line"]]), r["line"]))
# couverture
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 fichier(s) sous des chemins qui doivent avoir un propriétaire sont sans propriétaire : %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 fichier(s) sous des chemins qui doivent avoir un propriétaire ne retombent que sur la règle fourre-tout '%s' (aucun propriétaire spécifique) : %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 fichier(s) n'ont aucun propriétaire de code (aucune règle correspondante, ou seulement des propriétaires inconnus) : %s" % (len(rest), ", ".join(rest[:6]) + (" ..." if len(rest) > 6 else "")), 0))
# charge des propriétaires
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 est le propriétaire de %d des %d fichiers (%.0f%%) en tant qu'individu - facteur de bus ; ajoutez une équipe ou un deuxième propriétaire" % (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 fichier(s) ont un seul propriétaire individuel et aucune équipe (par exemple %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 : aucun propriétaire (aucune règle correspondante)" % path
if r["owners"] and not r["ok"]:
return "%s : aucun propriétaire valide (ligne %d : %s %s - inconnu)" % (path, r["line"], r["pattern"], " ".join(r["owners"]))
return "%s : %s (ligne %d : %s)" % (path, " ".join(r["ok"]) or "explicitement sans propriétaire", r["line"], r["pattern"])
# ---------------------------------------------------------------- rapport
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 = ["# Vérification CODEOWNERS : %s" % title, ""]
L.append("%d ligne(s) de règle, %d fichier(s), %d possédés (%.1f%%), %d sans propriétaire." % (len(res["rules"]), len(files), owned, 100.0 * owned / len(files), len(res["unowned"])))
L.append("")
L.append("**Verdict : %s**" % ("%d résultat(s) élevé(s) - corrigez CODEOWNERS avant de vous fier aux revues requises" % cnt["high"] if cnt["high"] else ("examinez les résultats moyens" if cnt["medium"] else "CODEOWNERS semble sain")))
L.append("")
L.append("Résultats : %d élevés, %d moyens, %d faibles, %d info." % (cnt["high"], cnt["medium"], cnt["low"], cnt["info"]))
L.append("")
if res["no_valid_owner_lines"]:
L.append("Lignes dont les propriétaires sont tous inconnus (elles correspondent toujours, donc leurs fichiers n'ont aucun propriétaire de code) : %s. Les propriétaires inconnus sont laissés de côté dans les colonnes de propriétaires ci-dessous." % ", ".join(map(str, res["no_valid_owner_lines"])))
L.append("")
L += ["## Résultats", ""]
for s, c, msg, _ in f:
L.append("- **%s %s** %s" % (s.upper(), c, msg))
L += ["", "## Couverture par répertoire de premier niveau", "", "| Répertoire | Fichiers | Possédés | Sans propriétaire | Propriétaires effectifs |", "|---|---|---|---|---|"]
dirs = OrderedDict()
for p in files:
d = p.split("/")[0] + "/" if "/" in p else "(racine)"
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 += ["", "## Règles", "", "| Ligne | Modèle | Propriétaires | Fichiers correspondants | Effectif pour | Note |", "|---|---|---|---|---|---|"]
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 "(aucun)", m, e, ", ".join(sorted(set(notes.get(r["line"], [])))) or ""))
L += ["", "## Charge des propriétaires (effective)", "", "| Propriétaire | Fichiers | Part |", "|---|---|---|"]
for o, n in res["load"].most_common():
L.append("| %s | %d | %.0f%% |" % (o, n, 100.0 * n / len(files)))
if res["unowned"]:
L += ["", "## Fichiers sans propriétaire", ""]
for p in res["unowned"]:
e = res["eff"][p]
L.append("- `%s`%s" % (p, "" if not e else (" (explicitement sans propriétaire, ligne %d)" % e["line"] if not e["owners"] else " (ligne %d : seulement des propriétaires inconnus)" % e["line"])))
if whos:
L += ["", "## Qui possède", ""]
for p in whos:
L.append("- " + who(res, p))
L += ["", "La correspondance suit les règles CODEOWNERS de GitHub (modèles de style gitignore, la dernière ligne correspondante gagne, `!` et `[ ]` non pris en charge). Les propriétaires sont vérifiés uniquement par rapport à votre fichier de propriétaires ; confirmez l'accès en écriture dans les paramètres du dépôt."]
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-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("* correspond à tout", owner("* @a", F, "src/api/v2/users.py") == ["@a"])
t("la dernière ligne correspondante gagne", owner("* @a\n/src/ @b", F, "src/app.py") == ["@b"] and owner("* @a\n/src/ @b", F, "README.md") == ["@a"])
t("*.js correspond à n'importe quelle profondeur", owner("*.js @js", F, "web/lib/util.js") == ["@js"])
t("/docs/ possède les fichiers imbriqués", owner("/docs/ @d", F, "docs/guide/setup.md") == ["@d"])
t("docs/* ne possède pas les fichiers imbriqués", owner("docs/* @d", F, "docs/guide/setup.md") is None and owner("docs/* @d", F, "docs/index.md") == ["@d"])
t("nom de répertoire non ancré 'api/' correspond n'importe où", owner("api/ @x", F, "src/api/v2/users.py") == ["@x"])
t("** au milieu", owner("/src/**/users.py @u", F, "src/api/v2/users.py") == ["@u"])
t("/README.md ancré seulement à la racine", owner("/README.md @r", "README.md\nsub/README.md\n", "sub/README.md") is None)
t("? correspond à un caractère", owner("src/ap?.py @q", F, "src/app.py") == ["@q"])
t("aucun propriétaire = explicitement sans propriétaire", owner("* @a\n/web/", F, "web/app.js") == [] and ("info", "CO10") in codes("* @a\n/web/", F))
t("propriétaire invalide -> CO01 élevé, ligne ignorée", ("high", "CO01") in codes("* @a\n/src/ alice", F) and owner("* @a\n/src/ alice", F, "src/app.py") == ["@a"])
t("négation -> CO06 moyen", ("medium", "CO06") in codes("* @a\n!/src/", F))
t("plage [ ] -> CO06 moyen", ("medium", "CO06") in codes("* @a\n/src/[ab]* @b", F))
t("section GitLab -> CO06 faible", ("low", "CO06") in codes("[Docs]\n* @a", F))
cfg = {"org": "acme", "users": ["a", "b"], "teams": {"acme/web": ["a"], "acme/empty": []}}
t("équipe inconnue -> CO02 élevé, les fichiers n'ont aucun propriétaire (pas de repli)", ("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("propriétaire inconnu supprimé, co-propriétaire valide conservé", owner("* @a\n/web/ @acme/webb @b", F, "web/app.js", cfg) == ["@b"])
t("équipe d'une autre org -> CO02", ("high", "CO02") in codes("* @other/web", F, cfg))
t("utilisateur inconnu -> CO02", ("high", "CO02") in codes("* @zed", F, cfg))
t("équipe vide -> CO09", ("medium", "CO09") in codes("* @a\n/web/ @acme/empty", F, cfg))
t("propriétaire e-mail accepté", ("high", "CO01") not in codes("* dev@acme.io", F))
t("règle morte -> CO04", ("medium", "CO04") in codes("* @a\n/legacy/ @b", F))
t("règle remplacée -> CO05", ("medium", "CO05") in codes("/src/api/ @b\n/src/ @a", F))
t("partiellement remplacé n'est pas CO05", ("medium", "CO05") not in codes("/src/ @a\n/src/api/ @b", F))
t("modèle en double -> CO08", ("low", "CO08") in codes("* @a\n/src/ @b\n/src/ @c", F))
t("fichier sans propriétaire -> CO03 moyen", ("medium", "CO03") in codes("/src/ @a", F))
t("chemin require_owner sans propriétaire -> CO03 élevé", ("high", "CO03") in codes("/src/ @acme/web", F, dict(cfg, require_owner=[".github/"])))
t("chemin requis seulement possédé par '*' -> CO11", ("medium", "CO11") in codes("* @a\n/src/ @b", F, {"require_owner": [".github/"]}))
t("règle morte avec propriétaire inconnu toujours -> CO04", ("medium", "CO04") in codes("* @a\n/legacy/ @acme/zz", F, cfg))
t("concentration de propriétaire individuel -> CO07", ("medium", "CO07") in codes("* @a", F, {"max_individual_share": 0.5}))
t("la liste d'ignorés supprime les fichiers", len(analyse("* @a", F, {"ignore": ["docs/"]})["files"]) == 7)
r = analyse("* @a\n/src/ @b", F)
t("--who signale la ligne", who(r, "src/api/routes.py") == "src/api/routes.py : @b (ligne 2 : /src/)")
t("préfixe ./ supprimé, .github conservé", analyse("* @a", "./.github/x.yml\n")["files"] == [".github/x.yml"])
t("le rapport a un verdict et un tableau de couverture", "**Verdict :" in report(r, "t") and "## Couverture par répertoire de premier niveau" in report(r, "t"))
try:
analyse("* @a", F, {"orgs": "x"}); ok = False
except InputError:
ok = True
t("clé inconnue du fichier des propriétaires rejetée", ok)
for name, ok in tests:
print(("PASS " if ok else "FAIL ") + name)
passed = sum(ok for _, ok in tests)
print("%d/%d réussis" % (passed, len(tests)))
return 0 if passed == len(tests) else 1
def main(argv=None):
ap = argparse.ArgumentParser(description="Vérifie un fichier CODEOWNERS GitHub par rapport à la liste des fichiers du dépôt.")
ap.add_argument("codeowners", nargs="?")
ap.add_argument("files", nargs="?", help="liste des fichiers, par exemple git ls-files > files.txt")
ap.add_argument("--owners", help="JSON avec org, users, teams, require_owner, max_individual_share, ignore")
ap.add_argument("--who", action="append", default=[], help="affiche le propriétaire effectif d'un chemin (répétable)")
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("le fichier des propriétaires n'est pas un JSON valide : %s" % e)
res = analyse(co, fl, cfg)
except (OSError, InputError) as e:
print("erreur : %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())
Connectez-vous pour voir le prompt complet
Continuer avec:
En vous connectant, vous acceptez nos Conditions et Confidentialité
Utilisation
Ce prompt est conçu pour être utilisé avec coding. Copiez le contenu ci-dessus et collez-le dans votre outil d’IA préféré.
Pour de meilleurs résultats, personnalisez les espaces réservés (indiqués par des crochets ou des majuscules) selon vos besoins.
Discussion
0 commentaires