msdevflow 0.8.5 → 0.8.7
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.
- package/package.json +1 -1
- package/skill/msd/README.md +1 -0
- package/skill/msd/SKILL.md +1 -0
- package/skill/msd/references/command-capabilities.md +1 -1
- package/skill/msd/references/create-issue.md +13 -8
- package/skill/msd/references/pr-and-ci.md +7 -7
- package/skill/msd/scripts/gitcode_issue.py +235 -0
- package/skill/msd/scripts/gitcode_verify.py +20 -3
package/package.json
CHANGED
package/skill/msd/README.md
CHANGED
|
@@ -421,5 +421,6 @@ typed CLI command
|
|
|
421
421
|
- [scripts/openlibing_ci.py](scripts/openlibing_ci.py):openLiBing 只读诊断与交互 OAuth 客户端。
|
|
422
422
|
- [scripts/gitcode_review.py](scripts/gitcode_review.py):`code-review` 固定 snapshot/context/finding/lgtm/cleanup 适配器。
|
|
423
423
|
- [scripts/gitcode_verify.py](scripts/gitcode_verify.py):`pr`/`ci` 的 PR 回读、评论回读、CI 轮询和监控 helper。
|
|
424
|
+
- [scripts/gitcode_issue.py](scripts/gitcode_issue.py):`create-issue` 的 schema/标签/里程碑/模板批量只读探测和 Issue 回读 helper。
|
|
424
425
|
|
|
425
426
|
测试文件(`scripts/test_openlibing_ci.py`、`scripts/test_gitcode_review.py`)只存在于 [源仓库](https://gitcode.com/msbuilder/msdevflow),不随 npm 包或 skill 安装产物分发;安装目录中没有这两个文件是预期行为。
|
package/skill/msd/SKILL.md
CHANGED
|
@@ -127,6 +127,7 @@ discovered -> claimed -> verified -> analyzed -> designed
|
|
|
127
127
|
- 创建 Issue/PR、评论、触发 CI、resolve 和 merge 前检查幂等状态;`create-issue` 在最终预览确认前禁止远端写入,每个 run 最多执行一次已确认的真实创建,禁止测试 Issue、POST 探测、失败重试或替代 Issue,只在写结果不确定时有界只读恢复;其他写结果不确定时也先回读。
|
|
128
128
|
- `code-review` 的 GitCode 网络访问只能由 `scripts/gitcode_review.py` 执行;helper 输出不完整或能力不足即阻断,不回退现场命令、文本解析或 endpoint 探测。受管 snapshot 必须结束时清理,受管 write receipt 只用于固定 head 上评论写入的幂等恢复。
|
|
129
129
|
- `pr`/`ci` 的 PR 回读、评论回读和 CI 轮询固定使用 `scripts/gitcode_verify.py`,不每轮现场生成 python/PowerShell 内联 JSON 解析;脚本报告 FAIL 或退出码非 0 时按验证失败阻断,先原地修复再回读。
|
|
130
|
+
- `create-issue` 的 schema 探测、标签/里程碑分页和创建后回读固定使用 `scripts/gitcode_issue.py prepare` / `inspect`,不逐条现场调用只读 CLI 命令;批量只读探测走单次 `python` 调用,避免每条命令单独触发权限确认。
|
|
130
131
|
- Agent 写入 GitCode 的 PR 正文、Issue/PR 评论、discussion 回复和其他人类可读正文必须按 [references/state-and-safety.md](references/state-and-safety.md) 在末尾恰好附加一次 `——msdevflow`。
|
|
131
132
|
- 所有人类可读远端文本必须通过 UTF-8 安全通道写入并逐字回读;发现乱码、`?`、截断或尾签损坏时阻断依赖动作,先原地修复。
|
|
132
133
|
- 严格机器协议 payload 保持原样,不把尾签拼进 `compile`、`/lgtm`、`/merge` 等命令。除 `code-review` 检视通过时只发送精确 `/lgtm` 外,其他协议按 [references/state-and-safety.md](references/state-and-safety.md) 发布并验证带尾签的独立人类可读说明。
|
|
@@ -35,7 +35,7 @@ GitCode CLI 版本和命令集合可能不同。工作流必须探测能力,
|
|
|
35
35
|
<gitcode-command> schema "pr merge"
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
若无 schema,对应使用 `<command> --help`。只探测当前 action 所需命令;`create-issue` 至少需要 `milestone list` 的显式 repo、JSON 和分页,`label list` 的显式 repo、JSON 和完整分页,`issue create` 的显式 repo、title、body-file、issue-type、JSON 和 dry-run,`issue view` 的 JSON 回读能力,以及 `issue edit` 的显式 repo、label、title、body-file、milestone number、yes 和 JSON
|
|
38
|
+
若无 schema,对应使用 `<command> --help`。只探测当前 action 所需命令;`create-issue` 至少需要 `milestone list` 的显式 repo、JSON 和分页,`label list` 的显式 repo、JSON 和完整分页,`issue create` 的显式 repo、title、body-file、issue-type、JSON 和 dry-run,`issue view` 的 JSON 回读能力,以及 `issue edit` 的显式 repo、label、title、body-file、milestone number、yes 和 JSON。这些只读探测和回读固定通过 `scripts/gitcode_issue.py prepare` / `inspect` 一次完成,不逐条现场调用 schema 和分页命令,避免每条命令单独触发权限确认。平台 `issue_type` 不在 typed view 输出时须用已验证的同 CLI 单 Issue GET JSON 回读;类型枚举和名称/ID 映射只采用当前官方文档、CLI 实现或仓库类型配置支持的只读路径,不猜 endpoint。`--issue-type` 的 enterprise 标注不是目标仓库支持证据;类型无法明确设置或回读时在创建前阻断,不能省略参数创建默认 Bug。标签完整分页证明必需标签缺失时也必须阻断,不降级为 `none`。官方 npm GitCode CLI 的 typed `issue create --title` 是 `create-issue` 唯一支持的非 ASCII 标题创建通道;不得调用 `api .../issues --method POST` 测试或创建 Issue。`pr` 至少需要读取关联 Issue 和 PR milestone 的 JSON 能力,以及 `pr edit` 的显式 repo、milestone number 和 JSON;typed `pr create` 不支持 milestone 时,必须在同一次已确认的 PR action 中以 `pr edit --milestone` 关联并回读。`openlibing-auth` 至少需要可有界列举 canonical PR、读取 PR head/labels/comments 的 JSON 能力。完整作者 E2E 记录仓库参数(`-R`/`--repo`)、JSON、body-file/comment-file/stdin、dry-run、fork、assignee、discussion/resolve、正文 edit 和标题 edit 能力。涉及人类可读远端文本时,必须确认 [state-and-safety.md](state-and-safety.md) 要求的 UTF-8 文件、stdin 或 `api --input <utf8-json-file>` 安全通道;Windows PowerShell 5.1 下只有 `--body`/`--comment`/`--title` 等字符串参数而入口为 `.cmd` wrapper 时,不把非 ASCII 文本直接放进命令行参数。
|
|
39
39
|
|
|
40
40
|
`code-review` 是能力降级顺序的专用例外:Agent 不直接探测或组合 GitCode 命令,只调用 Skill 随附的 `scripts/gitcode_review.py`。setup 必须验收 `issue view`、`pr view`、`pr comments`、`pr comment` 和 `api`;helper 内部只使用 [code-review.md](code-review.md) 固定的数据路径,以 `pulls/<number>/files` 而不是 typed `pr diff --json` 读取完整 changed-files。helper 的 `snapshot`/`context`/`finding`/`lgtm` 返回能力不足、未知 schema 或不完整响应时,停止为 `blocked: review-capability-required` 或 `review-incomplete`;不得退回 raw CLI、网页 raw URL、`curl`、现场 `python -c`/PowerShell JSON、展示文本 grep 或其他 endpoint 试探。
|
|
41
41
|
|
|
@@ -135,17 +135,22 @@ Duplicate search: not performed by design
|
|
|
135
135
|
|
|
136
136
|
本 Skill 固定使用 setup 已验收的官方 npm GitCode CLI typed command,不通过 Issue collection API 创建。能力验证只能使用 `schema`、`--help` 和 typed command 自带的 `--dry-run`;`api .../issues --method POST` 没有 dry-run 语义,禁止用于创建、试错或验证。
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
使用前只读探测固定使用 Skill 随附脚本一次完成,不逐条现场调用 schema 和分页命令:
|
|
139
139
|
|
|
140
140
|
```bash
|
|
141
|
-
|
|
142
|
-
<gitcode-command> schema "milestone list"
|
|
143
|
-
<gitcode-command> schema "issue create"
|
|
144
|
-
<gitcode-command> schema "issue view"
|
|
145
|
-
<gitcode-command> schema "issue edit"
|
|
141
|
+
MSDEVFLOW_GITCODE_COMMAND=<gitcode-command> python <skill-scripts-dir>/gitcode_issue.py prepare <canonical> --template-ref <canonical-remote>/<default-branch>
|
|
146
142
|
```
|
|
147
143
|
|
|
148
|
-
|
|
144
|
+
该命令一次返回:`issue create`/`issue view`/`issue edit`/`label list`/`milestone list` 的 schema flags、全部分页标签(含精确数量,避免把第一页截断误判为“标签不存在”)、开放里程碑列表,以及 `.gitcode` 模板树。缺少 `issue create --issue-type`、完整标签分页或同 Issue 编辑能力时在创建前阻断;具体类型的适用性仍按“平台类型”核验,不能只看 flag 是否存在。
|
|
145
|
+
|
|
146
|
+
创建后的权威回读同样固定使用脚本一次完成:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
MSDEVFLOW_GITCODE_COMMAND=<gitcode-command> python <skill-scripts-dir>/gitcode_issue.py inspect <canonical> <issue-number> \
|
|
150
|
+
--expect-type <platform-issue-type> --expect-label <required-label> --expect-milestone <milestone-number> --expect-title "<exact-title>"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
脚本用 typed `issue view` 加单 Issue GET API 读取平台类型、标签、里程碑和正文,逐项输出 PASS/FAIL,退出码非 0 即回读失败。命令本身不创建、编辑或删除任何远端对象;只有脚本报告全部 PASS 才能继续。
|
|
149
154
|
|
|
150
155
|
将最终正文写入唯一的系统临时文件,编码固定为 UTF-8 无 BOM。官方 npm GitCode CLI 的 `issue create --title` 是本 Skill 已验收的中文标题通道;Windows `.cmd` wrapper 也直接使用 typed command,不得为中文标题切换到 API POST。最终预览前必须用固定仓库、带前缀标题、正文文件和已验证的平台类型执行本地 dry-run:
|
|
151
156
|
|
|
@@ -175,7 +180,7 @@ dry-run 不得产生新 Issue number/URL;否则视为能力异常并停止。
|
|
|
175
180
|
|
|
176
181
|
## 回读、恢复与完成
|
|
177
182
|
|
|
178
|
-
创建返回 Issue number/URL
|
|
183
|
+
创建返回 Issue number/URL 后,先用 `gitcode_issue.py inspect` 回读目标与类型;仅在该门禁通过后应用已确认元数据,随后再次运行带 `--expect-*` 的 `inspect` 完整验证:
|
|
179
184
|
|
|
180
185
|
- repository 和 Issue number/URL;
|
|
181
186
|
- state 为 open;
|
|
@@ -90,10 +90,10 @@ PR 里程碑严格继承已核验目标 Issue:Issue 有里程碑时,PR 必
|
|
|
90
90
|
|
|
91
91
|
已经关联同一 number 时跳过编辑;关联了不同里程碑时必须展示当前值和目标 Issue 值,并在 guided 模式重新确认后才可覆盖。目标 Issue 为 `none` 但已有 PR 带里程碑时,只有当前 schema/help 明确提供并验证了清空语义才可在确认后清空,否则返回 `blocked: milestone-conflict`;不得猜测 `--milestone 0`。创建或编辑结果不确定时先回读同一 PR,不重复创建或盲目重试。
|
|
92
92
|
|
|
93
|
-
随后逐字回读标题和完整正文,验证末尾恰好存在一次尾签,再验证 canonical、source、head、base、Issue 关联、PR 为 open,以及 PR 里程碑的 number/title 与目标 Issue 精确一致;目标 Issue 为 `none` 时验证 PR 也未关联里程碑。PR 回读验证固定使用 Skill 随附脚本,不现场拼接 python/PowerShell JSON
|
|
93
|
+
随后逐字回读标题和完整正文,验证末尾恰好存在一次尾签,再验证 canonical、source、head、base、Issue 关联、PR 为 open,以及 PR 里程碑的 number/title 与目标 Issue 精确一致;目标 Issue 为 `none` 时验证 PR 也未关联里程碑。PR 回读验证固定使用 Skill 随附脚本,不现场拼接 python/PowerShell JSON 解析;调用时用 `MSDEVFLOW_GITCODE_COMMAND` 传入本次 run 固定的 `<gitcode-command>`,与 run 内其他命令保持一致:
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
|
-
python <skill-scripts-dir>/gitcode_verify.py verify-pr <canonical> <pr-number> \
|
|
96
|
+
MSDEVFLOW_GITCODE_COMMAND=<gitcode-command> python <skill-scripts-dir>/gitcode_verify.py verify-pr <canonical> <pr-number> \
|
|
97
97
|
--title <exact-title> --body-file <body-file> --issue <issue-number> --milestone <issue-milestone-or-omit>
|
|
98
98
|
```
|
|
99
99
|
|
|
@@ -135,18 +135,18 @@ Suggested next action: ci(CI 协议未确定时)| feedback(已有意见)
|
|
|
135
135
|
4. 分类当前改动、连带失败、canonical 基线、基础设施、权限或证据不足;
|
|
136
136
|
5. guided 模式确认修复;`autonomous-ci` 只在已授权最小范围内自动修改、验证、新 commit、push 和重触发;
|
|
137
137
|
6. 新 push 后获取新 head,旧 head 结果失效;
|
|
138
|
-
7. 轮询和回读固定使用 Skill 随附脚本,不每轮现场生成 python
|
|
138
|
+
7. 轮询和回读固定使用 Skill 随附脚本,不每轮现场生成 python 内联解析;统一用 `MSDEVFLOW_GITCODE_COMMAND` 传入本次 run 固定的 `<gitcode-command>`:
|
|
139
139
|
|
|
140
140
|
```bash
|
|
141
141
|
# 触发评论发布后逐字回读(说明评论按 body-file,机器命令按 --contains)
|
|
142
|
-
python <skill-scripts-dir>/gitcode_verify.py verify-comment <canonical> <pr-number> --body-file <note-file>
|
|
143
|
-
python <skill-scripts-dir>/gitcode_verify.py verify-comment <canonical> <pr-number> --contains "compile"
|
|
142
|
+
MSDEVFLOW_GITCODE_COMMAND=<gitcode-command> python <skill-scripts-dir>/gitcode_verify.py verify-comment <canonical> <pr-number> --body-file <note-file>
|
|
143
|
+
MSDEVFLOW_GITCODE_COMMAND=<gitcode-command> python <skill-scripts-dir>/gitcode_verify.py verify-comment <canonical> <pr-number> --contains "compile"
|
|
144
144
|
|
|
145
145
|
# 监控 CI:输出 ci/pipeline 标签与机器人评论摘要(机器人登录名按 ci-and-review.md 识别结果固定)
|
|
146
|
-
python <skill-scripts-dir>/gitcode_verify.py poll-ci <canonical> <pr-number> --comment-author <bot-login>
|
|
146
|
+
MSDEVFLOW_GITCODE_COMMAND=<gitcode-command> python <skill-scripts-dir>/gitcode_verify.py poll-ci <canonical> <pr-number> --comment-author <bot-login>
|
|
147
147
|
|
|
148
148
|
# 持续监控直到 passed/failed 或超时:自适应间隔,running 时逐步拉长到 --max-interval
|
|
149
|
-
python <skill-scripts-dir>/gitcode_verify.py watch-ci <canonical> <pr-number> --timeout 60
|
|
149
|
+
MSDEVFLOW_GITCODE_COMMAND=<gitcode-command> python <skill-scripts-dir>/gitcode_verify.py watch-ci <canonical> <pr-number> --timeout 60
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
8. openLiBing detail/log 返回 401/403 时,按 [openlibing-ci.md](openlibing-ci.md) 使用 setup 创建的受管 Python 运行时和其中的 Playwright,并用已有的系统 Chrome/Edge 或 Playwright Chromium 打开可见浏览器;action 内不调用 pip,也不自动下载浏览器。受管运行时不可用时要求运行 `npx msdevflow@latest setup`;用户完成 GitCode 登录/授权且固定 run 验证成功后恢复当前 CI 诊断,浏览器不可用时输出 OAuth 链接并停止为 `blocked: browser-required`;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fixed read-only helpers for msd create-issue.
|
|
3
|
+
|
|
4
|
+
Batches the read-only probes that create-issue otherwise issues as dozens of
|
|
5
|
+
separate CLI calls (schema, full label/milestone pagination, Issue readback),
|
|
6
|
+
so the workflow can invoke one `python` script instead of many gitcode
|
|
7
|
+
commands. Read-only: never creates, edits or deletes anything.
|
|
8
|
+
|
|
9
|
+
Subcommands:
|
|
10
|
+
prepare schema flags + all label pages + all milestone pages + template tree
|
|
11
|
+
inspect read back one Issue via typed view and authoritative GET API
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
if os.name == "nt":
|
|
24
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
25
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
26
|
+
|
|
27
|
+
DEFAULT_PAGE_SIZE = 100
|
|
28
|
+
GITCODE_COMMAND = os.environ.get("MSDEVFLOW_GITCODE_COMMAND", "gitcode")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_command() -> str:
|
|
32
|
+
selected = shutil.which(GITCODE_COMMAND)
|
|
33
|
+
if not selected:
|
|
34
|
+
print(f"gitcode command not found: {GITCODE_COMMAND}", file=sys.stderr)
|
|
35
|
+
raise SystemExit(3)
|
|
36
|
+
return selected
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def invocation(arguments: list[str]) -> list[str]:
|
|
40
|
+
exe = resolve_command()
|
|
41
|
+
if os.name == "nt" and Path(exe).suffix.lower() in {".cmd", ".bat"}:
|
|
42
|
+
# .cmd wrappers cannot be exec'd directly by subprocess.
|
|
43
|
+
return [
|
|
44
|
+
"powershell.exe", "-NoProfile", "-Command",
|
|
45
|
+
"& { $exe = $args[0]; $rest = @($args | Select-Object -Skip 1); & $exe @rest; exit $LASTEXITCODE }",
|
|
46
|
+
exe, *arguments,
|
|
47
|
+
]
|
|
48
|
+
return [exe, *arguments]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run(args: list[str], allow_failure: bool = False) -> str:
|
|
52
|
+
result = subprocess.run(
|
|
53
|
+
invocation(args),
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
encoding="utf-8",
|
|
57
|
+
timeout=180,
|
|
58
|
+
)
|
|
59
|
+
if result.returncode != 0:
|
|
60
|
+
if allow_failure:
|
|
61
|
+
return ""
|
|
62
|
+
print(f"{GITCODE_COMMAND} {' '.join(args)} failed: exit {result.returncode}", file=sys.stderr)
|
|
63
|
+
print(result.stderr[-2000:], file=sys.stderr)
|
|
64
|
+
raise SystemExit(3)
|
|
65
|
+
return result.stdout
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run_json(args: list[str], allow_failure: bool = False):
|
|
69
|
+
text = run(args, allow_failure=allow_failure)
|
|
70
|
+
if not text.strip():
|
|
71
|
+
return None
|
|
72
|
+
return json.loads(text)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def json_list(value) -> list:
|
|
76
|
+
if isinstance(value, list):
|
|
77
|
+
return value
|
|
78
|
+
if isinstance(value, dict):
|
|
79
|
+
for key in ("items", "data", "labels", "milestones", "comments"):
|
|
80
|
+
if isinstance(value.get(key), list):
|
|
81
|
+
return value[key]
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def page_all(kind: str, repo: str, page_size: int, max_pages: int) -> list:
|
|
86
|
+
"""Read every page of a list command. Stops on empty page, short page
|
|
87
|
+
(< page_size), repeated page, or max_pages."""
|
|
88
|
+
seen_page_signatures: set[str] = set()
|
|
89
|
+
items: list = []
|
|
90
|
+
for page in range(1, max_pages + 1):
|
|
91
|
+
payload = run_json(
|
|
92
|
+
[
|
|
93
|
+
kind, "list", "-R", repo,
|
|
94
|
+
"--limit", str(page_size),
|
|
95
|
+
"--per-page", str(page_size),
|
|
96
|
+
"--page", str(page),
|
|
97
|
+
"--json",
|
|
98
|
+
]
|
|
99
|
+
)
|
|
100
|
+
batch = json_list(payload)
|
|
101
|
+
signature = json.dumps(sorted(str(item.get("id") or item.get("number")) for item in batch))
|
|
102
|
+
if not batch:
|
|
103
|
+
break
|
|
104
|
+
if signature in seen_page_signatures:
|
|
105
|
+
print(f"warning: {kind} list page {page} repeats page {page - 1}; stopping", file=sys.stderr)
|
|
106
|
+
break
|
|
107
|
+
seen_page_signatures.add(signature)
|
|
108
|
+
items.extend(batch)
|
|
109
|
+
if len(batch) < page_size:
|
|
110
|
+
break
|
|
111
|
+
return items
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cmd_prepare(args: argparse.Namespace) -> int:
|
|
115
|
+
report: dict = {"repository": args.repo}
|
|
116
|
+
|
|
117
|
+
# schema flags for the commands create-issue needs
|
|
118
|
+
schemas: dict[str, list[str]] = {}
|
|
119
|
+
for name in ("issue create", "issue view", "issue edit", "label list", "milestone list"):
|
|
120
|
+
payload = run_json(["schema", name], allow_failure=True)
|
|
121
|
+
flags = []
|
|
122
|
+
if isinstance(payload, dict) and isinstance(payload.get("flags"), list):
|
|
123
|
+
flags = [flag.get("name") for flag in payload["flags"] if isinstance(flag, dict)]
|
|
124
|
+
schemas[name] = flags
|
|
125
|
+
report["schemas"] = schemas
|
|
126
|
+
|
|
127
|
+
labels = page_all("label", args.repo, args.page_size, args.max_pages)
|
|
128
|
+
report["labels"] = [item.get("name") for item in labels]
|
|
129
|
+
report["label_count"] = len(labels)
|
|
130
|
+
|
|
131
|
+
milestones = page_all("milestone", args.repo, args.page_size, args.max_pages)
|
|
132
|
+
report["open_milestones"] = [
|
|
133
|
+
{
|
|
134
|
+
"number": item.get("number") or item.get("id"),
|
|
135
|
+
"title": item.get("title"),
|
|
136
|
+
"state": item.get("state"),
|
|
137
|
+
"due_on": item.get("due_on") or item.get("due_date") or "",
|
|
138
|
+
}
|
|
139
|
+
for item in milestones
|
|
140
|
+
if str(item.get("state", "")).lower() in ("active", "open", "")
|
|
141
|
+
]
|
|
142
|
+
report["milestone_count"] = len(milestones)
|
|
143
|
+
|
|
144
|
+
if args.template_ref:
|
|
145
|
+
result = subprocess.run(
|
|
146
|
+
["git", "ls-tree", "-r", "--name-only", args.template_ref, "--", ".gitcode"],
|
|
147
|
+
capture_output=True,
|
|
148
|
+
text=True,
|
|
149
|
+
encoding="utf-8",
|
|
150
|
+
timeout=120,
|
|
151
|
+
)
|
|
152
|
+
report["gitcode_tree"] = (
|
|
153
|
+
[line for line in result.stdout.splitlines() if line.strip()]
|
|
154
|
+
if result.returncode == 0
|
|
155
|
+
else None
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
report["gitcode_tree"] = None
|
|
159
|
+
|
|
160
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def cmd_inspect(args: argparse.Namespace) -> int:
|
|
165
|
+
view = run_json(["issue", "view", str(args.number), "-R", args.repo, "--json"]) or {}
|
|
166
|
+
api = run_json(["api", f"repos/{args.repo}/issues/{args.number}"]) or {}
|
|
167
|
+
|
|
168
|
+
labels = [l.get("name") if isinstance(l, dict) else l for l in (api.get("labels") or view.get("labels") or [])]
|
|
169
|
+
type_detail = api.get("issue_type_detail") or {}
|
|
170
|
+
platform_type = api.get("issue_type") or type_detail.get("title") or ""
|
|
171
|
+
milestone = api.get("milestone") or view.get("milestone") or {}
|
|
172
|
+
body = view.get("body") or api.get("body") or ""
|
|
173
|
+
title = view.get("title") or api.get("title") or ""
|
|
174
|
+
|
|
175
|
+
report = {
|
|
176
|
+
"number": view.get("number") or api.get("number"),
|
|
177
|
+
"state": view.get("state") or api.get("state"),
|
|
178
|
+
"title": title,
|
|
179
|
+
"platform_issue_type": platform_type,
|
|
180
|
+
"labels": labels,
|
|
181
|
+
"milestone_number": milestone.get("number"),
|
|
182
|
+
"milestone_title": milestone.get("title"),
|
|
183
|
+
"signature_count": body.count("——msdevflow"),
|
|
184
|
+
"body": body,
|
|
185
|
+
}
|
|
186
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
187
|
+
|
|
188
|
+
ok = True
|
|
189
|
+
|
|
190
|
+
def check(condition: bool, label: str) -> None:
|
|
191
|
+
nonlocal ok
|
|
192
|
+
ok = ok and condition
|
|
193
|
+
print(f"{'PASS' if condition else 'FAIL'} {label}")
|
|
194
|
+
|
|
195
|
+
check(bool(platform_type), f"platform type present ({platform_type or 'empty'})")
|
|
196
|
+
check(len(labels) == len(set(labels)), "labels unique")
|
|
197
|
+
check(report["signature_count"] == 1, f"signature exactly once ({report['signature_count']})")
|
|
198
|
+
if args.expect_type:
|
|
199
|
+
check(platform_type == args.expect_type, f"platform type == {args.expect_type}")
|
|
200
|
+
if args.expect_label:
|
|
201
|
+
for name in args.expect_label:
|
|
202
|
+
check(name in labels, f"label present: {name}")
|
|
203
|
+
if args.expect_milestone is not None:
|
|
204
|
+
check(milestone.get("number") == args.expect_milestone, f"milestone number == {args.expect_milestone}")
|
|
205
|
+
if args.expect_title:
|
|
206
|
+
check(title == args.expect_title, "title verbatim")
|
|
207
|
+
return 0 if ok else 3
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def main() -> int:
|
|
211
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
212
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
213
|
+
|
|
214
|
+
p = sub.add_parser("prepare", help="batch schema + labels + milestones + template tree")
|
|
215
|
+
p.add_argument("repo")
|
|
216
|
+
p.add_argument("--page-size", type=int, default=DEFAULT_PAGE_SIZE)
|
|
217
|
+
p.add_argument("--max-pages", type=int, default=50)
|
|
218
|
+
p.add_argument("--template-ref", help="git ref to list .gitcode template tree from, e.g. upstream/HEAD")
|
|
219
|
+
|
|
220
|
+
p = sub.add_parser("inspect", help="read back one Issue via view + GET API")
|
|
221
|
+
p.add_argument("repo")
|
|
222
|
+
p.add_argument("number", type=int)
|
|
223
|
+
p.add_argument("--expect-type", help="expected platform issue_type")
|
|
224
|
+
p.add_argument("--expect-label", action="append", default=[], help="expected label (repeatable)")
|
|
225
|
+
p.add_argument("--expect-milestone", type=int, help="expected milestone number")
|
|
226
|
+
p.add_argument("--expect-title", help="expected exact title")
|
|
227
|
+
|
|
228
|
+
args = parser.parse_args()
|
|
229
|
+
if args.command == "prepare":
|
|
230
|
+
return cmd_prepare(args)
|
|
231
|
+
return cmd_inspect(args)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
raise SystemExit(main())
|
|
@@ -11,6 +11,7 @@ import argparse
|
|
|
11
11
|
import json
|
|
12
12
|
import os
|
|
13
13
|
import re
|
|
14
|
+
import shutil
|
|
14
15
|
import subprocess
|
|
15
16
|
import sys
|
|
16
17
|
import tempfile
|
|
@@ -18,22 +19,38 @@ from pathlib import Path
|
|
|
18
19
|
|
|
19
20
|
SIGNATURE = "——msdevflow"
|
|
20
21
|
DEFAULT_LIMIT = 100
|
|
22
|
+
GITCODE_COMMAND = os.environ.get("MSDEVFLOW_GITCODE_COMMAND", "gitcode")
|
|
21
23
|
|
|
22
24
|
if os.name == "nt":
|
|
23
25
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
24
26
|
sys.stderr.reconfigure(encoding="utf-8")
|
|
25
27
|
|
|
26
28
|
|
|
29
|
+
def invocation(arguments: list[str]) -> list[str]:
|
|
30
|
+
selected = shutil.which(GITCODE_COMMAND)
|
|
31
|
+
if not selected:
|
|
32
|
+
print(f"gitcode command not found: {GITCODE_COMMAND}", file=sys.stderr)
|
|
33
|
+
raise SystemExit(3)
|
|
34
|
+
if os.name == "nt" and Path(selected).suffix.lower() in {".cmd", ".bat"}:
|
|
35
|
+
# .cmd wrappers cannot be exec'd directly by subprocess.
|
|
36
|
+
return [
|
|
37
|
+
"powershell.exe", "-NoProfile", "-Command",
|
|
38
|
+
"& { $exe = $args[0]; $rest = @($args | Select-Object -Skip 1); & $exe @rest; exit $LASTEXITCODE }",
|
|
39
|
+
selected, *arguments,
|
|
40
|
+
]
|
|
41
|
+
return [selected, *arguments]
|
|
42
|
+
|
|
43
|
+
|
|
27
44
|
def run_gitcode(args: list[str]) -> dict:
|
|
28
45
|
result = subprocess.run(
|
|
29
|
-
|
|
46
|
+
invocation(args),
|
|
30
47
|
capture_output=True,
|
|
31
48
|
text=True,
|
|
32
49
|
encoding="utf-8",
|
|
33
|
-
timeout=
|
|
50
|
+
timeout=180,
|
|
34
51
|
)
|
|
35
52
|
if result.returncode != 0:
|
|
36
|
-
print(f"
|
|
53
|
+
print(f"{GITCODE_COMMAND} {' '.join(args)} failed: exit {result.returncode}", file=sys.stderr)
|
|
37
54
|
print(result.stderr[-2000:], file=sys.stderr)
|
|
38
55
|
raise SystemExit(3)
|
|
39
56
|
return json.loads(result.stdout)
|