315 lines
14 KiB
Python
315 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
"""Validate and render a OneOS delivery-model Yunxiao project profile."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
|
||
TARGET_LIFECYCLE = [
|
||
"待处理", "已确认", "分析中", "分析完成", "设计中", "设计完成",
|
||
"待开发", "开发中", "开发完成", "待测试", "测试中", "测试完成",
|
||
"发布中", "发布完成", "发布失败", "已关闭",
|
||
]
|
||
REQUIRED_TASK_LABELS = {"交付", "开发", "测试", "发版"}
|
||
REQUIRED_CONTROL_IDS = {
|
||
*(f"Y{index:02d}" for index in range(1, 17)),
|
||
"Y20", "Y21", "Y22",
|
||
*(f"Y{index:02d}" for index in range(30, 36)),
|
||
"Y40",
|
||
"A01", "A02", "A02B", *(f"A{index:02d}" for index in range(3, 11)),
|
||
*(f"TC{index:02d}" for index in range(1, 4)),
|
||
*(f"DF{index:02d}" for index in range(1, 4)),
|
||
*(f"CI{index:02d}" for index in range(1, 4)),
|
||
"L01", "L02",
|
||
}
|
||
VALID_MODES = {
|
||
"manual", "native_rule", "integration_bridge", "disabled_legacy", "not_supported"
|
||
}
|
||
VALID_PHASES = {"P0", "P1", "P2", "P3"}
|
||
VALID_TASK_WORKFLOW_MODES = {"A", "B"}
|
||
VALID_TASK_IDENTITY_MODES = {"work_item_type", "task_label", "title_prefix_process_control"}
|
||
VALID_LEGACY_DISPOSITIONS = {"keep_until_ready", "disable", "replace", "already_disabled"}
|
||
SENSITIVE_FRAGMENTS = (
|
||
"password", "passwd", "secret", "token", "cookie", "credential",
|
||
"private_key", "otp", "密码", "密钥",
|
||
)
|
||
|
||
|
||
def is_text(value: Any) -> bool:
|
||
return isinstance(value, str) and bool(value.strip())
|
||
|
||
|
||
def require_text(container: dict[str, Any], key: str, label: str, errors: list[str]) -> None:
|
||
if not is_text(container.get(key)):
|
||
errors.append(f"{label}.{key} 不能为空")
|
||
|
||
|
||
def walk_sensitive_keys(value: Any, prefix: str = "") -> list[str]:
|
||
found: list[str] = []
|
||
if isinstance(value, dict):
|
||
for key, nested in value.items():
|
||
current = f"{prefix}.{key}" if prefix else str(key)
|
||
if any(fragment in str(key).lower() for fragment in SENSITIVE_FRAGMENTS):
|
||
found.append(current)
|
||
found.extend(walk_sensitive_keys(nested, current))
|
||
elif isinstance(value, list):
|
||
for index, nested in enumerate(value):
|
||
found.extend(walk_sensitive_keys(nested, f"{prefix}[{index}]"))
|
||
return found
|
||
|
||
|
||
def validate_controls(controls: Any, label: str, errors: list[str]) -> None:
|
||
if not isinstance(controls, list):
|
||
errors.append(f"{label} 必须是数组")
|
||
return
|
||
seen: set[str] = set()
|
||
for index, control in enumerate(controls):
|
||
item_label = f"{label}[{index}]"
|
||
if not isinstance(control, dict):
|
||
errors.append(f"{item_label} 必须是对象")
|
||
continue
|
||
control_id = control.get("id")
|
||
if not is_text(control_id):
|
||
errors.append(f"{item_label}.id 不能为空")
|
||
continue
|
||
if control_id in seen:
|
||
errors.append(f"{label} 控制项重复:{control_id}")
|
||
seen.add(control_id)
|
||
if control_id not in REQUIRED_CONTROL_IDS:
|
||
errors.append(f"{label} 未知控制项:{control_id}")
|
||
mode = control.get("mode")
|
||
if mode not in VALID_MODES:
|
||
errors.append(f"{item_label}.mode 不合法")
|
||
if not isinstance(control.get("enabled"), bool):
|
||
errors.append(f"{item_label}.enabled 必须是布尔值")
|
||
if mode in {"disabled_legacy", "not_supported"} and control.get("enabled") is not False:
|
||
errors.append(f"{item_label} 的 {mode} 模式要求 enabled=false")
|
||
if control_id in {"L01", "L02"}:
|
||
if mode != "disabled_legacy" or control.get("enabled") is not False:
|
||
errors.append(f"{item_label} 必须停用,防止发布完成绕过验收")
|
||
for key in ("owner", "fallback", "evidence"):
|
||
require_text(control, key, item_label, errors)
|
||
missing = sorted(REQUIRED_CONTROL_IDS - seen)
|
||
if missing:
|
||
errors.append(f"{label} 缺少控制项:" + "、".join(missing))
|
||
|
||
|
||
def validate_project(project: Any, index: int, errors: list[str]) -> None:
|
||
label = f"projects[{index}]"
|
||
if not isinstance(project, dict):
|
||
errors.append(f"{label} 必须是对象")
|
||
return
|
||
require_text(project, "name", label, errors)
|
||
if project.get("migration_phase") not in VALID_PHASES:
|
||
errors.append(f"{label}.migration_phase 必须是 P0/P1/P2/P3")
|
||
statuses = project.get("requirement_statuses_observed")
|
||
if not isinstance(statuses, list) or not statuses or not all(is_text(v) for v in statuses):
|
||
errors.append(f"{label}.requirement_statuses_observed 必须是非空文本数组")
|
||
if project.get("task_workflow_mode") not in VALID_TASK_WORKFLOW_MODES:
|
||
errors.append(f"{label}.task_workflow_mode 必须是 A 或 B")
|
||
if project.get("task_identity_mode") not in VALID_TASK_IDENTITY_MODES:
|
||
errors.append(f"{label}.task_identity_mode 不合法")
|
||
if not isinstance(project.get("native_rule_can_filter_related_task_identity"), bool):
|
||
errors.append(f"{label}.native_rule_can_filter_related_task_identity 必须是布尔值")
|
||
for key in (
|
||
"main_task_prefix", "development_task_prefix", "test_task_prefix", "release_task_prefix"
|
||
):
|
||
require_text(project, key, label, errors)
|
||
|
||
repositories = project.get("repositories")
|
||
if not isinstance(repositories, list) or not repositories:
|
||
errors.append(f"{label}.repositories 至少包含一个仓库")
|
||
else:
|
||
for repo_index, repo in enumerate(repositories):
|
||
repo_label = f"{label}.repositories[{repo_index}]"
|
||
if not isinstance(repo, dict):
|
||
errors.append(f"{repo_label} 必须是对象")
|
||
continue
|
||
for key in ("name", "role", "integration_branch", "evidence"):
|
||
require_text(repo, key, repo_label, errors)
|
||
if repo.get("integration_branch") not in {"dev", "develop"}:
|
||
errors.append(f"{repo_label}.integration_branch 只能是实际存在的 dev/develop")
|
||
for key in ("codeup_integrated", "required_for_mr_gate"):
|
||
if not isinstance(repo.get(key), bool):
|
||
errors.append(f"{repo_label}.{key} 必须是布尔值")
|
||
|
||
test_plan = project.get("test_plan")
|
||
if not isinstance(test_plan, dict):
|
||
errors.append(f"{label}.test_plan 必须是对象")
|
||
else:
|
||
require_text(test_plan, "name", f"{label}.test_plan", errors)
|
||
require_text(test_plan, "evidence", f"{label}.test_plan", errors)
|
||
for key, required in (
|
||
("iteration_formally_related", True),
|
||
("cases_partitioned_by_requirement", True),
|
||
("fixed_defect_is_terminal", False),
|
||
("tester_retest_required", True),
|
||
):
|
||
if test_plan.get(key) is not required:
|
||
errors.append(f"{label}.test_plan.{key} 必须为 {str(required).lower()}")
|
||
|
||
release = project.get("release")
|
||
if not isinstance(release, dict):
|
||
errors.append(f"{label}.release 必须是对象")
|
||
else:
|
||
require_text(release, "evidence", f"{label}.release", errors)
|
||
for key in (
|
||
"release_task_formally_related_to_iteration",
|
||
"production_evidence_separate_from_test",
|
||
"acceptance_required_after_release",
|
||
"production_pipeline_unchanged",
|
||
):
|
||
if release.get(key) is not True:
|
||
errors.append(f"{label}.release.{key} 必须为 true")
|
||
|
||
legacy_rules = project.get("legacy_rules")
|
||
if not isinstance(legacy_rules, list):
|
||
errors.append(f"{label}.legacy_rules 必须是数组")
|
||
else:
|
||
for legacy_index, rule in enumerate(legacy_rules):
|
||
item_label = f"{label}.legacy_rules[{legacy_index}]"
|
||
if not isinstance(rule, dict):
|
||
errors.append(f"{item_label} 必须是对象")
|
||
continue
|
||
for key in ("name", "reason", "evidence"):
|
||
require_text(rule, key, item_label, errors)
|
||
if rule.get("disposition") not in VALID_LEGACY_DISPOSITIONS:
|
||
errors.append(f"{item_label}.disposition 不合法")
|
||
if not isinstance(rule.get("enabled"), bool):
|
||
errors.append(f"{item_label}.enabled 必须是布尔值")
|
||
|
||
validate_controls(project.get("controls"), f"{label}.controls", errors)
|
||
|
||
|
||
def validate(data: dict[str, Any]) -> list[str]:
|
||
errors: list[str] = []
|
||
if data.get("schema_version") != 1:
|
||
errors.append("schema_version 必须为 1")
|
||
if data.get("flow_model") != "oneos_delivery":
|
||
errors.append("flow_model 必须为 oneos_delivery")
|
||
for key in ("organization", "work_item_type"):
|
||
require_text(data, key, "profile", errors)
|
||
if data.get("target_lifecycle") != TARGET_LIFECYCLE:
|
||
errors.append("target_lifecycle 必须与OneOS终态16状态顺序一致")
|
||
labels = data.get("task_labels")
|
||
if not isinstance(labels, list) or not REQUIRED_TASK_LABELS.issubset(set(labels)):
|
||
errors.append("task_labels 必须包含:交付、开发、测试、发版")
|
||
policies = data.get("policies")
|
||
if not isinstance(policies, dict):
|
||
errors.append("policies 必须是对象")
|
||
else:
|
||
true_keys = (
|
||
"require_formal_relations", "require_current_state_condition",
|
||
"require_nonzero_development_tasks",
|
||
"require_signed_timestamped_replay_protected_callbacks",
|
||
)
|
||
false_keys = ("allow_automatic_final_close_without_acceptance", "test_success_is_production_release")
|
||
for key in true_keys:
|
||
if policies.get(key) is not True:
|
||
errors.append(f"policies.{key} 必须为 true")
|
||
for key in false_keys:
|
||
if policies.get(key) is not False:
|
||
errors.append(f"policies.{key} 必须为 false")
|
||
parts = policies.get("bridge_idempotency_key_components")
|
||
if not isinstance(parts, list) or not parts or not all(is_text(v) for v in parts):
|
||
errors.append("policies.bridge_idempotency_key_components 不能为空")
|
||
projects = data.get("projects")
|
||
if not isinstance(projects, list) or not projects:
|
||
errors.append("projects 至少包含一个项目")
|
||
else:
|
||
for index, project in enumerate(projects):
|
||
validate_project(project, index, errors)
|
||
sensitive = walk_sensitive_keys(data)
|
||
if sensitive:
|
||
errors.append("配置中禁止出现敏感字段:" + "、".join(sensitive))
|
||
if "请填写" in json.dumps(data, ensure_ascii=False):
|
||
errors.append("仍存在“请填写”占位符")
|
||
return errors
|
||
|
||
|
||
def render(data: dict[str, Any]) -> str:
|
||
lines = [
|
||
"# OneOS云效终态交付模型执行计划", "",
|
||
f"- 组织:{data['organization']}",
|
||
f"- 工作项类型:{data['work_item_type']}",
|
||
f"- 项目数:{len(data['projects'])}",
|
||
"- 完整性口径:每项目48项Y/A/测试/缺陷/集成/旧规则控制。",
|
||
"- 安全边界:不修改生产流水线;发布完成后保留产品验收门禁。", "",
|
||
]
|
||
for project in data["projects"]:
|
||
missing_statuses = [s for s in TARGET_LIFECYCLE if s not in project["requirement_statuses_observed"]]
|
||
lines.extend([
|
||
f"## 项目:{project['name']}", "",
|
||
f"- 迁移阶段:{project['migration_phase']}",
|
||
f"- 任务工作流方案:{project['task_workflow_mode']}",
|
||
f"- 任务身份方式:{project['task_identity_mode']}",
|
||
f"- 原生规则可过滤关联任务身份:{'是' if project['native_rule_can_filter_related_task_identity'] else '否'}",
|
||
f"- 缺少目标状态:{'、'.join(missing_statuses) if missing_statuses else '无'}", "",
|
||
"### 旧规则处置", "",
|
||
"| 规则 | 处置 | 启用 | 原因 | 证据 |", "|---|---|---|---|---|",
|
||
])
|
||
for rule in project["legacy_rules"]:
|
||
lines.append(
|
||
f"| {rule['name']} | {rule['disposition']} | {'是' if rule['enabled'] else '否'} | "
|
||
f"{rule['reason']} | {rule['evidence']} |"
|
||
)
|
||
lines.extend([
|
||
"", "### 48项控制台账", "",
|
||
"| ID | 方式 | 启用 | 负责人 | 回退方案 | 证据 |", "|---|---|---|---|---|---|",
|
||
])
|
||
for control in sorted(project["controls"], key=lambda item: item["id"]):
|
||
lines.append(
|
||
f"| {control['id']} | {control['mode']} | {'是' if control['enabled'] else '否'} | "
|
||
f"{control['owner']} | {control['fallback']} | {control['evidence']} |"
|
||
)
|
||
lines.extend([
|
||
"", "### 迁移门禁", "",
|
||
"- [ ] 目标状态与流转边已补齐。",
|
||
"- [ ] 主任务可可靠识别或已明确交桥接。",
|
||
"- [ ] 多仓库需求与开发任务均正式关联分支/MR。",
|
||
"- [ ] Testhub按需求分包且缺陷复测闭环。",
|
||
"- [ ] test、生产发布和产品验收证据分离。",
|
||
"- [ ] L01、L02已停用,旧规则已有备份和回滚。", "",
|
||
])
|
||
return "\n".join(lines)
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||
validate_parser = subparsers.add_parser("validate")
|
||
validate_parser.add_argument("profile", type=Path)
|
||
render_parser = subparsers.add_parser("render")
|
||
render_parser.add_argument("profile", type=Path)
|
||
render_parser.add_argument("--output", required=True, type=Path)
|
||
args = parser.parse_args()
|
||
try:
|
||
with args.profile.open("r", encoding="utf-8-sig") as handle:
|
||
data = json.load(handle)
|
||
if not isinstance(data, dict):
|
||
raise ValueError("配置根节点必须是JSON对象")
|
||
errors = validate(data)
|
||
except (OSError, json.JSONDecodeError, ValueError) as error:
|
||
print(f"配置读取失败:{error}", file=sys.stderr)
|
||
return 2
|
||
if errors:
|
||
for error in errors:
|
||
print(f"[错误] {error}", file=sys.stderr)
|
||
return 1
|
||
if args.command == "validate":
|
||
print(f"[通过] OneOS配置有效:{len(data['projects'])}个项目,每项目48项控制。")
|
||
return 0
|
||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||
args.output.write_text(render(data), encoding="utf-8")
|
||
print(f"[完成] 已生成执行计划:{args.output}")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|