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 ()
0 commit comments