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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zt-devops-cli
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: DevOps 平台迭代管理 CLI
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "zt-devops-cli"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "DevOps 平台迭代管理 CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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, help="测试开始日期")
174
- @click.option("--test-end",required=True, help="测试结束日期")
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
- if result.get("0") == "0":
195
- click.echo(f"创建成功! 迭代 ID: {result.get('id')}")
196
- else:
197
- click.echo(f"错误: {result.get('message')}", err=True)
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
- click.echo(f"启用成功!")
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
- click.echo(f"删除成功!")
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
- click.echo(f"完成成功!")
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", default="CENTRAL", show_default=True, help="缺陷优先级")
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
- "<h2><span style=\"font-size: 16px;\">【操作步骤】</span></h2>\n"
1034
- "<h2><span style=\"font-size: 16px;\">【实际结果】</span></h2>\n"
1035
- "<h2><span style=\"font-size: 16px;\">【预期结果】</span></h2>",show_default=True, help="缺陷描述(支持 HTML")
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
- if ctx.obj.get("output_format") == "json":
1094
- click.echo(json.dumps(result, ensure_ascii=False, indent=2, default=str))
1095
- return
1096
- bug_id = result.get("data", {}).get("id") if isinstance(result, dict) else None
1097
- click.echo(f"创建成功! 缺陷 ID: {bug_id or '-'}")
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
- if ctx.obj.get("output_format") == "json":
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
- if ctx.obj.get("output_format") == "json":
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
- if ctx.obj.get("output_format") == "json":
1338
- click.echo(json.dumps(result, ensure_ascii=False, indent=2, default=str))
1339
- return
1340
- task_id = result.get("data", {}).get("id") if isinstance(result, dict) else None
1341
- click.echo(f"创建成功! 任务 ID: {task_id or '-'}")
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zt-devops-cli
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: DevOps 平台迭代管理 CLI
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
File without changes
File without changes