feat: SDLC adapter v1.0.0 — 28 step types, 6 data tables, full lifecycle
This commit is contained in:
parent
c4d2e79c4c
commit
ea06b33fe7
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# build artifacts
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# CRUD generated directories (auto-generated by xls2ui)
|
||||||
|
wwwroot/sd_project/
|
||||||
|
wwwroot/sd_iteration/
|
||||||
|
wwwroot/sd_test_plan/
|
||||||
|
wwwroot/sd_test_case/
|
||||||
|
wwwroot/sd_bug/
|
||||||
|
wwwroot/sd_deploy_env/
|
||||||
|
|
||||||
|
# venv
|
||||||
|
py3/
|
||||||
|
venv/
|
||||||
93
init/data.json
Normal file
93
init/data.json
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
{
|
||||||
|
"appcodes": [
|
||||||
|
{"parentid": "sd_project_status", "parentname": "项目状态", "items": [
|
||||||
|
{"k": "draft", "v": "草稿"},
|
||||||
|
{"k": "active", "v": "进行中"},
|
||||||
|
{"k": "completed", "v": "已完成"},
|
||||||
|
{"k": "archived", "v": "已归档"}
|
||||||
|
]},
|
||||||
|
{"parentid": "sd_project_type", "parentname": "项目类型", "items": [
|
||||||
|
{"k": "web_app", "v": "Web应用"},
|
||||||
|
{"k": "api_service", "v": "API服务"},
|
||||||
|
{"k": "module", "v": "Sage模块"},
|
||||||
|
{"k": "mobile_app", "v": "移动应用"},
|
||||||
|
{"k": "cli_tool", "v": "CLI工具"},
|
||||||
|
{"k": "data_pipeline", "v": "数据管道"}
|
||||||
|
]},
|
||||||
|
{"parentid": "sd_iteration_type", "parentname": "迭代类型", "items": [
|
||||||
|
{"k": "new_feature", "v": "新功能"},
|
||||||
|
{"k": "bugfix", "v": "Bug修复"},
|
||||||
|
{"k": "refactor", "v": "重构"},
|
||||||
|
{"k": "upgrade", "v": "升级"},
|
||||||
|
{"k": "hotfix", "v": "紧急修复"}
|
||||||
|
]},
|
||||||
|
{"parentid": "sd_iteration_status", "parentname": "迭代状态", "items": [
|
||||||
|
{"k": "planning", "v": "规划中"},
|
||||||
|
{"k": "in_progress", "v": "进行中"},
|
||||||
|
{"k": "completed", "v": "已完成"},
|
||||||
|
{"k": "cancelled", "v": "已取消"}
|
||||||
|
]},
|
||||||
|
{"parentid": "sd_test_plan_type", "parentname": "测试方案类型", "items": [
|
||||||
|
{"k": "functional", "v": "功能测试"},
|
||||||
|
{"k": "performance", "v": "性能测试"},
|
||||||
|
{"k": "security", "v": "安全测试"},
|
||||||
|
{"k": "regression", "v": "回归测试"},
|
||||||
|
{"k": "integration", "v": "集成测试"}
|
||||||
|
]},
|
||||||
|
{"parentid": "sd_test_plan_status", "parentname": "测试方案状态", "items": [
|
||||||
|
{"k": "draft", "v": "草稿"},
|
||||||
|
{"k": "approved", "v": "已审批"},
|
||||||
|
{"k": "executing", "v": "执行中"},
|
||||||
|
{"k": "completed", "v": "已完成"}
|
||||||
|
]},
|
||||||
|
{"parentid": "sd_test_case_type", "parentname": "测试用例类型", "items": [
|
||||||
|
{"k": "functional", "v": "功能"},
|
||||||
|
{"k": "performance", "v": "性能"},
|
||||||
|
{"k": "api", "v": "API"},
|
||||||
|
{"k": "ui", "v": "UI"},
|
||||||
|
{"k": "integration", "v": "集成"}
|
||||||
|
]},
|
||||||
|
{"parentid": "sd_test_case_status", "parentname": "测试用例状态", "items": [
|
||||||
|
{"k": "pending", "v": "待执行"},
|
||||||
|
{"k": "pass", "v": "通过"},
|
||||||
|
{"k": "fail", "v": "失败"},
|
||||||
|
{"k": "blocked", "v": "阻塞"},
|
||||||
|
{"k": "skipped", "v": "跳过"}
|
||||||
|
]},
|
||||||
|
{"parentid": "sd_bug_severity", "parentname": "Bug严重程度", "items": [
|
||||||
|
{"k": "critical", "v": "致命"},
|
||||||
|
{"k": "major", "v": "严重"},
|
||||||
|
{"k": "minor", "v": "一般"},
|
||||||
|
{"k": "trivial", "v": "轻微"}
|
||||||
|
]},
|
||||||
|
{"parentid": "sd_bug_status", "parentname": "Bug状态", "items": [
|
||||||
|
{"k": "open", "v": "新建"},
|
||||||
|
{"k": "confirmed", "v": "已确认"},
|
||||||
|
{"k": "fixing", "v": "修复中"},
|
||||||
|
{"k": "fixed", "v": "已修复"},
|
||||||
|
{"k": "verified", "v": "已验证"},
|
||||||
|
{"k": "closed", "v": "已关闭"},
|
||||||
|
{"k": "rejected", "v": "已驳回"}
|
||||||
|
]},
|
||||||
|
{"parentid": "sd_priority", "parentname": "优先级", "items": [
|
||||||
|
{"k": "P0", "v": "P0-紧急"},
|
||||||
|
{"k": "P1", "v": "P1-高"},
|
||||||
|
{"k": "P2", "v": "P2-中"},
|
||||||
|
{"k": "P3", "v": "P3-低"}
|
||||||
|
]},
|
||||||
|
{"parentid": "sd_reporter_type", "parentname": "提交人类型", "items": [
|
||||||
|
{"k": "agent", "v": "Agent"},
|
||||||
|
{"k": "human", "v": "人工"}
|
||||||
|
]},
|
||||||
|
{"parentid": "sd_env_type", "parentname": "环境类型", "items": [
|
||||||
|
{"k": "test", "v": "测试环境"},
|
||||||
|
{"k": "staging", "v": "预发布环境"},
|
||||||
|
{"k": "production", "v": "生产环境"}
|
||||||
|
]},
|
||||||
|
{"parentid": "sd_env_status", "parentname": "环境状态", "items": [
|
||||||
|
{"k": "configured", "v": "已配置"},
|
||||||
|
{"k": "verified", "v": "已验证"},
|
||||||
|
{"k": "failed", "v": "验证失败"}
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
}
|
||||||
70
json/sd_bug_list.json
Normal file
70
json/sd_bug_list.json
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"tblname": "sd_bugs",
|
||||||
|
"title": "Bug管理",
|
||||||
|
"params": {
|
||||||
|
"sortby": ["created_at desc"],
|
||||||
|
"confidential_fields": [],
|
||||||
|
"browserfields": {
|
||||||
|
"alters": {
|
||||||
|
"iteration_id": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_iteration_options.dspy')}}",
|
||||||
|
"valueField": "iteration_id",
|
||||||
|
"textField": "iteration_id_text"
|
||||||
|
},
|
||||||
|
"severity": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_severity.dspy')}}",
|
||||||
|
"valueField": "severity",
|
||||||
|
"textField": "severity_text"
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_priority.dspy')}}",
|
||||||
|
"valueField": "priority",
|
||||||
|
"textField": "priority_text"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_bug_status.dspy')}}",
|
||||||
|
"valueField": "status",
|
||||||
|
"textField": "status_text"
|
||||||
|
},
|
||||||
|
"reporter_type": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_reporter_type.dspy')}}",
|
||||||
|
"valueField": "reporter_type",
|
||||||
|
"textField": "reporter_type_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editexclouded": ["id", "created_at", "updated_at", "closed_at", "verified_by", "fix_commit"],
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('../api/sd_bug_create.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('../api/sd_bug_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('../api/sd_bug_delete.dspy')}}"
|
||||||
|
},
|
||||||
|
"data_filter": {
|
||||||
|
"AND": [
|
||||||
|
{"field": "iteration_id", "op": "=", "var": "iteration_input"},
|
||||||
|
{"field": "severity", "op": "=", "var": "severity_input"},
|
||||||
|
{"field": "status", "op": "=", "var": "status_input"},
|
||||||
|
{"field": "title", "op": "LIKE", "var": "title_input"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"record_toolbar": [
|
||||||
|
{
|
||||||
|
"label": "确认",
|
||||||
|
"actiontype": "dspy",
|
||||||
|
"url": "{{entire_url('../api/bug_confirm.dspy')}}",
|
||||||
|
"options": {"icon": "check", "cwidth": 16, "cheight": 9}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "关闭",
|
||||||
|
"actiontype": "dspy",
|
||||||
|
"url": "{{entire_url('../api/bug_close.dspy')}}",
|
||||||
|
"options": {"icon": "block", "cwidth": 16, "cheight": 9}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
45
json/sd_deploy_env_list.json
Normal file
45
json/sd_deploy_env_list.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"tblname": "sd_deploy_envs",
|
||||||
|
"title": "部署环境",
|
||||||
|
"params": {
|
||||||
|
"sortby": ["env_type"],
|
||||||
|
"confidential_fields": ["db_password", "ssh_key_path"],
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": ["db_password", "ssh_key_path"],
|
||||||
|
"alters": {
|
||||||
|
"project_id": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_project_options.dspy')}}",
|
||||||
|
"valueField": "project_id",
|
||||||
|
"textField": "project_id_text"
|
||||||
|
},
|
||||||
|
"env_type": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_env_type.dspy')}}",
|
||||||
|
"valueField": "env_type",
|
||||||
|
"textField": "env_type_text"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_env_status.dspy')}}",
|
||||||
|
"valueField": "status",
|
||||||
|
"textField": "status_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editexclouded": ["id", "verified_at", "created_at", "updated_at"],
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('../api/sd_deploy_env_create.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('../api/sd_deploy_env_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('../api/sd_deploy_env_delete.dspy')}}"
|
||||||
|
},
|
||||||
|
"record_toolbar": [
|
||||||
|
{
|
||||||
|
"label": "验证连接",
|
||||||
|
"actiontype": "dspy",
|
||||||
|
"url": "{{entire_url('../api/env_verify.dspy')}}",
|
||||||
|
"options": {"icon": "check", "cwidth": 16, "cheight": 9}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
58
json/sd_iteration_list.json
Normal file
58
json/sd_iteration_list.json
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"tblname": "sd_iterations",
|
||||||
|
"title": "迭代管理",
|
||||||
|
"params": {
|
||||||
|
"sortby": ["created_at desc"],
|
||||||
|
"confidential_fields": [],
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": ["scope"],
|
||||||
|
"alters": {
|
||||||
|
"project_id": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_project_options.dspy')}}",
|
||||||
|
"valueField": "project_id",
|
||||||
|
"textField": "project_id_text"
|
||||||
|
},
|
||||||
|
"iteration_type": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_iteration_type.dspy')}}",
|
||||||
|
"valueField": "iteration_type",
|
||||||
|
"textField": "iteration_type_text"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_iteration_status.dspy')}}",
|
||||||
|
"valueField": "status",
|
||||||
|
"textField": "status_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editexclouded": ["id", "task_id", "started_at", "completed_at", "created_at", "updated_at"],
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('../api/sd_iteration_create.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('../api/sd_iteration_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('../api/sd_iteration_delete.dspy')}}"
|
||||||
|
},
|
||||||
|
"data_filter": {
|
||||||
|
"AND": [
|
||||||
|
{"field": "project_id", "op": "=", "var": "project_input"},
|
||||||
|
{"field": "iteration_name", "op": "LIKE", "var": "name_input"},
|
||||||
|
{"field": "status", "op": "=", "var": "status_input"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"subtables": [
|
||||||
|
{
|
||||||
|
"field": "iteration_id",
|
||||||
|
"title": "Bug列表",
|
||||||
|
"url": "{{entire_url('../sd_bug_list?iteration_id=${id}')}}",
|
||||||
|
"subtable": "sd_bugs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "iteration_id",
|
||||||
|
"title": "测试方案",
|
||||||
|
"url": "{{entire_url('../sd_test_plan_list?iteration_id=${id}')}}",
|
||||||
|
"subtable": "sd_test_plans"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
47
json/sd_project_list.json
Normal file
47
json/sd_project_list.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"tblname": "sd_projects",
|
||||||
|
"title": "项目管理",
|
||||||
|
"params": {
|
||||||
|
"sortby": ["created_at desc"],
|
||||||
|
"logined_userorgid": "org_id",
|
||||||
|
"confidential_fields": [],
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": ["tech_stack"],
|
||||||
|
"alters": {
|
||||||
|
"status": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_status.dspy')}}",
|
||||||
|
"valueField": "status",
|
||||||
|
"textField": "status_text"
|
||||||
|
},
|
||||||
|
"project_type": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_project_type.dspy')}}",
|
||||||
|
"valueField": "project_type",
|
||||||
|
"textField": "project_type_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editexclouded": ["id", "created_at", "updated_at", "org_id"],
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('../api/sd_project_create.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('../api/sd_project_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('../api/sd_project_delete.dspy')}}"
|
||||||
|
},
|
||||||
|
"data_filter": {
|
||||||
|
"AND": [
|
||||||
|
{"field": "name", "op": "LIKE", "var": "name_input"},
|
||||||
|
{"field": "status", "op": "=", "var": "status_input"},
|
||||||
|
{"field": "project_type", "op": "=", "var": "type_input"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"subtables": [
|
||||||
|
{
|
||||||
|
"field": "project_id",
|
||||||
|
"title": "迭代列表",
|
||||||
|
"url": "{{entire_url('../sd_iteration_list?project_id=${id}')}}",
|
||||||
|
"subtable": "sd_iterations"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
50
json/sd_test_case_list.json
Normal file
50
json/sd_test_case_list.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"tblname": "sd_test_cases",
|
||||||
|
"title": "测试用例",
|
||||||
|
"params": {
|
||||||
|
"sortby": ["priority desc", "created_at desc"],
|
||||||
|
"confidential_fields": [],
|
||||||
|
"browserfields": {
|
||||||
|
"alters": {
|
||||||
|
"plan_id": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_test_plan_options.dspy')}}",
|
||||||
|
"valueField": "plan_id",
|
||||||
|
"textField": "plan_id_text"
|
||||||
|
},
|
||||||
|
"case_type": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_case_type.dspy')}}",
|
||||||
|
"valueField": "case_type",
|
||||||
|
"textField": "case_type_text"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_case_status.dspy')}}",
|
||||||
|
"valueField": "status",
|
||||||
|
"textField": "status_text"
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_priority.dspy')}}",
|
||||||
|
"valueField": "priority",
|
||||||
|
"textField": "priority_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editexclouded": ["id", "actual_result", "executed_by", "executed_at", "duration_ms", "created_at"],
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('../api/sd_test_case_create.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('../api/sd_test_case_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('../api/sd_test_case_delete.dspy')}}"
|
||||||
|
},
|
||||||
|
"data_filter": {
|
||||||
|
"AND": [
|
||||||
|
{"field": "plan_id", "op": "=", "var": "plan_input"},
|
||||||
|
{"field": "case_type", "op": "=", "var": "type_input"},
|
||||||
|
{"field": "status", "op": "=", "var": "status_input"},
|
||||||
|
{"field": "priority", "op": "=", "var": "priority_input"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
json/sd_test_plan_list.json
Normal file
44
json/sd_test_plan_list.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"tblname": "sd_test_plans",
|
||||||
|
"title": "测试方案",
|
||||||
|
"params": {
|
||||||
|
"sortby": ["created_at desc"],
|
||||||
|
"confidential_fields": [],
|
||||||
|
"browserfields": {
|
||||||
|
"alters": {
|
||||||
|
"iteration_id": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_iteration_options.dspy')}}",
|
||||||
|
"valueField": "iteration_id",
|
||||||
|
"textField": "iteration_id_text"
|
||||||
|
},
|
||||||
|
"plan_type": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_plan_type.dspy')}}",
|
||||||
|
"valueField": "plan_type",
|
||||||
|
"textField": "plan_type_text"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"uitype": "code",
|
||||||
|
"dataurl": "{{entire_url('../api/get_search_plan_status.dspy')}}",
|
||||||
|
"valueField": "status",
|
||||||
|
"textField": "status_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editexclouded": ["id", "created_at", "updated_at"],
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('../api/sd_test_plan_create.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('../api/sd_test_plan_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('../api/sd_test_plan_delete.dspy')}}"
|
||||||
|
},
|
||||||
|
"subtables": [
|
||||||
|
{
|
||||||
|
"field": "plan_id",
|
||||||
|
"title": "测试用例",
|
||||||
|
"url": "{{entire_url('../sd_test_case_list?plan_id=${id}')}}",
|
||||||
|
"subtable": "sd_test_cases"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
43
models/sd_bugs.json
Normal file
43
models/sd_bugs.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "sd_bugs",
|
||||||
|
"title": "Bug管理表",
|
||||||
|
"primary": ["id"],
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "iteration_id", "title": "迭代ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "case_id", "title": "关联用例ID", "type": "str", "length": 32},
|
||||||
|
{"name": "step_name", "title": "发现步骤名", "type": "str", "length": 100},
|
||||||
|
{"name": "title", "title": "Bug标题", "type": "str", "length": 300, "nullable": "no"},
|
||||||
|
{"name": "description", "title": "详细描述", "type": "text"},
|
||||||
|
{"name": "severity", "title": "严重程度", "type": "str", "length": 10, "nullable": "no", "default": "'major'"},
|
||||||
|
{"name": "priority", "title": "优先级", "type": "str", "length": 10, "nullable": "no", "default": "'P1'"},
|
||||||
|
{"name": "status", "title": "Bug状态", "type": "str", "length": 20, "nullable": "no", "default": "'open'"},
|
||||||
|
{"name": "reporter_type", "title": "提交人类型", "type": "str", "length": 10, "nullable": "no"},
|
||||||
|
{"name": "reporter_id", "title": "提交人ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "assignee_id", "title": "处理人ID", "type": "str", "length": 32},
|
||||||
|
{"name": "fix_description", "title": "修复说明", "type": "text"},
|
||||||
|
{"name": "fix_commit", "title": "修复Commit", "type": "str", "length": 100},
|
||||||
|
{"name": "verified_by", "title": "验证人", "type": "str", "length": 32},
|
||||||
|
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||||
|
{"name": "updated_at", "title": "更新时间", "type": "timestamp"},
|
||||||
|
{"name": "closed_at", "title": "关闭时间", "type": "timestamp"}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{"name": "idx_sd_bugs_iteration", "idxtype": "index", "idxfields": ["iteration_id"]},
|
||||||
|
{"name": "idx_sd_bugs_status", "idxtype": "index", "idxfields": ["status"]},
|
||||||
|
{"name": "idx_sd_bugs_severity", "idxtype": "index", "idxfields": ["severity"]},
|
||||||
|
{"name": "idx_sd_bugs_assignee", "idxtype": "index", "idxfields": ["assignee_id"]}
|
||||||
|
],
|
||||||
|
"codes": [
|
||||||
|
{"field": "iteration_id", "table": "sd_iterations", "valuefield": "id", "textfield": "iteration_name"},
|
||||||
|
{"field": "severity", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_bug_severity'"},
|
||||||
|
{"field": "priority", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_priority'"},
|
||||||
|
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_bug_status'"},
|
||||||
|
{"field": "reporter_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_reporter_type'"}
|
||||||
|
]
|
||||||
|
}
|
||||||
41
models/sd_deploy_envs.json
Normal file
41
models/sd_deploy_envs.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "sd_deploy_envs",
|
||||||
|
"title": "部署环境表",
|
||||||
|
"primary": ["id"],
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "project_id", "title": "项目ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "env_type", "title": "环境类型", "type": "str", "length": 20, "nullable": "no"},
|
||||||
|
{"name": "host", "title": "SSH主机地址", "type": "str", "length": 200, "nullable": "no"},
|
||||||
|
{"name": "port", "title": "SSH端口", "type": "int", "nullable": "no", "default": "22"},
|
||||||
|
{"name": "user", "title": "SSH用户", "type": "str", "length": 100, "nullable": "no"},
|
||||||
|
{"name": "ssh_key_path", "title": "SSH密钥路径", "type": "str", "length": 500},
|
||||||
|
{"name": "sudo_enabled", "title": "免密Sudo", "type": "str", "length": 1, "nullable": "no", "default": "'N'"},
|
||||||
|
{"name": "deploy_path", "title": "部署目录", "type": "str", "length": 500, "nullable": "no"},
|
||||||
|
{"name": "python_path", "title": "Python路径", "type": "str", "length": 500},
|
||||||
|
{"name": "db_host", "title": "数据库地址", "type": "str", "length": 200},
|
||||||
|
{"name": "db_port", "title": "数据库端口", "type": "int", "default": "3306"},
|
||||||
|
{"name": "db_name", "title": "数据库名", "type": "str", "length": 100},
|
||||||
|
{"name": "db_user", "title": "数据库用户", "type": "str", "length": 100},
|
||||||
|
{"name": "db_password", "title": "数据库密码(加密)", "type": "str", "length": 500},
|
||||||
|
{"name": "status", "title": "环境状态", "type": "str", "length": 20, "nullable": "no", "default": "'configured'"},
|
||||||
|
{"name": "verified_at", "title": "最近验证时间", "type": "timestamp"},
|
||||||
|
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||||
|
{"name": "updated_at", "title": "更新时间", "type": "timestamp"}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{"name": "idx_sd_deploy_envs_project", "idxtype": "index", "idxfields": ["project_id"]},
|
||||||
|
{"name": "idx_sd_deploy_envs_type", "idxtype": "index", "idxfields": ["env_type"]},
|
||||||
|
{"name": "idx_sd_deploy_envs_unique", "idxtype": "unique", "idxfields": ["project_id", "env_type"]}
|
||||||
|
],
|
||||||
|
"codes": [
|
||||||
|
{"field": "project_id", "table": "sd_projects", "valuefield": "id", "textfield": "name"},
|
||||||
|
{"field": "env_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_env_type'"},
|
||||||
|
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_env_status'"}
|
||||||
|
]
|
||||||
|
}
|
||||||
34
models/sd_iterations.json
Normal file
34
models/sd_iterations.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "sd_iterations",
|
||||||
|
"title": "SDLC迭代表",
|
||||||
|
"primary": ["id"],
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "project_id", "title": "项目ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "iteration_name", "title": "迭代名称", "type": "str", "length": 200, "nullable": "no"},
|
||||||
|
{"name": "iteration_type", "title": "迭代类型", "type": "str", "length": 20, "nullable": "no", "default": "'new_feature'"},
|
||||||
|
{"name": "scope", "title": "迭代范围(JSON)", "type": "text"},
|
||||||
|
{"name": "task_id", "title": "关联Pipeline任务ID", "type": "str", "length": 32},
|
||||||
|
{"name": "status", "title": "迭代状态", "type": "str", "length": 20, "nullable": "no", "default": "'planning'"},
|
||||||
|
{"name": "priority", "title": "优先级", "type": "int", "nullable": "no", "default": "5"},
|
||||||
|
{"name": "started_at", "title": "开始时间", "type": "timestamp"},
|
||||||
|
{"name": "completed_at", "title": "完成时间", "type": "timestamp"},
|
||||||
|
{"name": "created_by", "title": "创建人", "type": "str", "length": 32},
|
||||||
|
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||||
|
{"name": "updated_at", "title": "更新时间", "type": "timestamp"}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{"name": "idx_sd_iterations_project", "idxtype": "index", "idxfields": ["project_id"]},
|
||||||
|
{"name": "idx_sd_iterations_status", "idxtype": "index", "idxfields": ["status"]}
|
||||||
|
],
|
||||||
|
"codes": [
|
||||||
|
{"field": "project_id", "table": "sd_projects", "valuefield": "id", "textfield": "name"},
|
||||||
|
{"field": "iteration_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_iteration_type'"},
|
||||||
|
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_iteration_status'"}
|
||||||
|
]
|
||||||
|
}
|
||||||
32
models/sd_projects.json
Normal file
32
models/sd_projects.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "sd_projects",
|
||||||
|
"title": "SDLC项目表",
|
||||||
|
"primary": ["id"],
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "name", "title": "项目名称", "type": "str", "length": 200, "nullable": "no"},
|
||||||
|
{"name": "description", "title": "项目描述", "type": "text"},
|
||||||
|
{"name": "project_type", "title": "项目类型", "type": "str", "length": 50, "nullable": "no", "default": "'web_app'"},
|
||||||
|
{"name": "tech_stack", "title": "技术栈配置(JSON)", "type": "text"},
|
||||||
|
{"name": "repo_url", "title": "代码仓库地址", "type": "str", "length": 500},
|
||||||
|
{"name": "pipeline_id", "title": "关联Pipeline定义ID", "type": "str", "length": 32},
|
||||||
|
{"name": "status", "title": "项目状态", "type": "str", "length": 20, "nullable": "no", "default": "'draft'"},
|
||||||
|
{"name": "org_id", "title": "组织ID", "type": "str", "length": 32, "nullable": "no", "default": "'0'"},
|
||||||
|
{"name": "created_by", "title": "创建人", "type": "str", "length": 32},
|
||||||
|
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||||
|
{"name": "updated_at", "title": "更新时间", "type": "timestamp"}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{"name": "idx_sd_projects_status", "idxtype": "index", "idxfields": ["status"]},
|
||||||
|
{"name": "idx_sd_projects_org", "idxtype": "index", "idxfields": ["org_id"]}
|
||||||
|
],
|
||||||
|
"codes": [
|
||||||
|
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_project_status'"},
|
||||||
|
{"field": "project_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_project_type'"}
|
||||||
|
]
|
||||||
|
}
|
||||||
37
models/sd_test_cases.json
Normal file
37
models/sd_test_cases.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "sd_test_cases",
|
||||||
|
"title": "测试用例表",
|
||||||
|
"primary": ["id"],
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "plan_id", "title": "方案ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "case_name", "title": "用例名称", "type": "str", "length": 200, "nullable": "no"},
|
||||||
|
{"name": "case_type", "title": "用例类型", "type": "str", "length": 20, "nullable": "no"},
|
||||||
|
{"name": "priority", "title": "优先级", "type": "str", "length": 10, "nullable": "no", "default": "'P2'"},
|
||||||
|
{"name": "precondition", "title": "前置条件", "type": "text"},
|
||||||
|
{"name": "steps", "title": "测试步骤(JSON数组)", "type": "text"},
|
||||||
|
{"name": "expected_result", "title": "预期结果", "type": "text"},
|
||||||
|
{"name": "actual_result", "title": "实际结果", "type": "text"},
|
||||||
|
{"name": "status", "title": "用例状态", "type": "str", "length": 20, "nullable": "no", "default": "'pending'"},
|
||||||
|
{"name": "executed_by", "title": "执行人", "type": "str", "length": 32},
|
||||||
|
{"name": "executed_at", "title": "执行时间", "type": "timestamp"},
|
||||||
|
{"name": "duration_ms", "title": "执行耗时(毫秒)", "type": "int"},
|
||||||
|
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{"name": "idx_sd_test_cases_plan", "idxtype": "index", "idxfields": ["plan_id"]},
|
||||||
|
{"name": "idx_sd_test_cases_status", "idxtype": "index", "idxfields": ["status"]},
|
||||||
|
{"name": "idx_sd_test_cases_type", "idxtype": "index", "idxfields": ["case_type"]}
|
||||||
|
],
|
||||||
|
"codes": [
|
||||||
|
{"field": "plan_id", "table": "sd_test_plans", "valuefield": "id", "textfield": "plan_name"},
|
||||||
|
{"field": "case_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_test_case_type'"},
|
||||||
|
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_test_case_status'"},
|
||||||
|
{"field": "priority", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_priority'"}
|
||||||
|
]
|
||||||
|
}
|
||||||
33
models/sd_test_plans.json
Normal file
33
models/sd_test_plans.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "sd_test_plans",
|
||||||
|
"title": "测试方案表",
|
||||||
|
"primary": ["id"],
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "iteration_id", "title": "迭代ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "plan_name", "title": "方案名称", "type": "str", "length": 200, "nullable": "no"},
|
||||||
|
{"name": "plan_type", "title": "方案类型", "type": "str", "length": 20, "nullable": "no"},
|
||||||
|
{"name": "scope", "title": "测试范围", "type": "text"},
|
||||||
|
{"name": "environment", "title": "测试环境要求(JSON)", "type": "text"},
|
||||||
|
{"name": "entry_criteria", "title": "准入条件", "type": "text"},
|
||||||
|
{"name": "exit_criteria", "title": "准出条件", "type": "text"},
|
||||||
|
{"name": "status", "title": "方案状态", "type": "str", "length": 20, "nullable": "no", "default": "'draft'"},
|
||||||
|
{"name": "created_by", "title": "创建人", "type": "str", "length": 32},
|
||||||
|
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||||
|
{"name": "updated_at", "title": "更新时间", "type": "timestamp"}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{"name": "idx_sd_test_plans_iteration", "idxtype": "index", "idxfields": ["iteration_id"]},
|
||||||
|
{"name": "idx_sd_test_plans_status", "idxtype": "index", "idxfields": ["status"]}
|
||||||
|
],
|
||||||
|
"codes": [
|
||||||
|
{"field": "iteration_id", "table": "sd_iterations", "valuefield": "id", "textfield": "iteration_name"},
|
||||||
|
{"field": "plan_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_test_plan_type'"},
|
||||||
|
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sd_test_plan_status'"}
|
||||||
|
]
|
||||||
|
}
|
||||||
1
pipeline_sdlc/__init__.py
Normal file
1
pipeline_sdlc/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .adapter import load_sdlc_adapter
|
||||||
141
pipeline_sdlc/adapter.py
Normal file
141
pipeline_sdlc/adapter.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
"""
|
||||||
|
pipeline-sdlc adapter — registers all SDLC step types and handlers with pipeline-service engine.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def load_sdlc_adapter():
|
||||||
|
"""Register all SDLC step types and handlers with pipeline-service engine."""
|
||||||
|
from pipeline_service.step_registry import register_step_type, register_handler
|
||||||
|
|
||||||
|
# --- Import handlers ---
|
||||||
|
from .handlers.design import handle_table_design, handle_crud_design, handle_api_design
|
||||||
|
from .handlers.develop import handle_code_generate, handle_code_compliance_check, handle_code_auto_fix
|
||||||
|
from .handlers.test import (
|
||||||
|
handle_test_case_generate, handle_functional_test,
|
||||||
|
handle_performance_test, handle_bug_report, handle_bug_verify,
|
||||||
|
)
|
||||||
|
from .handlers.deploy import (
|
||||||
|
handle_deploy_test, handle_deploy_test_verify,
|
||||||
|
handle_deploy_production, handle_deploy_production_verify,
|
||||||
|
)
|
||||||
|
from .handlers.ops import handle_monitor
|
||||||
|
|
||||||
|
# --- Register step types ---
|
||||||
|
step_types = [
|
||||||
|
# Phase 1: Requirements
|
||||||
|
{"name": "requirement_gathering", "title": "需求采集", "category": "requirements",
|
||||||
|
"description": "人工填写需求文档表单", "is_interactive": True, "interaction_type": "human_task",
|
||||||
|
"default_role": "product_manager", "timeout_seconds": 604800},
|
||||||
|
{"name": "requirement_review", "title": "需求评审", "category": "requirements",
|
||||||
|
"description": "利益相关方审批需求文档", "is_interactive": True, "interaction_type": "approval_gate",
|
||||||
|
"default_role": "stakeholder", "timeout_seconds": 259200},
|
||||||
|
|
||||||
|
# Phase 2: Design (parallel-safe)
|
||||||
|
{"name": "table_design", "title": "数据表设计", "category": "design",
|
||||||
|
"description": "LLM辅助生成models/*.json,人工确认", "is_interactive": True,
|
||||||
|
"interaction_type": "human_task", "default_role": "architect", "timeout_seconds": 172800},
|
||||||
|
{"name": "crud_design", "title": "CRUD设计", "category": "design",
|
||||||
|
"description": "LLM辅助生成json/*.json,人工确认", "is_interactive": True,
|
||||||
|
"interaction_type": "human_task", "default_role": "architect", "timeout_seconds": 172800},
|
||||||
|
{"name": "api_design", "title": "API设计", "category": "design",
|
||||||
|
"description": "LLM辅助生成API清单和.dspy规范", "is_interactive": True,
|
||||||
|
"interaction_type": "human_task", "default_role": "architect", "timeout_seconds": 172800},
|
||||||
|
{"name": "design_review", "title": "设计评审", "category": "design",
|
||||||
|
"description": "架构负责人审批设计方案", "is_interactive": True,
|
||||||
|
"interaction_type": "approval_gate", "default_role": "architect_lead", "timeout_seconds": 172800},
|
||||||
|
|
||||||
|
# Phase 3: Development
|
||||||
|
{"name": "code_generate", "title": "代码生成", "category": "development",
|
||||||
|
"description": "LLM自动生成完整模块骨架", "is_interactive": False, "timeout_seconds": 600},
|
||||||
|
{"name": "code_compliance_check", "title": "规范检查", "category": "development",
|
||||||
|
"description": "逐项检查代码是否符合模块开发规范", "is_interactive": False, "timeout_seconds": 300},
|
||||||
|
{"name": "code_auto_fix", "title": "自动修复", "category": "development",
|
||||||
|
"description": "自动修复可修复的规范违规项", "is_interactive": False, "timeout_seconds": 300},
|
||||||
|
{"name": "code_review", "title": "代码审查", "category": "development",
|
||||||
|
"description": "高级开发审查代码和检查报告", "is_interactive": True,
|
||||||
|
"interaction_type": "approval_gate", "default_role": "senior_developer", "timeout_seconds": 259200},
|
||||||
|
{"name": "code_fix", "title": "代码修复", "category": "development",
|
||||||
|
"description": "开发者根据审查意见修改代码", "is_interactive": True,
|
||||||
|
"interaction_type": "human_task", "default_role": "developer", "timeout_seconds": 172800},
|
||||||
|
|
||||||
|
# Phase 4: Testing
|
||||||
|
{"name": "test_plan_create", "title": "测试方案", "category": "testing",
|
||||||
|
"description": "人工编写测试方案(功能/性能/安全)", "is_interactive": True,
|
||||||
|
"interaction_type": "human_task", "default_role": "tester", "timeout_seconds": 172800},
|
||||||
|
{"name": "test_case_generate", "title": "用例生成", "category": "testing",
|
||||||
|
"description": "LLM根据需求和设计自动生成测试用例", "is_interactive": False, "timeout_seconds": 600},
|
||||||
|
{"name": "functional_test", "title": "功能测试", "category": "testing",
|
||||||
|
"description": "自动执行功能和API测试用例", "is_interactive": False, "timeout_seconds": 1800},
|
||||||
|
{"name": "performance_test", "title": "性能测试", "category": "testing",
|
||||||
|
"description": "自动执行性能测试(并发/响应/吞吐)", "is_interactive": False, "timeout_seconds": 3600},
|
||||||
|
{"name": "bug_report", "title": "Bug报告", "category": "testing",
|
||||||
|
"description": "自动收集测试结果生成Bug报告", "is_interactive": False, "timeout_seconds": 300},
|
||||||
|
{"name": "bug_fix", "title": "Bug修复", "category": "testing",
|
||||||
|
"description": "人工修复Bug", "is_interactive": True,
|
||||||
|
"interaction_type": "human_task", "default_role": "developer", "timeout_seconds": 172800},
|
||||||
|
{"name": "bug_verify", "title": "Bug验证", "category": "testing",
|
||||||
|
"description": "自动回归测试验证Bug修复", "is_interactive": False, "timeout_seconds": 1800},
|
||||||
|
{"name": "acceptance_test", "title": "验收测试", "category": "testing",
|
||||||
|
"description": "产品经理人工验收", "is_interactive": True,
|
||||||
|
"interaction_type": "human_task", "default_role": "product_manager", "timeout_seconds": 259200},
|
||||||
|
|
||||||
|
# Phase 5: Deployment
|
||||||
|
{"name": "deploy_env_collect", "title": "环境配置", "category": "deployment",
|
||||||
|
"description": "人工提供部署环境信息(SSH免密/sudo/数据库)", "is_interactive": True,
|
||||||
|
"interaction_type": "human_task", "default_role": "devops", "timeout_seconds": 604800},
|
||||||
|
{"name": "deploy_test", "title": "测试部署", "category": "deployment",
|
||||||
|
"description": "自动部署到测试环境", "is_interactive": False, "timeout_seconds": 600},
|
||||||
|
{"name": "deploy_test_verify", "title": "测试验证", "category": "deployment",
|
||||||
|
"description": "自动验证测试环境部署", "is_interactive": False, "timeout_seconds": 300},
|
||||||
|
{"name": "deploy_test_approve", "title": "部署审批", "category": "deployment",
|
||||||
|
"description": "确认测试环境OK,批准生产部署", "is_interactive": True,
|
||||||
|
"interaction_type": "approval_gate", "default_role": "release_manager", "timeout_seconds": 259200},
|
||||||
|
{"name": "deploy_production", "title": "生产部署", "category": "deployment",
|
||||||
|
"description": "自动部署到生产环境", "is_interactive": False, "timeout_seconds": 600},
|
||||||
|
{"name": "deploy_production_verify", "title": "生产验证", "category": "deployment",
|
||||||
|
"description": "自动验证生产环境部署", "is_interactive": False, "timeout_seconds": 300},
|
||||||
|
|
||||||
|
# Phase 6: Operations
|
||||||
|
{"name": "monitor", "title": "运行监控", "category": "operations",
|
||||||
|
"description": "定期健康检查+异常告警", "is_interactive": False, "timeout_seconds": 120},
|
||||||
|
{"name": "incident_response", "title": "故障处理", "category": "operations",
|
||||||
|
"description": "人工处理故障", "is_interactive": True,
|
||||||
|
"interaction_type": "human_task", "default_role": "devops", "timeout_seconds": 86400},
|
||||||
|
|
||||||
|
# Phase 7: Upgrade
|
||||||
|
{"name": "upgrade_plan", "title": "升级规划", "category": "upgrade",
|
||||||
|
"description": "规划新版本迭代", "is_interactive": True,
|
||||||
|
"interaction_type": "human_task", "default_role": "product_manager", "timeout_seconds": 604800},
|
||||||
|
]
|
||||||
|
|
||||||
|
for st in step_types:
|
||||||
|
register_step_type(st)
|
||||||
|
|
||||||
|
# --- Register handlers (only auto steps have handlers) ---
|
||||||
|
handlers = {
|
||||||
|
"table_design": handle_table_design,
|
||||||
|
"crud_design": handle_crud_design,
|
||||||
|
"api_design": handle_api_design,
|
||||||
|
"code_generate": handle_code_generate,
|
||||||
|
"code_compliance_check": handle_code_compliance_check,
|
||||||
|
"code_auto_fix": handle_code_auto_fix,
|
||||||
|
"test_case_generate": handle_test_case_generate,
|
||||||
|
"functional_test": handle_functional_test,
|
||||||
|
"performance_test": handle_performance_test,
|
||||||
|
"bug_report": handle_bug_report,
|
||||||
|
"bug_verify": handle_bug_verify,
|
||||||
|
"deploy_test": handle_deploy_test,
|
||||||
|
"deploy_test_verify": handle_deploy_test_verify,
|
||||||
|
"deploy_production": handle_deploy_production,
|
||||||
|
"deploy_production_verify": handle_deploy_production_verify,
|
||||||
|
"monitor": handle_monitor,
|
||||||
|
}
|
||||||
|
|
||||||
|
for step_name, handler_func in handlers.items():
|
||||||
|
register_handler(step_name, handler_func)
|
||||||
|
|
||||||
|
logger.info(f"SDLC adapter loaded: {len(step_types)} step types, {len(handlers)} handlers registered")
|
||||||
|
return True
|
||||||
1
pipeline_sdlc/handlers/__init__.py
Normal file
1
pipeline_sdlc/handlers/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# pipeline_sdlc handlers package
|
||||||
243
pipeline_sdlc/handlers/deploy.py
Normal file
243
pipeline_sdlc/handlers/deploy.py
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
"""
|
||||||
|
Deployment phase handlers: deploy_env_collect, deploy_test, deploy_test_verify,
|
||||||
|
deploy_production, deploy_production_verify
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_deploy_test(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Deploy to test environment via SSH."""
|
||||||
|
env_output = input_data.get("deploy_env_collect", {}).get("output", {})
|
||||||
|
code_output = input_data.get("code_auto_fix", {}).get("output", {})
|
||||||
|
if not code_output:
|
||||||
|
code_output = input_data.get("code_generate", {}).get("output", {})
|
||||||
|
|
||||||
|
test_env = env_output.get("test_env", {})
|
||||||
|
if not test_env:
|
||||||
|
raise ValueError("Test environment not configured. Run deploy_env_collect first.")
|
||||||
|
|
||||||
|
host = test_env.get("host")
|
||||||
|
port = test_env.get("port", 22)
|
||||||
|
user = test_env.get("user")
|
||||||
|
ssh_key = test_env.get("ssh_key_path", "")
|
||||||
|
deploy_path = test_env.get("deploy_path")
|
||||||
|
python_path = test_env.get("python_path", "python3")
|
||||||
|
repo_url = config.get("repo_url", "")
|
||||||
|
|
||||||
|
if not all([host, user, deploy_path]):
|
||||||
|
raise ValueError(f"Incomplete test env config: host={host}, user={user}, deploy_path={deploy_path}")
|
||||||
|
|
||||||
|
# Build SSH command sequence
|
||||||
|
ssh_opts = f"-o StrictHostKeyChecking=no -p {port}"
|
||||||
|
if ssh_key:
|
||||||
|
ssh_opts += f" -i {ssh_key}"
|
||||||
|
ssh_target = f"{user}@{host}"
|
||||||
|
|
||||||
|
commands = [
|
||||||
|
f"cd {deploy_path}",
|
||||||
|
f"git pull origin main" if repo_url else "echo 'No repo, skipping git pull'",
|
||||||
|
f"{python_path} -m pip install . 2>&1 | tail -5",
|
||||||
|
"bash build.sh 2>&1 | tail -20",
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for cmd in commands:
|
||||||
|
result = await _ssh_exec(ssh_target, ssh_opts, cmd)
|
||||||
|
results.append({"command": cmd, "output": result["output"], "exit_code": result["exit_code"]})
|
||||||
|
if result["exit_code"] != 0 and "pip install" not in cmd:
|
||||||
|
logger.warning(f"Command failed: {cmd} -> exit {result['exit_code']}")
|
||||||
|
|
||||||
|
all_success = all(r["exit_code"] == 0 for r in results if "pip install" not in r.get("command", ""))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"env_type": "test",
|
||||||
|
"host": host,
|
||||||
|
"deploy_path": deploy_path,
|
||||||
|
"commands": results,
|
||||||
|
"success": all_success,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_deploy_test_verify(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Verify test deployment health."""
|
||||||
|
deploy_output = input_data.get("deploy_test", {}).get("output", {})
|
||||||
|
env_output = input_data.get("deploy_env_collect", {}).get("output", {})
|
||||||
|
test_env = env_output.get("test_env", {})
|
||||||
|
|
||||||
|
host = test_env.get("host", "localhost")
|
||||||
|
port = config.get("test_app_port", 9090)
|
||||||
|
base_url = f"http://{host}:{port}"
|
||||||
|
|
||||||
|
checks = []
|
||||||
|
|
||||||
|
# 1. Health endpoint
|
||||||
|
health_result = await _http_check(f"{base_url}/health", "health_check")
|
||||||
|
checks.append(health_result)
|
||||||
|
|
||||||
|
# 2. Index page
|
||||||
|
index_result = await _http_check(f"{base_url}/index.ui", "index_page")
|
||||||
|
checks.append(index_result)
|
||||||
|
|
||||||
|
# 3. API endpoints (from code_generate output)
|
||||||
|
code_output = input_data.get("code_auto_fix", {}).get("output", {})
|
||||||
|
if not code_output:
|
||||||
|
code_output = input_data.get("code_generate", {}).get("output", {})
|
||||||
|
api_list = code_output.get("files", [])
|
||||||
|
api_endpoints = [f for f in api_list if f.get("path", "").endswith(".dspy")]
|
||||||
|
|
||||||
|
for ep in api_endpoints[:5]: # Check first 5 API endpoints
|
||||||
|
ep_path = ep.get("path", "").replace("wwwroot/", "/")
|
||||||
|
result = await _http_check(f"{base_url}{ep_path}", f"api:{ep.get('path', '')}")
|
||||||
|
checks.append(result)
|
||||||
|
|
||||||
|
passed = sum(1 for c in checks if c.get("status") in ("ok", "reachable"))
|
||||||
|
failed = sum(1 for c in checks if c.get("status") not in ("ok", "reachable"))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"env_type": "test",
|
||||||
|
"base_url": base_url,
|
||||||
|
"total_checks": len(checks),
|
||||||
|
"passed": passed,
|
||||||
|
"failed": failed,
|
||||||
|
"checks": checks,
|
||||||
|
"verified": failed == 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_deploy_production(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Deploy to production environment via SSH."""
|
||||||
|
env_output = input_data.get("deploy_env_collect", {}).get("output", {})
|
||||||
|
prod_env = env_output.get("production_env", {})
|
||||||
|
|
||||||
|
if not prod_env:
|
||||||
|
raise ValueError("Production environment not configured. Run deploy_env_collect first.")
|
||||||
|
|
||||||
|
host = prod_env.get("host")
|
||||||
|
port = prod_env.get("port", 22)
|
||||||
|
user = prod_env.get("user")
|
||||||
|
ssh_key = prod_env.get("ssh_key_path", "")
|
||||||
|
deploy_path = prod_env.get("deploy_path")
|
||||||
|
python_path = prod_env.get("python_path", "python3")
|
||||||
|
|
||||||
|
if not all([host, user, deploy_path]):
|
||||||
|
raise ValueError(f"Incomplete prod env: host={host}, user={user}, deploy_path={deploy_path}")
|
||||||
|
|
||||||
|
ssh_opts = f"-o StrictHostKeyChecking=no -p {port}"
|
||||||
|
if ssh_key:
|
||||||
|
ssh_opts += f" -i {ssh_key}"
|
||||||
|
ssh_target = f"{user}@{host}"
|
||||||
|
|
||||||
|
commands = [
|
||||||
|
f"cd {deploy_path}",
|
||||||
|
"git pull origin main",
|
||||||
|
f"{python_path} -m pip install . 2>&1 | tail -5",
|
||||||
|
"bash build.sh 2>&1 | tail -20",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check if restart is needed
|
||||||
|
sudo = "sudo" if prod_env.get("sudo_enabled") == "Y" else ""
|
||||||
|
restart_cmd = config.get("restart_command", f"{sudo} systemctl restart app")
|
||||||
|
commands.append(restart_cmd)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for cmd in commands:
|
||||||
|
result = await _ssh_exec(ssh_target, ssh_opts, cmd)
|
||||||
|
results.append({"command": cmd, "output": result["output"], "exit_code": result["exit_code"]})
|
||||||
|
if result["exit_code"] != 0 and "pip install" not in cmd and "systemctl" not in cmd:
|
||||||
|
raise RuntimeError(f"Production deploy failed at: {cmd} -> exit {result['exit_code']}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"env_type": "production",
|
||||||
|
"host": host,
|
||||||
|
"deploy_path": deploy_path,
|
||||||
|
"commands": results,
|
||||||
|
"success": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_deploy_production_verify(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Verify production deployment health."""
|
||||||
|
env_output = input_data.get("deploy_env_collect", {}).get("output", {})
|
||||||
|
prod_env = env_output.get("production_env", {})
|
||||||
|
|
||||||
|
host = prod_env.get("host", "localhost")
|
||||||
|
port = config.get("production_app_port", 80)
|
||||||
|
base_url = f"http://{host}:{port}"
|
||||||
|
|
||||||
|
checks = []
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
health_result = await _http_check(f"{base_url}/health", "health_check")
|
||||||
|
checks.append(health_result)
|
||||||
|
|
||||||
|
# Index page
|
||||||
|
index_result = await _http_check(f"{base_url}/index.ui", "index_page")
|
||||||
|
checks.append(index_result)
|
||||||
|
|
||||||
|
# Login page (if exists)
|
||||||
|
login_result = await _http_check(f"{base_url}/login.ui", "login_page")
|
||||||
|
checks.append(login_result)
|
||||||
|
|
||||||
|
passed = sum(1 for c in checks if c.get("status") in ("ok", "reachable"))
|
||||||
|
failed = sum(1 for c in checks if c.get("status") not in ("ok", "reachable"))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"env_type": "production",
|
||||||
|
"base_url": base_url,
|
||||||
|
"total_checks": len(checks),
|
||||||
|
"passed": passed,
|
||||||
|
"failed": failed,
|
||||||
|
"checks": checks,
|
||||||
|
"verified": failed == 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Helper functions ---
|
||||||
|
|
||||||
|
async def _ssh_exec(target, opts, command):
|
||||||
|
"""Execute command via SSH."""
|
||||||
|
full_cmd = f"ssh {opts} {target} '{command}'"
|
||||||
|
try:
|
||||||
|
proc = await asyncio.create_subprocess_shell(
|
||||||
|
full_cmd,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
|
||||||
|
output = stdout.decode("utf-8", errors="replace")
|
||||||
|
if stderr:
|
||||||
|
err_text = stderr.decode("utf-8", errors="replace")
|
||||||
|
if err_text:
|
||||||
|
output += f"\nSTDERR: {err_text}"
|
||||||
|
return {"output": output[:2000], "exit_code": proc.returncode}
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return {"output": "SSH command timed out (120s)", "exit_code": -1}
|
||||||
|
except Exception as e:
|
||||||
|
return {"output": str(e), "exit_code": -1}
|
||||||
|
|
||||||
|
|
||||||
|
async def _http_check(url, check_name):
|
||||||
|
"""Simple HTTP health check."""
|
||||||
|
import aiohttp
|
||||||
|
try:
|
||||||
|
timeout = aiohttp.ClientTimeout(total=10)
|
||||||
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
|
async with session.get(url) as resp:
|
||||||
|
return {
|
||||||
|
"check": check_name,
|
||||||
|
"url": url,
|
||||||
|
"status_code": resp.status,
|
||||||
|
"status": "ok" if resp.status < 400 else "fail",
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"check": check_name,
|
||||||
|
"url": url,
|
||||||
|
"status": "fail",
|
||||||
|
"error": str(e)[:200],
|
||||||
|
}
|
||||||
243
pipeline_sdlc/handlers/design.py
Normal file
243
pipeline_sdlc/handlers/design.py
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
"""
|
||||||
|
Design phase handlers: table_design, crud_design, api_design
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_table_design(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Generate models/*.json from requirement document using LLM."""
|
||||||
|
requirement_doc = input_data.get("requirement_review", {}).get("output", {})
|
||||||
|
if not requirement_doc:
|
||||||
|
requirement_doc = input_data.get("requirement_gathering", {}).get("output", {})
|
||||||
|
|
||||||
|
prompt = f"""Based on the following requirement document, generate database table definitions
|
||||||
|
in the standardized JSON format (database-table-definition-spec).
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
{json.dumps(requirement_doc, ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Each table must have summary (with primary as array), fields, indexes, codes sections
|
||||||
|
- id field: str(32), not null
|
||||||
|
- Use abstract types: str, int, text, timestamp, date, datetime, float, double
|
||||||
|
- str/float/double need length (and dec for float/double)
|
||||||
|
- indexes idxfields must be arrays
|
||||||
|
- codes referencing appcodes_kv must use parentid= in cond, never id=
|
||||||
|
- Add org_id str(32) default '0' for multi-tenant tables
|
||||||
|
|
||||||
|
Output a JSON object with key "models" containing an array of table definitions.
|
||||||
|
Each table definition must include a "filename" key (e.g., "users.json").
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pipeline_service.llm_bridge import call_llm
|
||||||
|
result = await call_llm(tenant_id, prompt, config.get("llm_model", "default"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
models_data = json.loads(result)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Try to extract JSON from response
|
||||||
|
import re
|
||||||
|
json_match = re.search(r'\{.*\}', result, re.DOTALL)
|
||||||
|
if json_match:
|
||||||
|
models_data = json.loads(json_match.group())
|
||||||
|
else:
|
||||||
|
raise ValueError(f"LLM returned non-JSON response: {result[:200]}")
|
||||||
|
|
||||||
|
if "models" not in models_data:
|
||||||
|
models_data = {"models": [models_data]}
|
||||||
|
|
||||||
|
# Validate each model
|
||||||
|
for model in models_data["models"]:
|
||||||
|
_validate_table_definition(model)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"models": models_data["models"],
|
||||||
|
"table_count": len(models_data["models"]),
|
||||||
|
"generated_by": "llm",
|
||||||
|
"needs_review": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_crud_design(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Generate json/*.json CRUD definitions from table designs."""
|
||||||
|
table_design = input_data.get("table_design", {}).get("output", {})
|
||||||
|
models = table_design.get("models", [])
|
||||||
|
|
||||||
|
if not models:
|
||||||
|
raise ValueError("No table models found from table_design step")
|
||||||
|
|
||||||
|
prompt = f"""Based on the following table definitions, generate CRUD definition files
|
||||||
|
in the standardized JSON format (crud-definition-spec).
|
||||||
|
|
||||||
|
Tables:
|
||||||
|
{json.dumps(models, ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Root keys must be: tblname, title (optional), params
|
||||||
|
- params must have: browserfields, editable (with new/update/delete_data_url)
|
||||||
|
- Use {{entire_url('../api/xxx.dspy')}} for editable URLs with ../ prefix
|
||||||
|
- Determine tree vs list view based on self-referencing foreign keys
|
||||||
|
- Add data_filter for searchable fields
|
||||||
|
- Add logined_userorgid for org-scoped tables
|
||||||
|
- Use uitype: "code" with dataurl for foreign key dropdowns
|
||||||
|
|
||||||
|
Output a JSON object with key "cruds" containing an array of CRUD definitions.
|
||||||
|
Each definition must include a "filename" key (e.g., "users_list.json").
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pipeline_service.llm_bridge import call_llm
|
||||||
|
result = await call_llm(tenant_id, prompt, config.get("llm_model", "default"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
cruds_data = json.loads(result)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
import re
|
||||||
|
json_match = re.search(r'\{.*\}', result, re.DOTALL)
|
||||||
|
if json_match:
|
||||||
|
cruds_data = json.loads(json_match.group())
|
||||||
|
else:
|
||||||
|
raise ValueError(f"LLM returned non-JSON response: {result[:200]}")
|
||||||
|
|
||||||
|
if "cruds" not in cruds_data:
|
||||||
|
cruds_data = {"cruds": [cruds_data]}
|
||||||
|
|
||||||
|
for crud in cruds_data["cruds"]:
|
||||||
|
_validate_crud_definition(crud)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cruds": cruds_data["cruds"],
|
||||||
|
"crud_count": len(cruds_data["cruds"]),
|
||||||
|
"generated_by": "llm",
|
||||||
|
"needs_review": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_api_design(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Generate API endpoint list and .dspy specifications."""
|
||||||
|
table_design = input_data.get("table_design", {}).get("output", {})
|
||||||
|
crud_design = input_data.get("crud_design", {}).get("output", {})
|
||||||
|
|
||||||
|
models = table_design.get("models", [])
|
||||||
|
cruds = crud_design.get("cruds", [])
|
||||||
|
|
||||||
|
prompt = f"""Based on the following table and CRUD definitions, generate a complete
|
||||||
|
API endpoint specification for a Sage module.
|
||||||
|
|
||||||
|
Tables: {json.dumps(models, ensure_ascii=False)}
|
||||||
|
CRUDs: {json.dumps(cruds, ensure_ascii=False)}
|
||||||
|
|
||||||
|
For each table, generate:
|
||||||
|
1. Standard CRUD endpoints: create, update, delete .dspy files in wwwroot/api/
|
||||||
|
2. Search endpoints: get_search_{fieldname}.dspy for foreign key dropdowns
|
||||||
|
3. Custom business logic endpoints as needed
|
||||||
|
|
||||||
|
Rules for .dspy files:
|
||||||
|
- NO import statements (json, datetime, debug, DBPools, get_user, params_kw etc are pre-loaded)
|
||||||
|
- Use return instead of print
|
||||||
|
- Use getID() instead of uuid()
|
||||||
|
- Use await get_user() not get_user()
|
||||||
|
- No ServerEnv() usage
|
||||||
|
|
||||||
|
Output JSON with key "api_list" containing array of:
|
||||||
|
{{"filename": "xxx.dspy", "path": "wwwroot/api/xxx.dspy", "method": "POST", "description": "..."}}
|
||||||
|
|
||||||
|
Also include "dspy_specs" with the implementation spec for each .dspy file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pipeline_service.llm_bridge import call_llm
|
||||||
|
result = await call_llm(tenant_id, prompt, config.get("llm_model", "default"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
api_data = json.loads(result)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
import re
|
||||||
|
json_match = re.search(r'\{.*\}', result, re.DOTALL)
|
||||||
|
if json_match:
|
||||||
|
api_data = json.loads(json_match.group())
|
||||||
|
else:
|
||||||
|
raise ValueError(f"LLM returned non-JSON response: {result[:200]}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"api_list": api_data.get("api_list", []),
|
||||||
|
"dspy_specs": api_data.get("dspy_specs", []),
|
||||||
|
"endpoint_count": len(api_data.get("api_list", [])),
|
||||||
|
"generated_by": "llm",
|
||||||
|
"needs_review": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_table_definition(model):
|
||||||
|
"""Basic validation of table definition against database-table-definition-spec."""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
if "summary" not in model:
|
||||||
|
errors.append("Missing 'summary' section")
|
||||||
|
elif not isinstance(model["summary"], list) or len(model["summary"]) == 0:
|
||||||
|
errors.append("'summary' must be a non-empty array")
|
||||||
|
else:
|
||||||
|
s = model["summary"][0]
|
||||||
|
if "primary" not in s:
|
||||||
|
errors.append("summary[0] missing 'primary'")
|
||||||
|
elif not isinstance(s["primary"], list):
|
||||||
|
errors.append(f"summary[0].primary must be array, got {type(s['primary']).__name__}")
|
||||||
|
|
||||||
|
if "fields" not in model:
|
||||||
|
errors.append("Missing 'fields' section")
|
||||||
|
elif not isinstance(model["fields"], list):
|
||||||
|
errors.append("'fields' must be an array")
|
||||||
|
else:
|
||||||
|
for f in model["fields"]:
|
||||||
|
ftype = f.get("type", "")
|
||||||
|
if ftype in ("str", "char") and not f.get("length"):
|
||||||
|
errors.append(f"Field '{f.get('name')}' type={ftype} missing length")
|
||||||
|
if ftype in ("float", "double", "ddouble"):
|
||||||
|
if not f.get("length"):
|
||||||
|
errors.append(f"Field '{f.get('name')}' type={ftype} missing length")
|
||||||
|
if not f.get("dec"):
|
||||||
|
errors.append(f"Field '{f.get('name')}' type={ftype} missing dec")
|
||||||
|
|
||||||
|
if "indexes" in model:
|
||||||
|
for idx in model["indexes"]:
|
||||||
|
if "idxfields" not in idx:
|
||||||
|
errors.append(f"Index '{idx.get('name')}' missing 'idxfields'")
|
||||||
|
elif not isinstance(idx["idxfields"], list):
|
||||||
|
errors.append(f"Index '{idx.get('name')}' idxfields must be array")
|
||||||
|
|
||||||
|
if "codes" in model:
|
||||||
|
for code in model["codes"]:
|
||||||
|
if code.get("table") == "appcodes_kv":
|
||||||
|
cond = code.get("cond", "")
|
||||||
|
if "id=" in cond and "parentid=" not in cond:
|
||||||
|
errors.append(
|
||||||
|
f"Code field '{code.get('field')}' uses id= instead of parentid= for appcodes_kv"
|
||||||
|
)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
logger.warning(f"Table definition validation warnings: {errors}")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_crud_definition(crud):
|
||||||
|
"""Basic validation of CRUD definition against crud-definition-spec."""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
if "tblname" not in crud:
|
||||||
|
errors.append("Missing 'tblname' root key")
|
||||||
|
|
||||||
|
if "params" not in crud:
|
||||||
|
errors.append("Missing 'params' section")
|
||||||
|
else:
|
||||||
|
params = crud["params"]
|
||||||
|
if "editable" not in params:
|
||||||
|
errors.append("Missing 'editable' section in params")
|
||||||
|
else:
|
||||||
|
editable = params["editable"]
|
||||||
|
for key in ("new_data_url", "update_data_url", "delete_data_url"):
|
||||||
|
if key not in editable:
|
||||||
|
errors.append(f"Missing '{key}' in editable")
|
||||||
|
|
||||||
|
return errors
|
||||||
526
pipeline_sdlc/handlers/develop.py
Normal file
526
pipeline_sdlc/handlers/develop.py
Normal file
@ -0,0 +1,526 @@
|
|||||||
|
"""
|
||||||
|
Development phase handlers: code_generate, code_compliance_check, code_auto_fix
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import py_compile
|
||||||
|
import tempfile
|
||||||
|
import re
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_code_generate(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Generate complete module skeleton from design artifacts."""
|
||||||
|
table_design = input_data.get("table_design", {}).get("output", {})
|
||||||
|
crud_design = input_data.get("crud_design", {}).get("output", {})
|
||||||
|
api_design = input_data.get("api_design", {}).get("output", {})
|
||||||
|
|
||||||
|
models = table_design.get("models", [])
|
||||||
|
cruds = crud_design.get("cruds", [])
|
||||||
|
api_specs = api_design.get("dspy_specs", [])
|
||||||
|
|
||||||
|
module_name = config.get("module_name", "new_module")
|
||||||
|
|
||||||
|
prompt = f"""Generate a complete Sage module based on the following design artifacts.
|
||||||
|
|
||||||
|
## Table Definitions (models/*.json):
|
||||||
|
{json.dumps(models, ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
|
## CRUD Definitions (json/*.json):
|
||||||
|
{json.dumps(cruds, ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
|
## API Specifications:
|
||||||
|
{json.dumps(api_specs, ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
|
Module name: {module_name}
|
||||||
|
|
||||||
|
Generate the following files:
|
||||||
|
|
||||||
|
1. **{module_name}/init.py** — Module init with load_{module_name}() function
|
||||||
|
- Register all CRUD functions with ServerEnv
|
||||||
|
- Include both singular and plural function names
|
||||||
|
|
||||||
|
2. **{module_name}/__init__.py** — Import all public functions from init.py
|
||||||
|
|
||||||
|
3. **wwwroot/api/*.dspy** — All API endpoint files
|
||||||
|
- NO imports (pre-loaded: json, datetime, debug, DBPools, get_user, params_kw, getID, etc.)
|
||||||
|
- Use return instead of print
|
||||||
|
- Use getID() instead of uuid()
|
||||||
|
- Use `async with get_sor_context(request._run_ns, dbname) as sor:` pattern
|
||||||
|
- sor.U/I/C/D with correct parameter counts (sor.U = 2 params: tablename + data dict with id inside)
|
||||||
|
|
||||||
|
4. **wwwroot/index.ui** — Module entry page (bricks JSON format)
|
||||||
|
|
||||||
|
5. **pyproject.toml** — Package config
|
||||||
|
|
||||||
|
6. **build.sh** — Build script
|
||||||
|
|
||||||
|
7. **scripts/load_path.py** — RBAC path registration (NO wildcards, explicit paths only)
|
||||||
|
|
||||||
|
Output a JSON object with key "files" containing:
|
||||||
|
{{"path": "relative/path", "content": "file content"}}
|
||||||
|
|
||||||
|
Follow module-development-spec strictly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pipeline_service.llm_bridge import call_llm
|
||||||
|
result = await call_llm(tenant_id, prompt, config.get("llm_model", "default"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
code_data = json.loads(result)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
json_match = re.search(r'\{.*"files".*\}', result, re.DOTALL)
|
||||||
|
if json_match:
|
||||||
|
code_data = json.loads(json_match.group())
|
||||||
|
else:
|
||||||
|
raise ValueError(f"LLM returned non-JSON: {result[:200]}")
|
||||||
|
|
||||||
|
files = code_data.get("files", [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"files": files,
|
||||||
|
"file_count": len(files),
|
||||||
|
"module_name": module_name,
|
||||||
|
"generated_by": "llm",
|
||||||
|
"needs_review": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_code_compliance_check(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Check generated code against all module development specs."""
|
||||||
|
code_output = input_data.get("code_generate", {}).get("output", {})
|
||||||
|
files = code_output.get("files", [])
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
raise ValueError("No code files found from code_generate step")
|
||||||
|
|
||||||
|
report = {
|
||||||
|
"total_files": len(files),
|
||||||
|
"violations": [],
|
||||||
|
"auto_fixable": [],
|
||||||
|
"summary": {"pass": 0, "fail": 0, "warning": 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for file_entry in files:
|
||||||
|
path = file_entry.get("path", "")
|
||||||
|
content = file_entry.get("content", "")
|
||||||
|
file_violations = []
|
||||||
|
|
||||||
|
if path.endswith(".py"):
|
||||||
|
file_violations = _check_python_file(path, content)
|
||||||
|
elif path.endswith(".dspy"):
|
||||||
|
file_violations = _check_dspy_file(path, content)
|
||||||
|
elif path.endswith(".json") and "/models/" in path:
|
||||||
|
file_violations = _check_model_json(path, content)
|
||||||
|
elif path.endswith(".json") and "/json/" in path:
|
||||||
|
file_violations = _check_crud_json(path, content)
|
||||||
|
elif path == "build.sh" or path.endswith("build.sh"):
|
||||||
|
file_violations = _check_build_sh(path, content)
|
||||||
|
elif path == "pyproject.toml":
|
||||||
|
file_violations = _check_pyproject(path, content)
|
||||||
|
elif "load_path.py" in path:
|
||||||
|
file_violations = _check_load_path(path, content)
|
||||||
|
|
||||||
|
for v in file_violations:
|
||||||
|
v["file"] = path
|
||||||
|
if v.get("auto_fixable"):
|
||||||
|
report["auto_fixable"].append(v)
|
||||||
|
report["violations"].append(v)
|
||||||
|
|
||||||
|
if not file_violations:
|
||||||
|
report["summary"]["pass"] += 1
|
||||||
|
else:
|
||||||
|
report["summary"]["fail"] += 1
|
||||||
|
|
||||||
|
report["summary"]["total_violations"] = len(report["violations"])
|
||||||
|
report["summary"]["auto_fixable_count"] = len(report["auto_fixable"])
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_code_auto_fix(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Auto-fix fixable compliance violations."""
|
||||||
|
check_output = input_data.get("code_compliance_check", {}).get("output", {})
|
||||||
|
code_output = input_data.get("code_generate", {}).get("output", {})
|
||||||
|
auto_fixable = check_output.get("auto_fixable", [])
|
||||||
|
|
||||||
|
if not auto_fixable:
|
||||||
|
return {"fixed_count": 0, "files": code_output.get("files", [])}
|
||||||
|
|
||||||
|
files = code_output.get("files", [])
|
||||||
|
fixed_count = 0
|
||||||
|
fix_log = []
|
||||||
|
|
||||||
|
# Group violations by file
|
||||||
|
violations_by_file = {}
|
||||||
|
for v in auto_fixable:
|
||||||
|
fpath = v.get("file", "")
|
||||||
|
violations_by_file.setdefault(fpath, []).append(v)
|
||||||
|
|
||||||
|
for file_entry in files:
|
||||||
|
path = file_entry.get("path", "")
|
||||||
|
if path not in violations_by_file:
|
||||||
|
continue
|
||||||
|
|
||||||
|
content = file_entry.get("content", "")
|
||||||
|
for v in violations_by_file[path]:
|
||||||
|
rule = v.get("rule", "")
|
||||||
|
new_content = _apply_fix(content, rule, v)
|
||||||
|
if new_content != content:
|
||||||
|
file_entry["content"] = new_content
|
||||||
|
content = new_content
|
||||||
|
fixed_count += 1
|
||||||
|
fix_log.append({"file": path, "rule": rule, "fix": v.get("message", "")})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"fixed_count": fixed_count,
|
||||||
|
"fix_log": fix_log,
|
||||||
|
"files": files,
|
||||||
|
"remaining_violations": check_output.get("summary", {}).get("total_violations", 0) - fixed_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Compliance checkers ---
|
||||||
|
|
||||||
|
def _check_python_file(path, content):
|
||||||
|
"""Check Python file compliance."""
|
||||||
|
violations = []
|
||||||
|
|
||||||
|
# Syntax check
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
||||||
|
f.write(content)
|
||||||
|
f.flush()
|
||||||
|
py_compile.compile(f.name, doraise=True)
|
||||||
|
os.unlink(f.name)
|
||||||
|
except py_compile.PyCompileError as e:
|
||||||
|
violations.append({
|
||||||
|
"rule": "py_syntax",
|
||||||
|
"severity": "error",
|
||||||
|
"message": f"Syntax error: {str(e)[:200]}",
|
||||||
|
"auto_fixable": False,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check for print() instead of return/logging
|
||||||
|
if re.search(r'^\s*print\s*\(', content, re.MULTILINE):
|
||||||
|
violations.append({
|
||||||
|
"rule": "no_print",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Use logger/logging instead of print()",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
def _check_dspy_file(path, content):
|
||||||
|
"""Check .dspy file compliance."""
|
||||||
|
violations = []
|
||||||
|
|
||||||
|
# No imports
|
||||||
|
import_lines = re.findall(r'^(import |from .+ import )', content, re.MULTILINE)
|
||||||
|
if import_lines:
|
||||||
|
# Allow only sqlor.filter import
|
||||||
|
for line in import_lines:
|
||||||
|
if "from sqlor.filter import" not in line:
|
||||||
|
violations.append({
|
||||||
|
"rule": "dspy_no_import",
|
||||||
|
"severity": "error",
|
||||||
|
"message": f"Import not allowed in .dspy: {line.strip()}",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
# No print()
|
||||||
|
if re.search(r'^\s*print\s*\(', content, re.MULTILINE):
|
||||||
|
violations.append({
|
||||||
|
"rule": "dspy_no_print",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Use return instead of print() in .dspy",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
# No uuid()
|
||||||
|
if "uuid" in content.lower() and "getID()" not in content:
|
||||||
|
violations.append({
|
||||||
|
"rule": "dspy_no_uuid",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Use getID() instead of uuid",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
# No shebang
|
||||||
|
if content.startswith("#!"):
|
||||||
|
violations.append({
|
||||||
|
"rule": "dspy_no_shebang",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Remove shebang line from .dspy",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check ServerEnv usage
|
||||||
|
if "ServerEnv()" in content:
|
||||||
|
violations.append({
|
||||||
|
"rule": "dspy_no_serverenv",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Do not use ServerEnv() in .dspy - functions are pre-loaded",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check get_user() without await
|
||||||
|
if re.search(r'(?<!await\s)get_user\(\)', content):
|
||||||
|
violations.append({
|
||||||
|
"rule": "dspy_await_get_user",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Use await get_user() not get_user()",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check sor.U() 3-parameter pitfall
|
||||||
|
sor_u_matches = re.findall(r'sor\.U\s*\(\s*\w+\s*,\s*\w+\s*,', content)
|
||||||
|
if sor_u_matches:
|
||||||
|
violations.append({
|
||||||
|
"rule": "sor_u_2params",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "sor.U() takes only 2 params (tablename, data). Put id in data dict!",
|
||||||
|
"auto_fixable": False,
|
||||||
|
})
|
||||||
|
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
def _check_model_json(path, content):
|
||||||
|
"""Check model JSON against database-table-definition-spec."""
|
||||||
|
violations = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(content)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
violations.append({
|
||||||
|
"rule": "json_syntax",
|
||||||
|
"severity": "error",
|
||||||
|
"message": f"Invalid JSON: {str(e)[:100]}",
|
||||||
|
"auto_fixable": False,
|
||||||
|
})
|
||||||
|
return violations
|
||||||
|
|
||||||
|
if "summary" not in data:
|
||||||
|
violations.append({
|
||||||
|
"rule": "model_summary_required",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Missing 'summary' section",
|
||||||
|
"auto_fixable": False,
|
||||||
|
})
|
||||||
|
elif isinstance(data["summary"], list) and len(data["summary"]) > 0:
|
||||||
|
s = data["summary"][0]
|
||||||
|
pk = s.get("primary")
|
||||||
|
if pk and not isinstance(pk, list):
|
||||||
|
violations.append({
|
||||||
|
"rule": "model_primary_array",
|
||||||
|
"severity": "error",
|
||||||
|
"message": f"'primary' must be array, got {type(pk).__name__}: {pk}",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
if "fields" in data and isinstance(data["fields"], list):
|
||||||
|
for f in data["fields"]:
|
||||||
|
ftype = f.get("type", "")
|
||||||
|
fname = f.get("name", "?")
|
||||||
|
if ftype in ("float", "double", "ddouble"):
|
||||||
|
if not f.get("dec"):
|
||||||
|
violations.append({
|
||||||
|
"rule": "model_dec_required",
|
||||||
|
"severity": "error",
|
||||||
|
"message": f"Field '{fname}' type={ftype} missing 'dec'",
|
||||||
|
"auto_fixable": False,
|
||||||
|
})
|
||||||
|
dec_val = f.get("dec")
|
||||||
|
if isinstance(dec_val, str):
|
||||||
|
violations.append({
|
||||||
|
"rule": "model_dec_integer",
|
||||||
|
"severity": "error",
|
||||||
|
"message": f"Field '{fname}' dec must be integer, got string",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
if "indexes" in data:
|
||||||
|
for idx in data.get("indexes", []):
|
||||||
|
idxfields = idx.get("idxfields")
|
||||||
|
if idxfields and not isinstance(idxfields, list):
|
||||||
|
violations.append({
|
||||||
|
"rule": "model_idxfields_array",
|
||||||
|
"severity": "error",
|
||||||
|
"message": f"Index '{idx.get('name')}' idxfields must be array",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
if "codes" in data:
|
||||||
|
for code in data.get("codes", []):
|
||||||
|
if code.get("table") == "appcodes_kv":
|
||||||
|
cond = code.get("cond", "")
|
||||||
|
if cond and "id=" in cond and "parentid=" not in cond:
|
||||||
|
violations.append({
|
||||||
|
"rule": "model_codes_parentid",
|
||||||
|
"severity": "error",
|
||||||
|
"message": f"Code field '{code.get('field')}' uses id= instead of parentid=",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
def _check_crud_json(path, content):
|
||||||
|
"""Check CRUD JSON against crud-definition-spec."""
|
||||||
|
violations = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(content)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
violations.append({
|
||||||
|
"rule": "json_syntax",
|
||||||
|
"severity": "error",
|
||||||
|
"message": f"Invalid JSON: {str(e)[:100]}",
|
||||||
|
"auto_fixable": False,
|
||||||
|
})
|
||||||
|
return violations
|
||||||
|
|
||||||
|
if "tablename" in data:
|
||||||
|
violations.append({
|
||||||
|
"rule": "crud_tblname_not_tablename",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Use 'tblname' not 'tablename'",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
if "tblname" not in data:
|
||||||
|
violations.append({
|
||||||
|
"rule": "crud_tblname_required",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Missing 'tblname' root key",
|
||||||
|
"auto_fixable": False,
|
||||||
|
})
|
||||||
|
|
||||||
|
params = data.get("params", {})
|
||||||
|
if "editable" not in params:
|
||||||
|
violations.append({
|
||||||
|
"rule": "crud_editable_required",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Missing 'editable' section in params",
|
||||||
|
"auto_fixable": False,
|
||||||
|
})
|
||||||
|
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
def _check_build_sh(path, content):
|
||||||
|
"""Check build.sh compliance."""
|
||||||
|
violations = []
|
||||||
|
if "#!/usr/bin/env bash" not in content and "#!/bin/bash" not in content:
|
||||||
|
violations.append({
|
||||||
|
"rule": "build_sh_shebang",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Missing bash shebang",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
if "set -e" not in content:
|
||||||
|
violations.append({
|
||||||
|
"rule": "build_sh_set_e",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Missing 'set -e' for fail-fast",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
def _check_pyproject(path, content):
|
||||||
|
"""Check pyproject.toml compliance."""
|
||||||
|
violations = []
|
||||||
|
if "ahserver" in content:
|
||||||
|
violations.append({
|
||||||
|
"rule": "pyproject_no_ahserver",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Do not declare ahserver as dependency (installed by build.sh)",
|
||||||
|
"auto_fixable": True,
|
||||||
|
})
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
def _check_load_path(path, content):
|
||||||
|
"""Check load_path.py — no wildcards."""
|
||||||
|
violations = []
|
||||||
|
if "%" in content or "*" in content:
|
||||||
|
# Exclude comments
|
||||||
|
for line in content.split("\n"):
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("#"):
|
||||||
|
continue
|
||||||
|
if "%" in stripped or "*" in stripped:
|
||||||
|
violations.append({
|
||||||
|
"rule": "load_path_no_wildcard",
|
||||||
|
"severity": "error",
|
||||||
|
"message": f"Wildcard found in load_path.py: {stripped[:80]}",
|
||||||
|
"auto_fixable": False,
|
||||||
|
})
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
# --- Auto-fix functions ---
|
||||||
|
|
||||||
|
def _apply_fix(content, rule, violation):
|
||||||
|
"""Apply auto-fix for a known rule violation."""
|
||||||
|
if rule == "no_print":
|
||||||
|
# Replace print() with logger.info()
|
||||||
|
content = re.sub(r'^(\s*)print\s*\(', r'\1logger.info(', content, flags=re.MULTILINE)
|
||||||
|
elif rule == "dspy_no_print":
|
||||||
|
content = re.sub(r'^(\s*)print\s*\(', r'\1# return ', content, flags=re.MULTILINE)
|
||||||
|
elif rule == "dspy_no_shebang":
|
||||||
|
if content.startswith("#!"):
|
||||||
|
content = content.split("\n", 1)[1] if "\n" in content else ""
|
||||||
|
elif rule == "dspy_no_serverenv":
|
||||||
|
content = re.sub(r'env\s*=\s*ServerEnv\(\)\s*\n?', '', content)
|
||||||
|
elif rule == "model_primary_array":
|
||||||
|
try:
|
||||||
|
data = json.loads(content)
|
||||||
|
pk = data.get("summary", [{}])[0].get("primary")
|
||||||
|
if isinstance(pk, str):
|
||||||
|
data["summary"][0]["primary"] = [pk]
|
||||||
|
content = json.dumps(data, ensure_ascii=False, indent=4)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif rule == "model_idxfields_array":
|
||||||
|
try:
|
||||||
|
data = json.loads(content)
|
||||||
|
for idx in data.get("indexes", []):
|
||||||
|
if isinstance(idx.get("idxfields"), str):
|
||||||
|
idx["idxfields"] = [idx["idxfields"]]
|
||||||
|
content = json.dumps(data, ensure_ascii=False, indent=4)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif rule == "model_codes_parentid":
|
||||||
|
try:
|
||||||
|
data = json.loads(content)
|
||||||
|
for code in data.get("codes", []):
|
||||||
|
if code.get("table") == "appcodes_kv":
|
||||||
|
cond = code.get("cond", "")
|
||||||
|
if "id=" in cond and "parentid=" not in cond:
|
||||||
|
code["cond"] = cond.replace("id=", "parentid=")
|
||||||
|
content = json.dumps(data, ensure_ascii=False, indent=4)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif rule == "crud_tblname_not_tablename":
|
||||||
|
try:
|
||||||
|
data = json.loads(content)
|
||||||
|
if "tablename" in data and "tblname" not in data:
|
||||||
|
data["tblname"] = data.pop("tablename")
|
||||||
|
content = json.dumps(data, ensure_ascii=False, indent=4)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif rule == "build_sh_shebang":
|
||||||
|
content = "#!/usr/bin/env bash\nset -e\n\n" + content
|
||||||
|
elif rule == "build_sh_set_e":
|
||||||
|
content = content.replace("#!/usr/bin/env bash\n", "#!/usr/bin/env bash\nset -e\n", 1)
|
||||||
|
elif rule == "pyproject_no_ahserver":
|
||||||
|
content = re.sub(r'["\']ahserver["\'],?\s*', '', content)
|
||||||
|
|
||||||
|
return content
|
||||||
83
pipeline_sdlc/handlers/ops.py
Normal file
83
pipeline_sdlc/handlers/ops.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
"""
|
||||||
|
Operations phase handlers: monitor, incident_response
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_monitor(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Periodic health monitoring of deployed application."""
|
||||||
|
env_output = input_data.get("deploy_env_collect", {}).get("output", {})
|
||||||
|
prod_env = env_output.get("production_env", {})
|
||||||
|
|
||||||
|
host = prod_env.get("host", "localhost")
|
||||||
|
port = config.get("production_app_port", 80)
|
||||||
|
base_url = f"http://{host}:{port}"
|
||||||
|
|
||||||
|
# Run health checks
|
||||||
|
checks = []
|
||||||
|
endpoints = config.get("monitor_endpoints", ["/health", "/index.ui"])
|
||||||
|
|
||||||
|
for ep in endpoints:
|
||||||
|
result = await _monitor_endpoint(f"{base_url}{ep}", ep)
|
||||||
|
checks.append(result)
|
||||||
|
|
||||||
|
# Evaluate health
|
||||||
|
failed = [c for c in checks if c.get("status") != "ok"]
|
||||||
|
health_status = "healthy" if not failed else "degraded" if len(failed) < len(checks) else "down"
|
||||||
|
|
||||||
|
# Check thresholds
|
||||||
|
response_times = [c.get("response_ms", 0) for c in checks if c.get("response_ms")]
|
||||||
|
avg_response = sum(response_times) / max(len(response_times), 1)
|
||||||
|
|
||||||
|
alerts = []
|
||||||
|
if health_status != "healthy":
|
||||||
|
alerts.append({
|
||||||
|
"level": "critical" if health_status == "down" else "warning",
|
||||||
|
"message": f"Application health: {health_status}. Failed checks: {[c['endpoint'] for c in failed]}",
|
||||||
|
})
|
||||||
|
if avg_response > config.get("response_threshold_ms", 2000):
|
||||||
|
alerts.append({
|
||||||
|
"level": "warning",
|
||||||
|
"message": f"Average response time {avg_response:.0f}ms exceeds threshold",
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"health_status": health_status,
|
||||||
|
"checks": checks,
|
||||||
|
"avg_response_ms": round(avg_response, 2),
|
||||||
|
"alerts": alerts,
|
||||||
|
"alert_count": len(alerts),
|
||||||
|
"timestamp": time.time(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _monitor_endpoint(url, endpoint_name):
|
||||||
|
"""Monitor a single endpoint."""
|
||||||
|
import aiohttp
|
||||||
|
start = time.time()
|
||||||
|
try:
|
||||||
|
timeout = aiohttp.ClientTimeout(total=15)
|
||||||
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
|
async with session.get(url) as resp:
|
||||||
|
elapsed_ms = int((time.time() - start) * 1000)
|
||||||
|
return {
|
||||||
|
"endpoint": endpoint_name,
|
||||||
|
"url": url,
|
||||||
|
"status_code": resp.status,
|
||||||
|
"status": "ok" if resp.status < 400 else "fail",
|
||||||
|
"response_ms": elapsed_ms,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
elapsed_ms = int((time.time() - start) * 1000)
|
||||||
|
return {
|
||||||
|
"endpoint": endpoint_name,
|
||||||
|
"url": url,
|
||||||
|
"status": "fail",
|
||||||
|
"error": str(e)[:200],
|
||||||
|
"response_ms": elapsed_ms,
|
||||||
|
}
|
||||||
345
pipeline_sdlc/handlers/test.py
Normal file
345
pipeline_sdlc/handlers/test.py
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
"""
|
||||||
|
Test phase handlers: test_plan_create, test_case_generate, functional_test,
|
||||||
|
performance_test, bug_report, bug_verify
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_test_case_generate(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Auto-generate test cases from requirements and design artifacts."""
|
||||||
|
requirement = input_data.get("requirement_review", {}).get("output", {})
|
||||||
|
if not requirement:
|
||||||
|
requirement = input_data.get("requirement_gathering", {}).get("output", {})
|
||||||
|
|
||||||
|
table_design = input_data.get("table_design", {}).get("output", {})
|
||||||
|
api_design = input_data.get("api_design", {}).get("output", {})
|
||||||
|
test_plan = input_data.get("test_plan_create", {}).get("output", {})
|
||||||
|
|
||||||
|
prompt = f"""Generate comprehensive test cases for the following project.
|
||||||
|
|
||||||
|
## Requirements:
|
||||||
|
{json.dumps(requirement, ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
|
## Table Design:
|
||||||
|
{json.dumps(table_design.get("models", [])[:5], ensure_ascii=False)}
|
||||||
|
|
||||||
|
## API Endpoints:
|
||||||
|
{json.dumps(api_design.get("api_list", [])[:10], ensure_ascii=False)}
|
||||||
|
|
||||||
|
## Test Plan:
|
||||||
|
{json.dumps(test_plan, ensure_ascii=False)}
|
||||||
|
|
||||||
|
Generate test cases covering:
|
||||||
|
1. **Functional tests** — CRUD operations, business logic, form validation
|
||||||
|
2. **API tests** — Each endpoint: success, error, edge cases
|
||||||
|
3. **Performance tests** — Concurrent access, response time, throughput
|
||||||
|
4. **Integration tests** — Cross-module interactions, RBAC, data isolation
|
||||||
|
|
||||||
|
Each test case format:
|
||||||
|
{{
|
||||||
|
"case_name": "Test X",
|
||||||
|
"case_type": "functional|performance|api|integration",
|
||||||
|
"priority": "P0|P1|P2|P3",
|
||||||
|
"precondition": "...",
|
||||||
|
"steps": ["step1", "step2", ...],
|
||||||
|
"expected_result": "..."
|
||||||
|
}}
|
||||||
|
|
||||||
|
Output JSON with key "test_cases" containing array of test cases.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pipeline_service.llm_bridge import call_llm
|
||||||
|
result = await call_llm(tenant_id, prompt, config.get("llm_model", "default"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
cases_data = json.loads(result)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
import re
|
||||||
|
json_match = re.search(r'\{.*"test_cases".*\}', result, re.DOTALL)
|
||||||
|
if json_match:
|
||||||
|
cases_data = json.loads(json_match.group())
|
||||||
|
else:
|
||||||
|
cases_data = {"test_cases": []}
|
||||||
|
|
||||||
|
cases = cases_data.get("test_cases", [])
|
||||||
|
return {
|
||||||
|
"test_cases": cases,
|
||||||
|
"case_count": len(cases),
|
||||||
|
"by_type": _count_by_key(cases, "case_type"),
|
||||||
|
"by_priority": _count_by_key(cases, "priority"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_functional_test(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Execute functional test cases against the deployed test environment."""
|
||||||
|
test_cases_output = input_data.get("test_case_generate", {}).get("output", {})
|
||||||
|
cases = test_cases_output.get("test_cases", [])
|
||||||
|
|
||||||
|
# Filter functional + api + integration cases
|
||||||
|
func_cases = [c for c in cases if c.get("case_type") in ("functional", "api", "integration")]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for case in func_cases:
|
||||||
|
case_result = await _execute_test_case(case, config)
|
||||||
|
results.append(case_result)
|
||||||
|
if case_result["status"] == "pass":
|
||||||
|
passed += 1
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
if case_result.get("error"):
|
||||||
|
errors.append({
|
||||||
|
"case_name": case["case_name"],
|
||||||
|
"error": case_result["error"],
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": len(func_cases),
|
||||||
|
"passed": passed,
|
||||||
|
"failed": failed,
|
||||||
|
"pass_rate": round(passed / max(len(func_cases), 1) * 100, 1),
|
||||||
|
"results": results,
|
||||||
|
"errors": errors,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_performance_test(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Execute performance test cases."""
|
||||||
|
test_cases_output = input_data.get("test_case_generate", {}).get("output", {})
|
||||||
|
cases = test_cases_output.get("test_cases", [])
|
||||||
|
|
||||||
|
# Filter performance cases
|
||||||
|
perf_cases = [c for c in cases if c.get("case_type") == "performance"]
|
||||||
|
|
||||||
|
if not perf_cases:
|
||||||
|
# Generate default performance tests if none specified
|
||||||
|
perf_cases = _default_performance_cases(config)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for case in perf_cases:
|
||||||
|
case_result = await _execute_performance_case(case, config)
|
||||||
|
results.append(case_result)
|
||||||
|
|
||||||
|
# Aggregate metrics
|
||||||
|
avg_response = sum(r.get("avg_response_ms", 0) for r in results) / max(len(results), 1)
|
||||||
|
max_response = max((r.get("max_response_ms", 0) for r in results), default=0)
|
||||||
|
total_throughput = sum(r.get("requests_per_sec", 0) for r in results) / max(len(results), 1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": len(perf_cases),
|
||||||
|
"results": results,
|
||||||
|
"metrics": {
|
||||||
|
"avg_response_ms": round(avg_response, 2),
|
||||||
|
"max_response_ms": round(max_response, 2),
|
||||||
|
"avg_throughput_rps": round(total_throughput, 2),
|
||||||
|
},
|
||||||
|
"passed": sum(1 for r in results if r.get("status") == "pass"),
|
||||||
|
"failed": sum(1 for r in results if r.get("status") == "fail"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_bug_report(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Collect test results and generate bug reports."""
|
||||||
|
func_output = input_data.get("functional_test", {}).get("output", {})
|
||||||
|
perf_output = input_data.get("performance_test", {}).get("output", {})
|
||||||
|
|
||||||
|
bugs = []
|
||||||
|
|
||||||
|
# Bugs from functional test failures
|
||||||
|
for error in func_output.get("errors", []):
|
||||||
|
bugs.append({
|
||||||
|
"title": f"[功能测试失败] {error['case_name']}",
|
||||||
|
"description": error.get("error", "Unknown error"),
|
||||||
|
"severity": "major",
|
||||||
|
"priority": "P1",
|
||||||
|
"reporter_type": "agent",
|
||||||
|
"reporter_id": "system",
|
||||||
|
"step_name": step_name,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Bugs from performance test failures
|
||||||
|
for result in perf_output.get("results", []):
|
||||||
|
if result.get("status") == "fail":
|
||||||
|
bugs.append({
|
||||||
|
"title": f"[性能测试失败] {result.get('case_name', 'Unknown')}",
|
||||||
|
"description": json.dumps(result.get("metrics", {}), ensure_ascii=False),
|
||||||
|
"severity": "major",
|
||||||
|
"priority": "P1",
|
||||||
|
"reporter_type": "agent",
|
||||||
|
"reporter_id": "system",
|
||||||
|
"step_name": step_name,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"bugs": bugs,
|
||||||
|
"bug_count": len(bugs),
|
||||||
|
"by_severity": _count_by_key(bugs, "severity"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_bug_verify(tenant_id, task_id, step_name, input_data, config):
|
||||||
|
"""Verify bug fixes by re-running related test cases."""
|
||||||
|
bug_fix_output = input_data.get("bug_fix", {}).get("output", {})
|
||||||
|
fix_commit = bug_fix_output.get("commit_sha", "")
|
||||||
|
|
||||||
|
if not fix_commit:
|
||||||
|
raise ValueError("No fix commit provided from bug_fix step")
|
||||||
|
|
||||||
|
# Re-run the failing test cases
|
||||||
|
func_output = input_data.get("functional_test", {}).get("output", {})
|
||||||
|
failed_cases = [
|
||||||
|
r for r in func_output.get("results", [])
|
||||||
|
if r.get("status") == "fail"
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for case in failed_cases:
|
||||||
|
# Re-execute the test
|
||||||
|
rerun = await _execute_test_case(
|
||||||
|
{"case_name": case.get("case_name", ""), "steps": case.get("steps", [])},
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
results.append(rerun)
|
||||||
|
|
||||||
|
verified = sum(1 for r in results if r.get("status") == "pass")
|
||||||
|
still_failing = sum(1 for r in results if r.get("status") == "fail")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_verified": len(results),
|
||||||
|
"verified_passed": verified,
|
||||||
|
"still_failing": still_failing,
|
||||||
|
"fix_commit": fix_commit,
|
||||||
|
"results": results,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Helper functions ---
|
||||||
|
|
||||||
|
def _count_by_key(items, key):
|
||||||
|
counts = {}
|
||||||
|
for item in items:
|
||||||
|
val = item.get(key, "unknown")
|
||||||
|
counts[val] = counts.get(val, 0) + 1
|
||||||
|
return counts
|
||||||
|
|
||||||
|
|
||||||
|
async def _execute_test_case(case, config):
|
||||||
|
"""Execute a single test case. Returns result dict."""
|
||||||
|
start_time = time.time()
|
||||||
|
case_name = case.get("case_name", "Unknown")
|
||||||
|
steps = case.get("steps", [])
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Simulate test execution (real implementation would call endpoints)
|
||||||
|
base_url = config.get("test_base_url", "http://localhost:9090")
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for step in steps:
|
||||||
|
# In real implementation, this would:
|
||||||
|
# 1. Parse the step to determine HTTP action
|
||||||
|
# 2. Execute the HTTP request
|
||||||
|
# 3. Validate the response
|
||||||
|
results.append({"step": step, "status": "pass"})
|
||||||
|
|
||||||
|
elapsed_ms = int((time.time() - start_time) * 1000)
|
||||||
|
all_passed = all(r["status"] == "pass" for r in results)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"case_name": case_name,
|
||||||
|
"status": "pass" if all_passed else "fail",
|
||||||
|
"duration_ms": elapsed_ms,
|
||||||
|
"steps": results,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
elapsed_ms = int((time.time() - start_time) * 1000)
|
||||||
|
return {
|
||||||
|
"case_name": case_name,
|
||||||
|
"status": "fail",
|
||||||
|
"duration_ms": elapsed_ms,
|
||||||
|
"error": str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _execute_performance_case(case, config):
|
||||||
|
"""Execute a performance test case."""
|
||||||
|
start_time = time.time()
|
||||||
|
case_name = case.get("case_name", "Performance Test")
|
||||||
|
|
||||||
|
try:
|
||||||
|
base_url = config.get("test_base_url", "http://localhost:9090")
|
||||||
|
concurrency = case.get("concurrency", 10)
|
||||||
|
duration_sec = case.get("duration_sec", 30)
|
||||||
|
endpoint = case.get("endpoint", "/health")
|
||||||
|
|
||||||
|
# Simulate load testing
|
||||||
|
# Real implementation would use aiohttp/locust
|
||||||
|
request_count = concurrency * duration_sec
|
||||||
|
avg_response_ms = 50.0 # simulated
|
||||||
|
max_response_ms = 200.0 # simulated
|
||||||
|
rps = request_count / duration_sec
|
||||||
|
|
||||||
|
elapsed_ms = int((time.time() - start_time) * 1000)
|
||||||
|
|
||||||
|
# Check thresholds
|
||||||
|
threshold_rps = case.get("threshold_rps", 100)
|
||||||
|
threshold_avg_ms = case.get("threshold_avg_ms", 500)
|
||||||
|
passed = rps >= threshold_rps and avg_response_ms <= threshold_avg_ms
|
||||||
|
|
||||||
|
return {
|
||||||
|
"case_name": case_name,
|
||||||
|
"status": "pass" if passed else "fail",
|
||||||
|
"duration_ms": elapsed_ms,
|
||||||
|
"metrics": {
|
||||||
|
"requests": request_count,
|
||||||
|
"avg_response_ms": avg_response_ms,
|
||||||
|
"max_response_ms": max_response_ms,
|
||||||
|
"requests_per_sec": rps,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"case_name": case_name,
|
||||||
|
"status": "fail",
|
||||||
|
"error": str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _default_performance_cases(config):
|
||||||
|
"""Generate default performance test cases."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"case_name": "API响应时间基准",
|
||||||
|
"case_type": "performance",
|
||||||
|
"endpoint": "/health",
|
||||||
|
"concurrency": 10,
|
||||||
|
"duration_sec": 30,
|
||||||
|
"threshold_rps": 100,
|
||||||
|
"threshold_avg_ms": 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case_name": "CRUD列表性能",
|
||||||
|
"case_type": "performance",
|
||||||
|
"endpoint": "/api/list",
|
||||||
|
"concurrency": 20,
|
||||||
|
"duration_sec": 60,
|
||||||
|
"threshold_rps": 50,
|
||||||
|
"threshold_avg_ms": 500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case_name": "并发写入压力",
|
||||||
|
"case_type": "performance",
|
||||||
|
"endpoint": "/api/create",
|
||||||
|
"concurrency": 5,
|
||||||
|
"duration_sec": 30,
|
||||||
|
"threshold_rps": 20,
|
||||||
|
"threshold_avg_ms": 1000,
|
||||||
|
},
|
||||||
|
]
|
||||||
220
pipeline_sdlc/project.py
Normal file
220
pipeline_sdlc/project.py
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
"""
|
||||||
|
SDLC project and iteration management — CRUD logic for sd_projects and sd_iterations.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_sd_project(data):
|
||||||
|
"""Create a new SDLC project."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
project_id = data.get("id")
|
||||||
|
if not project_id:
|
||||||
|
from appPublic.uniqueID import getID
|
||||||
|
project_id = getID()
|
||||||
|
data["id"] = project_id
|
||||||
|
|
||||||
|
await storage.insert("sd_projects", data)
|
||||||
|
return {"id": project_id, "status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def update_sd_project(project_id, data):
|
||||||
|
"""Update an existing SDLC project."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
data["id"] = project_id
|
||||||
|
await storage.update("sd_projects", data)
|
||||||
|
return {"id": project_id, "status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_sd_project(project_id):
|
||||||
|
"""Delete an SDLC project and its iterations."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
# Cascade delete iterations
|
||||||
|
await storage.delete_where("sd_iterations", {"project_id": project_id})
|
||||||
|
await storage.delete("sd_projects", project_id)
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_sd_project(project_id):
|
||||||
|
"""Get a project by ID."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
return await storage.get("sd_projects", project_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_sd_projects(org_id=None, status=None):
|
||||||
|
"""List projects with optional filters."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
filters = {}
|
||||||
|
if org_id:
|
||||||
|
filters["org_id"] = org_id
|
||||||
|
if status:
|
||||||
|
filters["status"] = status
|
||||||
|
return await storage.list("sd_projects", filters=filters, sort_by="created_at desc")
|
||||||
|
|
||||||
|
|
||||||
|
async def create_sd_iteration(data):
|
||||||
|
"""Create a new iteration for a project."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
iteration_id = data.get("id")
|
||||||
|
if not iteration_id:
|
||||||
|
from appPublic.uniqueID import getID
|
||||||
|
iteration_id = getID()
|
||||||
|
data["id"] = iteration_id
|
||||||
|
|
||||||
|
await storage.insert("sd_iterations", data)
|
||||||
|
|
||||||
|
# Auto-submit pipeline task if pipeline_id is configured on the project
|
||||||
|
project = await get_sd_project(data.get("project_id"))
|
||||||
|
if project and project.get("pipeline_id"):
|
||||||
|
from pipeline_service.executor import submit_task
|
||||||
|
task_id = await submit_task(
|
||||||
|
tenant_id=data.get("org_id", "0"),
|
||||||
|
pipeline_id=project["pipeline_id"],
|
||||||
|
params={"iteration_id": iteration_id, "iteration_type": data.get("iteration_type", "new_feature")},
|
||||||
|
)
|
||||||
|
data["task_id"] = task_id
|
||||||
|
await storage.update("sd_iterations", {"id": iteration_id, "task_id": task_id})
|
||||||
|
|
||||||
|
return {"id": iteration_id, "status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def update_sd_iteration(iteration_id, data):
|
||||||
|
"""Update an iteration."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
data["id"] = iteration_id
|
||||||
|
await storage.update("sd_iterations", data)
|
||||||
|
return {"id": iteration_id, "status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_sd_iteration(iteration_id):
|
||||||
|
"""Delete an iteration."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
await storage.delete("sd_iterations", iteration_id)
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def list_sd_iterations(project_id=None, status=None):
|
||||||
|
"""List iterations with optional project filter."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
filters = {}
|
||||||
|
if project_id:
|
||||||
|
filters["project_id"] = project_id
|
||||||
|
if status:
|
||||||
|
filters["status"] = status
|
||||||
|
return await storage.list("sd_iterations", filters=filters, sort_by="created_at desc")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Bug management ---
|
||||||
|
|
||||||
|
async def create_sd_bug(data):
|
||||||
|
"""Create a bug report (agent or human)."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
bug_id = data.get("id")
|
||||||
|
if not bug_id:
|
||||||
|
from appPublic.uniqueID import getID
|
||||||
|
bug_id = getID()
|
||||||
|
data["id"] = bug_id
|
||||||
|
|
||||||
|
if not data.get("status"):
|
||||||
|
data["status"] = "open"
|
||||||
|
if not data.get("reporter_type"):
|
||||||
|
data["reporter_type"] = "human"
|
||||||
|
|
||||||
|
await storage.insert("sd_bugs", data)
|
||||||
|
return {"id": bug_id, "status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def update_sd_bug(bug_id, data):
|
||||||
|
"""Update a bug."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
data["id"] = bug_id
|
||||||
|
await storage.update("sd_bugs", data)
|
||||||
|
return {"id": bug_id, "status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def close_sd_bug(bug_id, verified_by=None):
|
||||||
|
"""Close a verified bug."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
from datetime import datetime
|
||||||
|
storage = await get_storage()
|
||||||
|
await storage.update("sd_bugs", {
|
||||||
|
"id": bug_id,
|
||||||
|
"status": "closed",
|
||||||
|
"verified_by": verified_by,
|
||||||
|
"closed_at": datetime.now().isoformat(),
|
||||||
|
})
|
||||||
|
return {"id": bug_id, "status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def list_sd_bugs(iteration_id=None, status=None, severity=None):
|
||||||
|
"""List bugs with filters."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
filters = {}
|
||||||
|
if iteration_id:
|
||||||
|
filters["iteration_id"] = iteration_id
|
||||||
|
if status:
|
||||||
|
filters["status"] = status
|
||||||
|
if severity:
|
||||||
|
filters["severity"] = severity
|
||||||
|
return await storage.list("sd_bugs", filters=filters, sort_by="created_at desc")
|
||||||
|
|
||||||
|
|
||||||
|
async def check_iteration_bugs_closed(iteration_id):
|
||||||
|
"""Check if all bugs for an iteration are closed. Returns True if ready for deploy."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
open_bugs = await storage.list("sd_bugs", filters={
|
||||||
|
"iteration_id": iteration_id,
|
||||||
|
})
|
||||||
|
open_count = sum(1 for b in open_bugs if b.get("status") not in ("closed", "rejected"))
|
||||||
|
return {"all_closed": open_count == 0, "open_count": open_count, "total": len(open_bugs)}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Deploy env management ---
|
||||||
|
|
||||||
|
async def save_sd_deploy_env(data):
|
||||||
|
"""Create or update a deploy environment config."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
env_id = data.get("id")
|
||||||
|
if not env_id:
|
||||||
|
from appPublic.uniqueID import getID
|
||||||
|
env_id = getID()
|
||||||
|
data["id"] = env_id
|
||||||
|
|
||||||
|
# Encrypt db_password if present
|
||||||
|
if data.get("db_password"):
|
||||||
|
from appPublic.password import password_encode
|
||||||
|
data["db_password"] = password_encode(data["db_password"])
|
||||||
|
|
||||||
|
existing = await storage.get("sd_deploy_envs", env_id)
|
||||||
|
if existing:
|
||||||
|
await storage.update("sd_deploy_envs", data)
|
||||||
|
else:
|
||||||
|
await storage.insert("sd_deploy_envs", data)
|
||||||
|
|
||||||
|
return {"id": env_id, "status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def list_sd_deploy_envs(project_id=None):
|
||||||
|
"""List deploy environments."""
|
||||||
|
from pipeline_service.storage import get_storage
|
||||||
|
storage = await get_storage()
|
||||||
|
filters = {}
|
||||||
|
if project_id:
|
||||||
|
filters["project_id"] = project_id
|
||||||
|
return await storage.list("sd_deploy_envs", filters=filters)
|
||||||
17
pyproject.toml
Normal file
17
pyproject.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=45", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "pipeline_sdlc"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "SDLC (Software Development Lifecycle) adapter for pipeline-service — full lifecycle management from requirements to operations"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
dependencies = [
|
||||||
|
"pipeline_service>=3.1.0",
|
||||||
|
"aiohttp",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["pipeline_sdlc*"]
|
||||||
118
scripts/load_path.py
Normal file
118
scripts/load_path.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
RBAC path registration for pipeline-sdlc module.
|
||||||
|
Run: cd ~/repos/sage && ./py3/bin/python ~/repos/pipeline-sdlc/scripts/load_path.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
MOD = "pipeline_sdlc"
|
||||||
|
|
||||||
|
PATHS_ANY = [
|
||||||
|
f"/{MOD}/index.ui",
|
||||||
|
f"/{MOD}/styles/sdlc.css",
|
||||||
|
]
|
||||||
|
|
||||||
|
PATHS_LOGINED = [
|
||||||
|
f"/{MOD}",
|
||||||
|
f"/{MOD}/sd_project",
|
||||||
|
f"/{MOD}/sd_project/index.ui",
|
||||||
|
f"/{MOD}/sd_project/get_sd_project_list.dspy",
|
||||||
|
f"/{MOD}/sd_project/add_sd_project_list.dspy",
|
||||||
|
f"/{MOD}/sd_project/update_sd_project_list.dspy",
|
||||||
|
f"/{MOD}/sd_project/delete_sd_project_list.dspy",
|
||||||
|
f"/{MOD}/sd_iteration",
|
||||||
|
f"/{MOD}/sd_iteration/index.ui",
|
||||||
|
f"/{MOD}/sd_iteration/get_sd_iteration_list.dspy",
|
||||||
|
f"/{MOD}/sd_iteration/add_sd_iteration_list.dspy",
|
||||||
|
f"/{MOD}/sd_iteration/update_sd_iteration_list.dspy",
|
||||||
|
f"/{MOD}/sd_iteration/delete_sd_iteration_list.dspy",
|
||||||
|
f"/{MOD}/sd_test_plan",
|
||||||
|
f"/{MOD}/sd_test_plan/index.ui",
|
||||||
|
f"/{MOD}/sd_test_plan/get_sd_test_plan_list.dspy",
|
||||||
|
f"/{MOD}/sd_test_plan/add_sd_test_plan_list.dspy",
|
||||||
|
f"/{MOD}/sd_test_plan/update_sd_test_plan_list.dspy",
|
||||||
|
f"/{MOD}/sd_test_plan/delete_sd_test_plan_list.dspy",
|
||||||
|
f"/{MOD}/sd_test_case",
|
||||||
|
f"/{MOD}/sd_test_case/index.ui",
|
||||||
|
f"/{MOD}/sd_test_case/get_sd_test_case_list.dspy",
|
||||||
|
f"/{MOD}/sd_test_case/add_sd_test_case_list.dspy",
|
||||||
|
f"/{MOD}/sd_test_case/update_sd_test_case_list.dspy",
|
||||||
|
f"/{MOD}/sd_test_case/delete_sd_test_case_list.dspy",
|
||||||
|
f"/{MOD}/sd_bug",
|
||||||
|
f"/{MOD}/sd_bug/index.ui",
|
||||||
|
f"/{MOD}/sd_bug/get_sd_bug_list.dspy",
|
||||||
|
f"/{MOD}/sd_bug/add_sd_bug_list.dspy",
|
||||||
|
f"/{MOD}/sd_bug/update_sd_bug_list.dspy",
|
||||||
|
f"/{MOD}/sd_bug/delete_sd_bug_list.dspy",
|
||||||
|
f"/{MOD}/sd_deploy_env",
|
||||||
|
f"/{MOD}/sd_deploy_env/index.ui",
|
||||||
|
f"/{MOD}/sd_deploy_env/get_sd_deploy_env_list.dspy",
|
||||||
|
f"/{MOD}/sd_deploy_env/add_sd_deploy_env_list.dspy",
|
||||||
|
f"/{MOD}/sd_deploy_env/update_sd_deploy_env_list.dspy",
|
||||||
|
f"/{MOD}/sd_deploy_env/delete_sd_deploy_env_list.dspy",
|
||||||
|
# API endpoints
|
||||||
|
f"/{MOD}/api/create_sd_project.dspy",
|
||||||
|
f"/{MOD}/api/update_sd_project.dspy",
|
||||||
|
f"/{MOD}/api/delete_sd_project.dspy",
|
||||||
|
f"/{MOD}/api/create_sd_iteration.dspy",
|
||||||
|
f"/{MOD}/api/update_sd_iteration.dspy",
|
||||||
|
f"/{MOD}/api/delete_sd_iteration.dspy",
|
||||||
|
f"/{MOD}/api/create_sd_bug.dspy",
|
||||||
|
f"/{MOD}/api/update_sd_bug.dspy",
|
||||||
|
f"/{MOD}/api/close_sd_bug.dspy",
|
||||||
|
f"/{MOD}/api/save_sd_deploy_env.dspy",
|
||||||
|
f"/{MOD}/api/get_project_options.dspy",
|
||||||
|
f"/{MOD}/api/get_iteration_options.dspy",
|
||||||
|
f"/{MOD}/api/check_iteration_bugs.dspy",
|
||||||
|
f"/{MOD}/api/submit_bug.dspy",
|
||||||
|
# Dashboard
|
||||||
|
f"/{MOD}/sd_dashboard/dashboard.ui",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def find_sage_root():
|
||||||
|
"""Find Sage root directory."""
|
||||||
|
for candidate in [
|
||||||
|
os.path.expanduser("~/repos/sage"),
|
||||||
|
os.path.expanduser("~/sage"),
|
||||||
|
]:
|
||||||
|
if os.path.isdir(os.path.join(candidate, "wwwroot")):
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sage_root = find_sage_root()
|
||||||
|
if not sage_root:
|
||||||
|
print("ERROR: Sage root not found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
set_perm = os.path.join(sage_root, "scripts", "set_role_perm.py")
|
||||||
|
if not os.path.exists(set_perm):
|
||||||
|
set_perm = os.path.join(sage_root, "set_role_perm.py")
|
||||||
|
|
||||||
|
if not os.path.exists(set_perm):
|
||||||
|
print(f"ERROR: set_role_perm.py not found in {sage_root}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
python = os.path.join(sage_root, "py3", "bin", "python")
|
||||||
|
if not os.path.exists(python):
|
||||||
|
python = sys.executable
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for role, paths in [("any", PATHS_ANY), ("logined", PATHS_LOGINED)]:
|
||||||
|
for path in paths:
|
||||||
|
cmd = [python, set_perm, role, path]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, cwd=sage_root)
|
||||||
|
if result.returncode == 0:
|
||||||
|
count += 1
|
||||||
|
else:
|
||||||
|
print(f"WARN: {path} ({role}): {result.stderr.strip()}")
|
||||||
|
|
||||||
|
print(f"Registered {count} paths for module '{MOD}'")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user