CODEOWNERSカバレッジとオーナーシップチェッカー
フリーのプロンプト百科事典 Wikiprompt より
CODEOWNERSカバレッジとオーナーシップチェッカー GitHub CODEOWNERSファイルを監査するための完全なスキルで、カバレッジ、無効なオーナー、順序の誤りをチェックし、動作するPythonチェッカーとテンプレートを含みます。エンジニアリングプラットフォームチームに最適です。
プロンプト内容保存
🌐
---
name: codeowners-coverage-and-ownership-checker
description: GitHubのCODEOWNERSファイルを実際のリポジトリファイルリストと照合し、全ファイルの所有者を特定します(最後に一致した行が優先されます)。その後、無効または不明な所有者、所有者がいないファイル、キャッチオールルールに委ねられている重要なパス、何にも一致しないルールや後続の行によって完全に上書きされるルール、サポートされていない!および[ ]構文、重複、空のチーム、一人によるボトルネックを報告します。オフラインのstdlib Pythonチェッカーが含まれています。
---
# CODEOWNERSカバレッジおよび所有権チェッカー
あなたはエンジニアリングチームがCODEOWNERSファイルを信頼できるように支援します。必須レビューと自動割り当てレビュアーは、すべての行が有効で、すべての所有者が存在してアクセス権を持ち、ルールが正しい順序である場合にのみ機能します。実際には、ファイルは静かに所有者を失います:チームが改名され、そのファイルは静かにレビュアーがいなくなり、特定のルールが一般的なルールの上にあり適用されず、`docs/*`が`docs/guide/`をカバーせず、.gitignoreからコピーされた`!`否定が何もせず、請求コードの唯一の所有者が休暇中の一人である場合があります。あなたはすべてのファイルの実効所有者を計算し、何が問題でどう修正するかを正確に示します。
あなたはファイルをGitHubの文書化されたCODEOWNERSの動作とユーザーが提供する所有者リストに対してチェックします。リポジトリの権限は見えません。その旨を伝え、リポジトリ設定または`gh api`で書き込みアクセスを確認するようユーザーに依頼してください。
## このスキルのファイル
- `scripts/codeowners_check.py` - CODEOWNERSを解析し(コメント、インラインコメント、エスケープされたスペース)、`git ls-files`のすべてのパスをGitHubのgitignoreスタイルのルール(アンカーおよびフローティングパターン、`*`、`**`、`?`、ディレクトリパターン、`dir/*` = 直接の子のみ、最後に一致した行が優先)と照合し、無効な構文の行をスキップし、GitHubと同様に不明な所有者を除外し、ディレクトリごとのカバレッジ、ルールテーブル、所有者負荷、`--who PATH`ルックアップを含む調査結果CO01-CO11を報告します(Python 3.8+標準ライブラリのみ)
- `references/codeowners-syntax-and-matching.md` - 例付きのパターンセマンティクス、順序ルール、所有者形式、ファイルの場所、GitHubの制限
- `references/checks-and-codes.md` - すべての調査結果コード、その重大度、通常の修正方法
- `templates/ownership-review.md` - CODEOWNERS修正のためのプルリクエスト説明/レビューノート
- `examples/CODEOWNERS` - 典型的な間違いを含むWebアプリモノレポの現実的なファイル(改名されたチーム、一般的なルールの上の特定のルール、`docs/*`、否定、文字範囲、GitLabセクション、@なしの所有者、削除されたディレクトリ、重複行、明示的に所有者がいない生成コード)
- `examples/files.txt` - リポジトリファイルリスト(`git ls-files`出力、43ファイル)
- `examples/owners.json` - 組織、アクセス権のあるユーザー、メンバーがいるチーム、特定の所有者が必要なパス、個人シェアの制限
- `examples/ownership-report.md` - 例に対する実際のチェッカー出力
## ワークフロー
### 1. 入力を収集する
CODEOWNERSファイル(`.github/`、リポジトリルート、または`docs/`から;GitHubはその順序で最初に見つけたものを使います)とデフォルトブランチのファイルリストを尋ねます:`git ls-files > files.txt`。次に所有者リストを尋ねます:組織名、書き込みアクセス権のあるユーザー、メンバーがいるチーム(`gh api orgs/<org>/teams`、`gh api orgs/<org>/teams/<team>/members`)、および特定の所有者が必要なパス(CIワークフロー、インフラ、支払い、マイグレーション、CODEOWNERS自体)。それらを`examples/owners.json`のようなJSONに書き込みます。所有者リストがなくても、チェッカーは構文、順序、カバレッジをチェックします。
### 2. チェッカーを実行する
```
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
```
終了コード0 = 高重大度の調査結果なし、1 = 高重大度の調査結果あり、2 = 不正な入力。シェルがない場合は、`references/codeowners-syntax-and-matching.md`を使用して各重要なパスについてルールをボトムアップで確認します:下から最初に一致する行が所有者です。
### 3. 結果を読む
- 高優先度:CO01(@なしの名前など無効な所有者形式):GitHubはその行全体を無効な構文としてスキップするため、それらのファイルは以前のルールにフォールバックします。CO02(不明なユーザーまたはチーム、アクセス権なし):その所有者は要求されません。行に有効な所有者が残っていない場合、行は依然として一致し、そのファイルにはコード所有者がまったくいません。CO03高:必須所有パス下のファイルに所有者がいません。
- 中優先度:CO05(ルールが完全に上書き - 通常は一般的なルールの上の特定のパス)、CO04(ルールが何にも一致しない - 改名または削除されたパス)、CO06(`!`または`[ ]` - 無視)、CO11(重要なパスが`*`のみでカバーされている)、CO09(空のチーム)、CO07(一人が多すぎるファイルを所有)、CO03中(ルールのないファイル)。
- 低/情報:GitLabセクションヘッダー、単独所有、重複パターン、明示的に所有者がいないパス(意図的なら問題なし)。
- 人々が気にするパスに`--who`を使用して、修正前後を確認します。
### 4. 修正を提案する
修正されたCODEOWNERSを書きます:一般的なルールを最初に、特定のルールを後に;`!`除外をより具体的な後のルールに置き換えます(空の所有者リストはパスを意図的に所有者なしにします);ネストされたファイルをカバーする必要がある場合は`dir/*`の代わりに`/dir/`を使用します;チームスラッグを修正します;一人のパスに2番目の所有者またはチームを追加します;CODEOWNERS自体に行を追加します。高重大度の調査結果がなくなるまで再実行し、`templates/ownership-review.md`を記入します。
### 5. 配信する
調査結果を平易な言葉で返し、修正されたファイル(または差分)、主要パスの修正前後の所有者、および管理者が必要なフォローアップ(チームアクセスの付与、空のチームへのメンバー追加、ファイルがクリーンになったらブランチ保護で「コード所有者からのレビューを要求」を有効化)を返します。
## 例の結果
例では43ファイル中38のみがコード所有者を持ち、4つの高重大度の調査結果があります:`/web/`は`@tidyq/frontend-team`を指定しています(チームは`@tidyq/frontend`)ため、package.json、App.tsx、Home.tsx、Checkout.tsxにはコード所有者がまったくいません;`/docs/api/`は@なしの`omar`を使用しておりスキップされます;`/mobile/`は削除されたチームを指定し、どのファイルにも一致しません。`/services/api/billing/`は`/services/api/`の上にあり適用されないため、支払いは請求コードをレビューしません;`docs/*`は`/docs/`によって上書きされ、`@tidyq/docs`にはメンバーがいません;`!`および`[a-z]`行は無視されます;Terraformモジュールとdependabot.ymlは`/infra/terraform/*`が直接の子のみをカバーするため`*`にのみフォールバックします。
## ルール
- 最後に一致した行が優先:常にボトムアップで推論し、どの行がパスを所有するかを述べます。
- 無効な構文の行は完全にスキップされます。不明またはアクセス権のない所有者は単に要求されず、不明な所有者のみの行はそのファイルに対して依然として優先され、コード所有者なしのままになります。その場合、以前のルールが引き継ぐと想定しないでください。
- ユーザーが提供したリストに対してのみ所有者を不明と呼びます。それ以外の場合は「確認できませんでした」と言い、`gh api`呼び出しを提案します。
- 重要なものには個人よりもチームを優先します。チームの同意なしに人を所有権から削除しないでください。
- ファイルリストにないパスのパターンを発明しないでください。新しいディレクトリが来るかどうか尋ねてください。
- リポジトリファイルリストとチームメンバーシップを内部情報として扱います。
FILE:references/codeowners-syntax-and-matching.md
# CODEOWNERS構文とマッチング(GitHub)
## ファイルの場所
GitHubは`.github/`、次にリポジトリルート、次に`docs/`で`CODEOWNERS`を探し、プルリクエストがターゲットとするブランチ(ベースブランチ)で最初に見つけたものを使います。3MBを超えるファイルは無視されます。GitHubはWeb UIでファイルを開いたときに無効な行のエラーを表示します(「CODEOWNERSエラー」バナー)および`gh api repos/<owner>/<repo>/codeowners/errors`経由でも表示します。
## 行形式
```
<パターン> <所有者> [<所有者> ...] # オプションのコメント
```
- 行頭(または空白の後)の`#`はコメントを開始します。.gitignoreとは異なり、`#`で始まるパターンを`\#`としてエスケープすることはCODEOWNERSでは機能しません。
- パスは大文字と小文字を区別します。ラップトップのファイルシステムが区別しなくてもです。
- パターンのすべての所有者は同じ行になければなりません。同じパターンの2行は後の行のみがカウントされます。
- 所有者:`@username`、`@org/team-slug`(同じ組織のチーム、書き込みアクセス権あり)、またはGitHubアカウントにリンクされたメールアドレス。`omar`のような裸の名前は無効です。
- 所有者のいないパターンは、一致するファイルを意図的に所有者なしにします(デフォルト所有者から生成コードを除外するのに便利です)。
- 無効な構文の行(たとえば@なしの所有者)はスキップされるため、そのファイルは以前の一致する行にフォールバックします。
- 存在しないか書き込みアクセス権のない所有者は単に割り当てられません。行は依然として一致するため、その所有者のいずれも有効でない場合、そのファイルはコード所有者なしになります(以前の行にフォールバックしません)。
## 順序:最後に一致した行が優先
GitHubはファイルを上から下に読み、パスに一致する最後の行がその所有者を決定します。キャッチオール`*`を最初に、広いディレクトリを次に、最も特定のパスを最後に置きます。
```
* @tidyq/platform # 1: デフォルト
/services/api/ @tidyq/backend # 2: API全体
/services/api/billing/ @tidyq/payments # 3: より特定、2の後に来なければならない
```
行2と3が入れ替わると、請求ファイルは`@tidyq/backend`のみによって所有されます(CO05)。
## パターンセマンティクス(gitignoreスタイル、例外あり)
| パターン | 一致 | 一致しない |
|---|---|---|
| `*` | すべてのファイル | - |
| `*.js` | `app.js`、`web/lib/util.js`(任意の深さ) | `app.jsx` |
| `/build/logs/` | ルートの`build/logs/`の下のすべて、任意の深さ | `src/build/logs/x` |
| `docs/`または`apps/`(先頭スラッシュなし) | 任意の場所の`docs`/`apps`ディレクトリとその下のすべて | - |
| `docs/*` | `docs/`内のファイル直接 | `docs/guide/setup.md`(ネスト) |
| `/docs/`または`/docs/**` | ルート`docs/`の下のすべて | - |
| `**/logs` | 任意の深さの`logs`ディレクトリまたはファイル | - |
| `/src/**/users.py` | `src/users.py`、`src/api/v2/users.py` | - |
| `README.md` | 任意の深さの`README.md` | - |
| `/README.md` | ルート`README.md`のみ | `sub/README.md` |
| `src/ap?.py` | `src/app.py` | `src/apps.py` |
経験則:
- 先頭または途中にスラッシュがあるパターンはリポジトリルートにアンカーされます。ないもの(末尾のスラッシュを除く)はフロートし、任意の深さで一致します。
- 末尾のスラッシュは「このディレクトリとその中のすべて」を意味します。
- 一致するディレクトリはその下のすべてのファイルを所有します。ただし、`dir/*`は直接の子のみをカバーします。
## サポートされていない(行は無視またはフラグ付けされます)
- `!pattern`否定 - 代わりに後のより特定のルールを使用します(所有者なしにして所有者なしにします)。
- `[a-z]`文字範囲 - パスをリストするか`*`を使用します。
- GitLabセクション(`[Docs]`、`^[Optional]`、`[Docs][2]`)およびセクションヘッダーのデフォルト所有者 - GitLabのみ。
## チームとアクセス
- チームは可視で、リポジトリへの書き込みアクセス権が必要です。ユーザーは書き込みアクセス権が必要です。
- 空のチームはレビューリクエストを受け取りますが、誰も通知されません。
- 必須コード所有者レビュー(ブランチ保護またはルールセット)は、所有者の1人が承認するまでマージをブロックします(任意の1人の所有者で十分です)。ファイルが有効な所有者なしになった場合、そのファイルに対してコード所有者の承認は要求されないため、保護はそのファイルに対して静かに何もしません。
## 入力の取得
```
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
# チェックと調査結果コード
| コード | 重大度 | チェック | 通常の修正 |
|---|---|---|---|
| CO01 | 高 | 所有者が`@user`、`@org/team`、またはメールではない(例:`omar`);パターンを解析できない | @を追加/スラッグを修正;それまで行全体がスキップされます |
| CO02 | 高 | ユーザーがユーザーリストにない、チームがチームリストにない、または別の組織のチーム(`--owners`のみ);2番目のCO02行は、行に有効な所有者が残っていない場合、そのファイルがコード所有者なしになることを示します | スラッグを修正(チェッカーが近いチームを提案)、チームに書き込みアクセスを付与、または所有者を削除 |
| CO03 | 高:`require_owner`パス下の所有者なしファイル;中:ルールがないか不明な所有者のみの他のファイル(明示的に所有者なしのファイルはCO10) | カバレッジ | ルール、またはキャッチオール`*`デフォルトを追加 |
| CO04 | 中 | パターンがリスト内のどのファイルにも一致しない | パスが改名または削除された:行を更新または削除 |
| CO05 | 中 | ルールが一致するすべてのファイルが後の行によって所有されている(最後の一致が優先) | 特定のルールを一般的なルールの下に移動 |
| CO06 | 中:`!`否定、`[ ]`範囲(GitHubによって無視);低:GitLabセクションヘッダー | サポートされていない構文 | 後のより特定のルールを使用;空の所有者リスト = 所有者なし |
| CO07 | 中:個人(チームではない)が`max_individual_share`を超えるファイルの実効所有者;低:単一の個人とチームなしで所有されるファイル | バス係数 | チームまたは2番目の所有者を追加 |
| CO08 | 低 | 同じパターンが2回表示される;後の行のみがカウントされる | 所有者を1行にマージ |
| CO09 | 中 | チームリストにメンバーがいないチーム | メンバーを追加するか別のチームを使用 |
| CO10 | 情報 | 所有者のないパターン:意図的に所有者なしのファイル | 意図的なら問題なし(生成コード、ベンダーファイル) |
| CO11 | 中 | `require_owner`パス下のファイルがキャッチオール`*`ルールのみで所有されている | そのパスに特定のルールを追加 |
## 判定
- **高重大度の調査結果**:「コード所有者からのレビューを要求」に依存する前に修正します。
- **中のみ**:ファイルは機能しますが、一部のファイルが間違った人に行きます。
- **健全**:低/情報のみ。
## 所有者ファイル(`--owners`、JSON)
| キー | 意味 |
|---|---|
| `org` | 組織名;他の組織のチームはフラグ付けされます |
| `users` | 書き込みアクセス権のあるログイン(@なし) |
| `teams` | `{"org/slug": [メンバー]}` |
| `require_owner` | 特定の所有者が必要なパスのパターン(同じ構文) |
| `max_individual_share` | 一人が単独で所有できるファイルの割合(デフォルト0.4) |
| `ignore` | 除外するファイルのパターン(ベンダーコード、ロックファイル) |
## チェックの制限
- リポジトリの権限とチームの可視性はチェックされません - 構文リファレンスの`gh api`呼び出しまたは「CODEOWNERSエラー」バナーで確認してください。
- ファイルリストは1つのブランチのスナップショットです。プルリクエストのベースブランチに対して実行してください。
FILE:templates/ownership-review.md
# CODEOWNERS修正:<リポジトリ>
**ブランチ:** <ベースブランチ> **チェック日:** <YYYY-MM-DD> **ファイル:** <n> **所有前/後:** <x% / y%>
## 何が問題だったか
| 行 | ルール | 問題 | 影響 |
|---|---|---|---|
| <11> | `<パターン> <所有者>` | <不明なチーム @org/frontend-team> | <Webファイルは@org/platformにフォールバック> |
| <6> | `<パターン>` | <行7によって上書き> | <支払いは請求をレビューしない> |
## 変更
```diff
<- /services/api/billing/ @org/payments
< /services/api/ @org/backend
<+ /services/api/billing/ @org/payments>
```
## 主要パスの現在の所有者
| パス | 前 | 後 |
|---|---|---|
| <services/api/billing/invoices.py> | <@org/backend(行7)> | <@org/payments(行8)> |
| <infra/terraform/modules/vpc/main.tf> | <@org/platform(行3、キャッチオール)> | <@org/sre> |
## 管理者が必要
- [ ] <@org/frontendに書き込みアクセスを付与 / @org/docsにメンバーを追加>
- [ ] <このマージ後に<ブランチ>で「コード所有者からのレビューを要求」を有効化>
## チェック
`python3 scripts/codeowners_check.py .github/CODEOWNERS files.txt --owners owners.json` -> <0高、n中(残りを説明)>
*マッチングはGitHubのCODEOWNERSルールに従います。リポジトリの権限は<gh api / 設定ページ | 未チェック>で確認しました。*
FILE:examples/CODEOWNERS
# tidyq/tidyq-appのCODEOWNERS(.github/CODEOWNERSにコピー)
# 以下で一致しないすべてのデフォルト所有者
* @tidyq/platform
# バックエンド
/services/api/billing/ @tidyq/payments
/services/api/ @tidyq/backend
/services/worker/ @tidyq/backend @marta-k
# フロントエンド
/web/ @tidyq/frontend-team
/web/src/design-system/ @lena-s
*.css @tidyq/frontend
# インフラ
/infra/terraform/* @tidyq/sre
/infra/k8s/ @tidyq/sre
!/infra/k8s/sandbox/
# CIとリポジトリ設定
/.github/workflows/ @tidyq/sre
/.github/CODEOWNERS @tidyq/platform-leads
# ドキュメント
[Documentation]
docs/* @tidyq/docs
/docs/ @tidyq/docs
/docs/api/ omar
# 古いモバイルアプリ(2026年にtidyq/tidyq-mobileに移動)
/mobile/ @tidyq/mobile
# データ
/data/migrations/ @jonas-b
/data/migrations/ @jonas-b @tidyq/backend
/data/fixtures/[a-z]* @tidyq/qa
# 生成クライアント:意図的に誰も所有しない
/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チェック:examples/CODEOWNERS
20ルール行、43ファイル、38所有(88.4%)、5所有者なし。
**判定:4つの高重大度の調査結果 - 必須レビューに依存する前にCODEOWNERSを修正**
調査結果:4高、9中、3低、1情報。
所有者がすべて不明な行(依然として一致するため、そのファイルはコード所有者なしになります):11、31。不明な所有者は以下の所有者列から除外されます。
## 調査結果
- **高 CO01** 行28:無効な所有者 'omar'(@user、@org/team、またはメールを使用)- GitHubは行全体をエラーとしてフラグ付けしスキップします
- **高 CO02** 行11:不明なチーム @tidyq/frontend-team(@tidyq/frontendのことですか?)- GitHubはそれからレビューを要求しません
- **高 CO02** 行11:有効な所有者が残っていない - その4ファイルはコード所有者なしになります(以前の行にフォールバックしません)
- **高 CO02** 行31:不明なチーム @tidyq/mobile - GitHubはそれからレビューを要求しません
- **中 CO03** 4ファイルにコード所有者がいません(一致するルールがない、または不明な所有者のみ):web/package.json、web/src/App.tsx、web/src/pages/Home.tsx、web/src/pages/Checkout.tsx
- **中 CO04** 行31:'/mobile/'がどのファイルにも一致しない - パスが改名または削除されましたか?
- **中 CO05** 行6:'/services/api/billing/'(2ファイル)が後の行7によって完全に上書き - 最後の一致が優先、特定のルールを一般的なルールの後に置く
- **中 CO05** 行26:'docs/*'(2ファイル)が後の行27によって完全に上書き - 最後の一致が優先、特定のルールを一般的なルールの後に置く
- **中 CO05** 行34:'/data/migrations/'(2ファイル)が後の行35によって完全に上書き - 最後の一致が優先、特定のルールを一般的なルールの後に置く
- **中 CO06** 行18:'!/infra/k8s/sandbox/'はCODEOWNERSがサポートしない!否定を使用 - GitHubは行を無視
- **中 CO06** 行36:'/data/fixtures/[a-z]*'はCODEOWNERSがサポートしない[ ]文字範囲を使用 - GitHubは行を無視
- **中 CO09** 行26:チーム @tidyq/docsにメンバーがいない - レビューリクエストはどこにも行かない
- **中 CO11** 所有者が必要なパス下の3ファイルがキャッチオール'*'ルールのみにフォールバック(特定の所有者なし):.github/dependabot.yml、infra/terraform/modules/vpc/main.tf、infra/terraform/modules/rds/main.tf
- **低 CO06** 行25:'[Documentation]'はGitLabセクションヘッダー;GitHubはそれを無効な行として扱う
- **低 CO07** 2ファイルが単一の個人所有者とチームなし(例:web/src/design-system/Button.tsx、web/src/design-system/Modal.tsx)
- **低 CO08** 行35が行34のパターン'/data/migrations/'を繰り返す - 後の行のみがカウントされる
- **情報 CO10** 行39:'/web/src/generated/'に所有者がいない - その1ファイルは意図的に所有者なし
## トップレベルディレクトリごとのカバレッジ
| ディレクトリ | ファイル | 所有 | 所有者なし | 実効所有者 |
|---|---|---|---|---|
| (ルート) | 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) |
## ルール
| 行 | パターン | 所有者 | 一致ファイル | 実効対象 | 注記 |
|---|---|---|---|---|---|
| 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/` | (なし) | - | - | 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/` | (なし) | 1 | 1 | CO10 |
## 所有者負荷(実効)
| 所有者 | ファイル | シェア |
|---|---|---|
| @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% |
## 所有者なしファイル
- `web/package.json`(行11:不明な所有者のみ)
- `web/src/App.tsx`(行11:不明な所有者のみ)
- `web/src/pages/Home.tsx`(行11:不明な所有者のみ)
- `web/src/pages/Checkout.tsx`(行11:不明な所有者のみ)
- `web/src/generated/api-client.ts`(明示的に所有者なし、行39)
## 誰が所有
- services/api/billing/invoices.py:@tidyq/backend(行7:/services/api/)
- infra/terraform/modules/vpc/main.tf:@tidyq/platform(行3:*)
- web/src/pages/Checkout.tsx:有効な所有者なし(行11:/web/ @tidyq/frontend-team - 不明)
マッチングはGitHub CODEOWNERSルールに従います(gitignoreスタイルのパターン、最後に一致した行が優先、`!`および`[ ]`はサポートされていません)。所有者は所有者ファイルに対してのみチェックされます。リポジトリ設定で書き込みアクセスを確認してください。
FILE:scripts/codeowners_check.py
#!/usr/bin/env python3
"""CODEOWNERSカバレッジおよび所有権チェッカー(標準ライブラリのみ、Python 3.8+)。
GitHub CODEOWNERSファイルとリポジトリファイルリスト(`git ls-files`出力)を読み取り、GitHubのルール(gitignoreスタイルのパターン、最後に一致した行が優先)で各ファイルの実効所有者を計算し、構文エラー、不明な所有者、所有者なしファイル(所有者が必要なパス下も)、何にも一致しないルール、後の行によって完全に上書きされるルール、サポートされていない構文、重複、空のチーム、所有者の集中を報告します。
使用法:
codeowners_check.py CODEOWNERS FILES.txt [--owners OWNERS.json] [--who PATH]... [--json]
codeowners_check.py --self-test
終了:0 = 高重大度の調査結果なし、1 = 高重大度の調査結果あり、2 = 不正な入力。
"""
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
# ---------------------------------------------------------------- パターン
def pattern_regex(pat):
"""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)): # 一致するディレクトリはその下のすべてを所有
if rx.match("/".join(parts[:k])):
return True
return False
# ---------------------------------------------------------------- 解析
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", "line %d: '%s' is a GitLab section header; GitHub treats it as an invalid line" % (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", "line %d: '%s' uses ! negation, which CODEOWNERS does not support - GitHub ignores the line" % (n, pat), n))
rule["valid"] = False
elif "[" in pat or "]" in pat:
findings.append(("medium", "CO06", "line %d: '%s' uses [ ] character ranges, which CODEOWNERS does not support - GitHub ignores the line" % (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", "line %d: invalid owner %s (use @user, @org/team or an email) - GitHub flags the whole line as an error and skips it" % (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", "line %d: pattern '%s' cannot be parsed (%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("file list is empty (use: git ls-files > files.txt)")
return files
# ---------------------------------------------------------------- 分析
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("unknown key(s) in owners file: %s" % ", ".join(sorted(unknown)))
rules, findings = parse_codeowners(co_text)
if not rules:
raise InputError("CODEOWNERS has no rules")
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)
# 既知の所有者
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 is not 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 = "unknown team %s%s" % (o, (" (did you mean @%s?)" % close[0]) if close else "")
elif not teams[name] and key not in reported:
findings.append(("medium", "CO09", "line %d: team %s has no members - review requests go nowhere" % (r["line"], o), r["line"])); reported.add(key)
elif USER_RE.match(o) and have_list and key.lstrip("@") not in users:
problem = "unknown user %s (not in the owners list / no write access)" % o
if problem:
unknown_owners.add(key)
if key not in reported:
findings.append(("high", "CO02", "line %d: %s - GitHub will not request reviews from it" % (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", "line %d: no valid owner left - its %d file(s) get no code owner (they do not fall back to earlier lines)" % (ln, effective[ln]), ln))
# デッド、シャドウ、重複ルール
seen = {}
for r in valid:
if r["pattern"] in seen:
findings.append(("low", "CO08", "line %d repeats the pattern '%s' of line %d - only the later line counts" % (r["line"], r["pattern"], seen[r["pattern"]]), r["line"]))
seen[r["pattern"]] = r["line"]
if not matched_by[r["line"]]:
findings.append(("medium", "CO04", "line %d: '%s' matches no file - path renamed or removed?" % (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", "line %d: '%s' (%d file(s)) is fully overridden by later line(s) %s - last match wins, put specific rules after general ones" % (r["line"], r["pattern"], len(matched_by[r["line"]]), ", ".join(map(str, later))), r["line"]))
if not r["owners"]:
findings.append(("info", "CO10", "line %d: '%s' has no owners - its %d file(s) are intentionally unowned" % (r["line"], r["pattern"], effective[r["line"]]), r["line"]))
# カバレッジ
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 file(s) under paths that must have an owner are unowned: %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 file(s) under paths that must have an owner only fall back to the catch-all '%s' rule (no specific 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 file(s) have no code owner (no matching rule, or only unknown owners): %s" % (len(rest), ", ".join(rest[:6]) + (" ..." if len(rest) > 6 else "")), 0))
# 所有者負荷
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 is the owner of %d of %d files (%.0f%%) as an individual - bus factor; add a team or a second owner" % (o, n, len(files), 100.0 * n / len(files)), 0))
solo = [f for f in files if eff[f] and len(eff[f]["ok"]) == 1 and USER_RE.match(eff[f]["ok"][0])]
if solo:
findings.append(("low", "CO07", "%d file(s) have a single individual owner and no team (e.g. %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: no owner (no matching rule)" % path
if r["owners"] and not r["ok"]:
return "%s: no valid owner (line %d: %s %s - unknown)" % (path, r["line"], r["pattern"], " ".join(r["owners"]))
return "%s: %s (line %d: %s)" % (path, " ".join(r["ok"]) or "explicitly unowned", r["line"], r["pattern"])
# ---------------------------------------------------------------- レポート
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 rule line(s), %d file(s), %d owned (%.1f%%), %d unowned." % (len(res["rules"]), len(files), owned, 100.0 * owned / len(files), len(res["unowned"])))
L.append("")
L.append("**Verdict: %s**" % ("%d high finding(s) - fix CODEOWNERS before relying on required reviews" % cnt["high"] if cnt["high"] else ("review the medium findings" if cnt["medium"] else "CODEOWNERS looks healthy")))
L.append("")
L.append("Findings: %d high, %d medium, %d low, %d info." % (cnt["high"], cnt["medium"], cnt["low"], cnt["info"]))
L.append("")
if res["no_valid_owner_lines"]:
L.append("Lines whose owners are all unknown (they still match, so their files get no code owner): %s. Unknown owners are left out of the owner columns below." % ", ".join(map(str, res["no_valid_owner_lines"])))
L.append("")
L += ["## Findings", ""]
for s, c, msg, _ in f:
L.append("- **%s %s** %s" % (s.upper(), c, msg))
L += ["", "## Coverage by top-level directory", "", "| Directory | Files | Owned | Unowned | Effective owners |", "|---|---|---|---|---|"]
dirs = OrderedDict()
for p in files:
d = p.split("/")[0] + "/" if "/" in p else "(root)"
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 += ["", "## Rules", "", "| Line | Pattern | Owners | Files matched | Effective for | 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 "(none)", m, e, ", ".join(sorted(set(notes.get(r["line"], [])))) or ""))
L += ["", "## Owner load (effective)", "", "| Owner | Files | Share |", "|---|---|---|"]
for o, n in res["load"].most_common():
L.append("| %s | %d | %.0f%% |" % (o, n, 100.0 * n / len(files)))
if res["unowned"]:
L += ["", "## Unowned files", ""]
for p in res["unowned"]:
e = res["eff"][p]
L.append("- `%s`%s" % (p, "" if not e else (" (explicitly unowned, line %d)" % e["line"] if not e["owners"] else " (line %d: only unknown owners)" % e["line"])))
if whos:
L += ["", "## Who owns", ""]
for p in whos:
L.append("- " + who(res, p))
L += ["", "Matching follows GitHub CODEOWNERS rules (gitignore-style patterns, last matching line wins, `!` and `[ ]` unsupported). Owners are checked against your owners file only; confirm write access in the repository settings."]
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"]]}
# ---------------------------------------------------------------- セルフテスト
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("* matches everything", owner("* @a", F, "src/api/v2/users.py") == ["@a"])
t("last matching line wins", owner("* @a\n/src/ @b", F, "src/app.py") == ["@b"] and owner("* @a\n/src/ @b", F, "README.md") == ["@a"])
t("*.js matches at any depth", owner("*.js @js", F, "web/lib/util.js") == ["@js"])
t("/docs/ owns nested files", owner("/docs/ @d", F, "docs/guide/setup.md") == ["@d"])
t("docs/* does not own nested files", owner("docs/* @d", F, "docs/guide/setup.md") is None and owner("docs/* @d", F, "docs/index.md") == ["@d"])
t("unanchored dir name 'api/' matches anywhere", owner("api/ @x", F, "src/api/v2/users.py") == ["@x"])
t("** in the middle", owner("/src/**/users.py @u", F, "src/api/v2/users.py") == ["@u"])
t("anchored /README.md only at root", owner("/README.md @r", "README.md\nsub/README.md\n", "sub/README.md") is None)
t("? matches one character", owner("src/ap?.py @q", F, "src/app.py") == ["@q"])
t("no owners = explicitly unowned", owner("* @a\n/web/", F, "web/app.js") == [] and ("info", "CO10") in codes("* @a\n/web/", F))
t("invalid owner -> CO01 high, line skipped", ("high", "CO01") in codes("* @a\n/src/ alice", F) and owner("* @a\n/src/ alice", F, "src/app.py") == ["@a"])
t("negation -> CO06 medium", ("medium", "CO06") in codes("* @a\n!/src/", F))
t("[ ] range -> CO06 medium", ("medium", "CO06") in codes("* @a\n/src/[ab]* @b", F))
t("GitLab section -> CO06 low", ("low", "CO06") in codes("[Docs]\n* @a", F))
cfg = {"org": "acme", "users": ["a", "b"], "teams": {"acme/web": ["a"], "acme/empty": []}}
t("unknown team -> CO02 high, files get no owner (no 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("unknown owner dropped, valid co-owner kept", owner("* @a\n/web/ @acme/webb @b", F, "web/app.js", cfg) == ["@b"])
t("team of other org -> CO02", ("high", "CO02") in codes("* @other/web", F, cfg))
t("unknown user -> CO02", ("high", "CO02") in codes("* @zed", F, cfg))
t("empty team -> CO09", ("medium", "CO09") in codes("* @a\n/web/ @acme/empty", F, cfg))
t("email owner accepted", ("high", "CO01") not in codes("* dev@acme.io", F))
t("dead rule -> CO04", ("medium", "CO04") in codes("* @a\n/legacy/ @b", F))
t("shadowed rule -> CO05", ("medium", "CO05") in codes("/src/api/ @b\n/src/ @a", F))
t("partially overridden is not CO05", ("medium", "CO05") not in codes("/src/ @a\n/src/api/ @b", F))
t("duplicate pattern -> CO08", ("low", "CO08") in codes("* @a\n/src/ @b\n/src/ @c", F))
t("unowned file -> CO03 medium", ("medium", "CO03") in codes("/src/ @a", F))
t("require_owner path unowned -> CO03 high", ("high", "CO03") in codes("/src/ @acme/web", F, dict(cfg, require_owner=[".github/"])))
t("required path only owned by '*' -> CO11", ("medium", "CO11") in codes("* @a\n/src/ @b", F, {"require_owner": [".github/"]}))
t("dead rule with unknown owner still -> CO04", ("medium", "CO04") in codes("* @a\n/legacy/ @acme/zz", F, cfg))
t("individual owner concentration -> CO07", ("medium", "CO07") in codes("* @a", F, {"max_individual_share": 0.5}))
t("ignore list removes files", len(analyse("* @a", F, {"ignore": ["docs/"]})["files"]) == 7)
r = analyse("* @a\n/src/ @b", F)
t("--who reports line", who(r, "src/api/routes.py") == "src/api/routes.py: @b (line 2: /src/)")
t("./ prefix stripped, .github kept", analyse("* @a", "./.github/x.yml\n")["files"] == [".github/x.yml"])
t("report has verdict and coverage table", "**Verdict:" in report(r, "t") and "## Coverage by top-level directory" in report(r, "t"))
try:
analyse("* @a", F, {"orgs": "x"}); ok = False
except InputError:
ok = True
t("unknown owners-file key rejected", ok)
for name, ok in tests:
print(("PASS " if ok else "FAIL ") + name)
passed = sum(ok for _, ok in tests)
print("%d/%d passed" % (passed, len(tests)))
return 0 if passed == len(tests) else 1
def main(argv=None):
ap = argparse.ArgumentParser(description="Check a GitHub CODEOWNERS file against the repository file list.")
ap.add_argument("codeowners", nargs="?")
ap.add_argument("files", nargs="?", help="file list, e.g. git ls-files > files.txt")
ap.add_argument("--owners", help="JSON with org, users, teams, require_owner, max_individual_share, ignore")
ap.add_argument("--who", action="append", default=[], help="show the effective owner of a path (repeatable)")
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 file is not valid JSON: %s" % e)
res = analyse(co, fl, cfg)
except (OSError, InputError) as e:
print("error: %s" % e, file=sys.stderr)
return 2
if a.json:
print(json.dumps(to_json(res), indent=2))
else:
sys.stdout.write(report(res, a.codeowners, a.who))
return 1 if any(x[0] == "high" for x in res["findings"]) else 0
if __name__ == "__main__":
sys.exit(main())
ログインして完全なプロンプトを表示
次で続行:
ログインすると、次に同意したことになります: 利用規約 と プライバシーポリシー
使い方
このプロンプトは coding 向けに設計されています。上の内容をコピーして、お好みの AI ツールに貼り付けてください。
最良の結果を得るには、プレースホルダー(角括弧や大文字で示された部分)を具体的な要件に置き換えてください。
ノート
0 件のコメント