zt-devops-cli 0.2.2__tar.gz → 0.2.4__tar.gz
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.
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/PKG-INFO +1 -1
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/pyproject.toml +1 -1
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/src/zt_devops_cli/api.py +7 -0
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/src/zt_devops_cli/cli.py +196 -36
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/src/zt_devops_cli.egg-info/PKG-INFO +1 -1
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/README.md +0 -0
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/setup.cfg +0 -0
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/src/zt_devops_cli/__init__.py +0 -0
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/src/zt_devops_cli/auth.py +0 -0
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/src/zt_devops_cli/config.py +0 -0
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/src/zt_devops_cli.egg-info/SOURCES.txt +0 -0
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/src/zt_devops_cli.egg-info/dependency_links.txt +0 -0
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/src/zt_devops_cli.egg-info/entry_points.txt +0 -0
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/src/zt_devops_cli.egg-info/requires.txt +0 -0
- {zt_devops_cli-0.2.2 → zt_devops_cli-0.2.4}/src/zt_devops_cli.egg-info/top_level.txt +0 -0
|
@@ -346,6 +346,13 @@ class DevOpsAPI:
|
|
|
346
346
|
data=data,
|
|
347
347
|
)
|
|
348
348
|
|
|
349
|
+
def get_bug(self, bug_id: str) -> dict:
|
|
350
|
+
"""查询单个工作项详情(GET issue/{projectId}/{issueId},缺陷详情页同源接口)。"""
|
|
351
|
+
return self._request(
|
|
352
|
+
"GET",
|
|
353
|
+
f"/ms/vteam/api/user/issue/{self.project_id}/{bug_id}",
|
|
354
|
+
)
|
|
355
|
+
|
|
349
356
|
def bug_direction_next(
|
|
350
357
|
self,
|
|
351
358
|
issue_id: str,
|
|
@@ -3,7 +3,7 @@ import os
|
|
|
3
3
|
import click
|
|
4
4
|
import json
|
|
5
5
|
import shutil
|
|
6
|
-
from typing import Optional
|
|
6
|
+
from typing import Callable, Optional
|
|
7
7
|
|
|
8
8
|
from .api import BUG_DIRECTION_NEXT_NODE_IDS, DevOpsAPI
|
|
9
9
|
from .auth import login
|
|
@@ -98,6 +98,85 @@ def render_table(headers, rows, max_width=120, no_truncate_headers=None):
|
|
|
98
98
|
click.echo(border)
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
def _extract_api_message(payload) -> str:
|
|
102
|
+
if not isinstance(payload, dict):
|
|
103
|
+
return ""
|
|
104
|
+
for key in ("message", "msg", "errorMsg", "error"):
|
|
105
|
+
value = payload.get(key)
|
|
106
|
+
if isinstance(value, str) and value.strip():
|
|
107
|
+
return value.strip()
|
|
108
|
+
return ""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _is_api_success(payload) -> bool:
|
|
112
|
+
"""兼容不同后端返回结构,判断请求是否成功。"""
|
|
113
|
+
if payload is None:
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
# HTTP 2xx 但无 body 的场景,_request 会返回 {}
|
|
117
|
+
if isinstance(payload, dict) and not payload:
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
if not isinstance(payload, dict):
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
for key in ("success", "ok"):
|
|
124
|
+
if key in payload:
|
|
125
|
+
return bool(payload.get(key))
|
|
126
|
+
|
|
127
|
+
code = payload.get("code")
|
|
128
|
+
if code is not None:
|
|
129
|
+
if str(code).strip() in {"0", "200"}:
|
|
130
|
+
return True
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
status = payload.get("status")
|
|
134
|
+
if isinstance(status, str):
|
|
135
|
+
norm_status = status.strip().lower()
|
|
136
|
+
if norm_status in {"success", "ok", "succeeded"}:
|
|
137
|
+
return True
|
|
138
|
+
if norm_status in {"fail", "failed", "error"}:
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
message = _extract_api_message(payload).lower()
|
|
142
|
+
if message:
|
|
143
|
+
if any(word in message for word in ("失败", "错误", "error", "fail")):
|
|
144
|
+
return False
|
|
145
|
+
if any(word in message for word in ("成功", "success")):
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
if "data" in payload:
|
|
149
|
+
return payload.get("data") is not None
|
|
150
|
+
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _print_action_result(
|
|
155
|
+
ctx,
|
|
156
|
+
result,
|
|
157
|
+
success_text: str = "",
|
|
158
|
+
fail_prefix: str = "操作失败",
|
|
159
|
+
success_checker: Optional[Callable[[dict], bool]] = None,
|
|
160
|
+
success_text_builder: Optional[Callable[[dict], str]] = None,
|
|
161
|
+
) -> bool:
|
|
162
|
+
"""统一处理命令结果输出;返回是否成功。"""
|
|
163
|
+
checker = success_checker or _is_api_success
|
|
164
|
+
|
|
165
|
+
if ctx.obj.get("output_format") == "json":
|
|
166
|
+
click.echo(json.dumps(result, ensure_ascii=False, indent=2, default=str))
|
|
167
|
+
return checker(result)
|
|
168
|
+
|
|
169
|
+
if checker(result):
|
|
170
|
+
text = success_text_builder(result) if success_text_builder else success_text
|
|
171
|
+
if text:
|
|
172
|
+
click.echo(text)
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
msg = _extract_api_message(result) or "未返回成功状态"
|
|
176
|
+
click.echo(f"{fail_prefix}: {msg}", err=True)
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
|
|
101
180
|
@click.group()
|
|
102
181
|
@click.option("--project", "-p", default=None, help="项目 ID")
|
|
103
182
|
@click.option(
|
|
@@ -170,8 +249,8 @@ def zteam_project():
|
|
|
170
249
|
@click.option("--start-date", "-s", required=True, help="开始日期 (YYYY-MM-DD)")
|
|
171
250
|
@click.option("--end-date", "-e", required=True, help="结束日期 (YYYY-MM-DD)")
|
|
172
251
|
@click.option("--purpose", required=True, help="迭代目标")
|
|
173
|
-
@click.option("--test-start", required=True,
|
|
174
|
-
@click.option("--test-end",required=True,
|
|
252
|
+
@click.option("--test-start", required=True, help="测试开始日期")
|
|
253
|
+
@click.option("--test-end", required=True, help="测试结束日期")
|
|
175
254
|
@click.pass_context
|
|
176
255
|
def create_sprint(ctx, project, title, start_date, end_date, purpose, test_start, test_end):
|
|
177
256
|
"""创建迭代"""
|
|
@@ -191,10 +270,15 @@ def create_sprint(ctx, project, title, start_date, end_date, purpose, test_start
|
|
|
191
270
|
test_end_date=test_end,
|
|
192
271
|
)
|
|
193
272
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
273
|
+
_print_action_result(
|
|
274
|
+
ctx,
|
|
275
|
+
result,
|
|
276
|
+
fail_prefix="创建失败",
|
|
277
|
+
success_checker=lambda payload: bool(
|
|
278
|
+
payload.get("data", {}).get("id")
|
|
279
|
+
) if isinstance(payload, dict) else False,
|
|
280
|
+
success_text_builder=lambda payload: f"创建成功! 迭代 ID: {payload.get('data', {}).get('id')}",
|
|
281
|
+
)
|
|
198
282
|
|
|
199
283
|
except Exception as e:
|
|
200
284
|
click.echo(f"错误: {e}", err=True)
|
|
@@ -225,7 +309,7 @@ def start_sprint(ctx, project, sprint_id):
|
|
|
225
309
|
return
|
|
226
310
|
|
|
227
311
|
result = api.start_sprint(sprint_data)
|
|
228
|
-
|
|
312
|
+
_print_action_result(ctx, result, "启用成功!", "启用失败")
|
|
229
313
|
except Exception as e:
|
|
230
314
|
click.echo(f"错误: {e}", err=True)
|
|
231
315
|
|
|
@@ -246,8 +330,8 @@ def delete_sprint(ctx, project, sprint_id):
|
|
|
246
330
|
return
|
|
247
331
|
|
|
248
332
|
try:
|
|
249
|
-
api.delete_sprint(sprint_id)
|
|
250
|
-
|
|
333
|
+
result = api.delete_sprint(sprint_id)
|
|
334
|
+
_print_action_result(ctx, result, "删除成功!", "删除失败")
|
|
251
335
|
except Exception as e:
|
|
252
336
|
click.echo(f"错误: {e}", err=True)
|
|
253
337
|
|
|
@@ -268,8 +352,8 @@ def done_sprint(ctx, project, sprint_id):
|
|
|
268
352
|
return
|
|
269
353
|
|
|
270
354
|
try:
|
|
271
|
-
api.done_sprint(sprint_id)
|
|
272
|
-
|
|
355
|
+
result = api.done_sprint(sprint_id)
|
|
356
|
+
_print_action_result(ctx, result, "完成成功!", "完成失败")
|
|
273
357
|
except Exception as e:
|
|
274
358
|
click.echo(f"错误: {e}", err=True)
|
|
275
359
|
|
|
@@ -798,6 +882,79 @@ def list_bugs(
|
|
|
798
882
|
click.echo(f"错误: {e}", err=True)
|
|
799
883
|
|
|
800
884
|
|
|
885
|
+
def _flatten_issue_detail_for_table(detail):
|
|
886
|
+
"""将详情接口返回的 data 展平为 {字段: 显示值},供 human 表格使用。"""
|
|
887
|
+
if not isinstance(detail, dict):
|
|
888
|
+
return {}
|
|
889
|
+
|
|
890
|
+
def _unwrap_value(v):
|
|
891
|
+
if not isinstance(v, dict):
|
|
892
|
+
return v
|
|
893
|
+
for key in ("value", "label", "name", "displayName", "text", "title"):
|
|
894
|
+
if key in v and v.get(key) not in (None, ""):
|
|
895
|
+
return v.get(key)
|
|
896
|
+
return json.dumps(v, ensure_ascii=False)
|
|
897
|
+
|
|
898
|
+
rows = {}
|
|
899
|
+
prop = detail.get("property")
|
|
900
|
+
if isinstance(prop, dict):
|
|
901
|
+
for k, v in prop.items():
|
|
902
|
+
rows[k] = _unwrap_value(v)
|
|
903
|
+
|
|
904
|
+
for k, v in detail.items():
|
|
905
|
+
if k == "property":
|
|
906
|
+
continue
|
|
907
|
+
if k in rows:
|
|
908
|
+
continue
|
|
909
|
+
if isinstance(v, (dict, list)):
|
|
910
|
+
rows[k] = json.dumps(v, ensure_ascii=False) if v else ""
|
|
911
|
+
else:
|
|
912
|
+
rows[k] = v
|
|
913
|
+
|
|
914
|
+
return rows
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
@bug.command("detail")
|
|
918
|
+
@click.option("--project", "-p", default=None, help="项目 ID")
|
|
919
|
+
@click.option("--bug-id", "-i", required=True, help="缺陷 ID(与列表「缺陷ID」、详情页 URL 中 id 一致)")
|
|
920
|
+
@click.pass_context
|
|
921
|
+
def get_bug_detail(ctx, project, bug_id):
|
|
922
|
+
"""查询单个缺陷详情"""
|
|
923
|
+
project_id = project or ctx.obj["project"]
|
|
924
|
+
if not project_id:
|
|
925
|
+
click.echo("错误: 请通过 -p 指定项目 ID 或在配置中设置 default_project", err=True)
|
|
926
|
+
raise click.Abort()
|
|
927
|
+
|
|
928
|
+
api = DevOpsAPI(project_id)
|
|
929
|
+
try:
|
|
930
|
+
payload = api.get_bug(bug_id)
|
|
931
|
+
data = payload.get("data") if isinstance(payload, dict) else None
|
|
932
|
+
|
|
933
|
+
if ctx.obj.get("output_format") == "json":
|
|
934
|
+
out = data if data is not None else payload
|
|
935
|
+
click.echo(json.dumps(out, ensure_ascii=False, indent=2, default=str))
|
|
936
|
+
return
|
|
937
|
+
|
|
938
|
+
if data is None:
|
|
939
|
+
msg = _extract_api_message(payload) if isinstance(payload, dict) else ""
|
|
940
|
+
click.echo(msg or "未返回详情数据", err=True)
|
|
941
|
+
return
|
|
942
|
+
|
|
943
|
+
flat = _flatten_issue_detail_for_table(data)
|
|
944
|
+
if not flat:
|
|
945
|
+
click.echo(json.dumps(data, ensure_ascii=False, indent=2, default=str))
|
|
946
|
+
return
|
|
947
|
+
|
|
948
|
+
keys = sorted(flat.keys(), key=lambda x: (str(x).lower(), str(x)))
|
|
949
|
+
render_table(
|
|
950
|
+
headers=["字段", "值"],
|
|
951
|
+
rows=[[k, flat[k]] for k in keys],
|
|
952
|
+
no_truncate_headers=["字段", "值"],
|
|
953
|
+
)
|
|
954
|
+
except Exception as e:
|
|
955
|
+
click.echo(f"错误: {e}", err=True)
|
|
956
|
+
|
|
957
|
+
|
|
801
958
|
@bug.command("get-states")
|
|
802
959
|
@click.option("--project", "-p", required=True, help="项目 ID")
|
|
803
960
|
@click.option(
|
|
@@ -1027,12 +1184,13 @@ def list_bug_types(ctx, project, all_options, field_id):
|
|
|
1027
1184
|
@click.option("--zteam-project-id", required=True, help="ZTeam 项目 ID")
|
|
1028
1185
|
@click.option("--sprint-id", required=True, default="", show_default=True, help="迭代 id")
|
|
1029
1186
|
@click.option("--title", "-t", required=True, help="缺陷标题")
|
|
1030
|
-
@click.option("--priority",
|
|
1187
|
+
@click.option("--priority", default="CENTRAL", show_default=True, help="缺陷优先级")
|
|
1031
1188
|
@click.option("--desc-editor-type", default="RICHTEXT", show_default=True, help="缺陷描述编辑器类型")
|
|
1032
|
-
@click.option("--desc",default="<h2><span style=\"font-size: 16px;\">【前置条件】</span></h2>\n"
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1189
|
+
@click.option("--desc", default="<h2><span style=\"font-size: 16px;\">【前置条件】</span></h2>\n"
|
|
1190
|
+
"<h2><span style=\"font-size: 16px;\">【操作步骤】</span></h2>\n"
|
|
1191
|
+
"<h2><span style=\"font-size: 16px;\">【实际结果】</span></h2>\n"
|
|
1192
|
+
"<h2><span style=\"font-size: 16px;\">【预期结果】</span></h2>", show_default=True,
|
|
1193
|
+
help="缺陷描述(支持 HTML")
|
|
1036
1194
|
@click.option("--severity", required=True, default="", show_default=True, help="缺陷严重程度")
|
|
1037
1195
|
@click.option("--type", required=True, default="", show_default=True, help="缺陷分类")
|
|
1038
1196
|
@click.option("--discovery-phase", required=True, default="", show_default=True, help="缺陷发现阶段")
|
|
@@ -1090,11 +1248,15 @@ def create_bug(
|
|
|
1090
1248
|
demand_classify=demand_classify,
|
|
1091
1249
|
instance_values=instance_values,
|
|
1092
1250
|
)
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1251
|
+
_print_action_result(
|
|
1252
|
+
ctx,
|
|
1253
|
+
result,
|
|
1254
|
+
fail_prefix="创建失败",
|
|
1255
|
+
success_checker=lambda payload: bool(
|
|
1256
|
+
payload.get("data", {}).get("id")
|
|
1257
|
+
) if isinstance(payload, dict) else False,
|
|
1258
|
+
success_text_builder=lambda payload: f"创建成功! 缺陷 ID: {payload.get('data', {}).get('id')}",
|
|
1259
|
+
)
|
|
1098
1260
|
except Exception as e:
|
|
1099
1261
|
click.echo(f"错误: {e}", err=True)
|
|
1100
1262
|
|
|
@@ -1139,16 +1301,12 @@ def _bug_direction_go(ctx, project, bug_id, target_key, comment, operators):
|
|
|
1139
1301
|
operators=op_list,
|
|
1140
1302
|
comment=comment,
|
|
1141
1303
|
)
|
|
1142
|
-
|
|
1143
|
-
click.echo(json.dumps(result, ensure_ascii=False, indent=2, default=str))
|
|
1144
|
-
return
|
|
1145
|
-
click.echo("流转请求已提交")
|
|
1304
|
+
_print_action_result(ctx, result, "流转请求已提交", "流转失败")
|
|
1146
1305
|
except Exception as e:
|
|
1147
1306
|
click.echo(f"错误: {e}", err=True)
|
|
1148
1307
|
|
|
1149
1308
|
|
|
1150
1309
|
def _bug_direction_options(cmd):
|
|
1151
|
-
|
|
1152
1310
|
cmd = click.option(
|
|
1153
1311
|
"-i",
|
|
1154
1312
|
"--bug-id",
|
|
@@ -1258,10 +1416,7 @@ def bug_delete(ctx, project, bug_id, yes):
|
|
|
1258
1416
|
api = DevOpsAPI(project_id)
|
|
1259
1417
|
try:
|
|
1260
1418
|
result = api.delete_issue(bug_id)
|
|
1261
|
-
|
|
1262
|
-
click.echo(json.dumps(result, ensure_ascii=False, indent=2, default=str))
|
|
1263
|
-
return
|
|
1264
|
-
click.echo("删除成功")
|
|
1419
|
+
_print_action_result(ctx, result, "删除成功", "删除失败")
|
|
1265
1420
|
except Exception as e:
|
|
1266
1421
|
click.echo(f"错误: {e}", err=True)
|
|
1267
1422
|
|
|
@@ -1334,11 +1489,16 @@ def create_task(
|
|
|
1334
1489
|
demand_classify=demand_classify,
|
|
1335
1490
|
instance_values=instance_values,
|
|
1336
1491
|
)
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1492
|
+
_print_action_result(
|
|
1493
|
+
ctx,
|
|
1494
|
+
result,
|
|
1495
|
+
fail_prefix="创建失败",
|
|
1496
|
+
success_checker=lambda payload: bool(
|
|
1497
|
+
payload.get("data", {}).get("id")
|
|
1498
|
+
) if isinstance(payload, dict) else False,
|
|
1499
|
+
success_text_builder=lambda payload: f"创建成功! 任务 ID: {payload.get('data', {}).get('id')}",
|
|
1500
|
+
)
|
|
1501
|
+
|
|
1342
1502
|
except Exception as e:
|
|
1343
1503
|
click.echo(f"错误: {e}", err=True)
|
|
1344
1504
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|