import os
import sys
import yaml
import requests
import argparse
from typing import Dict, List, Any, Optional, Set
GITCODE_BASE_URL = "https://api.gitcode.com/api/v5/"
class RobotConfigChecker:
def __init__(self, token: str = ""):
self.token = token or os.environ.get("GITCODE_TOKEN")
self.session = requests.Session()
if self.token:
self.session.params = {"access_token": self.token}
self.errors = []
self.org_repos_cache = {}
def add_error(self, message: str):
self.errors.append(message)
def get_org_repos(self, org: str) -> Set[str]:
"""Fetch all repositories for an organization using OpenAPI."""
if org in self.org_repos_cache:
return self.org_repos_cache[org]
if not self.token:
return set()
repo_names = set()
page = 1
per_page = 100
try:
while True:
url = f"{GITCODE_BASE_URL}orgs/{org}/repos"
params = {"page": page, "per_page": per_page}
response = self.session.get(url, params=params, timeout=15)
if response.status_code != 200:
print(f"Warning: Failed to fetch repos for org {org}: {response.status_code}")
break
data = response.json()
if not data:
break
for r in data:
html_url = r.get("html_url", "")
if html_url:
repo_path = html_url.replace("https://gitcode.com/", "").strip("/").lower()
repo_names.add(repo_path)
if len(data) < per_page:
break
page += 1
except Exception as e:
print(f"Error fetching repos for org {org}: {e}")
self.org_repos_cache[org] = repo_names
return repo_names
def check_repo_exists(self, repo_path: str) -> bool:
"""Check if repo exists by looking up in organization's repo list."""
if not self.token:
return True
parts = repo_path.split('/')
if len(parts) != 2:
return False
org = parts[0]
org_repos = self.get_org_repos(org)
if not org_repos:
url = f"{GITCODE_BASE_URL}repos/{repo_path}"
try:
response = self.session.get(url, timeout=10)
print(f"DEBUG: Direct check for {repo_path}, status: {response.status_code}")
return response.status_code == 200
except Exception as e:
print(f"DEBUG: Direct check for {repo_path} failed with exception: {e}")
return False
exists = repo_path.lower() in org_repos
if not exists:
print(f"DEBUG: {repo_path} not found in org {org} repo list (total {len(org_repos)} repos)")
print(f"DEBUG: Org {org} repo list contents: {sorted(list(org_repos))}")
return exists
def run_check(self, config_path: str):
if not os.path.exists(config_path):
self.add_error(f"Config file not found: {config_path}")
return False
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
except Exception as e:
self.add_error(f"Failed to parse YAML: {e}")
return False
if not config or 'configs' not in config:
self.add_error("Missing 'configs' root element")
return False
for idx, item in enumerate(config['configs']):
self.validate_item(item, idx)
return len(self.errors) == 0
def validate_item(self, item: Dict[str, Any], index: int):
for field in ['lgtm_need_nums', 'approve_need_nums']:
val = item.get(field)
if not isinstance(val, int) or not (0 < val < 10):
self.add_error(f"{field} 必须是 1-9 之间的整数 (当前值: {val})")
branch_configs = item.get('branch_configs', [])
if not isinstance(branch_configs, list) or len(branch_configs) == 0:
self.add_error(f"缺少 branch_configs 或 branch_configs 不是列表")
else:
valid_methods = ['squash', 'merge', 'rebase']
for bi, bc in enumerate(branch_configs):
if not isinstance(bc, dict):
self.add_error(f"branch_configs 必须是映射类型")
continue
method = bc.get('merge_method')
if method not in valid_methods:
self.add_error(
f"merge_method 必须是 {valid_methods} 之一 (当前值: {method})"
)
has_branch = 'branch' in bc
is_default = bc.get('is_default') is True
if not has_branch and not is_default:
self.add_error(
f"branch_configs 必须包含 'branch' 字段或设置 'is_default: true'"
)
repos = item.get('repos', [])
if not isinstance(repos, list):
self.add_error(f"repos 字段必须是列表")
else:
for repo in repos:
if not isinstance(repo, str) or '/' not in repo or len(repo.split('/')) != 2:
self.add_error(f"仓库名 '{repo}' 格式错误,必须为 '组织/仓库' 格式")
elif not self.check_repo_exists(repo):
self.add_error(f"仓库 '{repo}' 不存在")
def generate_result_md(self):
"""
生成 result.md 文件,输出格式为 markdown 表格
参考了 print_results 的逻辑,将扁平的错误列表整理为表格形式
"""
errors_by_category = {
"字段数值错误": [],
"branch_configs结构错误": [],
"合并策略错误": [],
"仓库格式错误": [],
"仓库存在性错误": [],
"其他错误": []
}
for error in self.errors:
categorized = False
if "lgtm_need_nums" in error or "approve_need_nums" in error:
errors_by_category["字段数值错误"].append(error)
categorized = True
elif "merge_method" in error:
errors_by_category["合并策略错误"].append(error)
categorized = True
elif "branch_configs" in error:
errors_by_category["branch_configs结构错误"].append(error)
categorized = True
elif "格式错误" in error or "repos 字段必须是列表" in error:
errors_by_category["仓库格式错误"].append(error)
categorized = True
elif "不存在" in error:
errors_by_category["仓库存在性错误"].append(error)
categorized = True
if not categorized:
errors_by_category["其他错误"].append(error)
total_errors = sum(len(errs) for errs in errors_by_category.values())
results = []
if total_errors == 0:
results.append("✅ 机器人配置检查通过!\n")
else:
results.append(f"❌ 机器人配置检查未通过 (共 {total_errors} 个错误)\n")
results.append("检查项 | 检查结果 | 错误详情")
results.append("--- | --- | ---")
check_items = [
("lgtm/approve数值检查", ["字段数值错误"]),
("branch_configs结构检查", ["branch_configs结构错误"]),
("merge method检查", ["合并策略错误"]),
("repos格式检查", ["仓库格式错误"]),
("仓库存在性检查", ["仓库存在性错误"]),
("其他检查", ["其他错误"]),
]
for item_name, categories in check_items:
item_errors = []
for cat in categories:
if cat in errors_by_category:
item_errors.extend(errors_by_category[cat])
if item_errors:
unique_errors = list(dict.fromkeys(item_errors))
error_count = len(unique_errors)
error_summary = "<br>".join(unique_errors)
results.append(f"{item_name} | ❌ 未通过 ({error_count}) | {error_summary}")
else:
if item_name == "其他检查":
continue
results.append(f"{item_name} | ✅ 已通过 | -")
result_filename = "result.md"
try:
with open(result_filename, "w", encoding="utf-8") as f:
f.write("\n".join(results))
print("\n" + "="*60)
print("门禁检查结果")
print("="*60)
print("\n".join(results))
if total_errors > 0:
print(f"\n{'='*60}")
print("详细错误列表")
print(f"{'='*60}")
for i, error in enumerate(self.errors, 1):
print(f"{i}. {error}")
print(f"\n{'='*60}")
print(f"详细结果已保存到 {result_filename}")
print("="*60)
except Exception as e:
print(f"❌ 写入 {result_filename} 失败: {e}")
print("\n".join(results))
def main():
parser = argparse.ArgumentParser(description='Robot config validator')
parser.add_argument('--config', default='.infra/robot-config.yaml', help='Path to robot-config.yaml')
parser.add_argument('--token', help='GitCode API token')
parser.add_argument('--no-cleanup', action='store_true', help='检查完成后保留克隆的临时目录(用于调试)')
args = parser.parse_args()
checker = RobotConfigChecker(
token=args.token
)
success = checker.run_check(args.config)
checker.generate_result_md()
if success:
print("✅ Configuration is valid")
sys.exit(0)
else:
print("❌ Configuration errors found:")
for err in checker.errors:
print(f" - {err}")
sys.exit(1)
if __name__ == "__main__":
main()