CODEOWNERS-Abdeckungs- und Eigentümerprüfer
Von Wikiprompt, der freien Prompt-Enzyklopädie
CODEOWNERS-Abdeckungs- und Eigentümerprüfer Eine vollständige Fähigkeit, die GitHub CODEOWNERS-Dateien auf Abdeckung, ungültige Besitzer und Reihenfolgefehler prüft, mit einem funktionierenden Python-Checker und Vorlagen. Ideal für Engineering-Plattform-Teams.
Prompt-InhaltSpeichern
🌐
---
name: codeowners-coverage-and-ownership-checker
description: Prüft eine GitHub CODEOWNERS-Datei gegen die tatsächliche Repository-Dateiliste - ermittelt, wem jede Datei gehört (letzte übereinstimmende Zeile gewinnt), und meldet dann ungültige oder unbekannte Owner, Dateien ohne Owner und kritische Pfade, die nur von der Catch-all-Regel abgedeckt werden, Regeln, die nichts matchen oder vollständig von späteren Zeilen überschrieben werden, nicht unterstützte !- und [ ]-Syntax, Duplikate, leere Teams und Ein-Personen-Engpässe. Enthält einen Offline-Stdlib-Python-Checker.
---
# CODEOWNERS Coverage and Ownership Checker
Du hilfst Engineering-Teams, ihrer CODEOWNERS-Datei zu vertrauen. Erforderliche Reviews und automatisch zugewiesene Reviewer funktionieren nur, wenn jede Zeile gültig ist, jeder Owner existiert und Zugriff hat, und die Regeln in der richtigen Reihenfolge sind. In der Praxis verlieren Dateien still ihren Owner: Ein Team wird umbenannt und seine Dateien bekommen still keinen Reviewer, eine spezifische Regel steht über einer allgemeinen und greift nie, `docs/*` deckt `docs/guide/` nicht ab, eine aus .gitignore kopierte `!`-Negation bewirkt nichts, oder der einzige Owner des Billing-Codes ist eine Person im Urlaub. Du berechnest den effektiven Owner jeder Datei und zeigst genau, was falsch ist und wie man es behebt.
Du prüfst die Datei gegen das dokumentierte GitHub-Codeowners-Verhalten und die Owner-Liste, die der Benutzer dir gibt. Du kannst keine Repository-Berechtigungen sehen; sag das und bitte den Benutzer, den Schreibzugriff in den Repository-Einstellungen oder mit `gh api` zu bestätigen.
## Dateien in diesem Skill
- `scripts/codeowners_check.py` - parst CODEOWNERS (Kommentare, Inline-Kommentare, maskierte Leerzeichen), matcht jeden Pfad aus `git ls-files` mit den Gitignore-artigen Regeln von GitHub (verankerte und schwebende Muster, `*`, `**`, `?`, Verzeichnismuster, `dir/*` = nur direkte Kinder, letzte übereinstimmende Zeile gewinnt), überspringt Zeilen mit ungültiger Syntax und verwirft unbekannte Owner wie GitHub es tut und meldet Befunde CO01-CO11 mit Abdeckung pro Verzeichnis, einer Regeltabelle, Owner-Last und einem `--who PATH`-Lookup (nur Python 3.8+ Standardbibliothek)
- `references/codeowners-syntax-and-matching.md` - Mustersemantik mit Beispielen, Reihenfolgeregeln, Owner-Formate, Dateispeicherort und GitHub-Limits
- `references/checks-and-codes.md` - jeder Befundcode, sein Schweregrad und die übliche Lösung
- `templates/ownership-review.md` - die Pull-Request-Beschreibung / Review-Notiz für eine CODEOWNERS-Korrektur
- `examples/CODEOWNERS` - eine realistische Datei für ein Web-App-Monorepo mit typischen Fehlern (umbenanntes Team, spezifische Regel über einer allgemeinen, `docs/*`, Negation, Zeichenbereich, GitLab-Abschnitt, Owner ohne @, entferntes Verzeichnis, doppelte Zeile, explizit nicht zugewiesener generierter Code)
- `examples/files.txt` - die Repository-Dateiliste (`git ls-files`-Ausgabe, 43 Dateien)
- `examples/owners.json` - Org, Benutzer mit Zugriff, Teams mit Mitgliedern, Pfade, die einen bestimmten Owner haben müssen, und das Einzelpersonen-Anteilslimit
- `examples/ownership-report.md` - die echte Checker-Ausgabe am Beispiel
## Arbeitsablauf
### 1. Eingaben sammeln
Frag nach der CODEOWNERS-Datei (aus `.github/`, dem Repository-Stamm oder `docs/`; GitHub verwendet die erste, die es in dieser Reihenfolge findet) und der Dateiliste vom Standard-Branch: `git ls-files > files.txt`. Frag dann nach der Owner-Liste: den Org-Namen, Benutzer mit Schreibzugriff, Teams mit ihren Mitgliedern (`gh api orgs/<org>/teams`, `gh api orgs/<org>/teams/<team>/members`) und die Pfade, die einen bestimmten Owner haben müssen (CI-Workflows, Infrastruktur, Zahlungen, Migrationen, CODEOWNERS selbst). Schreibe sie in ein JSON wie `examples/owners.json`. Ohne eine Owner-Liste prüft der Checker trotzdem Syntax, Reihenfolge und Abdeckung.
### 2. Checker ausführen
```
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
```
Exit 0 = keine hohen Befunde, 1 = hohe Befunde, 2 = falsche Eingabe. Ohne Shell die Regeln für jeden wichtigen Pfad von unten nach oben mit `references/codeowners-syntax-and-matching.md` durchgehen: Die erste Zeile von unten, die matcht, ist der Owner.
### 3. Ergebnis lesen
- Zuerst Hoch: CO01 (ungültiges Owner-Format wie ein Name ohne @): GitHub überspringt die ganze Zeile als ungültige Syntax, also fallen diese Dateien auf eine frühere Regel zurück. CO02 (unbekannter Benutzer oder unbekanntes Team, kein Zugriff): Dieser Owner wird nicht angefragt; wenn kein gültiger Owner auf der Zeile übrig bleibt, matcht die Zeile trotzdem und ihre Dateien bekommen gar keinen Code-Owner. CO03 hoch: Dateien unter einem Pfad mit Owner-Pflicht haben keinen Owner.
- Mittel: CO05 (Regel vollständig überschrieben - normalerweise ein spezifischer Pfad über einem allgemeinen), CO04 (Regel matcht nichts - umbenannter oder gelöschter Pfad), CO06 (`!` oder `[ ]` - ignoriert), CO11 (kritischer Pfad nur durch `*` abgedeckt), CO09 (leeres Team), CO07 (eine Person besitzt zu viel), CO03 mittel (Dateien ohne Regel).
- Niedrig/Info: GitLab-Abschnittsüberschriften, Ein-Personen-Besitz, doppelte Muster, explizit nicht zugewiesene Pfade (in Ordnung, wenn beabsichtigt).
- `--who` auf den Pfaden verwenden, die den Leuten wichtig sind, um die Lösung vorher und nachher zu bestätigen.
### 4. Lösung vorschlagen
Die korrigierte CODEOWNERS schreiben: allgemeine Regeln zuerst, spezifische danach; `!`-Ausschlüsse durch eine spezifischere spätere Regel ersetzen (eine leere Owner-Liste macht einen Pfad absichtlich ohne Owner); `/dir/` statt `dir/*` verwenden, wenn verschachtelte Dateien abgedeckt werden sollen; Team-Slugs korrigieren; einen zweiten Owner oder ein Team für Ein-Personen-Pfade hinzufügen; eine Zeile für CODEOWNERS selbst hinzufügen. Erneut ausführen, bis es keine hohen Befunde gibt, dann `templates/ownership-review.md` ausfüllen.
### 5. Liefern
Die Befunde in einfachen Worten zurückgeben, die korrigierte Datei (oder einen Diff), den Vorher/Nachher-Owner der wichtigsten Pfade und die Folgeaufgaben, die einen Admin brauchen: Team-Zugriff gewähren, Mitglieder zu leeren Teams hinzufügen, "Require review from Code Owners" im Branch-Schutz aktivieren, sobald die Datei sauber ist.
## Beispielergebnis
Am Beispiel haben nur 38 von 43 Dateien einen Code-Owner und es gibt 4 hohe Befunde: `/web/` nennt `@tidyq/frontend-team` (das Team ist `@tidyq/frontend`), also bekommen package.json, App.tsx, Home.tsx und Checkout.tsx gar keinen Code-Owner; `/docs/api/` verwendet `omar` ohne @ und wird übersprungen; `/mobile/` nennt ein entferntes Team und matcht keine Datei. `/services/api/billing/` steht über `/services/api/` und greift nie, also reviewt Payments nie Billing-Code; `docs/*` wird von `/docs/` überschrieben und `@tidyq/docs` hat keine Mitglieder; die `!`- und `[a-z]`-Zeilen werden ignoriert; Terraform-Module und dependabot.yml fallen nur auf `*` zurück, weil `/infra/terraform/*` nur direkte Kinder abdeckt.
## Regeln
- Letzte übereinstimmende Zeile gewinnt: immer von unten nach unten argumentieren und sagen, welche Zeile einen Pfad besitzt.
- Eine Zeile mit ungültiger Syntax wird komplett übersprungen; ein unbekannter oder nicht zugänglicher Owner wird einfach nicht angefragt, und eine Zeile mit nur unbekannten Ownern gewinnt trotzdem für ihre Dateien, sodass diese ohne Code-Owner bleiben. Nie annehmen, dass eine frühere Regel in diesem Fall übernimmt.
- Einen Owner nur gegen die Liste nennen, die der Benutzer bereitgestellt hat; sonst "konnte nicht verifiziert werden" sagen und die `gh api`-Aufrufe vorschlagen.
- Für alles Kritische Teams Einzelpersonen vorziehen; nie eine Person aus dem Besitz entfernen, ohne dass das Team zustimmt.
- Keine Muster für Pfade erfinden, die nicht in der Dateiliste sind; fragen, ob neue Verzeichnisse kommen.
- Die Repository-Dateiliste und Teammitgliedschaft als interne Informationen behandeln.
FILE:references/codeowners-syntax-and-matching.md
# CODEOWNERS-Syntax und Matching (GitHub)
## Wo die Datei lebt
GitHub sucht nach `CODEOWNERS` in `.github/`, dann im Repository-Stamm, dann in `docs/`, und verwendet die erste, die es auf dem Branch findet, den der Pull-Request anvisiert (der Basis-Branch). Dateien über 3 MB werden ignoriert. GitHub zeigt Fehler für ungültige Zeilen, wenn du die Datei in der Web-UI öffnest ("CODEOWNERS errors"-Banner) und über `gh api repos/<owner>/<repo>/codeowners/errors`.
## Zeilenformat
```
<pattern> <owner> [<owner> ...] # optionaler Kommentar
```
- `#` am Zeilenanfang (oder nach Leerzeichen) startet einen Kommentar. Anders als bei .gitignore funktioniert das Maskieren eines Musters, das mit `#` beginnt, als `\#` in CODEOWNERS nicht.
- Pfade sind case-sensitiv, auch wenn das Dateisystem deines Laptops es nicht ist.
- Alle Owner für ein Muster müssen in derselben Zeile stehen; zwei Zeilen mit demselben Muster bedeuten, dass nur die spätere zählt.
- Owner: `@username`, `@org/team-slug` (Team in derselben Org, mit Schreibzugriff) oder eine E-Mail-Adresse, die mit einem GitHub-Konto verknüpft ist. Ein bloßer Name wie `omar` ist ungültig.
- Ein Muster ohne Owner macht passende Dateien absichtlich ohne Owner (nützlich, um generierten Code aus einem Standard-Owner herauszunehmen).
- Eine Zeile mit ungültiger Syntax (zum Beispiel ein Owner ohne @) wird übersprungen, also fallen ihre Dateien auf die vorherige passende Zeile zurück.
- Ein Owner, der nicht existiert oder keinen Schreibzugriff hat, wird einfach nicht zugewiesen. Die Zeile matcht trotzdem, also wenn keiner ihrer Owner gültig ist, enden ihre Dateien ohne Code-Owner (sie fallen NICHT auf eine frühere Zeile zurück).
## Reihenfolge: Die letzte übereinstimmende Zeile gewinnt
GitHub liest die Datei von oben nach unten und die letzte Zeile, die einen Pfad matcht, entscheidet über seine Owner. Setze den Catch-all `*` zuerst, breite Verzeichnisse als Nächstes und die spezifischsten Pfade zuletzt.
```
* @tidyq/platform # 1: Standard
/services/api/ @tidyq/backend # 2: ganze API
/services/api/billing/ @tidyq/payments # 3: spezifischer, muss NACH 2 kommen
```
Mit den Zeilen 2 und 3 vertauscht gehören Billing-Dateien nur `@tidyq/backend` (CO05).
## Mustersemantik (gitignore-artig, mit Ausnahmen)
| Muster | Matcht | Matcht nicht |
|---|---|---|
| `*` | jede Datei | - |
| `*.js` | `app.js`, `web/lib/util.js` (beliebige Tiefe) | `app.jsx` |
| `/build/logs/` | alles unter `build/logs/` im Stamm, in beliebiger Tiefe | `src/build/logs/x` |
| `docs/` oder `apps/` (ohne führenden Schrägstrich) | ein `docs`/`apps`-Verzeichnis irgendwo und alles darunter | - |
| `docs/*` | Dateien direkt in `docs/` | `docs/guide/setup.md` (verschachtelt) |
| `/docs/` oder `/docs/**` | alles unter dem Stamm-`docs/` | - |
| `**/logs` | `logs`-Verzeichnis oder -Datei in beliebiger Tiefe | - |
| `/src/**/users.py` | `src/users.py`, `src/api/v2/users.py` | - |
| `README.md` | `README.md` in beliebiger Tiefe | - |
| `/README.md` | nur das Stamm-`README.md` | `sub/README.md` |
| `src/ap?.py` | `src/app.py` | `src/apps.py` |
Faustregeln:
- Ein Muster mit einem Schrägstrich am Anfang oder in der Mitte ist am Repository-Stamm verankert; eines ohne (abgesehen von einem abschließenden Schrägstrich) schwebt und matcht in beliebiger Tiefe.
- Ein abschließender Schrägstrich bedeutet "dieses Verzeichnis und alles darin".
- Ein passendes Verzeichnis besitzt alle Dateien darunter, außer bei `dir/*`, das nur direkte Kinder abdeckt.
## Nicht unterstützt (die Zeile wird ignoriert oder markiert)
- `!pattern`-Negation - stattdessen eine spätere, spezifischere Regel verwenden (ohne Owner, um es ohne Owner zu machen).
- `[a-z]`-Zeichenbereiche - die Pfade auflisten oder `*` verwenden.
- GitLab-Abschnitte (`[Docs]`, `^[Optional]`, `[Docs][2]`) und Standard-Owner auf Abschnittsüberschriften - nur GitLab.
## Teams und Zugriff
- Das Team muss sichtbar sein und Schreibzugriff auf das Repository haben; der Benutzer muss Schreibzugriff haben.
- Ein leeres Team bekommt die Review-Anfrage, aber niemand wird benachrichtigt.
- Erforderlicher Code-Owner-Review (Branch-Schutz oder Rulesets) blockiert Merges, bis einer der Owner genehmigt (jeder einzelne Owner reicht). Wenn eine Datei ohne gültigen Owner endet, wird für sie keine Code-Owner-Genehmigung angefragt, also tut der Schutz für diese Datei still nichts.
## Eingaben holen
```
git ls-files > files.txt
gh api orgs/<org>/teams --paginate -q '.[].slug'
gh api orgs/<org>/teams/<slug>/members --paginate -q '.[].login'
gh api repos/<owner>/<repo>/collaborators --paginate -q '.[] | select(.permissions.push) | .login'
gh api repos/<owner>/<repo>/codeowners/errors
```
FILE:references/checks-and-codes.md
# Checks und Befundcodes
| Code | Schweregrad | Check | Übliche Lösung |
|---|---|---|---|
| CO01 | hoch | Owner ist nicht `@user`, `@org/team` oder eine E-Mail (z. B. `omar`); Muster kann nicht geparst werden | das @ hinzufügen / den Slug korrigieren; die ganze Zeile wird bis dahin übersprungen |
| CO02 | hoch | Benutzer nicht in deiner Benutzerliste, Team nicht in deiner Teamliste, oder Team aus einer anderen Org (nur mit `--owners`); eine zweite CO02-Zeile sagt, wenn kein gültiger Owner auf einer Zeile übrig bleibt, sodass ihre Dateien keinen Code-Owner bekommen | den Slug korrigieren (der Checker schlägt ein ähnliches Team vor), dem Team Schreibzugriff geben oder den Owner entfernen |
| CO03 | hoch: Datei ohne Owner unter einem `require_owner`-Pfad; mittel: andere Dateien ohne Regel oder nur mit unbekannten Ownern (explizit ohne Owner sind CO10) | Abdeckung | eine Regel hinzufügen oder einen Catch-all-`*`-Standard |
| CO04 | mittel | das Muster matcht keine Datei in der Liste | Pfad umbenannt oder gelöscht: Zeile aktualisieren oder entfernen |
| CO05 | mittel | jede Datei, die die Regel matcht, wird von späteren Zeilen besessen (letzter Match gewinnt) | die spezifische Regel unter die allgemeine verschieben |
| CO06 | mittel: `!`-Negation, `[ ]`-Bereiche (von GitHub ignoriert); niedrig: GitLab-Abschnittsüberschrift | nicht unterstützte Syntax | eine spätere spezifischere Regel verwenden; leere Owner-Liste = ohne Owner |
| CO07 | mittel: eine Einzelperson (kein Team) ist der effektive Owner von mehr als `max_individual_share` der Dateien; niedrig: Dateien, die einer einzelnen Person und keinem Team gehören | Bus-Faktor | ein Team oder einen zweiten Owner hinzufügen |
| CO08 | niedrig | dasselbe Muster erscheint zweimal; nur die spätere Zeile zählt | die Owner in einer Zeile zusammenführen |
| CO09 | mittel | Team hat keine Mitglieder in deiner Teamliste | Mitglieder hinzufügen oder ein anderes Team verwenden |
| CO10 | info | Muster ohne Owner: Dateien absichtlich ohne Owner | in Ordnung, wenn beabsichtigt (generierter Code, vendored Dateien) |
| CO11 | mittel | Dateien unter einem `require_owner`-Pfad werden nur von der Catch-all-`*`-Regel besessen | eine spezifische Regel für diesen Pfad hinzufügen |
## Urteil
- **Hohe Befunde**: beheben, bevor man sich auf "Require review from Code Owners" verlässt.
- **Nur mittel**: Die Datei funktioniert, aber einige Dateien gehen an die falschen Leute.
- **Gesund**: nur niedrig/info.
## Owners-Datei (`--owners`, JSON)
| Schlüssel | Bedeutung |
|---|---|
| `org` | Org-Name; Teams aus anderen Orgs werden markiert |
| `users` | Logins mit Schreibzugriff (ohne @) |
| `teams` | `{"org/slug": [Mitglieder]}` |
| `require_owner` | Muster (gleiche Syntax) für Pfade, die einen bestimmten Owner haben müssen |
| `max_individual_share` | Anteil der Dateien, den eine Person allein besitzen darf (Standard 0.4) |
| `ignore` | Muster für Dateien, die ausgelassen werden sollen (vendored Code, Lock-Dateien) |
## Grenzen des Checks
- Repository-Berechtigungen und Team-Sichtbarkeit werden nicht geprüft - mit den `gh api`-Aufrufen in der Syntax-Referenz oder dem "CODEOWNERS errors"-Banner bestätigen.
- Die Dateiliste ist ein Schnappschuss eines Branches; gegen den Basis-Branch deiner Pull-Requests ausführen.
FILE:templates/ownership-review.md
# CODEOWNERS-Fix: <Repository>
**Branch:** <Basis-Branch> **Geprüft:** <JJJJ-MM-TT> **Dateien:** <n> **Besessen vorher / nachher:** <x% / y%>
## Was falsch war
| Zeile | Regel | Problem | Auswirkung |
|---|---|---|---|
| <11> | `<pattern> <owners>` | <unbekanntes Team @org/frontend-team> | <Web-Dateien fallen auf @org/platform zurück> |
| <6> | `<pattern>` | <von Zeile 7 überschrieben> | <Payments reviewt nie Billing> |
## Änderungen
```diff
<- /services/api/billing/ @org/payments
< /services/api/ @org/backend
<+ /services/api/billing/ @org/payments>
```
## Wem gehören die wichtigsten Pfade jetzt
| Pfad | Vorher | Nachher |
|---|---|---|
| <services/api/billing/invoices.py> | <@org/backend (Zeile 7)> | <@org/payments (Zeile 8)> |
| <infra/terraform/modules/vpc/main.tf> | <@org/platform (Zeile 3, Catch-all)> | <@org/sre> |
## Braucht einen Admin
- [ ] <@org/frontend Schreibzugriff geben / Mitglieder zu @org/docs hinzufügen>
- [ ] <"Require review from Code Owners" auf <Branch> aktivieren, sobald dies gemerged ist>
## Check
`python3 scripts/codeowners_check.py .github/CODEOWNERS files.txt --owners owners.json` -> <0 hoch, n mittel (den Rest erklären)>
*Matching folgt den GitHub-Codeowners-Regeln; Repository-Berechtigungen wurden mit <gh api / Einstellungsseite | nicht geprüft> geprüft.*
FILE:examples/CODEOWNERS
# CODEOWNERS für tidyq/tidyq-app (kopiert in .github/CODEOWNERS)
# Standard-Owner für alles, was unten nicht gematcht wird
* @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
# Infrastruktur
/infra/terraform/* @tidyq/sre
/infra/k8s/ @tidyq/sre
!/infra/k8s/sandbox/
# CI und Repository-Einstellungen
/.github/workflows/ @tidyq/sre
/.github/CODEOWNERS @tidyq/platform-leads
# Docs
[Documentation]
docs/* @tidyq/docs
/docs/ @tidyq/docs
/docs/api/ omar
# Alte Mobile-App (2026 nach tidyq/tidyq-mobile verschoben)
/mobile/ @tidyq/mobile
# Daten
/data/migrations/ @jonas-b
/data/migrations/ @jonas-b @tidyq/backend
/data/fixtures/[a-z]* @tidyq/qa
# Generierter Client: gehört absichtlich niemandem
/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
# CODEOWNERS-Check: examples/CODEOWNERS
20 Regelzeile(n), 43 Datei(en), 38 besessen (88.4%), 5 ohne Owner.
**Urteil: 4 hohe(r) Befund(e) - CODEOWNERS beheben, bevor man sich auf erforderliche Reviews verlässt**
Befunde: 4 hoch, 9 mittel, 3 niedrig, 1 info.
Zeilen, deren Owner alle unbekannt sind (sie matchen trotzdem, also bekommen ihre Dateien keinen Code-Owner): 11, 31. Unbekannte Owner werden in den Owner-Spalten unten weggelassen.
## Befunde
- **HOCH CO01** Zeile 28: ungültiger Owner 'omar' (@user, @org/team oder eine E-Mail verwenden) - GitHub markiert die ganze Zeile als Fehler und überspringt sie
- **HOCH CO02** Zeile 11: unbekanntes Team @tidyq/frontend-team (meintest du @tidyq/frontend?) - GitHub wird keine Reviews von ihm anfragen
- **HOCH CO02** Zeile 11: kein gültiger Owner übrig - seine 4 Datei(en) bekommen keinen Code-Owner (sie fallen nicht auf frühere Zeilen zurück)
- **HOCH CO02** Zeile 31: unbekanntes Team @tidyq/mobile - GitHub wird keine Reviews von ihm anfragen
- **MITTEL CO03** 4 Datei(en) haben keinen Code-Owner (keine passende Regel oder nur unbekannte Owner): web/package.json, web/src/App.tsx, web/src/pages/Home.tsx, web/src/pages/Checkout.tsx
- **MITTEL CO04** Zeile 31: '/mobile/' matcht keine Datei - Pfad umbenannt oder entfernt?
- **MITTEL CO05** Zeile 6: '/services/api/billing/' (2 Datei(en)) wird vollständig von späteren Zeilen 7 überschrieben - letzter Match gewinnt, spezifische Regeln nach allgemeinen setzen
- **MITTEL CO05** Zeile 26: 'docs/*' (2 Datei(en)) wird vollständig von späteren Zeilen 27 überschrieben - letzter Match gewinnt, spezifische Regeln nach allgemeinen setzen
- **MITTEL CO05** Zeile 34: '/data/migrations/' (2 Datei(en)) wird vollständig von späteren Zeilen 35 überschrieben - letzter Match gewinnt, spezifische Regeln nach allgemeinen setzen
- **MITTEL CO06** Zeile 18: '!/infra/k8s/sandbox/' verwendet !-Negation, die CODEOWNERS nicht unterstützt - GitHub ignoriert die Zeile
- **MITTEL CO06** Zeile 36: '/data/fixtures/[a-z]*' verwendet [ ]-Zeichenbereiche, die CODEOWNERS nicht unterstützt - GitHub ignoriert die Zeile
- **MITTEL CO09** Zeile 26: Team @tidyq/docs hat keine Mitglieder - Review-Anfragen gehen ins Leere
- **MITTEL CO11** 3 Datei(en) unter Pfaden, die einen Owner haben müssen, fallen nur auf die Catch-all-'*'-Regel zurück (kein spezifischer Owner): .github/dependabot.yml, infra/terraform/modules/vpc/main.tf, infra/terraform/modules/rds/main.tf
- **NIEDRIG CO06** Zeile 25: '[Documentation]' ist eine GitLab-Abschnittsüberschrift; GitHub behandelt sie als ungültige Zeile
- **NIEDRIG CO07** 2 Datei(en) haben einen einzelnen individuellen Owner und kein Team (z. B. web/src/design-system/Button.tsx, web/src/design-system/Modal.tsx)
- **NIEDRIG CO08** Zeile 35 wiederholt das Muster '/data/migrations/' von Zeile 34 - nur die spätere Zeile zählt
- **INFO CO10** Zeile 39: '/web/src/generated/' hat keine Owner - seine 1 Datei(en) sind absichtlich ohne Owner
## Abdeckung nach Top-Level-Verzeichnis
| Verzeichnis | Dateien | Besessen | Ohne Owner | Effektive Owner |
|---|---|---|---|---|
| (Stamm) | 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) |
## Regeln
| Zeile | Muster | Owner | Gematchte Dateien | Effektiv für | Hinweis |
|---|---|---|---|---|---|
| 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/` | (keine) | - | - | 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/` | (keine) | 1 | 1 | CO10 |
## Owner-Last (effektiv)
| Owner | Dateien | Anteil |
|---|---|---|
| @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% |
## Dateien ohne Owner
- `web/package.json` (Zeile 11: nur unbekannte Owner)
- `web/src/App.tsx` (Zeile 11: nur unbekannte Owner)
- `web/src/pages/Home.tsx` (Zeile 11: nur unbekannte Owner)
- `web/src/pages/Checkout.tsx` (Zeile 11: nur unbekannte Owner)
- `web/src/generated/api-client.ts` (explizit ohne Owner, Zeile 39)
## Wem gehört
- services/api/billing/invoices.py: @tidyq/backend (Zeile 7: /services/api/)
- infra/terraform/modules/vpc/main.tf: @tidyq/platform (Zeile 3: *)
- web/src/pages/Checkout.tsx: kein gültiger Owner (Zeile 11: /web/ @tidyq/frontend-team - unbekannt)
Matching folgt den GitHub-Codeowners-Regeln (gitignore-artige Muster, letzte übereinstimmende Zeile gewinnt, `!` und `[ ]` nicht unterstützt). Owner werden nur gegen deine Owners-Datei geprüft; Schreibzugriff in den Repository-Einstellungen bestätigen.
FILE:scripts/codeowners_check.py
#!/usr/bin/env python3
"""CODEOWNERS-Abdeckungs- und Ownership-Checker (nur Stdlib, Python 3.8+).
Liest eine GitHub-Codeowners-Datei und die Repository-Dateiliste (`git ls-files`-Ausgabe), ermittelt
den effektiven Owner jeder Datei mit den GitHub-Regeln (gitignore-artige Muster, die LETZTE
übereinstimmende Zeile gewinnt) und meldet Syntaxfehler, unbekannte Owner, Dateien ohne Owner (auch unter Pfaden,
die einen Owner haben müssen), Regeln, die nichts matchen, Regeln, die vollständig von späteren
Zeilen überschrieben werden, nicht unterstützte Syntax, Duplikate, leere Teams und Owner-Konzentration.
Verwendung:
codeowners_check.py CODEOWNERS FILES.txt [--owners OWNERS.json] [--who PATH]... [--json]
codeowners_check.py --self-test
Exit: 0 = keine hohen Befunde, 1 = hohe Befunde, 2 = falsche Eingabe.
"""
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
# ---------------------------------------------------------------- Muster
def pattern_regex(pat):
"""Gibt (kompilierter Regex, nur_verzeichnis, nur_direkte_kinder) für ein Codeowners-Muster zurück."""
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)): # ein passendes Verzeichnis besitzt alles darunter
if rx.match("/".join(parts[:k])):
return True
return False
# ---------------------------------------------------------------- Parsen
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", "Zeile %d: '%s' ist eine GitLab-Abschnittsüberschrift; GitHub behandelt sie als ungültige Zeile" % (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", "Zeile %d: '%s' verwendet !-Negation, die CODEOWNERS nicht unterstützt - GitHub ignoriert die Zeile" % (n, pat), n))
rule["valid"] = False
elif "[" in pat or "]" in pat:
findings.append(("medium", "CO06", "Zeile %d: '%s' verwendet [ ]-Zeichenbereiche, die CODEOWNERS nicht unterstützt - GitHub ignoriert die Zeile" % (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", "Zeile %d: ungültiger Owner %s (@user, @org/team oder eine E-Mail verwenden) - GitHub markiert die ganze Zeile als Fehler und überspringt sie" % (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", "Zeile %d: Muster '%s' kann nicht geparst werden (%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("Dateiliste ist leer (verwenden: 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("unbekannte(r) Schlüssel in Owners-Datei: %s" % ", ".join(sorted(unknown)))
rules, findings = parse_codeowners(co_text)
if not rules:
raise InputError("CODEOWNERS hat keine Regeln")
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)
# bekannte Owner
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 = "Team %s ist nicht in 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 = "unbekanntes Team %s%s" % (o, (" (meintest du @%s?)" % close[0]) if close else "")
elif not teams[name] and key not in reported:
findings.append(("medium", "CO09", "Zeile %d: Team %s hat keine Mitglieder - Review-Anfragen gehen ins Leere" % (r["line"], o), r["line"])); reported.add(key)
elif USER_RE.match(o) and have_list and key.lstrip("@") not in users:
problem = "unbekannter Benutzer %s (nicht in der Owner-Liste / kein Schreibzugriff)" % o
if problem:
unknown_owners.add(key)
if key not in reported:
findings.append(("high", "CO02", "Zeile %d: %s - GitHub wird keine Reviews von ihm anfragen" % (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", "Zeile %d: kein gültiger Owner übrig - seine %d Datei(en) bekommen keinen Code-Owner (sie fallen nicht auf frühere Zeilen zurück)" % (ln, effective[ln]), ln))
# tote, überschattete, doppelte Regeln
seen = {}
for r in valid:
if r["pattern"] in seen:
findings.append(("low", "CO08", "Zeile %d wiederholt das Muster '%s' von Zeile %d - nur die spätere Zeile zählt" % (r["line"], r["pattern"], seen[r["pattern"]]), r["line"]))
seen[r["pattern"]] = r["line"]
if not matched_by[r["line"]]:
findings.append(("medium", "CO04", "Zeile %d: '%s' matcht keine Datei - Pfad umbenannt oder entfernt?" % (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", "Zeile %d: '%s' (%d Datei(en)) wird vollständig von späteren Zeilen %s überschrieben - letzter Match gewinnt, spezifische Regeln nach allgemeinen setzen" % (r["line"], r["pattern"], len(matched_by[r["line"]]), ", ".join(map(str, later))), r["line"]))
if not r["owners"]:
findings.append(("info", "CO10", "Zeile %d: '%s' hat keine Owner - seine %d Datei(en) sind absichtlich ohne Owner" % (r["line"], r["pattern"], effective[r["line"]]), r["line"]))
# Abdeckung
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 Datei(en) unter Pfaden, die einen Owner haben müssen, sind ohne Owner: %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 Datei(en) unter Pfaden, die einen Owner haben müssen, fallen nur auf die Catch-all-'%s'-Regel zurück (kein spezifischer Owner): %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 Datei(en) haben keinen Code-Owner (keine passende Regel oder nur unbekannte Owner): %s" % (len(rest), ", ".join(rest[:6]) + (" ..." if len(rest) > 6 else "")), 0))
# Owner-Last
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 ist der Owner von %d von %d Dateien (%.0f%%) als Einzelperson - Bus-Faktor; ein Team oder einen zweiten Owner hinzufügen" % (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 Datei(en) haben einen einzelnen individuellen Owner und kein Team (z. B. %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: kein Owner (keine passende Regel)" % path
if r["owners"] and not r["ok"]:
return "%s: kein gültiger Owner (Zeile %d: %s %s - unbekannt)" % (path, r["line"], r["pattern"], " ".join(r["owners"]))
return "%s: %s (Zeile %d: %s)" % (path, " ".join(r["ok"]) or "explizit ohne Owner", r["line"], r["pattern"])
# ---------------------------------------------------------------- Bericht
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 = ["# CODEOWNERS-Check: %s" % title, ""]
L.append("%d Regelzeile(n), %d Datei(en), %d besessen (%.1f%%), %d ohne Owner." % (len(res["rules"]), len(files), owned, 100.0 * owned / len(files), len(res["unowned"])))
L.append("")
L.append("**Urteil: %s**" % ("%d hohe(r) Befund(e) - CODEOWNERS beheben, bevor man sich auf erforderliche Reviews verlässt" % cnt["high"] if cnt["high"] else ("die mittleren Befunde prüfen" if cnt["medium"] else "CODEOWNERS sieht gesund aus")))
L.append("")
L.append("Befunde: %d hoch, %d mittel, %d niedrig, %d info." % (cnt["high"], cnt["medium"], cnt["low"], cnt["info"]))
L.append("")
if res["no_valid_owner_lines"]:
L.append("Zeilen, deren Owner alle unbekannt sind (sie matchen trotzdem, also bekommen ihre Dateien keinen Code-Owner): %s. Unbekannte Owner werden in den Owner-Spalten unten weggelassen." % ", ".join(map(str, res["no_valid_owner_lines"])))
L.append("")
L += ["## Befunde", ""]
for s, c, msg, _ in f:
L.append("- **%s %s** %s" % (s.upper(), c, msg))
L += ["", "## Abdeckung nach Top-Level-Verzeichnis", "", "| Verzeichnis | Dateien | Besessen | Ohne Owner | Effektive Owner |", "|---|---|---|---|---|"]
dirs = OrderedDict()
for p in files:
d = p.split("/")[0] + "/" if "/" in p else "(Stamm)"
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 += ["", "## Regeln", "", "| Zeile | Muster | Owner | Gematchte Dateien | Effektiv für | Hinweis |", "|---|---|---|---|---|---|"]
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 "(keine)", m, e, ", ".join(sorted(set(notes.get(r["line"], [])))) or ""))
L += ["", "## Owner-Last (effektiv)", "", "| Owner | Dateien | Anteil |", "|---|---|---|"]
for o, n in res["load"].most_common():
L.append("| %s | %d | %.0f%% |" % (o, n, 100.0 * n / len(files)))
if res["unowned"]:
L += ["", "## Dateien ohne Owner", ""]
for p in res["unowned"]:
e = res["eff"][p]
L.append("- `%s`%s" % (p, "" if not e else (" (explizit ohne Owner, Zeile %d)" % e["line"] if not e["owners"] else " (Zeile %d: nur unbekannte Owner)" % e["line"])))
if whos:
L += ["", "## Wem gehört", ""]
for p in whos:
L.append("- " + who(res, p))
L += ["", "Matching folgt den GitHub-Codeowners-Regeln (gitignore-artige Muster, letzte übereinstimmende Zeile gewinnt, `!` und `[ ]` nicht unterstützt). Owner werden nur gegen deine Owners-Datei geprüft; Schreibzugriff in den Repository-Einstellungen bestätigen."]
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"]]}
# ---------------------------------------------------------------- Selbsttest
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("* matcht alles", owner("* @a", F, "src/api/v2/users.py") == ["@a"])
t("letzte übereinstimmende Zeile gewinnt", owner("* @a\n/src/ @b", F, "src/app.py") == ["@b"] and owner("* @a\n/src/ @b", F, "README.md") == ["@a"])
t("*.js matcht in beliebiger Tiefe", owner("*.js @js", F, "web/lib/util.js") == ["@js"])
t("/docs/ besitzt verschachtelte Dateien", owner("/docs/ @d", F, "docs/guide/setup.md") == ["@d"])
t("docs/* besitzt keine verschachtelten Dateien", owner("docs/* @d", F, "docs/guide/setup.md") is None and owner("docs/* @d", F, "docs/index.md") == ["@d"])
t("unverankertes Verzeichnis 'api/' matcht überall", owner("api/ @x", F, "src/api/v2/users.py") == ["@x"])
t("** in der Mitte", owner("/src/**/users.py @u", F, "src/api/v2/users.py") == ["@u"])
t("verankertes /README.md nur im Stamm", owner("/README.md @r", "README.md\nsub/README.md\n", "sub/README.md") is None)
t("? matcht ein Zeichen", owner("src/ap?.py @q", F, "src/app.py") == ["@q"])
t("keine Owner = explizit ohne Owner", owner("* @a\n/web/", F, "web/app.js") == [] and ("info", "CO10") in codes("* @a\n/web/", F))
t("ungültiger Owner -> CO01 hoch, Zeile übersprungen", ("high", "CO01") in codes("* @a\n/src/ alice", F) and owner("* @a\n/src/ alice", F, "src/app.py") == ["@a"])
t("Negation -> CO06 mittel", ("medium", "CO06") in codes("* @a\n!/src/", F))
t("[ ]-Bereich -> CO06 mittel", ("medium", "CO06") in codes("* @a\n/src/[ab]* @b", F))
t("GitLab-Abschnitt -> CO06 niedrig", ("low", "CO06") in codes("[Docs]\n* @a", F))
cfg = {"org": "acme", "users": ["a", "b"], "teams": {"acme/web": ["a"], "acme/empty": []}}
t("unbekanntes Team -> CO02 hoch, Dateien bekommen keinen Owner (kein Fallback)", ("high", "CO02") in codes("* @a\n/web/ @acme/webb", F, cfg) and owner("* @a\n/web/ @acme/webb", F, "web/app.js", cfg) == [] and ("medium", "CO03") in codes("* @a\n/web/ @acme/webb", F, cfg))
t("unbekannter Owner verworfen, gültiger Co-Owner behalten", owner("* @a\n/web/ @acme/webb @b", F, "web/app.js", cfg) == ["@b"])
t("Team aus anderer Org -> CO02", ("high", "CO02") in codes("* @other/web", F, cfg))
t("unbekannter Benutzer -> CO02", ("high", "CO02") in codes("* @zed", F, cfg))
t("leeres Team -> CO09", ("medium", "CO09") in codes("* @a\n/web/ @acme/empty", F, cfg))
t("E-Mail-Owner akzeptiert", ("high", "CO01") not in codes("* dev@acme.io", F))
t("tote Regel -> CO04", ("medium", "CO04") in codes("* @a\n/legacy/ @b", F))
t("überschattete Regel -> CO05", ("medium", "CO05") in codes("/src/api/ @b\n/src/ @a", F))
t("teilweise überschrieben ist nicht CO05", ("medium", "CO05") not in codes("/src/ @a\n/src/api/ @b", F))
t("doppeltes Muster -> CO08", ("low", "CO08") in codes("* @a\n/src/ @b\n/src/ @c", F))
t("Datei ohne Owner -> CO03 mittel", ("medium", "CO03") in codes("/src/ @a", F))
t("require_owner-Pfad ohne Owner -> CO03 hoch", ("high", "CO03") in codes("/src/ @acme/web", F, dict(cfg, require_owner=[".github/"])))
t("erforderlicher Pfad nur von '*' besessen -> CO11", ("medium", "CO11") in codes("* @a\n/src/ @b", F, {"require_owner": [".github/"]}))
t("tote Regel mit unbekanntem Owner trotzdem -> CO04", ("medium", "CO04") in codes("* @a\n/legacy/ @acme/zz", F, cfg))
t("Einzelpersonen-Owner-Konzentration -> CO07", ("medium", "CO07") in codes("* @a", F, {"max_individual_share": 0.5}))
t("Ignore-Liste entfernt Dateien", len(analyse("* @a", F, {"ignore": ["docs/"]})["files"]) == 7)
r = analyse("* @a\n/src/ @b", F)
t("--who meldet Zeile", who(r, "src/api/routes.py") == "src/api/routes.py: @b (Zeile 2: /src/)")
t("./-Präfix entfernt, .github behalten", analyse("* @a", "./.github/x.yml\n")["files"] == [".github/x.yml"])
t("Bericht hat Urteil und Abdeckungstabelle", "**Urteil:" in report(r, "t") and "## Abdeckung nach Top-Level-Verzeichnis" in report(r, "t"))
try:
analyse("* @a", F, {"orgs": "x"}); ok = False
except InputError:
ok = True
t("unbekannter Owners-Datei-Schlüssel abgelehnt", ok)
for name, ok in tests:
print(("PASS " if ok else "FAIL ") + name)
passed = sum(ok for _, ok in tests)
print("%d/%d bestanden" % (passed, len(tests)))
return 0 if passed == len(tests) else 1
def main(argv=None):
ap = argparse.ArgumentParser(description="Prüft eine GitHub-Codeowners-Datei gegen die Repository-Dateiliste.")
ap.add_argument("codeowners", nargs="?")
ap.add_argument("files", nargs="?", help="Dateiliste, z. B. git ls-files > files.txt")
ap.add_argument("--owners", help="JSON mit org, users, teams, require_owner, max_individual_share, ignore")
ap.add_argument("--who", action="append", default=[], help="zeigt den effektiven Owner eines Pfads (wiederholbar)")
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("Owners-Datei ist kein gültiges JSON: %s" % e)
res = analyse(co, fl, cfg)
except (OSError, InputError) as e:
print("Fehler: %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())
Melde dich an, um den vollständigen Prompt zu sehen
Weiter mit:
Mit der Anmeldung akzeptierst du unsere Nutzungsbedingungen und Datenschutz
Verwendung
Dieser Prompt ist für die Verwendung mit coding gedacht. Kopiere den Inhalt oben und füge ihn in dein bevorzugtes KI-Tool ein.
Für beste Ergebnisse passe die Platzhalter (eckige Klammern oder Großbuchstaben) an deine Anforderungen an.
Diskussion
0 Kommentare