Skip to content

Commit bd3ecb3

Browse files
committed
feat:添加自动审核PR
1 parent 0a5ef84 commit bd3ecb3

2 files changed

Lines changed: 193 additions & 0 deletions

File tree

.github/scripts/validate_pr.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import sys
2+
import os
3+
import yaml
4+
import re
5+
6+
VALID_PREFIXES = ['tool_', 'app_', 'db_', 'kbwf_']
7+
REQUIRED_FILES = ['data.yaml', 'logo.png', 'README.md']
8+
REQUIRED_YAML_FIELDS = ['name', 'tags', 'title', 'description']
9+
VERSION_RE = re.compile(r'^\d+\.\d+\.\d+$')
10+
11+
errors = []
12+
warnings = []
13+
14+
def check(cond, msg, is_warn=False):
15+
if not cond:
16+
(warnings if is_warn else errors).append(msg)
17+
18+
def get_valid_tags(root_yaml_path='data.yaml'):
19+
if not os.path.isfile(root_yaml_path):
20+
print(f"⚠️ 未找到根配置文件 {root_yaml_path},跳过 tags 校验")
21+
return set()
22+
try:
23+
with open(root_yaml_path, encoding='utf-8') as f:
24+
data = yaml.safe_load(f)
25+
tags = data.get('additionalProperties', {}).get('tags', [])
26+
return {tag['name'] for tag in tags if 'name' in tag}
27+
except yaml.YAMLError as e:
28+
print(f"⚠️ 根配置文件解析失败: {e}")
29+
return set()
30+
31+
def get_changed_tool_dirs(changed_files_path):
32+
dirs = set()
33+
with open(changed_files_path) as f:
34+
for line in f:
35+
parts = line.strip().split('/')
36+
if len(parts) >= 2 and parts[0] == 'tools':
37+
tool_dir = parts[1]
38+
if any(tool_dir.startswith(p) for p in VALID_PREFIXES):
39+
dirs.add(os.path.join('tools', tool_dir))
40+
return dirs
41+
42+
def validate_tool_dir(tool_path, valid_tags):
43+
tool_name = os.path.basename(tool_path)
44+
print(f"\n🔍 校验: {tool_path}")
45+
46+
# 1. 目录前缀
47+
check(
48+
any(tool_name.startswith(p) for p in VALID_PREFIXES),
49+
f"[{tool_name}] 目录名前缀无效,必须以 {VALID_PREFIXES} 之一开头"
50+
)
51+
52+
# 2. 必要文件
53+
for fname in REQUIRED_FILES:
54+
check(
55+
os.path.isfile(os.path.join(tool_path, fname)),
56+
f"[{tool_name}] 缺少必要文件: {fname}"
57+
)
58+
59+
# 3. 至少一个版本目录
60+
version_dirs = [
61+
d for d in os.listdir(tool_path)
62+
if os.path.isdir(os.path.join(tool_path, d)) and VERSION_RE.match(d)
63+
] if os.path.isdir(tool_path) else []
64+
check(len(version_dirs) > 0, f"[{tool_name}] 缺少版本目录(如 1.0.0)")
65+
66+
# 4. data.yaml 内容校验
67+
yaml_path = os.path.join(tool_path, 'data.yaml')
68+
if os.path.isfile(yaml_path):
69+
try:
70+
with open(yaml_path, encoding='utf-8') as f:
71+
data = yaml.safe_load(f)
72+
for field in REQUIRED_YAML_FIELDS:
73+
check(
74+
field in data and data[field],
75+
f"[{tool_name}] data.yaml 缺少字段或为空: {field}"
76+
)
77+
if valid_tags and 'tags' in data and isinstance(data['tags'], list):
78+
for tag in data['tags']:
79+
check(
80+
tag in valid_tags,
81+
f"[{tool_name}] data.yaml 中 tag '{tag}' 不合法,可选值: {sorted(valid_tags)}"
82+
)
83+
except yaml.YAMLError as e:
84+
errors.append(f"[{tool_name}] data.yaml 解析失败: {e}")
85+
86+
# 5. README.md 非空
87+
readme_path = os.path.join(tool_path, 'README.md')
88+
if os.path.isfile(readme_path):
89+
content = open(readme_path, encoding='utf-8').read().strip()
90+
check(len(content) > 50, f"[{tool_name}] README.md 内容过少,请补充工具说明")
91+
if tool_name.startswith('tool_'):
92+
check(
93+
'参数' in content or 'parameter' in content.lower(),
94+
f"[{tool_name}] 工具类 README.md 建议包含参数说明",
95+
is_warn=True
96+
)
97+
98+
# 6. logo.png 大小
99+
logo_path = os.path.join(tool_path, 'logo.png')
100+
if os.path.isfile(logo_path):
101+
size_kb = os.path.getsize(logo_path) / 1024
102+
check(size_kb <= 500, f"[{tool_name}] logo.png 文件过大 ({size_kb:.1f}KB),建议不超过 500KB", is_warn=True)
103+
104+
def main():
105+
changed_files_path = sys.argv[1]
106+
valid_tags = get_valid_tags('data.yaml')
107+
tool_dirs = get_changed_tool_dirs(changed_files_path)
108+
109+
if not tool_dirs:
110+
print("ℹ️ 本次 PR 未涉及 tools/ 目录下的工具,跳过校验。")
111+
sys.exit(0)
112+
113+
print(f"本次 PR 涉及 {len(tool_dirs)} 个工具目录:")
114+
for d in tool_dirs:
115+
validate_tool_dir(d, valid_tags)
116+
117+
print("\n" + "=" * 50)
118+
if warnings:
119+
print(f"⚠️ 警告 ({len(warnings)} 条):")
120+
for w in warnings:
121+
print(f" - {w}")
122+
123+
if errors:
124+
print(f"\n❌ 错误 ({len(errors)} 条):")
125+
for e in errors:
126+
print(f" - {e}")
127+
print("\n请修复以上问题后重新提交 PR。")
128+
sys.exit(1)
129+
else:
130+
print("✅ 所有校验通过!")
131+
sys.exit(0)
132+
133+
if __name__ == '__main__':
134+
main()

.github/workflows/pr-validate.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: PR Validation
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
paths:
7+
- 'tools/**'
8+
workflow_dispatch:
9+
10+
jobs:
11+
validate:
12+
runs-on: ubuntu-latest
13+
permissions:
14+
pull-requests: write
15+
contents: read
16+
17+
steps:
18+
- name: Checkout PR
19+
uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Get changed files
24+
run: git diff --name-only origin/main...HEAD > changed_files.txt
25+
26+
- name: Set up Python
27+
uses: actions/setup-python@v5
28+
with:
29+
python-version: '3.11'
30+
31+
- name: Install dependencies
32+
run: pip install pyyaml
33+
34+
- name: Run validation
35+
id: validate
36+
run: |
37+
python .github/scripts/validate_pr.py changed_files.txt > result.txt 2>&1
38+
echo "exit_code=$?" >> $GITHUB_OUTPUT
39+
cat result.txt
40+
41+
- name: Comment on PR
42+
uses: actions/github-script@v7
43+
with:
44+
script: |
45+
const fs = require('fs');
46+
const result = fs.readFileSync('result.txt', 'utf8');
47+
const exitCode = '${{ steps.validate.outputs.exit_code }}';
48+
const icon = exitCode === '0' ? '✅' : '❌';
49+
const status = exitCode === '0' ? '校验通过' : '校验失败,请修复后重新提交';
50+
github.rest.issues.createComment({
51+
issue_number: context.issue.number,
52+
owner: context.repo.owner,
53+
repo: context.repo.repo,
54+
body: `## ${icon} PR 自动校验结果\n\n**状态**: ${status}\n\n\`\`\`\n${result}\n\`\`\``
55+
});
56+
57+
- name: Fail if validation failed
58+
if: steps.validate.outputs.exit_code != '0'
59+
run: exit 1

0 commit comments

Comments
 (0)