CODEOWNERS覆盖与所有权检查器
来自 Wikiprompt,自由的提示词百科全书
CODEOWNERS覆盖与所有权检查器 一个完整的技能,用于审计GitHub CODEOWNERS文件,检查覆盖范围、无效所有者和排序错误,附带可用的Python检查器和模板。非常适合工程平台团队。
提示词内容收藏
🌐
---
name: codeowners-coverage-and-ownership-checker
description: 检查 GitHub CODEOWNERS 文件与真实仓库文件列表的匹配情况,计算每个文件的有效所有者(最后匹配的行生效),然后报告无效或未知的所有者、无所有者的文件、仅由兜底规则覆盖的关键路径、不匹配任何文件或被后续行完全覆盖的规则、不支持的 ! 和 [ ] 语法、重复项、空团队和单点瓶颈。包含一个离线标准库 Python 检查器。
---
# CODEOWNERS 覆盖率和所有权检查器
你帮助工程团队信任他们的 CODEOWNERS 文件。只有当每一行都有效、每个所有者都存在且有权限、并且规则顺序正确时,必需的审查和自动分配的审查者才能正常工作。实际上,文件会悄悄失去所有者:团队被重命名,其文件静默地没有审查者;特定规则位于通用规则之上且永远不会生效;`docs/*` 不覆盖 `docs/guide/`;从 .gitignore 复制的 `!` 否定不起作用;或者计费代码的唯一所有者是一个正在休假的人。你计算每个文件的有效所有者,并准确显示问题所在及修复方法。
你根据 GitHub 文档化的 CODEOWNERS 行为以及用户提供的所有者列表来检查文件。你无法看到仓库权限;请说明这一点,并要求用户在仓库设置中或使用 `gh api` 确认写权限。
## 此技能中的文件
- `scripts/codeowners_check.py` - 解析 CODEOWNERS(注释、行内注释、转义空格),使用 GitHub 的 gitignore 风格规则(锚定和浮动模式、`*`、`**`、`?`、目录模式、`dir/*` = 仅直接子项,最后匹配的行生效)匹配 `git ls-files` 中的每个路径,跳过语法无效的行并像 GitHub 一样丢弃未知所有者,报告 CO01-CO11 的发现,包括每个目录的覆盖率、规则表、所有者负载和 `--who PATH` 查找(仅 Python 3.8+ 标准库)
- `references/codeowners-syntax-and-matching.md` - 模式语义及示例、排序规则、所有者格式、文件位置和 GitHub 限制
- `references/checks-and-codes.md` - 每个发现代码、其严重性和通常的修复方法
- `templates/ownership-review.md` - CODEOWNERS 修复的拉取请求描述/审查说明
- `examples/CODEOWNERS` - 一个 Web 应用 monorepo 的真实文件,包含典型错误(重命名的团队、特定规则在通用规则之上、`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 = 输入错误。没有 shell 时,使用 `references/codeowners-syntax-and-matching.md` 对每个重要路径自底向上遍历规则:从底部开始第一个匹配的行就是所有者。
### 3. 读取结果
- 先看高严重性:CO01(无效的所有者格式,例如没有 @ 的名称):GitHub 将整行视为无效语法跳过,因此这些文件回退到较早的规则。CO02(未知用户或团队,无权限):该所有者不会被请求;如果该行没有留下有效所有者,该行仍然匹配,其文件完全没有代码所有者。CO03 高:必须拥有的路径下的文件没有所有者。
- 中严重性:CO05(规则被完全覆盖 - 通常是特定路径在通用路径之上)、CO04(规则不匹配任何内容 - 路径被重命名或删除)、CO06(`!` 或 `[ ]` - 被忽略)、CO11(关键路径仅由 `*` 覆盖)、CO09(空团队)、CO07(一个人拥有太多)、CO03 中(没有规则的文件)。
- 低/信息:GitLab 部分标题、单人所有权、重复模式、明确无所有者的路径(如果是有意的则没问题)。
- 在人们关心的路径上使用 `--who` 来确认修复前后的情况。
### 4. 提出修复
编写修正后的 CODEOWNERS:通用规则在前,特定规则在后;用更具体的后续规则替换 `!` 排除(空所有者列表使路径有意无主);当应覆盖嵌套文件时使用 `/dir/` 而不是 `dir/*`;修复团队 slug;为单人路径添加第二个所有者或团队;为 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`,并使用它在拉取请求目标分支(基础分支)上找到的第一个文件。超过 3 MB 的文件被忽略。当你在 Web UI 中打开文件时,GitHub 会显示无效行的错误(“CODEOWNERS errors”横幅),也可以通过 `gh api repos/<owner>/<repo>/codeowners/errors` 查看。
## 行格式
```
<pattern> <owner> [<owner> ...] # 可选注释
```
- 行首的 `#`(或空白后的 `#`)开始注释。与 .gitignore 不同,在 CODEOWNERS 中,将 `#` 开头的模式转义为 `\#` 不起作用。
- 路径区分大小写,即使你笔记本电脑的文件系统不区分。
- 一个模式的所有所有者必须在同一行;两行具有相同模式意味着只有后一行有效。
- 所有者:`@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。
## 团队和权限
- 团队必须可见且对仓库有写权限;用户必须有写权限。
- 空团队会收到审查请求,但没有人被通知。
- 必需的代码所有者审查(分支保护或规则集)会阻止合并,直到其中一个所有者批准(任何一个所有者就足够)。如果文件最终没有有效所有者,则不会为其请求代码所有者批准,因此保护对该文件静默无效。
## 获取输入
```
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`);模式无法解析 | 添加 @ / 修复 slug;在此之前整行被跳过 |
| CO02 | 高 | 用户不在你的用户列表中,团队不在你的团队列表中,或团队来自其他组织(仅使用 `--owners`);第二个 CO02 行说明当一行没有留下有效所有者时,其文件没有代码所有者 | 修复 slug(检查器建议相近的团队),授予团队写权限,或移除所有者 |
| CO03 | 高:`require_owner` 路径下的无主文件;中:其他没有规则或只有未知所有者的文件(明确无主的文件是 CO10) | 覆盖率 | 添加规则,或添加兜底 `*` 默认值 |
| CO04 | 中 | 模式不匹配列表中的任何文件 | 路径被重命名或删除:更新或移除该行 |
| CO05 | 中 | 规则匹配的每个文件都由后续行拥有(最后匹配生效) | 将特定规则移到通用规则之下 |
| CO06 | 中:`!` 否定、`[ ]` 范围(被 GitHub 忽略);低:GitLab 部分标题 | 不支持的语法 | 使用更具体的后续规则;空所有者列表 = 无主 |
| CO07 | 中:一个人(不是团队)是超过 `max_individual_share` 文件的有效所有者;低:由单一个人且没有团队拥有的文件 | 巴士因子 | 添加团队或第二个所有者 |
| CO08 | 低 | 相同模式出现两次;只有后一行有效 | 将所有者合并到一行 |
| CO09 | 中 | 团队在你的团队列表中没有成员 | 添加成员或使用另一个团队 |
| CO10 | 信息 | 没有所有者的模式:文件有意无主 | 如果有意则没问题(生成的代码、供应商文件) |
| CO11 | 中 | `require_owner` 路径下的文件仅由兜底 `*` 规则拥有 | 为该路径添加特定规则 |
## 判定
- **高严重性发现**:在依赖“要求代码所有者审查”之前修复。
- **仅中严重性**:文件有效,但某些文件归错误的人所有。
- **健康**:只有低/信息。
## 所有者文件(`--owners`,JSON)
| 键 | 含义 |
|---|---|
| `org` | 组织名称;来自其他组织的团队被标记 |
| `users` | 有写权限的登录名(不带 @) |
| `teams` | `{"org/slug": [members]}` |
| `require_owner` | 必须有特定所有者的路径的模式(相同语法) |
| `max_individual_share` | 一个人可以单独拥有的文件份额(默认 0.4) |
| `ignore` | 要排除的文件的模式(供应商代码、锁文件) |
## 检查的限制
- 不检查仓库权限和团队可见性 - 使用语法参考中的 `gh api` 调用或“CODEOWNERS errors”横幅确认。
- 文件列表是一个分支的快照;针对拉取请求的基础分支运行它。
FILE:templates/ownership-review.md
# CODEOWNERS 修复:<repository>
**分支:** <base branch> **检查日期:** <YYYY-MM-DD> **文件数:** <n> **修复前/后拥有率:** <x% / y%>
## 问题所在
| 行 | 规则 | 问题 | 影响 |
|---|---|---|---|
| <11> | `<pattern> <owners>` | <未知团队 @org/frontend-team> | <web 文件回退到 @org/platform> |
| <6> | `<pattern>` | <被第 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 添加成员>
- [ ] <合并后在 <branch> 上启用“要求代码所有者审查”>
## 检查
`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", "第 %d 行:'%s' 是 GitLab 部分标题;GitHub 将其视为无效行" % (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", "第 %d 行:'%s' 使用 ! 否定,CODEOWNERS 不支持 - GitHub 忽略该行" % (n, pat), n))
rule["valid"] = False
elif "[" in pat or "]" in pat:
findings.append(("medium", "CO06", "第 %d 行:'%s' 使用 [ ] 字符范围,CODEOWNERS 不支持 - GitHub 忽略该行" % (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", "第 %d 行:无效所有者 %s(使用 @user、@org/team 或电子邮件)- GitHub 将整行标记为错误并跳过" % (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", "第 %d 行:模式 '%s' 无法解析(%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("文件列表为空(使用: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("所有者文件中的未知键:%s" % ", ".join(sorted(unknown)))
rules, findings = parse_codeowners(co_text)
if not rules:
raise InputError("CODEOWNERS 没有规则")
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 = "团队 %s 不在组织 @%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 = "未知团队 %s%s" % (o, ("(你的意思是 @%s 吗?)" % close[0]) if close else "")
elif not teams[name] and key not in reported:
findings.append(("medium", "CO09", "第 %d 行:团队 %s 没有成员 - 审查请求无处可去" % (r["line"], o), r["line"])); reported.add(key)
elif USER_RE.match(o) and have_list and key.lstrip("@") not in users:
problem = "未知用户 %s(不在所有者列表中 / 没有写权限)" % o
if problem:
unknown_owners.add(key)
if key not in reported:
findings.append(("high", "CO02", "第 %d 行:%s - GitHub 不会向其请求审查" % (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", "第 %d 行:没有留下有效所有者 - 其 %d 个文件没有代码所有者(它们不会回退到较早的行)" % (ln, effective[ln]), ln))
# 无效、被覆盖、重复规则
seen = {}
for r in valid:
if r["pattern"] in seen:
findings.append(("low", "CO08", "第 %d 行重复了第 %d 行的模式 '%s' - 只有后一行有效" % (r["line"], seen[r["pattern"]], r["pattern"]), r["line"]))
seen[r["pattern"]] = r["line"]
if not matched_by[r["line"]]:
findings.append(("medium", "CO04", "第 %d 行:'%s' 不匹配任何文件 - 路径被重命名或删除?" % (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", "第 %d 行:'%s'(%d 个文件)被后续行 %s 完全覆盖 - 最后匹配生效,将特定规则放在通用规则之后" % (r["line"], r["pattern"], len(matched_by[r["line"]]), ", ".join(map(str, later))), r["line"]))
if not r["owners"]:
findings.append(("info", "CO10", "第 %d 行:'%s' 没有所有者 - 其 %d 个文件有意无主" % (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 个必须拥有所有者的路径下的文件无主:%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 个必须拥有所有者的路径下的文件仅回退到兜底 '%s' 规则(没有特定所有者):%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 个文件没有代码所有者(没有匹配的规则,或只有未知所有者):%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 作为个人拥有 %d 个文件中的 %d 个(%.0f%%)- 巴士因子;添加团队或第二个所有者" % (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 个文件有单个个人所有者且没有团队(例如 %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:无所有者(没有匹配的规则)" % path
if r["owners"] and not r["ok"]:
return "%s:无有效所有者(第 %d 行:%s %s - 未知)" % (path, r["line"], r["pattern"], " ".join(r["owners"]))
return "%s:%s(第 %d 行:%s)" % (path, " ".join(r["ok"]) or "明确无主", 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 检查:%s" % title, ""]
L.append("%d 条规则行,%d 个文件,%d 个已拥有(%.1f%%),%d 个无主。" % (len(res["rules"]), len(files), owned, 100.0 * owned / len(files), len(res["unowned"])))
L.append("")
L.append("**判定:%s**" % ("%d 个高严重性发现 - 在依赖必需审查之前修复 CODEOWNERS" % cnt["high"] if cnt["high"] else ("审查中严重性发现" if cnt["medium"] else "CODEOWNERS 看起来健康")))
L.append("")
L.append("发现:%d 高,%d 中,%d 低,%d 信息。" % (cnt["high"], cnt["medium"], cnt["low"], cnt["info"]))
L.append("")
if res["no_valid_owner_lines"]:
L.append("所有者全部未知的行(它们仍然匹配,因此其文件没有代码所有者):%s。未知所有者从下面的所有者列中省略。" % ", ".join(map(str, res["no_valid_owner_lines"])))
L.append("")
L += ["## 发现", ""]
for s, c, msg, _ in f:
L.append("- **%s %s** %s" % (s.upper(), c, msg))
L += ["", "## 按顶级目录的覆盖率", "", "| 目录 | 文件数 | 已拥有 | 无主 | 有效所有者 |", "|---|---|---|---|---|"]
dirs = OrderedDict()
for p in files:
d = p.split("/")[0] + "/" if "/" in p else "(根)"
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 += ["", "## 规则", "", "| 行 | 模式 | 所有者 | 匹配的文件数 | 有效于 | 备注 |", "|---|---|---|---|---|---|"]
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 "(无)", m, e, ", ".join(sorted(set(notes.get(r["line"], [])))) or ""))
L += ["", "## 所有者负载(有效)", "", "| 所有者 | 文件数 | 份额 |", "|---|---|---|"]
for o, n in res["load"].most_common():
L.append("| %s | %d | %.0f%% |" % (o, n, 100.0 * n / len(files)))
if res["unowned"]:
L += ["", "## 无主文件", ""]
for p in res["unowned"]:
e = res["eff"][p]
L.append("- `%s`%s" % (p, "" if not e else ("(明确无主,第 %d 行)" % e["line"] if not e["owners"] else "(第 %d 行:只有未知所有者)" % e["line"])))
if whos:
L += ["", "## 谁拥有", ""]
for p in whos:
L.append("- " + who(res, p))
L += ["", "匹配遵循 GitHub CODEOWNERS 规则(gitignore 风格模式,最后匹配的行生效,不支持 `!` 和 `[ ]`)。所有者仅根据你的所有者文件检查;请在仓库设置中确认写权限。"]
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("* 匹配所有内容", owner("* @a", F, "src/api/v2/users.py") == ["@a"])
t("最后匹配的行生效", owner("* @a\n/src/ @b", F, "src/app.py") == ["@b"] and owner("* @a\n/src/ @b", F, "README.md") == ["@a"])
t("*.js 在任意深度匹配", owner("*.js @js", F, "web/lib/util.js") == ["@js"])
t("/docs/ 拥有嵌套文件", owner("/docs/ @d", F, "docs/guide/setup.md") == ["@d"])
t("docs/* 不拥有嵌套文件", owner("docs/* @d", F, "docs/guide/setup.md") is None and owner("docs/* @d", F, "docs/index.md") == ["@d"])
t("非锚定目录名 'api/' 在任意位置匹配", owner("api/ @x", F, "src/api/v2/users.py") == ["@x"])
t("中间的 **", owner("/src/**/users.py @u", F, "src/api/v2/users.py") == ["@u"])
t("锚定的 /README.md 仅在根目录", owner("/README.md @r", "README.md\nsub/README.md\n", "sub/README.md") is None)
t("? 匹配一个字符", owner("src/ap?.py @q", F, "src/app.py") == ["@q"])
t("没有所有者 = 明确无主", owner("* @a\n/web/", F, "web/app.js") == [] and ("info", "CO10") in codes("* @a\n/web/", F))
t("无效所有者 -> CO01 高,行被跳过", ("high", "CO01") in codes("* @a\n/src/ alice", F) and owner("* @a\n/src/ alice", F, "src/app.py") == ["@a"])
t("否定 -> CO06 中", ("medium", "CO06") in codes("* @a\n!/src/", F))
t("[ ] 范围 -> CO06 中", ("medium", "CO06") in codes("* @a\n/src/[ab]* @b", F))
t("GitLab 部分 -> CO06 低", ("low", "CO06") in codes("[Docs]\n* @a", F))
cfg = {"org": "acme", "users": ["a", "b"], "teams": {"acme/web": ["a"], "acme/empty": []}}
t("未知团队 -> CO02 高,文件没有所有者(无回退)", ("high", "CO02") in codes("* @a\n/web/ @acme/webb", F, cfg) and owner("* @a\n/web/ @acme/webb", F, "web/app.js", cfg) == [] and ("medium", "CO03") in codes("* @a\n/web/ @acme/webb", F, cfg))
t("未知所有者被丢弃,有效共同所有者保留", owner("* @a\n/web/ @acme/webb @b", F, "web/app.js", cfg) == ["@b"])
t("其他组织的团队 -> CO02", ("high", "CO02") in codes("* @other/web", F, cfg))
t("未知用户 -> CO02", ("high", "CO02") in codes("* @zed", F, cfg))
t("空团队 -> CO09", ("medium", "CO09") in codes("* @a\n/web/ @acme/empty", F, cfg))
t("电子邮件所有者被接受", ("high", "CO01") not in codes("* dev@acme.io", F))
t("无效规则 -> CO04", ("medium", "CO04") in codes("* @a\n/legacy/ @b", F))
t("被覆盖的规则 -> CO05", ("medium", "CO05") in codes("/src/api/ @b\n/src/ @a", F))
t("部分覆盖不是 CO05", ("medium", "CO05") not in codes("/src/ @a\n/src/api/ @b", F))
t("重复模式 -> CO08", ("low", "CO08") in codes("* @a\n/src/ @b\n/src/ @c", F))
t("无主文件 -> CO03 中", ("medium", "CO03") in codes("/src/ @a", F))
t("require_owner 路径无主 -> CO03 高", ("high", "CO03") in codes("/src/ @acme/web", F, dict(cfg, require_owner=[".github/"])))
t("必需路径仅由 '*' 拥有 -> CO11", ("medium", "CO11") in codes("* @a\n/src/ @b", F, {"require_owner": [".github/"]}))
t("具有未知所有者的无效规则仍然 -> CO04", ("medium", "CO04") in codes("* @a\n/legacy/ @acme/zz", F, cfg))
t("个人所有者集中度 -> CO07", ("medium", "CO07") in codes("* @a", F, {"max_individual_share": 0.5}))
t("忽略列表移除文件", len(analyse("* @a", F, {"ignore": ["docs/"]})["files"]) == 7)
r = analyse("* @a\n/src/ @b", F)
t("--who 报告行", who(r, "src/api/routes.py") == "src/api/routes.py:@b(第 2 行:/src/)")
t("./ 前缀被剥离,.github 保留", analyse("* @a", "./.github/x.yml\n")["files"] == [".github/x.yml"])
t("报告有判定和覆盖率表", "**判定:" in report(r, "t") and "## 按顶级目录的覆盖率" in report(r, "t"))
try:
analyse("* @a", F, {"orgs": "x"}); ok = False
except InputError:
ok = True
t("未知的所有者文件键被拒绝", ok)
for name, ok in tests:
print(("PASS " if ok else "FAIL ") + name)
passed = sum(ok for _, ok in tests)
print("%d/%d 通过" % (passed, len(tests)))
return 0 if passed == len(tests) else 1
def main(argv=None):
ap = argparse.ArgumentParser(description="检查 GitHub CODEOWNERS 文件与仓库文件列表。")
ap.add_argument("codeowners", nargs="?")
ap.add_argument("files", nargs="?", help="文件列表,例如 git ls-files > files.txt")
ap.add_argument("--owners", help="包含 org、users、teams、require_owner、max_individual_share、ignore 的 JSON")
ap.add_argument("--who", action="append", default=[], help="显示路径的有效所有者(可重复)")
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("所有者文件不是有效的 JSON:%s" % e)
res = analyse(co, fl, cfg)
except (OSError, InputError) as e:
print("错误:%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 条评论