@hupan56/wlkj 3.1.32 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/bin/cli.js +117 -0
  2. package/package.json +1 -1
  3. package/templates/qoder/agents/insight-planning.md +67 -67
  4. package/templates/qoder/agents/prd-reference.md +47 -47
  5. package/templates/qoder/commands/optional/wl-insight.md +4 -4
  6. package/templates/qoder/commands/optional/wl-report.md +1 -1
  7. package/templates/qoder/commands/optional/wl-spec.md +23 -3
  8. package/templates/qoder/commands/optional/wl-status.md +12 -1
  9. package/templates/qoder/commands/wl-code.md +138 -7
  10. package/templates/qoder/commands/wl-commit.md +12 -1
  11. package/templates/qoder/commands/wl-design.md +70 -6
  12. package/templates/qoder/commands/wl-init.md +27 -0
  13. package/templates/qoder/commands/wl-prd.md +230 -15
  14. package/templates/qoder/commands/wl-req.md +10 -3
  15. package/templates/qoder/commands/wl-search.md +74 -20
  16. package/templates/qoder/commands/wl-task.md +3 -3
  17. package/templates/qoder/commands/wl-test.md +17 -2
  18. package/templates/qoder/contracts/CHANGELOG.md +418 -0
  19. package/templates/qoder/contracts/README.md +180 -0
  20. package/templates/qoder/contracts/code.md +82 -0
  21. package/templates/qoder/contracts/commit.md +86 -0
  22. package/templates/qoder/contracts/contract-header.md +76 -0
  23. package/templates/qoder/contracts/design.md +106 -0
  24. package/templates/qoder/contracts/fallback.md +126 -0
  25. package/templates/qoder/contracts/isolation.md +119 -0
  26. package/templates/qoder/contracts/prd.md +118 -0
  27. package/templates/qoder/contracts/schemas/design-spec.schema.json +46 -0
  28. package/templates/qoder/contracts/schemas/prd.schema.json +36 -0
  29. package/templates/qoder/contracts/schemas/test-cases.schema.json +40 -0
  30. package/templates/qoder/contracts/spec.md +116 -0
  31. package/templates/qoder/contracts/task.md +125 -0
  32. package/templates/qoder/contracts/test.md +112 -0
  33. package/templates/qoder/hooks/post-tool-use.py +61 -0
  34. package/templates/qoder/hooks/session-start.py +34 -66
  35. package/templates/qoder/hooks/stop-eval.py +47 -0
  36. package/templates/qoder/rules/wl-pipeline.md +37 -0
  37. package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
  38. package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
  39. package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
  40. package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
  41. package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
  42. package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
  43. package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
  44. package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
  45. package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
  46. package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
  47. package/templates/qoder/scripts/capability/caps/__init__.py +1 -1
  48. package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
  49. package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
  50. package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
  51. package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
  52. package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
  53. package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
  54. package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
  55. package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
  56. package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
  57. package/templates/qoder/scripts/capability/caps/memory.py +1 -1
  58. package/templates/qoder/scripts/capability/registry.py +21 -23
  59. package/templates/qoder/scripts/capability/registry_mcp.py +69 -5
  60. package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
  61. package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
  62. package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
  63. package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +11 -0
  64. package/templates/qoder/scripts/deployment/setup/wlkj_shim.py +104 -0
  65. package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
  66. package/templates/qoder/scripts/domain/deployment/deploy_to_test.py +298 -0
  67. package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
  68. package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
  69. package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
  70. package/templates/qoder/scripts/domain/integration/return_to_platform.py +392 -0
  71. package/templates/qoder/scripts/domain/integration/spec_upload.py +209 -0
  72. package/templates/qoder/scripts/domain/kg/build/kg_build.py +268 -25
  73. package/templates/qoder/scripts/domain/kg/build/kg_incremental.py +108 -3
  74. package/templates/qoder/scripts/domain/kg/build/kg_signatures.py +169 -0
  75. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
  76. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
  77. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
  78. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
  79. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
  80. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
  81. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
  82. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
  83. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
  84. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
  85. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
  86. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
  87. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
  88. package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
  89. package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +430 -0
  90. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
  91. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
  92. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
  93. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
  94. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
  95. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
  96. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
  97. package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
  98. package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
  99. package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
  100. package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
  101. package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
  102. package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
  103. package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
  104. package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
  105. package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
  106. package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
  107. package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
  108. package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
  109. package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
  110. package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
  111. package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
  112. package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
  113. package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
  114. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
  115. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
  116. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
  117. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
  118. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
  119. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
  120. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
  121. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
  122. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
  123. package/templates/qoder/scripts/domain/kg/extract/ts_extract.py +111 -0
  124. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
  125. package/templates/qoder/scripts/domain/kg/kg.py +42 -5
  126. package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
  127. package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
  128. package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
  129. package/templates/qoder/scripts/domain/kg/storage/kg_duckdb.py +43 -0
  130. package/templates/qoder/scripts/domain/kg/switch_project.py +159 -0
  131. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
  132. package/templates/qoder/scripts/domain/requirement/req.py +134 -28
  133. package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -0
  135. package/templates/qoder/scripts/engine/poller.py +219 -0
  136. package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
  137. package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
  138. package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
  139. package/templates/qoder/scripts/foundation/core/paths.py +102 -0
  140. package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
  141. package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
  142. package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
  143. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +24 -11
  144. package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
  145. package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
  146. package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
  147. package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
  148. package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
  149. package/templates/qoder/scripts/protocol/transports/http.py +7 -1
  150. package/templates/qoder/scripts/validation/eval/qwork_harness.py +1 -1
  151. package/templates/qoder/scripts/validation/eval/report-commands.md +2 -2
  152. package/templates/qoder/settings.json +27 -9
  153. package/templates/qoder/skills/design-import/SKILL.md +3 -3
  154. package/templates/qoder/skills/design-review/SKILL.md +1 -1
  155. package/templates/qoder/skills/prd-generator/SKILL.md +4 -4
  156. package/templates/qoder/skills/prd-review/SKILL.md +1 -1
  157. package/templates/qoder/skills/prototype-generator/SKILL.md +3 -3
  158. package/templates/qoder/skills/spec-coder/SKILL.md +1 -1
  159. package/templates/qoder/skills/spec-generator/SKILL.md +80 -23
  160. package/templates/qoder/skills/test-generator/SKILL.md +1 -1
  161. package/templates/qoder/skills/wl-code/SKILL.md +13 -1
  162. package/templates/qoder/skills/wl-commit/SKILL.md +1 -1
  163. package/templates/qoder/skills/wl-design/SKILL.md +6 -6
  164. package/templates/qoder/skills/wl-init/SKILL.md +2 -2
  165. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  166. package/templates/qoder/skills/wl-prd/SKILL.md +60 -0
  167. package/templates/qoder/skills/wl-report/SKILL.md +2 -2
  168. package/templates/qoder/skills/wl-search/SKILL.md +1 -1
  169. package/templates/qoder/skills/wl-spec/SKILL.md +2 -2
  170. package/templates/qoder/skills/wl-status/SKILL.md +2 -2
  171. package/templates/qoder/skills/wl-task/SKILL.md +3 -3
  172. package/templates/qoder/skills/wl-test/SKILL.md +2 -2
  173. package/templates/qoder/templates/spec-template.md +124 -0
  174. package/templates/root/AGENTS.md +41 -14
  175. package/templates/qoder/scripts/domain/task/zentao_panel.py +0 -451
  176. package/templates/qoder/skills/wl-prd-full/SKILL.md +0 -121
  177. package/templates/qoder/skills/wl-prd-quick/SKILL.md +0 -50
  178. package/templates/qoder/skills/wl-prd-review/SKILL.md +0 -47
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ deploy_to_test.py - 提交部署: 拉取 → cherry-pick到test分支 → 触发Jenkins构建
5
+
6
+ 面板「提交」按钮调用。在 fywl-ics 代码仓库执行 git 操作。
7
+
8
+ 三步:
9
+ 1. 拉取当前分支最新代码
10
+ 2. cherry-pick 当前提交到 test 分支 (冲突→abort→提示手动)
11
+ 3. 从代码改动推断该构建哪个 Jenkins job → 触发构建
12
+
13
+ 用法:
14
+ python deploy_to_test.py --json # 执行全部三步
15
+ python deploy_to_test.py --json --no-deploy # 只拉取+cherry-pick, 不触发Jenkins
16
+ python deploy_to_test.py --jobs # 列出可选的Jenkins job (供面板展示)
17
+ python deploy_to_test.py --json --job qas_ics_quality # 指定构建哪个job
18
+
19
+ 配置 (从 .qoder/config.yaml 读, 没有就用默认):
20
+ deploy:
21
+ code_repo: data/code/fywl-ics # 代码仓库相对路径
22
+ test_branch: test # cherry-pick目标分支
23
+ jenkins_url: http://10.54.6.35:8866
24
+ jenkins_user: public_ics
25
+ jenkins_pass: a123456
26
+ jenkins_view: ICS-QAS # Jenkins视图名
27
+
28
+ 退出码: 0成功; 3配置错误; 4冲突; 5网络/构建失败
29
+ """
30
+ import os
31
+ import sys
32
+ import json
33
+ import argparse
34
+ import subprocess
35
+
36
+ # 路径自举
37
+ _HERE = os.path.dirname(os.path.abspath(__file__))
38
+ _SCRIPTS = os.path.dirname(os.path.dirname(os.path.dirname(_HERE))) # .qoder/scripts
39
+ _COMMON = os.path.join(_SCRIPTS, 'foundation')
40
+ for _p in (_SCRIPTS, _COMMON):
41
+ if _p not in sys.path:
42
+ sys.path.insert(0, _p)
43
+ try:
44
+ from bootstrap import setup; setup()
45
+ except Exception:
46
+ pass
47
+
48
+
49
+ def _load_config():
50
+ """从 config.yaml 读 deploy 段, 没有用默认。"""
51
+ import yaml
52
+ cfg_path = os.path.join(os.path.dirname(_SCRIPTS), 'config.yaml')
53
+ defaults = {
54
+ 'code_repo': 'data/code/fywl-ics',
55
+ 'test_branch': 'test',
56
+ 'jenkins_url': 'http://10.54.6.35:8866',
57
+ 'jenkins_user': 'public_ics',
58
+ 'jenkins_pass': 'a123456',
59
+ 'jenkins_view': 'ICS-QAS',
60
+ }
61
+ try:
62
+ with open(cfg_path, encoding='utf-8') as f:
63
+ full = yaml.safe_load(f) or {}
64
+ deploy = full.get('deploy', {})
65
+ defaults.update({k: v for k, v in deploy.items() if v})
66
+ except Exception:
67
+ pass
68
+ return defaults
69
+
70
+
71
+ def _git(args, repo_dir, check=True):
72
+ """在指定仓库目录执行 git 命令。返回 (returncode, stdout, stderr)。"""
73
+ r = subprocess.run(['git'] + args, capture_output=True, text=True,
74
+ cwd=repo_dir, timeout=30)
75
+ if check and r.returncode != 0:
76
+ raise RuntimeError('git %s 失败: %s' % (' '.join(args), r.stderr.strip()))
77
+ return r.returncode, r.stdout.strip(), r.stderr.strip()
78
+
79
+
80
+ def _repo_root(cfg):
81
+ """定位代码仓库根目录。"""
82
+ from foundation.core.paths import get_repo_root
83
+ base = get_repo_root()
84
+ repo = os.path.join(str(base), cfg['code_repo'])
85
+ if not os.path.isdir(os.path.join(repo, '.git')):
86
+ # 兜底: 往上找
87
+ repo = os.path.join(str(base), 'data', 'code', 'fywl-ics')
88
+ return repo
89
+
90
+
91
+ def step_pull(repo_dir):
92
+ """第1步: 拉取当前分支最新代码。"""
93
+ _, branch, _ = _git(['branch', '--show-current'], repo_dir)
94
+ _git(['fetch', 'origin'], repo_dir, check=False)
95
+ rc, out, err = _git(['pull', 'origin', branch], repo_dir, check=False)
96
+ if rc != 0:
97
+ return {'ok': False, 'error': '拉取失败: %s' % err, 'branch': branch}
98
+ return {'ok': True, 'branch': branch, 'message': '已拉取 %s 分支' % branch}
99
+
100
+
101
+ def step_cherry_pick(repo_dir, test_branch):
102
+ """第2步: cherry-pick 当前提交到 test 分支。冲突→abort→提示手动。"""
103
+ # 拿当前分支最新 commit
104
+ _, commit, _ = _git(['rev-parse', 'HEAD'], repo_dir)
105
+ _, cur_branch, _ = _git(['branch', '--show-current'], repo_dir)
106
+
107
+ # 切到 test 分支
108
+ _git(['fetch', 'origin', test_branch], repo_dir, check=False)
109
+ rc, out, err = _git(['checkout', test_branch], repo_dir, check=False)
110
+ if rc != 0:
111
+ # test 分支不存在, 从 origin 拉远程
112
+ rc2, out2, err2 = _git(['checkout', '-b', test_branch, 'origin/%s' % test_branch], repo_dir, check=False)
113
+ if rc2 != 0:
114
+ return {'ok': False, 'error': '切换test分支失败: %s' % err2}
115
+
116
+ _git(['pull', 'origin', test_branch], repo_dir, check=False)
117
+
118
+ # cherry-pick
119
+ rc, out, err = _git(['cherry-pick', commit], repo_dir, check=False)
120
+ if rc != 0:
121
+ # 冲突 → abort → 切回原分支 → 提示手动
122
+ _git(['cherry-pick', '--abort'], repo_dir, check=False)
123
+ _git(['checkout', cur_branch], repo_dir, check=False)
124
+ return {'ok': False, 'error': 'cherry-pick 到 %s 分支冲突, 已撤销。请手动解决:\n'
125
+ ' git checkout %s && git cherry-pick %s' % (test_branch, test_branch, commit[:8]),
126
+ 'conflict': True}
127
+
128
+ # 推送
129
+ rc, out, err = _git(['push', 'origin', test_branch], repo_dir, check=False)
130
+ if rc != 0:
131
+ return {'ok': False, 'error': '推送到 %s 失败: %s' % (test_branch, err)}
132
+
133
+ # 切回原分支
134
+ _git(['checkout', cur_branch], repo_dir, check=False)
135
+
136
+ return {'ok': True, 'commit': commit[:8], 'test_branch': test_branch,
137
+ 'message': '已 cherry-pick %s 到 %s 并推送' % (commit[:8], test_branch)}
138
+
139
+
140
+ def _get_changed_modules(repo_dir):
141
+ """从 git diff 提取改动的模块名 (用于推断 Jenkins job)。"""
142
+ _, out, _ = _git(['diff', '--name-only', 'HEAD~1'], repo_dir, check=False)
143
+ if not out:
144
+ _, out, _ = _git(['diff', '--name-only', '--cached'], repo_dir, check=False)
145
+ if not out:
146
+ _, out, _ = _git(['status', '--porcelain'], repo_dir, check=False)
147
+ modules = set()
148
+ for line in out.split('\n'):
149
+ line = line.strip().split()[-1] if line.strip() else ''
150
+ if not line:
151
+ continue
152
+ # 从路径提取模块: ics-modules/ics-quality/xxx → ics-quality
153
+ parts = line.split('/')
154
+ for i, p in enumerate(parts):
155
+ if p.startswith('ics-') and i + 1 < len(parts):
156
+ modules.add(p)
157
+ break
158
+ # fywl-ui 前端
159
+ if 'fywl-ui' in line or line.startswith('apps/'):
160
+ modules.add('ui')
161
+ return modules
162
+
163
+
164
+ def _list_jenkins_jobs(cfg):
165
+ """拉 Jenkins 视图下的 job 列表。"""
166
+ import urllib.request, base64
167
+ url = '%s/view/%s/api/json?tree=jobs[name]' % (cfg['jenkins_url'], cfg['jenkins_view'])
168
+ req = urllib.request.Request(url)
169
+ cred = base64.b64encode(('%s:%s' % (cfg['jenkins_user'], cfg['jenkins_pass'])).encode()).decode()
170
+ req.add_header('Authorization', 'Basic ' + cred)
171
+ r = urllib.request.urlopen(req, timeout=10)
172
+ data = json.loads(r.read())
173
+ return [j['name'] for j in data.get('jobs', [])]
174
+
175
+
176
+ def _guess_jobs(modules, all_jobs):
177
+ """从改动模块推断该构建哪些 Jenkins job。"""
178
+ matched = []
179
+ for mod in modules:
180
+ # 模块名 ics-quality → job名 qas_ics_quality 或 qas-ics-quality
181
+ mod_norm = mod.replace('-', '_').replace('_', '')
182
+ for job in all_jobs:
183
+ job_norm = job.replace('-', '_').replace('_', '').lower()
184
+ if mod_norm in job_norm:
185
+ matched.append(job)
186
+ return list(set(matched))
187
+
188
+
189
+ def step_jenkins_build(cfg, job_name):
190
+ """第3步: 触发 Jenkins 构建。"""
191
+ import urllib.request, base64
192
+ # 先拿 crumb (Jenkins CSRF)
193
+ crumb_url = '%s/crumbIssuer/api/json' % cfg['jenkins_url']
194
+ cred = base64.b64encode(('%s:%s' % (cfg['jenkins_user'], cfg['jenkins_pass'])).encode()).decode()
195
+ headers = {'Authorization': 'Basic ' + cred}
196
+ try:
197
+ req = urllib.request.Request(crumb_url, headers=headers)
198
+ crumb_data = json.loads(urllib.request.urlopen(req, timeout=10).read())
199
+ crumb_field = crumb_data.get('crumbRequestField', '')
200
+ crumb_value = crumb_data.get('crumb', '')
201
+ headers[crumb_field] = crumb_value
202
+ except Exception:
203
+ pass # 没 crumb 也试
204
+
205
+ build_url = '%s/job/%s/build' % (cfg['jenkins_url'], job_name)
206
+ req = urllib.request.Request(build_url, data=b'', headers=headers, method='POST')
207
+ try:
208
+ r = urllib.request.urlopen(req, timeout=15)
209
+ return {'ok': True, 'job': job_name, 'message': '已触发 %s 构建 (HTTP %d)' % (job_name, r.status)}
210
+ except urllib.error.HTTPError as e:
211
+ if e.code == 201:
212
+ return {'ok': True, 'job': job_name, 'message': '已触发 %s 构建' % job_name}
213
+ return {'ok': False, 'error': '触发构建失败 (HTTP %d): %s' % (e.code, str(e)[:60])}
214
+ except Exception as e:
215
+ return {'ok': False, 'error': '触发构建异常: %s' % str(e)[:80]}
216
+
217
+
218
+ def deploy(job_name=None, skip_deploy=False):
219
+ """执行部署全流程。返回 (result_dict, exit_code)。"""
220
+ cfg = _load_config()
221
+ repo_dir = _repo_root(cfg)
222
+ if not os.path.isdir(os.path.join(repo_dir, '.git')):
223
+ return {'ok': False, 'error': '代码仓库不存在: %s' % repo_dir}, 3
224
+
225
+ results = {'steps': []}
226
+
227
+ # ① 拉取
228
+ r1 = step_pull(repo_dir)
229
+ results['steps'].append({'step': 'pull', **r1})
230
+ if not r1['ok']:
231
+ return {'ok': False, 'error': r1['error'], 'results': results}, 5
232
+
233
+ # ② cherry-pick
234
+ r2 = step_cherry_pick(repo_dir, cfg['test_branch'])
235
+ results['steps'].append({'step': 'cherry-pick', **r2})
236
+ if not r2['ok']:
237
+ # 冲突不阻塞 Jenkins (开发可能只想先构建), 但标记冲突
238
+ results['conflict'] = r2.get('conflict', False)
239
+
240
+ # ③ Jenkins 构建
241
+ if not skip_deploy:
242
+ all_jobs = _list_jenkins_jobs(cfg)
243
+ if job_name:
244
+ # 指定了 job
245
+ r3 = step_jenkins_build(cfg, job_name)
246
+ else:
247
+ # 自动推断
248
+ modules = _get_changed_modules(repo_dir)
249
+ guessed = _guess_jobs(modules, all_jobs)
250
+ results['guessed_modules'] = list(modules)
251
+ results['guessed_jobs'] = guessed
252
+ if len(guessed) == 1:
253
+ r3 = step_jenkins_build(cfg, guessed[0])
254
+ elif len(guessed) > 1:
255
+ # 多个匹配 → 不自动构建, 让面板让用户选
256
+ r3 = {'ok': False, 'need_select': True, 'jobs': guessed,
257
+ 'error': '改动涉及多个模块, 请选择要构建的job: %s' % ', '.join(guessed)}
258
+ else:
259
+ # 匹配不到 → 返回全部 job 让用户选
260
+ r3 = {'ok': False, 'need_select': True, 'jobs': all_jobs,
261
+ 'error': '无法从改动推断构建模板, 请选择'}
262
+ results['steps'].append({'step': 'jenkins', **r3})
263
+
264
+ return {'ok': not results.get('conflict', False), 'results': results}, 0
265
+
266
+
267
+ def main():
268
+ parser = argparse.ArgumentParser(description='提交部署: 拉取→cherry-pick→Jenkins构建')
269
+ parser.add_argument('--json', action='store_true', help='JSON输出')
270
+ parser.add_argument('--no-deploy', action='store_true', help='只拉取+cherry-pick, 不触发Jenkins')
271
+ parser.add_argument('--job', help='指定构建的Jenkins job名')
272
+ parser.add_argument('--jobs', action='store_true', help='列出可选的Jenkins job')
273
+ args = parser.parse_args()
274
+
275
+ cfg = _load_config()
276
+
277
+ if args.jobs:
278
+ try:
279
+ jobs = _list_jenkins_jobs(cfg)
280
+ result = {'ok': True, 'jobs': jobs}
281
+ print(json.dumps(result, ensure_ascii=False))
282
+ except Exception as e:
283
+ print(json.dumps({'ok': False, 'error': str(e)}))
284
+ sys.exit(0)
285
+
286
+ result, code = deploy(job_name=args.job, skip_deploy=args.no_deploy)
287
+
288
+ if args.json:
289
+ print(json.dumps(result, ensure_ascii=False, default=str))
290
+ else:
291
+ for step in result.get('results', {}).get('steps', []):
292
+ status = 'OK' if step.get('ok') else 'FAIL'
293
+ print('[%s] %s: %s' % (status, step.get('step'), step.get('message', step.get('error', ''))))
294
+ sys.exit(code)
295
+
296
+
297
+ if __name__ == '__main__':
298
+ main()