zt-devops-cli 0.1.0__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.
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: zt-devops-cli
3
+ Version: 0.1.0
4
+ Summary: DevOps 平台迭代管理 CLI
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: playwright>=1.40.0
8
+ Requires-Dist: requests>=2.31.0
9
+ Requires-Dist: click>=8.1.0
10
+ Requires-Dist: pyyaml>=6.0
11
+
12
+ # DevOps CLI
13
+
14
+ 蓝鲸 DevOps 平台迭代管理命令行工具。
15
+
16
+ ## 打包
17
+ ```bash
18
+ python -m pip install --upgrade build twine
19
+ python -m build
20
+ twine upload dist/*
21
+ ```
22
+
23
+ ## 安装
24
+ ```bash
25
+ pip install -e .
26
+ ```
27
+
28
+ ## 使用
29
+
30
+ ### 登陆
31
+
32
+ 首次运行时会自动打开浏览器,请登录蓝鲸 DevOps。登录成功后 cookie 会缓存到本地。
33
+ ```bash
34
+ devops login
35
+ ```
36
+
37
+ ### 项目列表
38
+ ```bash
39
+ devops project list
40
+ ```
41
+
42
+ ### 迭代列表
43
+ ```bash
44
+ devops sprint list --project k64352
45
+ ```
46
+
47
+ ### 创建迭代
48
+
49
+ ```bash
50
+ devops sprint create --project k64352 \
51
+ --title "迭代名称" \
52
+ --start-date 2026-04-04 \
53
+ --end-date 2026-05-30 \
54
+ --purpose "迭代目标"
55
+ ```
56
+
57
+ ### 启用迭代
58
+ ```bash
59
+ devops sprint start --project k64352 --sprint-id <id>
60
+ ```
61
+
62
+ ### 删除迭代
63
+ ```bash
64
+ devops sprint delete --project k64352 --sprint-id <id>
65
+ ```
66
+
67
+ ### 删除迭代
68
+ ```bash
69
+ devops sprint done --project k64352 --sprint-id <id>
70
+ ```
71
+
72
+ ## 配置
73
+ - cookie 缓存:`~/.devops-cli/cookies.json`
74
+ - 配置文件:`~/.devops-cli/config.yaml`
@@ -0,0 +1,63 @@
1
+ # DevOps CLI
2
+
3
+ 蓝鲸 DevOps 平台迭代管理命令行工具。
4
+
5
+ ## 打包
6
+ ```bash
7
+ python -m pip install --upgrade build twine
8
+ python -m build
9
+ twine upload dist/*
10
+ ```
11
+
12
+ ## 安装
13
+ ```bash
14
+ pip install -e .
15
+ ```
16
+
17
+ ## 使用
18
+
19
+ ### 登陆
20
+
21
+ 首次运行时会自动打开浏览器,请登录蓝鲸 DevOps。登录成功后 cookie 会缓存到本地。
22
+ ```bash
23
+ devops login
24
+ ```
25
+
26
+ ### 项目列表
27
+ ```bash
28
+ devops project list
29
+ ```
30
+
31
+ ### 迭代列表
32
+ ```bash
33
+ devops sprint list --project k64352
34
+ ```
35
+
36
+ ### 创建迭代
37
+
38
+ ```bash
39
+ devops sprint create --project k64352 \
40
+ --title "迭代名称" \
41
+ --start-date 2026-04-04 \
42
+ --end-date 2026-05-30 \
43
+ --purpose "迭代目标"
44
+ ```
45
+
46
+ ### 启用迭代
47
+ ```bash
48
+ devops sprint start --project k64352 --sprint-id <id>
49
+ ```
50
+
51
+ ### 删除迭代
52
+ ```bash
53
+ devops sprint delete --project k64352 --sprint-id <id>
54
+ ```
55
+
56
+ ### 删除迭代
57
+ ```bash
58
+ devops sprint done --project k64352 --sprint-id <id>
59
+ ```
60
+
61
+ ## 配置
62
+ - cookie 缓存:`~/.devops-cli/cookies.json`
63
+ - 配置文件:`~/.devops-cli/config.yaml`
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "zt-devops-cli"
7
+ version = "0.1.0"
8
+ description = "DevOps 平台迭代管理 CLI"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "playwright>=1.40.0",
13
+ "requests>=2.31.0",
14
+ "click>=8.1.0",
15
+ "pyyaml>=6.0",
16
+ ]
17
+
18
+ [project.scripts]
19
+ devops-cli = "devops_cli.cli:main"
20
+
21
+ [tool.pytest.ini_options]
22
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,103 @@
1
+ """API 请求封装"""
2
+ import json as json_mod
3
+ import requests
4
+ from typing import Optional
5
+
6
+ from .auth import get_auth_cookies, get_auth_headers
7
+ from .config import config
8
+
9
+
10
+ class DevOpsAPI:
11
+ """DevOps API 客户端"""
12
+
13
+ def __init__(self, project_id: str):
14
+ self.project_id = project_id
15
+ self.base_url = config.BASE_URL
16
+
17
+ def _request(self, method: str, path: str, **kwargs) -> dict:
18
+ """发送 API 请求"""
19
+ cookies = get_auth_cookies()
20
+ headers = get_auth_headers(cookies, self.project_id)
21
+
22
+ url = f"{self.base_url}/{path}"
23
+
24
+ # 处理 data 参数
25
+ if "data" in kwargs:
26
+ kwargs["json"] = kwargs.pop("data")
27
+
28
+ #############################################
29
+ # # 打印 curl 命令
30
+ # curl_parts = [f"curl -X {method}"]
31
+ # for k, v in headers.items():
32
+ # curl_parts.append(f"-H '{k}: {v}'")
33
+ # if "json" in kwargs:
34
+ # curl_parts.append(f"-d '{json_mod.dumps(kwargs['json'], ensure_ascii=False)}'")
35
+ # curl_parts.append(f"'{url}'")
36
+ # print(" ".join(curl_parts))
37
+ #############################################
38
+
39
+ response = requests.request(
40
+ method=method,
41
+ url=url,
42
+ headers=headers,
43
+ cookies=cookies,
44
+ **kwargs
45
+ )
46
+
47
+ if response.status_code >= 400:
48
+ raise Exception(f"API 错误: {response.status_code} {response.text}")
49
+
50
+ return response.json()
51
+
52
+ # ========== 迭代操作 ==========
53
+
54
+ def create_sprint(
55
+ self,
56
+ title: str,
57
+ start_date: str,
58
+ end_date: str,
59
+ purpose: str = "",
60
+ test_start_date: Optional[str] = None,
61
+ test_end_date: Optional[str] = None,
62
+ development_pic: str = "",
63
+ test_pic: str = "",
64
+ acceptance_pic: str = "",
65
+ ) -> dict:
66
+ """创建迭代"""
67
+ data = {
68
+ "title": title,
69
+ "startDate": start_date,
70
+ "endDate": end_date,
71
+ "purpose": purpose,
72
+ "daterange": [f"{start_date}T16:00:00.000Z", f"{end_date}T16:00:00.000Z"],
73
+ "reviewTime": "",
74
+ "testStartDate": test_start_date or start_date,
75
+ "testEndDate": test_end_date or end_date,
76
+ "developmentPic": development_pic,
77
+ "testPic": test_pic,
78
+ "acceptancePic": acceptance_pic,
79
+ "excludeTime": "",
80
+ "state": "ACTIVE",
81
+ }
82
+ return self._request("POST", f"/ms/vteam/api/user/issue_sprint/{self.project_id}", data=data)
83
+
84
+ def start_sprint(self, sprint_data: dict) -> dict:
85
+ """启用迭代"""
86
+ return self._request("PUT", f"/ms/vteam/api/user/issue_sprint/{self.project_id}/start", data=sprint_data)
87
+
88
+ def delete_sprint(self, sprint_id: str) -> dict:
89
+ """删除迭代"""
90
+ return self._request("DELETE", f"/ms/vteam/api/user/issue_sprint/{self.project_id}/{sprint_id}")
91
+
92
+ def done_sprint(self, sprint_id: str) -> dict:
93
+ """完成迭代"""
94
+ return self._request("PUT", f"/ms/vteam/api/user/issue_sprint/{self.project_id}/complete/{sprint_id}")
95
+
96
+ def list_sprints(self) -> dict:
97
+ """查询迭代列表"""
98
+ return self._request("GET", f"/ms/vteam/api/user/issue_sprint/{self.project_id}/flat?num=1&count=1&size=20&content=&start_time=&end_time=&state=ACTIVE%2CNOT_STARTED&sort_field=CREATED_TIME&sort_rule=DESC")
99
+
100
+ def list_projects(self) -> dict:
101
+ """查询项目列表"""
102
+ return self._request("GET", f"/ms/projectmanager/api/user/project/cw/selectByType?showTree=false&haveUser=false")
103
+
@@ -0,0 +1,161 @@
1
+ """认证模块 - 通过 Playwright 获取浏览器 cookie"""
2
+ import json
3
+ import os
4
+
5
+ from playwright.sync_api import sync_playwright
6
+
7
+ from .config import config
8
+
9
+
10
+ # DevOps 域名
11
+ DEVOPS_DOMAIN = ".ztn.cn"
12
+
13
+ # SSO 登录页面
14
+ LOGIN_URL = "https://sso.ztn.cn/v2/cas/login?service=https%3A%2F%2Fpaas.ztn.cn%2Flogin%2F%3Fc_url%3Dhttps%3A%2F%2Fdevops.ztn.cn%2Fconsole%2F&locale=zh_CN"
15
+
16
+ def save_cookies(cookies: dict):
17
+ """保存 cookie 到缓存文件"""
18
+ config.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
19
+ with open(config.COOKIE_FILE, "w") as f:
20
+ json.dump(cookies, f)
21
+
22
+ def load_cookies() -> dict:
23
+ """从缓存文件加载 cookie"""
24
+ if config.COOKIE_FILE.exists():
25
+ with open(config.COOKIE_FILE) as f:
26
+ return json.load(f)
27
+ return {}
28
+
29
+ def get_auth_headers(cookies: dict, project_id: str) -> dict:
30
+ """从 cookie 构建认证请求头"""
31
+ return {
32
+ "Accept": "application/json, text/plain, */*",
33
+ "Accept-Language": "zh-CN",
34
+ "Content-Type": "application/json;charset=UTF-8",
35
+ "Origin": "https://devops.ztn.cn",
36
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
37
+ "X-DEVOPS-TENANT-ID": cookies.get("X-DEVOPS-TENANT-ID", "ZTN"),
38
+ "bk_token": cookies.get("bk_token", ""),
39
+ "X-DEVOPS-TOKEN": cookies.get("X-DEVOPS-TOKEN", ""),
40
+ "X-DEVOPS-PROJECT-ID": project_id,
41
+ }
42
+
43
+ def get_auth_cookies() -> dict:
44
+ """获取认证 cookie,优先使用缓存,失效则重新获取"""
45
+ cookies = load_cookies()
46
+
47
+ # 检查必要的 cookie 是否存在
48
+ required = ["bk_token", "X-DEVOPS-TOKEN"]
49
+ if all(cookies.get(k) for k in required):
50
+ return cookies
51
+
52
+ # 缓存失效,重新获取
53
+ cookies = get_cookies_from_browser()
54
+ save_cookies(cookies)
55
+
56
+ print(f"已获取新的 cookie: {cookies}")
57
+ return cookies
58
+
59
+ def open_login_page(page) -> bool:
60
+ """自动登录 - 尝试读取环境变量自动填写账号密码"""
61
+ username = os.environ.get("DEVOPS_USERNAME")
62
+ password = os.environ.get("DEVOPS_PASSWORD")
63
+
64
+ if not username or not password:
65
+ return False
66
+
67
+ print(f"检测到环境变量,自动登录中...")
68
+
69
+ # 等待页面加载
70
+ page.wait_for_selector("#username")
71
+
72
+ try:
73
+ # 填写用户名
74
+ page.fill("#username", username)
75
+ print(f"已填写用户名")
76
+
77
+ # 填写密码 - 使用 #password2
78
+ page.fill("#password2", password)
79
+ print(f"已填写密码")
80
+
81
+ # 点击登录按钮
82
+ page.click(".login_in")
83
+ print("已点击登录按钮")
84
+
85
+ return True
86
+
87
+ except Exception as e:
88
+ print(f"自动登录失败: {e}")
89
+
90
+ return False
91
+
92
+ def get_cookies_from_browser() -> dict:
93
+ """通过 Playwright 从浏览器获取 cookie"""
94
+ print("正在打开浏览器,请登录...")
95
+
96
+ with sync_playwright() as p:
97
+ browser = p.chromium.launch(headless=False)
98
+ context = browser.new_context()
99
+ page = context.new_page()
100
+
101
+ # 导航到 SSO 登录页
102
+ page.goto(LOGIN_URL)
103
+
104
+ # 尝试自动登录
105
+ if not open_login_page(page):
106
+ print("自动登陆失败. 请在浏览器中完成登录,登录成功后会自动继续...")
107
+
108
+ print("登陆中.检测关键 cookie 出现")
109
+ page.wait_for_function(
110
+ f"""() => {{
111
+ const cookies = document.cookie;
112
+ return cookies.includes('X-DEVOPS-TOKEN');
113
+ }}""",
114
+ timeout=300000 # 5分钟超时
115
+ )
116
+
117
+ print("登陆成功. 已获取关键 cookie")
118
+
119
+ # 获取所有 cookie
120
+ cookies = context.cookies()
121
+
122
+ # 提取需要的 cookie
123
+ cookie_dict = {}
124
+ for cookie in cookies:
125
+ if cookie["domain"] == DEVOPS_DOMAIN or DEVOPS_DOMAIN in cookie["domain"]:
126
+ cookie_dict[cookie["name"]] = cookie["value"]
127
+
128
+ # 从 cookie 获取 X-DEVOPS-PROJECT-ID
129
+ project_id = page.evaluate("""() => {
130
+ const name = 'X-DEVOPS-PROJECT-ID=';
131
+ const decodedCookie = decodeURIComponent(document.cookie);
132
+ const ca = decodedCookie.split(';');
133
+ for (let i = 0; i < ca.length; i++) {
134
+ let c = ca[i];
135
+ while (c.charAt(0) == ' ') {
136
+ c = c.substring(1);
137
+ }
138
+ if (c.indexOf(name) == 0) {
139
+ return c.substring(name.length, c.length);
140
+ }
141
+ }
142
+ return '';
143
+ }""")
144
+
145
+ if project_id:
146
+ cookie_dict["X-DEVOPS-PROJECT-ID"] = project_id
147
+
148
+ browser.close()
149
+
150
+ return cookie_dict
151
+
152
+ def login() -> dict:
153
+ """手动登录 - 强制打开浏览器重新获取 cookie"""
154
+ print("正在打开浏览器,请重新登录...")
155
+ cookies = get_cookies_from_browser()
156
+ save_cookies(cookies)
157
+ print("登录成功,cookie 已缓存!")
158
+ return cookies
159
+
160
+ if __name__ == "__main__":
161
+ print(login())
@@ -0,0 +1,317 @@
1
+ """CLI 入口"""
2
+ import click
3
+ import json
4
+ import shutil
5
+ from .api import DevOpsAPI
6
+ from .auth import login
7
+ from .config import config
8
+
9
+
10
+ def _normalize_cell(value):
11
+ if value is None:
12
+ return "-"
13
+ return str(value)
14
+
15
+
16
+ def _truncate_text(text, width):
17
+ if width <= 0:
18
+ return ""
19
+ if len(text) <= width:
20
+ return text
21
+ if width <= 1:
22
+ return text[:width]
23
+ return text[: width - 1] + "…"
24
+
25
+
26
+ def render_table(headers, rows, max_width=120, no_truncate_headers=None):
27
+ if not rows:
28
+ click.echo("暂无数据")
29
+ return
30
+
31
+ terminal_width = shutil.get_terminal_size((max_width, 20)).columns
32
+ table_max_width = min(max_width, terminal_width)
33
+
34
+ no_truncate_headers = set(no_truncate_headers or [])
35
+
36
+ normalized_rows = []
37
+ for row in rows:
38
+ normalized = [_normalize_cell(cell) for cell in row[: len(headers)]]
39
+ if len(normalized) < len(headers):
40
+ normalized.extend(["-"] * (len(headers) - len(normalized)))
41
+ normalized_rows.append(normalized)
42
+
43
+ widths = [len(h) for h in headers]
44
+ for row in normalized_rows:
45
+ for idx, cell in enumerate(row):
46
+ widths[idx] = max(widths[idx], len(cell))
47
+
48
+ # 边框和分隔占用: 左右边框 + 每列两侧空格 + 列分隔符
49
+ overhead = 3 * len(headers) + 1
50
+ available = max(20, table_max_width - overhead)
51
+ min_col_width = 10
52
+ widths = [max(min_col_width, w) for w in widths]
53
+
54
+ total = sum(widths)
55
+ if total > available:
56
+ fixed_idx = {i for i, h in enumerate(headers) if h in no_truncate_headers}
57
+ shrinkable_idx = [i for i in range(len(headers)) if i not in fixed_idx]
58
+
59
+ if shrinkable_idx:
60
+ while sum(widths) > available:
61
+ idx = max(shrinkable_idx, key=lambda i: widths[i])
62
+ if widths[idx] > min_col_width:
63
+ widths[idx] -= 1
64
+ else:
65
+ if all(widths[i] <= min_col_width for i in shrinkable_idx):
66
+ break
67
+ # 若仍超出可用宽度,保留不截断列完整内容,允许终端自动换行/横向滚动查看
68
+
69
+ def format_row(cells):
70
+ parts = []
71
+ for i, cell in enumerate(cells):
72
+ text = cell if headers[i] in no_truncate_headers else _truncate_text(cell, widths[i])
73
+ parts.append(f" {text.ljust(widths[i])} ")
74
+ return "|" + "|".join(parts) + "|"
75
+
76
+ border = "+" + "+".join("-" * (w + 2) for w in widths) + "+"
77
+ click.echo(border)
78
+ click.echo(format_row(headers))
79
+ click.echo(border)
80
+ for row in normalized_rows:
81
+ click.echo(format_row(row))
82
+ click.echo(border)
83
+
84
+
85
+ @click.group()
86
+ @click.option("--project", "-p", default=None, help="项目 ID")
87
+ @click.option(
88
+ "--output",
89
+ "-o",
90
+ "output_format",
91
+ type=click.Choice(["human", "json"], case_sensitive=False),
92
+ default="json",
93
+ show_default=True,
94
+ help="输出格式: human(表格) / json",
95
+ )
96
+ @click.pass_context
97
+ def cli(ctx, project, output_format):
98
+ """DevOps 平台迭代管理工具"""
99
+ ctx.ensure_object(dict)
100
+ ctx.obj["project"] = project or config.default_project
101
+ ctx.obj["output_format"] = output_format.lower()
102
+
103
+
104
+ @cli.command("login")
105
+ def cmd_login():
106
+ """登录 - 打开浏览器重新获取 cookie"""
107
+ login()
108
+
109
+
110
+ @cli.group()
111
+ def sprint():
112
+ """迭代管理命令组"""
113
+ pass
114
+
115
+ @cli.group()
116
+ def project():
117
+ """项目管理命令组"""
118
+ pass
119
+
120
+ @sprint.command("create")
121
+ @click.option("--project", "-p", default=None, help="项目 ID")
122
+ @click.option("--title", "-t", required=True, help="迭代名称")
123
+ @click.option("--start-date", "-s", required=True, help="开始日期 (YYYY-MM-DD)")
124
+ @click.option("--end-date", "-e", required=True, help="结束日期 (YYYY-MM-DD)")
125
+ @click.option("--purpose", help="迭代目标")
126
+ @click.option("--test-start", help="测试开始日期")
127
+ @click.option("--test-end", help="测试结束日期")
128
+ @click.pass_context
129
+ def create_sprint(ctx, project, title, start_date, end_date, purpose, test_start, test_end):
130
+ """创建迭代"""
131
+ project_id = project or ctx.obj["project"]
132
+ if not project_id:
133
+ click.echo("错误: 请通过 -p 指定项目 ID 或在配置中设置 default_project")
134
+ raise click.Abort()
135
+ api = DevOpsAPI(project_id)
136
+
137
+ try:
138
+ result = api.create_sprint(
139
+ title=title,
140
+ start_date=start_date,
141
+ end_date=end_date,
142
+ purpose=purpose or "",
143
+ test_start_date=test_start,
144
+ test_end_date=test_end,
145
+ )
146
+ click.echo(f"创建成功! 迭代 ID: {result.get('id')}")
147
+ except Exception as e:
148
+ click.echo(f"错误: {e}", err=True)
149
+
150
+
151
+ @sprint.command("start")
152
+ @click.option("--project", "-p", default=None, help="项目 ID")
153
+ @click.option("--sprint-id", "-i", required=True, help="迭代 ID")
154
+ @click.pass_context
155
+ def start_sprint(ctx, project, sprint_id):
156
+ """启用迭代"""
157
+ project_id = project or ctx.obj["project"]
158
+ if not project_id:
159
+ click.echo("错误: 请通过 -p 指定项目 ID 或在配置中设置 default_project")
160
+ raise click.Abort()
161
+ api = DevOpsAPI(project_id)
162
+
163
+ try:
164
+ sprints = api.list_sprints()
165
+ sprint_data = None
166
+ for s in sprints.get("data").get("content"):
167
+ if s.get("id") == sprint_id:
168
+ sprint_data = s
169
+ break
170
+
171
+ if not sprint_data:
172
+ click.echo(f"未找到迭代: {sprint_id}", err=True)
173
+ return
174
+
175
+ result = api.start_sprint(sprint_data)
176
+ click.echo(f"启用成功!")
177
+ except Exception as e:
178
+ click.echo(f"错误: {e}", err=True)
179
+
180
+
181
+ @sprint.command("delete")
182
+ @click.option("--project", "-p", default=None, help="项目 ID")
183
+ @click.option("--sprint-id", "-i", required=True, help="迭代 ID")
184
+ @click.pass_context
185
+ def delete_sprint(ctx, project, sprint_id):
186
+ """删除迭代"""
187
+ project_id = project or ctx.obj["project"]
188
+ if not project_id:
189
+ click.echo("错误: 请通过 -p 指定项目 ID 或在配置中设置 default_project")
190
+ raise click.Abort()
191
+ api = DevOpsAPI(project_id)
192
+
193
+ if not click.confirm(f"确认删除迭代 {sprint_id}?"):
194
+ return
195
+
196
+ try:
197
+ api.delete_sprint(sprint_id)
198
+ click.echo(f"删除成功!")
199
+ except Exception as e:
200
+ click.echo(f"错误: {e}", err=True)
201
+
202
+
203
+ @sprint.command("done")
204
+ @click.option("--project", "-p", default=None, help="项目 ID")
205
+ @click.option("--sprint-id", "-i", required=True, help="迭代 ID")
206
+ @click.pass_context
207
+ def done_sprint(ctx, project, sprint_id):
208
+ """ 完成迭代"""
209
+ project_id = project or ctx.obj["project"]
210
+ if not project_id:
211
+ click.echo("错误: 请通过 -p 指定项目 ID 或在配置中设置 default_project")
212
+ raise click.Abort()
213
+ api = DevOpsAPI(project_id)
214
+
215
+ if not click.confirm(f"确认完成迭代 {sprint_id}?"):
216
+ return
217
+
218
+ try:
219
+ api.done_sprint(sprint_id)
220
+ click.echo(f"完成成功!")
221
+ except Exception as e:
222
+ click.echo(f"错误: {e}", err=True)
223
+
224
+
225
+
226
+ @sprint.command("list")
227
+ @click.option("--project", "-p", default=None, help="项目 ID")
228
+ @click.pass_context
229
+ def list_sprint(ctx, project):
230
+
231
+ project_id = project or ctx.obj["project"]
232
+ if not project_id:
233
+ click.echo("错误: 请通过 -p 指定项目 ID 或在配置中设置 default_project")
234
+ raise click.Abort()
235
+ api = DevOpsAPI(project_id)
236
+
237
+
238
+ try:
239
+ sprints = api.list_sprints()
240
+ sprint_list = sprints.get("data", {}).get("content", [])
241
+ if ctx.obj.get("output_format") == "json":
242
+ click.echo(
243
+ json.dumps(
244
+ sprint_list,
245
+ ensure_ascii=False,
246
+ indent=2,
247
+ default=str,
248
+ )
249
+ )
250
+ return
251
+ render_table(
252
+ headers=["迭代名称", "迭代ID", "开始时间", "结束时间", "迭代状态", "提测时间", "测试开始时间", "测试完成时间","验收完成", "负责人"],
253
+ rows=[
254
+ [
255
+ s.get("title"),
256
+ s.get("id"),
257
+ s.get("startDate"),
258
+ s.get("endDate"),
259
+ s.get("state"),
260
+ s.get("testStartDate"),
261
+ s.get("smokeEndDate"),
262
+ s.get("testEndDate"),
263
+ s.get("acceptanceEndDate"),
264
+ s.get("developmentPic"),
265
+ s.get("testPic"),
266
+ s.get("acceptancePic")
267
+ ]
268
+ for s in sprint_list
269
+ ],
270
+ no_truncate_headers=["迭代ID","迭代名称"],
271
+ )
272
+
273
+ except Exception as e:
274
+ click.echo(f"错误: {e}", err=True)
275
+
276
+
277
+
278
+ @project.command("list")
279
+ @click.pass_context
280
+ def list_project(ctx):
281
+
282
+ api = DevOpsAPI("")
283
+
284
+ try:
285
+ projects = api.list_projects()
286
+ project_list = projects.get("data", [])
287
+ if ctx.obj.get("output_format") == "json":
288
+ click.echo(
289
+ json.dumps(
290
+ project_list,
291
+ ensure_ascii=False,
292
+ indent=2,
293
+ default=str,
294
+ )
295
+ )
296
+ return
297
+ render_table(
298
+ headers=["项目代码", "项目名称", "部门名称"],
299
+ rows=[
300
+ [
301
+ s.get("projectCode"),
302
+ s.get("projectName"),
303
+ s.get("deptName"),
304
+ ]
305
+ for s in project_list
306
+ ],
307
+ )
308
+
309
+ except Exception as e:
310
+ click.echo(f"错误: {e}", err=True)
311
+
312
+ def main():
313
+ cli()
314
+
315
+
316
+ if __name__ == "__main__":
317
+ main()
@@ -0,0 +1,38 @@
1
+ """配置管理模块"""
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import yaml
7
+
8
+
9
+ class Config:
10
+ """配置类"""
11
+
12
+ CONFIG_DIR = Path.home() / ".devops-cli"
13
+ COOKIE_FILE = CONFIG_DIR / "cookies.json"
14
+ CONFIG_FILE = CONFIG_DIR / "config.yaml"
15
+
16
+ # API 配置
17
+ BASE_URL = "https://devops.ztn.cn"
18
+
19
+ def __init__(self):
20
+ self._config = self._load_config()
21
+
22
+ def _load_config(self) -> dict:
23
+ """加载配置文件"""
24
+ if self.CONFIG_FILE.exists():
25
+ with open(self.CONFIG_FILE) as f:
26
+ return yaml.safe_load(f) or {}
27
+ return {}
28
+
29
+ @property
30
+ def default_project(self) -> Optional[str]:
31
+ return self._config.get("default_project")
32
+
33
+ @property
34
+ def browser_channel(self) -> str:
35
+ return self._config.get("browser_channel", "chromium")
36
+
37
+
38
+ config = Config()
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: zt-devops-cli
3
+ Version: 0.1.0
4
+ Summary: DevOps 平台迭代管理 CLI
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: playwright>=1.40.0
8
+ Requires-Dist: requests>=2.31.0
9
+ Requires-Dist: click>=8.1.0
10
+ Requires-Dist: pyyaml>=6.0
11
+
12
+ # DevOps CLI
13
+
14
+ 蓝鲸 DevOps 平台迭代管理命令行工具。
15
+
16
+ ## 打包
17
+ ```bash
18
+ python -m pip install --upgrade build twine
19
+ python -m build
20
+ twine upload dist/*
21
+ ```
22
+
23
+ ## 安装
24
+ ```bash
25
+ pip install -e .
26
+ ```
27
+
28
+ ## 使用
29
+
30
+ ### 登陆
31
+
32
+ 首次运行时会自动打开浏览器,请登录蓝鲸 DevOps。登录成功后 cookie 会缓存到本地。
33
+ ```bash
34
+ devops login
35
+ ```
36
+
37
+ ### 项目列表
38
+ ```bash
39
+ devops project list
40
+ ```
41
+
42
+ ### 迭代列表
43
+ ```bash
44
+ devops sprint list --project k64352
45
+ ```
46
+
47
+ ### 创建迭代
48
+
49
+ ```bash
50
+ devops sprint create --project k64352 \
51
+ --title "迭代名称" \
52
+ --start-date 2026-04-04 \
53
+ --end-date 2026-05-30 \
54
+ --purpose "迭代目标"
55
+ ```
56
+
57
+ ### 启用迭代
58
+ ```bash
59
+ devops sprint start --project k64352 --sprint-id <id>
60
+ ```
61
+
62
+ ### 删除迭代
63
+ ```bash
64
+ devops sprint delete --project k64352 --sprint-id <id>
65
+ ```
66
+
67
+ ### 删除迭代
68
+ ```bash
69
+ devops sprint done --project k64352 --sprint-id <id>
70
+ ```
71
+
72
+ ## 配置
73
+ - cookie 缓存:`~/.devops-cli/cookies.json`
74
+ - 配置文件:`~/.devops-cli/config.yaml`
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/devops_cli/__init__.py
4
+ src/devops_cli/api.py
5
+ src/devops_cli/auth.py
6
+ src/devops_cli/cli.py
7
+ src/devops_cli/config.py
8
+ src/zt_devops_cli.egg-info/PKG-INFO
9
+ src/zt_devops_cli.egg-info/SOURCES.txt
10
+ src/zt_devops_cli.egg-info/dependency_links.txt
11
+ src/zt_devops_cli.egg-info/entry_points.txt
12
+ src/zt_devops_cli.egg-info/requires.txt
13
+ src/zt_devops_cli.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ devops-cli = devops_cli.cli:main
@@ -0,0 +1,4 @@
1
+ playwright>=1.40.0
2
+ requests>=2.31.0
3
+ click>=8.1.0
4
+ pyyaml>=6.0