|
| 1 | +import sys |
| 2 | +import os |
| 3 | +import yaml |
| 4 | +import re |
| 5 | + |
| 6 | +VALID_PREFIXES = ['tool_', 'app_', 'db_', 'kbwf_'] |
| 7 | +REQUIRED_YAML_FIELDS = ['name', 'tags', 'title', 'description'] |
| 8 | +VERSION_RE = re.compile(r'^\d+\.\d+\.\d+$') |
| 9 | + |
| 10 | +# 每种前缀允许的文件扩展名 |
| 11 | +PREFIX_EXT = { |
| 12 | + 'app_': '.mk', |
| 13 | + 'db_': '.tool', |
| 14 | + 'tool_': '.tool', |
| 15 | + 'kbwf_': '.kbwf', |
| 16 | +} |
| 17 | + |
| 18 | +errors = [] |
| 19 | +warnings = [] |
| 20 | + |
| 21 | +def check(cond, msg, is_warn=False): |
| 22 | + if not cond: |
| 23 | + (warnings if is_warn else errors).append(msg) |
| 24 | + |
| 25 | +def get_valid_tags(): |
| 26 | + root = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../..') |
| 27 | + yaml_path = os.path.join(root, 'tools/data.yaml') |
| 28 | + if not os.path.isfile(yaml_path): |
| 29 | + print(f"⚠️ 未找到 tools/data.yaml,跳过 tags 校验") |
| 30 | + return set() |
| 31 | + try: |
| 32 | + with open(yaml_path, encoding='utf-8') as f: |
| 33 | + data = yaml.safe_load(f) |
| 34 | + tags = data.get('additionalProperties', {}).get('tags', []) |
| 35 | + return {tag['name'] for tag in tags if 'name' in tag} |
| 36 | + except yaml.YAMLError as e: |
| 37 | + print(f"⚠️ tools/data.yaml 解析失败: {e}") |
| 38 | + return set() |
| 39 | + |
| 40 | +def get_changed_tool_dirs(changed_files_path): |
| 41 | + dirs = set() |
| 42 | + with open(changed_files_path) as f: |
| 43 | + for line in f: |
| 44 | + parts = line.strip().split('/') |
| 45 | + if len(parts) >= 2 and parts[0] == 'tools': |
| 46 | + tool_dir = parts[1] |
| 47 | + if any(tool_dir.startswith(p) for p in VALID_PREFIXES): |
| 48 | + dirs.add(os.path.join('tools', tool_dir)) |
| 49 | + return dirs |
| 50 | + |
| 51 | +def get_prefix(tool_name): |
| 52 | + for p in VALID_PREFIXES: |
| 53 | + if tool_name.startswith(p): |
| 54 | + return p |
| 55 | + return None |
| 56 | + |
| 57 | +def validate_tool_dir(tool_path, valid_tags): |
| 58 | + tool_name = os.path.basename(tool_path) |
| 59 | + print(f"\n🔍 校验: {tool_path}") |
| 60 | + |
| 61 | + # 1. 目录前缀校验 |
| 62 | + prefix = get_prefix(tool_name) |
| 63 | + check(prefix is not None, |
| 64 | + f"[{tool_name}] 目录名前缀无效,必须以 {VALID_PREFIXES} 之一开头") |
| 65 | + if prefix is None: |
| 66 | + return |
| 67 | + |
| 68 | + # 2. 根目录必要文件校验 |
| 69 | + for fname in ['data.yaml', 'logo.png', 'README.md']: |
| 70 | + check( |
| 71 | + os.path.isfile(os.path.join(tool_path, fname)), |
| 72 | + f"[{tool_name}] 缺少必要文件: {fname}" |
| 73 | + ) |
| 74 | + |
| 75 | + # 3. 至少一个版本目录 |
| 76 | + if not os.path.isdir(tool_path): |
| 77 | + check(False, f"[{tool_name}] 工具目录不存在") |
| 78 | + return |
| 79 | + |
| 80 | + version_dirs = [ |
| 81 | + d for d in os.listdir(tool_path) |
| 82 | + if os.path.isdir(os.path.join(tool_path, d)) and VERSION_RE.match(d) |
| 83 | + ] |
| 84 | + check(len(version_dirs) > 0, f"[{tool_name}] 缺少版本目录(如 1.0.0)") |
| 85 | + |
| 86 | + # 4. 每个版本目录校验 |
| 87 | + expected_ext = PREFIX_EXT[prefix] |
| 88 | + for version in version_dirs: |
| 89 | + version_path = os.path.join(tool_path, version) |
| 90 | + files_in_version = os.listdir(version_path) |
| 91 | + |
| 92 | + # 4.1 只能有一个对应扩展名的文件 |
| 93 | + matched = [f for f in files_in_version if f.endswith(expected_ext)] |
| 94 | + check( |
| 95 | + len(matched) == 1, |
| 96 | + f"[{tool_name}/{version}] 版本目录下应有且仅有 1 个 {expected_ext} 文件," |
| 97 | + f"当前找到 {len(matched)} 个: {matched}" |
| 98 | + ) |
| 99 | + |
| 100 | + # 4.2 版本目录下不应有其他多余文件 |
| 101 | + extra = [f for f in files_in_version if not f.endswith(expected_ext)] |
| 102 | + check( |
| 103 | + len(extra) == 0, |
| 104 | + f"[{tool_name}/{version}] 版本目录下存在多余文件: {extra}", |
| 105 | + is_warn=True |
| 106 | + ) |
| 107 | + |
| 108 | + # 5. data.yaml 内容校验 |
| 109 | + yaml_path = os.path.join(tool_path, 'data.yaml') |
| 110 | + if os.path.isfile(yaml_path): |
| 111 | + try: |
| 112 | + with open(yaml_path, encoding='utf-8') as f: |
| 113 | + data = yaml.safe_load(f) |
| 114 | + for field in REQUIRED_YAML_FIELDS: |
| 115 | + check( |
| 116 | + field in data and data[field], |
| 117 | + f"[{tool_name}] data.yaml 缺少字段或为空: {field}" |
| 118 | + ) |
| 119 | + if valid_tags and 'tags' in data and isinstance(data['tags'], list): |
| 120 | + for tag in data['tags']: |
| 121 | + check( |
| 122 | + tag in valid_tags, |
| 123 | + f"[{tool_name}] data.yaml 中 tag '{tag}' 未在 tools/data.yaml 中定义," |
| 124 | + f"可选值: {sorted(valid_tags)}" |
| 125 | + ) |
| 126 | + except yaml.YAMLError as e: |
| 127 | + errors.append(f"[{tool_name}] data.yaml 解析失败: {e}") |
| 128 | + |
| 129 | + # 6. README.md 非空 |
| 130 | + readme_path = os.path.join(tool_path, 'README.md') |
| 131 | + if os.path.isfile(readme_path): |
| 132 | + content = open(readme_path, encoding='utf-8').read().strip() |
| 133 | + check(len(content) > 50, f"[{tool_name}] README.md 内容过少,请补充工具说明") |
| 134 | + if tool_name.startswith('tool_') or tool_name.startswith('db_'): |
| 135 | + check( |
| 136 | + '参数' in content or 'parameter' in content.lower(), |
| 137 | + f"[{tool_name}] README.md 建议包含参数说明", |
| 138 | + is_warn=True |
| 139 | + ) |
| 140 | + |
| 141 | + # 7. logo.png 大小 |
| 142 | + logo_path = os.path.join(tool_path, 'logo.png') |
| 143 | + if os.path.isfile(logo_path): |
| 144 | + size_kb = os.path.getsize(logo_path) / 1024 |
| 145 | + check(size_kb <= 500, |
| 146 | + f"[{tool_name}] logo.png 过大 ({size_kb:.1f}KB),建议不超过 500KB", |
| 147 | + is_warn=True) |
| 148 | + |
| 149 | +def main(): |
| 150 | + changed_files_path = sys.argv[1] |
| 151 | + valid_tags = get_valid_tags() |
| 152 | + tool_dirs = get_changed_tool_dirs(changed_files_path) |
| 153 | + |
| 154 | + if not tool_dirs: |
| 155 | + print("ℹ️ 本次 PR 未涉及 tools/ 目录下的工具,跳过校验。") |
| 156 | + sys.exit(0) |
| 157 | + |
| 158 | + print(f"本次 PR 涉及 {len(tool_dirs)} 个工具目录:") |
| 159 | + for d in sorted(tool_dirs): |
| 160 | + validate_tool_dir(d, valid_tags) |
| 161 | + |
| 162 | + print("\n" + "=" * 50) |
| 163 | + if warnings: |
| 164 | + print(f"⚠️ 警告 ({len(warnings)} 条):") |
| 165 | + for w in warnings: |
| 166 | + print(f" - {w}") |
| 167 | + |
| 168 | + if errors: |
| 169 | + print(f"\n❌ 错误 ({len(errors)} 条):") |
| 170 | + for e in errors: |
| 171 | + print(f" - {e}") |
| 172 | + print("\n请修复以上问题后重新提交 PR。") |
| 173 | + sys.exit(1) |
| 174 | + else: |
| 175 | + print("✅ 所有校验通过!") |
| 176 | + sys.exit(0) |
| 177 | + |
| 178 | +if __name__ == '__main__': |
| 179 | + main() |
0 commit comments