msdevflow 0.8.4 → 0.8.6
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 +4 -2
- 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 +20 -1
- package/skill/msd/references/setup-and-issue.md +6 -0
- package/skill/msd/references/state-and-safety.md +13 -0
- package/skill/msd/scripts/gitcode_issue.py +235 -0
package/package.json
CHANGED
package/skill/msd/README.md
CHANGED
|
@@ -420,5 +420,7 @@ typed CLI command
|
|
|
420
420
|
- [references/ci-and-review.md](references/ci-and-review.md):CI 和作者 discussion 深层规则。
|
|
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
|
-
- [scripts/
|
|
424
|
-
- [scripts/
|
|
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。
|
|
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;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
只读确认:
|
|
10
10
|
|
|
11
|
-
- Issue 已核验,canonical 默认分支没有闭环实现或重复 PR
|
|
11
|
+
- Issue 已核验,canonical 默认分支没有闭环实现或重复 PR;用户明确说明本次是不关联 Issue 的独立文档/维护修复时,按下方“独立 PR”执行,不要求 Issue 核验;
|
|
12
12
|
- 当前分支不是默认分支,且基于已确认的 canonical 基线;
|
|
13
13
|
- 任务实现与已确认范围一致;
|
|
14
14
|
- 适用本地门禁有实际通过证据;
|
|
@@ -17,6 +17,16 @@
|
|
|
17
17
|
|
|
18
18
|
前置条件缺失时返回 `blocked` 和建议的上游 `action=issue` 或 `action=develop`,不得自动补跑。为防止过期证据,提交前复验受影响的最小本地门禁属于 `pr` action。
|
|
19
19
|
|
|
20
|
+
### 独立 PR(无 Issue)
|
|
21
|
+
|
|
22
|
+
用户明确说明本次是不关联 Issue 的独立修复(文档文件名/typo/注释、模板或 CI 配置微调等)时,跳过 Issue 接取和核验,不返回 `blocked: issue-required`。要求:
|
|
23
|
+
|
|
24
|
+
- change_type 记录为 `standalone-docs` 或 `standalone-maintenance`,issue/milestone 均为 `none`;
|
|
25
|
+
- 用户没有明确说明独立修复、改动又明显超出文档/维护范围时,不得自行归类,返回 `blocked` 并建议 `action=issue`;
|
|
26
|
+
- PR 正文关联 Issue 写 `N/A`,不虚构 Issue 编号;仓库模板要求 Issue 关联字段时按模板结构保留 `N/A`;
|
|
27
|
+
- PR milestone 必须为 `none`,不继承、不选择其他里程碑;
|
|
28
|
+
- 仍执行完整 commit 规范、本地门禁、push、PR 创建、回读验证和后续 CI 触发监控;禁止把独立 PR 当理由跳过任何门禁。
|
|
29
|
+
|
|
20
30
|
### Commit
|
|
21
31
|
|
|
22
32
|
只暂存本任务文件,检查凭证和大文件。遵循仓库 commit 规范;无规范时使用 Conventional Commits。贡献规范要求时 sign-off:
|
|
@@ -38,6 +48,15 @@ git push -u <source-remote> HEAD:<branch>
|
|
|
38
48
|
|
|
39
49
|
默认不 force push。
|
|
40
50
|
|
|
51
|
+
push 被服务器以 `Git LFS upload failed: missing objects`、`missing or corrupt local objects` 等 LFS 错误拒绝时:
|
|
52
|
+
|
|
53
|
+
1. 先确认本次待推送 diff 是否包含 LFS tracked 文件(`git lfs ls-files` 对照 `git diff --name-only <remote>/<default>..HEAD`);
|
|
54
|
+
2. 本次 diff 不含 LFS 文件、缺失对象属于历史数据时,在 guided 模式展示原因并取得用户确认后,使用一次性配置完成本次 push,不写入全局或仓库永久配置:
|
|
55
|
+
```bash
|
|
56
|
+
git -c lfs.allowincompletepush=true push -u <source-remote> HEAD:<branch>
|
|
57
|
+
```
|
|
58
|
+
3. 本次 diff 包含 LFS tracked 文件时,不得使用该 bypass;先执行 `git lfs fetch --all` 和 `git lfs fsck` 补齐对象,无法补齐时返回 `blocked` 并报告缺失对象清单。
|
|
59
|
+
|
|
41
60
|
### 普通 PR
|
|
42
61
|
|
|
43
62
|
使用仓库画像从 canonical repository 最新默认分支确认的 PR 模板,保留其标题、章节、检查项和说明,不从 Fork、工作分支或本地未提交文件重新取模板。正文按模板填写背景与 Issue、变更、验证、风险/回滚和未覆盖项;模板缺少必要信息时补入最合适的现有章节,不随意删除模板要求。若仓库没有模板,使用:
|
|
@@ -72,6 +72,12 @@ pr_number(如已有)
|
|
|
72
72
|
|
|
73
73
|
从 URL、Git remotes、remote HEAD 和 PR base 交叉验证。canonical 身份不等于写授权。
|
|
74
74
|
|
|
75
|
+
source repository 按以下优先级确定:
|
|
76
|
+
1. 用户本轮显式提供的 `source` 优先,不得被本地 remote 自动识别结果覆盖。
|
|
77
|
+
2. 本地 remote 精确匹配该 source 且指向同一仓库时直接复用。
|
|
78
|
+
3. 本地没有匹配 remote 时,允许创建 `fork` remote;若已有同名 remote 但指向不同仓库,生成唯一 remote 名或请求用户确认。
|
|
79
|
+
4. 后续 push 和 PR create 必须使用用户指定的 source,不得因后续 remote 探测结果而静默更换。
|
|
80
|
+
|
|
75
81
|
### 仓库画像
|
|
76
82
|
|
|
77
83
|
读取适用范围的 `AGENTS.md`、`CLAUDE.md`、`CONTRIBUTING*`、README、Issue/PR 模板、OWNERS/CODEOWNERS、CI、pre-commit 和测试配置。PR 模板必须来自 canonical repository 最新默认分支,不得从个人 Fork、当前工作分支或本地未提交文件读取。先检查贡献指南或仓库配置明确指定的路径,再依次检查:
|
|
@@ -97,6 +97,19 @@ Agent 创建或修改的 Issue/PR 正文,以及发布的 Issue 评论、PR 普
|
|
|
97
97
|
- `code-review` 完整检视通过时只发送正文恰好为 `/lgtm` 的评论,不发布检视摘要、前置说明或其他伴随评论;发送前后都回读并确认 `review_head_sha` 未变化,再逐字回读 `/lgtm`。
|
|
98
98
|
- 尾签只标识文本由 msdevflow 发布,不代表 reviewer/approver 身份,也不替代独立审批或仓库门禁。
|
|
99
99
|
|
|
100
|
+
### 机器人命令分级
|
|
101
|
+
|
|
102
|
+
所有必须精确匹配的机器人/机器协议命令按风险分级执行,行为不再依赖临场判断:
|
|
103
|
+
|
|
104
|
+
| 命令 | 风险级别 | 行为 |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `/check-cla`、`/check-sign` 等只读状态重检 | 低风险 | 用户明确要求该命令后,可直接发送精确 payload 并回读,无需前置说明评论 |
|
|
107
|
+
| `compile`、`retry`、`stop` 等 CI 触发命令 | 中风险 | 必须先发布带唯一尾签的独立说明评论并逐字回读,再单独发送精确 payload |
|
|
108
|
+
| `/lgtm` | reviewer 门禁 | 仅 `code-review` 完整检视通过且用户针对当前 PR/head 明确确认后发送精确 payload |
|
|
109
|
+
| `/merge`、`/approve` | 最终合入/审批入口 | 仅 `merge` 阶段、门禁满足且用户确认后发送;作者不得发送 `/approve` |
|
|
110
|
+
|
|
111
|
+
低风险命令的判定标准:命令只触发只读重检或状态刷新,不启动构建、不改变 PR/Issue 状态、不产生审批语义。不确定分类时按中风险处理(前置说明评论 + 精确 payload),不猜测。仓库机器人协议与该分级冲突时以仓库文档为准并记录适配器证据。
|
|
112
|
+
|
|
100
113
|
## 授权范围
|
|
101
114
|
|
|
102
115
|
`autonomous` 授权至少包含 canonical、Issue、source、branch 和目标。授权只适用于该运行,不能扩展到其他 Issue、PR 或仓库。
|
|
@@ -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())
|