@fitlab-ai/agent-infra 0.4.3 → 0.4.4
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/lib/defaults.json +2 -1
- package/package.json +1 -1
- package/templates/.agents/rules/issue-sync.md +9 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +9 -0
- package/templates/.agents/rules/milestone-inference.md +102 -0
- package/templates/.agents/rules/milestone-inference.zh-CN.md +102 -0
- package/templates/.agents/scripts/validate-artifact.js +176 -12
- package/templates/.agents/skills/analyze-task/config/verify.json +3 -1
- package/templates/.agents/skills/cancel-task/SKILL.md +142 -0
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +142 -0
- package/templates/.agents/skills/cancel-task/config/verify.json +30 -0
- package/templates/.agents/skills/complete-task/SKILL.md +1 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/complete-task/config/verify.json +6 -1
- package/templates/.agents/skills/create-issue/SKILL.md +2 -2
- package/templates/.agents/skills/create-issue/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-issue/config/verify.json +3 -1
- package/templates/.agents/skills/create-issue/reference/label-and-type.md +3 -1
- package/templates/.agents/skills/create-issue/reference/label-and-type.zh-CN.md +3 -1
- package/templates/.agents/skills/create-pr/SKILL.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/config/verify.json +2 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.md +4 -12
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +4 -12
- package/templates/.agents/skills/implement-task/SKILL.md +12 -8
- package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +12 -8
- package/templates/.agents/skills/implement-task/config/verify.json +3 -1
- package/templates/.agents/skills/import-issue/SKILL.md +12 -2
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +12 -2
- package/templates/.agents/skills/plan-task/config/verify.json +3 -1
- package/templates/.agents/skills/refine-task/SKILL.md +4 -10
- package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +4 -10
- package/templates/.agents/skills/refine-task/config/verify.json +3 -1
- package/templates/.agents/skills/refine-task/reference/fix-workflow.md +7 -7
- package/templates/.agents/skills/refine-task/reference/fix-workflow.zh-CN.md +7 -7
- package/templates/.agents/skills/review-task/config/verify.json +3 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +3 -2
- package/templates/.agents/templates/task.md +3 -7
- package/templates/.agents/templates/task.zh-CN.md +3 -7
- package/templates/.claude/commands/cancel-task.md +9 -0
- package/templates/.claude/commands/cancel-task.zh-CN.md +9 -0
- package/templates/.gemini/commands/_project_/cancel-task.toml +8 -0
- package/templates/.gemini/commands/_project_/cancel-task.zh-CN.toml +8 -0
- package/templates/.github/workflows/status-label.yml +4 -1
- package/templates/.opencode/commands/cancel-task.md +11 -0
- package/templates/.opencode/commands/cancel-task.zh-CN.md +11 -0
package/README.md
CHANGED
|
@@ -380,7 +380,7 @@ The generated `.agents/.airc.json` file is the central contract between the boot
|
|
|
380
380
|
"project": "my-project",
|
|
381
381
|
"org": "my-org",
|
|
382
382
|
"language": "en",
|
|
383
|
-
"templateVersion": "v0.4.
|
|
383
|
+
"templateVersion": "v0.4.4",
|
|
384
384
|
"files": {
|
|
385
385
|
"managed": [
|
|
386
386
|
".agents/workspace/README.md",
|
package/README.zh-CN.md
CHANGED
package/lib/defaults.json
CHANGED
package/package.json
CHANGED
|
@@ -23,6 +23,15 @@ Use `while IFS= read -r label` so labels like `status: in-progress` are handled
|
|
|
23
23
|
|
|
24
24
|
If `gh` fails, skip and continue. Do not fail the skill.
|
|
25
25
|
|
|
26
|
+
## Assignee Sync
|
|
27
|
+
|
|
28
|
+
When a skill creates or imports an Issue, automatically add the current executor as assignee:
|
|
29
|
+
|
|
30
|
+
- `create-issue`: use `--assignee @me` in the `gh issue create` command
|
|
31
|
+
- `import-issue`: run `gh issue edit {issue-number} --add-assignee @me 2>/dev/null || true` after import
|
|
32
|
+
|
|
33
|
+
`@me` is resolved by `gh` CLI to the authenticated user. The operation is idempotent (adding an existing assignee is a no-op). If the command fails (e.g. insufficient permissions), skip and continue.
|
|
34
|
+
|
|
26
35
|
## `in:` Label Sync
|
|
27
36
|
|
|
28
37
|
Read the `labels.in` mapping from `.agents/.airc.json`.
|
|
@@ -23,6 +23,15 @@ fi
|
|
|
23
23
|
|
|
24
24
|
如果 `gh` 命令失败,跳过并继续,不中断技能执行。
|
|
25
25
|
|
|
26
|
+
## Assignee 同步
|
|
27
|
+
|
|
28
|
+
当技能创建或导入 Issue 时,自动将当前执行者添加为 assignee:
|
|
29
|
+
|
|
30
|
+
- `create-issue`:在 `gh issue create` 命令中使用 `--assignee @me` 参数
|
|
31
|
+
- `import-issue`:导入后执行 `gh issue edit {issue-number} --add-assignee @me 2>/dev/null || true`
|
|
32
|
+
|
|
33
|
+
`@me` 由 `gh` CLI 自动解析为当前认证用户。此操作是幂等的(重复添加不会报错)。如果命令失败(如权限不足),跳过并继续。
|
|
34
|
+
|
|
26
35
|
## `in:` label 同步
|
|
27
36
|
|
|
28
37
|
读取 `.agents/.airc.json` 的 `labels.in` 映射。
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Milestone Inference Rules
|
|
2
|
+
|
|
3
|
+
Read this file before `create-issue`, `implement-task`, or `create-pr` handles a milestone.
|
|
4
|
+
|
|
5
|
+
## General Principles
|
|
6
|
+
|
|
7
|
+
- Narrow the milestone over the skill lifecycle: release line -> concrete version -> reuse
|
|
8
|
+
- Every phase must fall back safely instead of blocking the skill
|
|
9
|
+
- If `gh` is unavailable, unauthenticated, or the GitHub API call fails, skip milestone handling and continue
|
|
10
|
+
- Only use milestones that actually exist in the repository; if a target milestone is unavailable, apply the fallback for that phase
|
|
11
|
+
|
|
12
|
+
## Branch Mode Detection
|
|
13
|
+
|
|
14
|
+
Use the following command to detect whether the repository has remote release-line branches:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
git branch -r | grep -v 'HEAD' | grep -E 'origin/[0-9]+\.[0-9]+\.x$'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- Any output: multi-version branch mode
|
|
21
|
+
- No output: trunk mode
|
|
22
|
+
|
|
23
|
+
## Phase 1: `create-issue`
|
|
24
|
+
|
|
25
|
+
Goal: choose a coarse-grained release line when the Issue is created.
|
|
26
|
+
|
|
27
|
+
Priority:
|
|
28
|
+
1. If task.md provides a valid explicit `milestone`, use it
|
|
29
|
+
2. Otherwise infer a release line:
|
|
30
|
+
- Trunk mode: query open `X.Y.x` milestones and choose the lowest release line
|
|
31
|
+
- Multi-version branch mode: try open `X.Y.x` milestones and choose the lowest release line; if that is not reliable, fall back to `General Backlog`
|
|
32
|
+
3. If the inferred release line does not exist, fall back to `General Backlog`
|
|
33
|
+
4. If `General Backlog` also does not exist, omit `--milestone`
|
|
34
|
+
|
|
35
|
+
Suggested release-line query:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
gh api "repos/{owner}/{repo}/milestones?state=open&per_page=100" \
|
|
39
|
+
--jq '.[].title'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Only match titles in `X.Y.x` format and choose the smallest major/minor pair numerically.
|
|
43
|
+
|
|
44
|
+
## Phase 2: `implement-task`
|
|
45
|
+
|
|
46
|
+
Goal: narrow the Issue milestone from a release line to a concrete version when implementation starts.
|
|
47
|
+
|
|
48
|
+
Preconditions:
|
|
49
|
+
- task.md contains a valid `issue_number`
|
|
50
|
+
- the current Issue milestone matches the release-line format `X.Y.x`
|
|
51
|
+
|
|
52
|
+
Sequence:
|
|
53
|
+
1. Query the current Issue milestone
|
|
54
|
+
2. If it is not in `X.Y.x` format, treat it as already specific enough and keep it unchanged
|
|
55
|
+
3. If it is in `X.Y.x` format, narrow it according to branch mode:
|
|
56
|
+
- Trunk mode: query open concrete-version milestones on that release line (for example `0.4.4`) and choose the latest one
|
|
57
|
+
- Multi-version branch mode:
|
|
58
|
+
- If the task branch was created from `origin/X.Y.x`, choose the latest concrete version on that line
|
|
59
|
+
- If the task branch was created from `main`, find the highest release line and choose the latest concrete version on that line
|
|
60
|
+
4. When a target concrete version is found, run:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
gh issue edit {issue-number} --milestone "{version}"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
5. If the target milestone does not exist or the branch ancestry cannot be determined reliably, keep the original milestone unchanged
|
|
67
|
+
|
|
68
|
+
Suggested concrete-version query:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
gh api "repos/{owner}/{repo}/milestones?state=open&per_page=100" \
|
|
72
|
+
--jq '.[].title'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- Release-line match: `^X\.Y\.x$`
|
|
76
|
+
- Concrete-version match: `^X\.Y\.[0-9]+$`
|
|
77
|
+
- "Latest" means the largest patch number
|
|
78
|
+
|
|
79
|
+
Suggested branch-origin checks:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
git merge-base --is-ancestor origin/{release-line} HEAD
|
|
83
|
+
git merge-base --is-ancestor origin/main HEAD
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If neither check is reliable or the remote refs are unavailable, keep the original milestone and avoid guessing.
|
|
87
|
+
|
|
88
|
+
## Phase 3: `create-pr`
|
|
89
|
+
|
|
90
|
+
Goal: reuse the linked Issue milestone on the PR instead of inferring a new one.
|
|
91
|
+
|
|
92
|
+
Sequence:
|
|
93
|
+
1. If `issue_number` exists, query the Issue milestone
|
|
94
|
+
2. If the Issue has a milestone, run:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
gh pr edit {pr-number} --milestone "{milestone}"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
3. If the Issue has no milestone, skip PR milestone assignment
|
|
101
|
+
|
|
102
|
+
Do not infer a PR milestone separately from task.md, branch names, tags, or `General Backlog`.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Milestone 推断规则
|
|
2
|
+
|
|
3
|
+
在 `create-issue`、`implement-task` 或 `create-pr` 处理 milestone 之前先读取本文件。
|
|
4
|
+
|
|
5
|
+
## 通用原则
|
|
6
|
+
|
|
7
|
+
- milestone 在技能生命周期中逐步收窄:版本线 -> 具体版本 -> 复用
|
|
8
|
+
- 任一步骤推断失败时都必须回退,不得阻塞技能执行
|
|
9
|
+
- 如果 `gh` CLI 不可用、未认证,或 GitHub API 请求失败,跳过 milestone 处理并继续
|
|
10
|
+
- 只使用仓库中实际存在的 milestone;目标 milestone 不存在时按各阶段 fallback 处理
|
|
11
|
+
|
|
12
|
+
## 分支模式检测
|
|
13
|
+
|
|
14
|
+
使用以下命令检测仓库是否存在远程 release line 分支:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
git branch -r | grep -v 'HEAD' | grep -E 'origin/[0-9]+\.[0-9]+\.x$'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- 有输出:多版本分支模式
|
|
21
|
+
- 无输出:主干模式
|
|
22
|
+
|
|
23
|
+
## 阶段 1:`create-issue`
|
|
24
|
+
|
|
25
|
+
目标:在创建 Issue 时先确定粗粒度版本线。
|
|
26
|
+
|
|
27
|
+
优先级:
|
|
28
|
+
1. `task.md` 显式存在 `milestone` 字段且值有效 -> 直接使用
|
|
29
|
+
2. 否则推断版本线:
|
|
30
|
+
- 主干模式:查询 open 的 `X.Y.x` 里程碑,取最低版本线
|
|
31
|
+
- 多版本分支模式:优先查询 open 的 `X.Y.x` 里程碑,取最低版本线;如果无法确定则回退到 `General Backlog`
|
|
32
|
+
3. 推断出的版本线不存在 -> 回退到 `General Backlog`
|
|
33
|
+
4. `General Backlog` 也不存在 -> 省略 `--milestone`
|
|
34
|
+
|
|
35
|
+
版本线查询建议:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
gh api "repos/{owner}/{repo}/milestones?state=open&per_page=100" \
|
|
39
|
+
--jq '.[].title'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
只匹配 `X.Y.x` 格式的标题;按 major、minor 数值升序取最小版本线。
|
|
43
|
+
|
|
44
|
+
## 阶段 2:`implement-task`
|
|
45
|
+
|
|
46
|
+
目标:开始开发时,把 Issue milestone 从版本线收窄到具体版本。
|
|
47
|
+
|
|
48
|
+
前置条件:
|
|
49
|
+
- `task.md` 存在有效 `issue_number`
|
|
50
|
+
- 当前 Issue milestone 为版本线格式 `X.Y.x`
|
|
51
|
+
|
|
52
|
+
执行顺序:
|
|
53
|
+
1. 查询 Issue 当前 milestone
|
|
54
|
+
2. 如果 milestone 不是 `X.Y.x` 格式 -> 视为已足够具体,保持不变
|
|
55
|
+
3. 如果 milestone 是 `X.Y.x` -> 按分支模式收窄:
|
|
56
|
+
- 主干模式:查询该版本线下 open 的具体版本 milestone(如 `0.4.4`),取最新版本
|
|
57
|
+
- 多版本分支模式:
|
|
58
|
+
- 当前任务分支来自 `origin/X.Y.x` release line -> 在该版本线下取最新具体版本
|
|
59
|
+
- 当前任务分支来自 `main` -> 找最高版本线,再取该版本线下的最新具体版本
|
|
60
|
+
4. 找到目标具体版本后,执行:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
gh issue edit {issue-number} --milestone "{version}"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
5. 如果目标 milestone 不存在或无法可靠判断 -> 保持原 milestone 不变
|
|
67
|
+
|
|
68
|
+
具体版本查询建议:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
gh api "repos/{owner}/{repo}/milestones?state=open&per_page=100" \
|
|
72
|
+
--jq '.[].title'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- 版本线匹配:`^X\.Y\.x$`
|
|
76
|
+
- 具体版本匹配:`^X\.Y\.[0-9]+$`
|
|
77
|
+
- “最新”按 patch 数值最大确定
|
|
78
|
+
|
|
79
|
+
分支来源判断建议:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
git merge-base --is-ancestor origin/{release-line} HEAD
|
|
83
|
+
git merge-base --is-ancestor origin/main HEAD
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
如果两个判断都不可靠或远程引用缺失,保持原 milestone,不做错误推断。
|
|
87
|
+
|
|
88
|
+
## 阶段 3:`create-pr`
|
|
89
|
+
|
|
90
|
+
目标:PR 直接复用关联 Issue 的 milestone,不再独立推断。
|
|
91
|
+
|
|
92
|
+
执行顺序:
|
|
93
|
+
1. 如果存在 `issue_number`,查询 Issue milestone
|
|
94
|
+
2. Issue 有 milestone -> 执行:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
gh pr edit {pr-number} --milestone "{milestone}"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
3. Issue 没有 milestone -> 跳过,不设置 PR milestone
|
|
101
|
+
|
|
102
|
+
不要再使用 `task.md`、分支名、tag 或 `General Backlog` 为 PR 单独推断 milestone。
|
|
@@ -154,6 +154,8 @@ function runCheck(type, context) {
|
|
|
154
154
|
return checkArtifact(context);
|
|
155
155
|
case "activity-log":
|
|
156
156
|
return checkActivityLog(context);
|
|
157
|
+
case "completion-checklist":
|
|
158
|
+
return checkCompletionChecklist(context);
|
|
157
159
|
case "github-sync":
|
|
158
160
|
return checkGithubSync(context);
|
|
159
161
|
default:
|
|
@@ -176,7 +178,7 @@ function checkTaskMeta({ taskDir, config }) {
|
|
|
176
178
|
return failResult("task-meta", `Missing required fields: ${missingFields.join(", ")}`);
|
|
177
179
|
}
|
|
178
180
|
|
|
179
|
-
const invalidDates = ["created_at", "updated_at", "completed_at", "blocked_at"]
|
|
181
|
+
const invalidDates = ["created_at", "updated_at", "completed_at", "blocked_at", "cancelled_at"]
|
|
180
182
|
.filter((field) => !isBlank(metadata[field]) && !DATE_TIME_PATTERN.test(metadata[field]));
|
|
181
183
|
if (invalidDates.length > 0) {
|
|
182
184
|
return failResult("task-meta", `Invalid date format in: ${invalidDates.join(", ")}`);
|
|
@@ -216,6 +218,10 @@ function checkTaskMeta({ taskDir, config }) {
|
|
|
216
218
|
return failResult("task-meta", "Expected blocked_at to be present");
|
|
217
219
|
}
|
|
218
220
|
|
|
221
|
+
if (config.require_cancelled_at && isBlank(metadata.cancelled_at)) {
|
|
222
|
+
return failResult("task-meta", "Expected cancelled_at to be present");
|
|
223
|
+
}
|
|
224
|
+
|
|
219
225
|
if (config.match_task_dir !== false) {
|
|
220
226
|
const expectedTaskId = path.basename(taskDir);
|
|
221
227
|
if (metadata.id !== expectedTaskId) {
|
|
@@ -338,6 +344,43 @@ function checkActivityLog({ taskDir, config }) {
|
|
|
338
344
|
return passResult("activity-log", `Latest entry '${latestAction}' at ${latestTimestamp}`);
|
|
339
345
|
}
|
|
340
346
|
|
|
347
|
+
function checkCompletionChecklist({ taskDir, config }) {
|
|
348
|
+
const task = loadTask(taskDir);
|
|
349
|
+
if (!task.ok) {
|
|
350
|
+
return failResult("completion-checklist", task.message);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const checklist = getSectionContent(task.content, ["完成检查清单", "Completion Checklist"]);
|
|
354
|
+
if (!checklist) {
|
|
355
|
+
return failResult("completion-checklist", "Completion Checklist section not found");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const items = checklist
|
|
359
|
+
.split(/\r?\n/)
|
|
360
|
+
.map((line) => line.trim())
|
|
361
|
+
.filter((line) => /^- \[(?: |x|X)\] .+$/.test(line));
|
|
362
|
+
|
|
363
|
+
if (items.length === 0) {
|
|
364
|
+
return failResult("completion-checklist", "Completion Checklist has no checkbox items");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (config.require_all_checked) {
|
|
368
|
+
const unchecked = items
|
|
369
|
+
.map((line) => line.match(/^- \[ \] (.+)$/))
|
|
370
|
+
.filter(Boolean)
|
|
371
|
+
.map((match) => match[1].trim());
|
|
372
|
+
|
|
373
|
+
if (unchecked.length > 0) {
|
|
374
|
+
return failResult(
|
|
375
|
+
"completion-checklist",
|
|
376
|
+
`Completion Checklist has unchecked items: ${unchecked.join(", ")}`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return passResult("completion-checklist", `Completion Checklist valid (${items.length} items checked)`);
|
|
382
|
+
}
|
|
383
|
+
|
|
341
384
|
function checkGithubSync({ taskDir, config, artifactFile }) {
|
|
342
385
|
const context = buildSyncContext({ taskDir, config, artifactFile });
|
|
343
386
|
if (context.earlyReturn) {
|
|
@@ -356,7 +399,9 @@ function checkGithubSync({ taskDir, config, artifactFile }) {
|
|
|
356
399
|
checkCommentContent,
|
|
357
400
|
checkTaskCommentContent,
|
|
358
401
|
checkInLabelsMatchPr,
|
|
359
|
-
checkSyncedRequirements
|
|
402
|
+
checkSyncedRequirements,
|
|
403
|
+
checkIssueType,
|
|
404
|
+
checkMilestone
|
|
360
405
|
];
|
|
361
406
|
|
|
362
407
|
for (const subCheck of subChecks) {
|
|
@@ -536,7 +581,7 @@ function fetchRemoteData(context) {
|
|
|
536
581
|
"view",
|
|
537
582
|
String(context.issueNumber),
|
|
538
583
|
"--json",
|
|
539
|
-
"state,labels,body"
|
|
584
|
+
"state,labels,body,milestone"
|
|
540
585
|
], context.taskDir));
|
|
541
586
|
if (!issueResult.ok) {
|
|
542
587
|
return {
|
|
@@ -599,14 +644,37 @@ function fetchRemoteData(context) {
|
|
|
599
644
|
prComments = flattenComments(prCommentsResult.value);
|
|
600
645
|
}
|
|
601
646
|
|
|
647
|
+
let issueType;
|
|
648
|
+
if (context.config.verify_issue_type) {
|
|
649
|
+
const issueTypeResult = withRetry(() => ghText([
|
|
650
|
+
"api",
|
|
651
|
+
`repos/${context.ownerRepo}/issues/${context.issueNumber}`,
|
|
652
|
+
"--jq",
|
|
653
|
+
".type.name // empty"
|
|
654
|
+
], context.taskDir));
|
|
655
|
+
|
|
656
|
+
if (issueTypeResult.ok) {
|
|
657
|
+
issueType = issueTypeResult.value || null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
602
661
|
let prLabels = null;
|
|
603
|
-
|
|
662
|
+
let prMilestone;
|
|
663
|
+
if ((context.config.verify_in_labels_match_pr || context.config.verify_milestone) && context.prNumber) {
|
|
664
|
+
const prFields = [];
|
|
665
|
+
if (context.config.verify_in_labels_match_pr) {
|
|
666
|
+
prFields.push("labels");
|
|
667
|
+
}
|
|
668
|
+
if (context.config.verify_milestone) {
|
|
669
|
+
prFields.push("milestone");
|
|
670
|
+
}
|
|
671
|
+
|
|
604
672
|
const prResult = withRetry(() => ghJson([
|
|
605
673
|
"pr",
|
|
606
674
|
"view",
|
|
607
675
|
String(context.prNumber),
|
|
608
676
|
"--json",
|
|
609
|
-
"
|
|
677
|
+
prFields.join(",")
|
|
610
678
|
], context.taskDir));
|
|
611
679
|
|
|
612
680
|
if (!prResult.ok) {
|
|
@@ -617,14 +685,21 @@ function fetchRemoteData(context) {
|
|
|
617
685
|
};
|
|
618
686
|
}
|
|
619
687
|
|
|
620
|
-
prLabels =
|
|
688
|
+
prLabels = context.config.verify_in_labels_match_pr
|
|
689
|
+
? extractLabelNames(prResult.value?.labels)
|
|
690
|
+
: null;
|
|
691
|
+
prMilestone = context.config.verify_milestone
|
|
692
|
+
? prResult.value?.milestone ?? null
|
|
693
|
+
: undefined;
|
|
621
694
|
}
|
|
622
695
|
|
|
623
696
|
return {
|
|
624
697
|
issue,
|
|
625
698
|
comments,
|
|
626
699
|
prComments,
|
|
627
|
-
prLabels
|
|
700
|
+
prLabels,
|
|
701
|
+
issueType,
|
|
702
|
+
prMilestone
|
|
628
703
|
};
|
|
629
704
|
}
|
|
630
705
|
|
|
@@ -810,6 +885,59 @@ function checkSyncedRequirements(context, remoteData) {
|
|
|
810
885
|
);
|
|
811
886
|
}
|
|
812
887
|
|
|
888
|
+
function checkIssueType(context, remoteData) {
|
|
889
|
+
if (!context.config.verify_issue_type) {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (remoteData.issueType === undefined) {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (!remoteData.issueType) {
|
|
898
|
+
return failResult(
|
|
899
|
+
"github-sync",
|
|
900
|
+
`Issue #${context.issueNumber} has no Issue Type set`,
|
|
901
|
+
"check_failed"
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const expectedType = mapTaskTypeToIssueType(context.task.metadata.type);
|
|
906
|
+
if (expectedType && remoteData.issueType !== expectedType) {
|
|
907
|
+
return failResult(
|
|
908
|
+
"github-sync",
|
|
909
|
+
`Issue #${context.issueNumber} has type '${remoteData.issueType}', expected '${expectedType}' (from task type '${context.task.metadata.type}')`,
|
|
910
|
+
"check_failed"
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function checkMilestone(context, remoteData) {
|
|
918
|
+
if (!context.config.verify_milestone) {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (!remoteData.issue?.milestone?.title) {
|
|
923
|
+
return failResult(
|
|
924
|
+
"github-sync",
|
|
925
|
+
`Issue #${context.issueNumber} has no milestone set`,
|
|
926
|
+
"check_failed"
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (context.prNumber && remoteData.prMilestone !== undefined && !remoteData.prMilestone?.title) {
|
|
931
|
+
return failResult(
|
|
932
|
+
"github-sync",
|
|
933
|
+
`PR #${context.prNumber} has no milestone set`,
|
|
934
|
+
"check_failed"
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
|
|
813
941
|
function findCommentByMarker(comments, marker) {
|
|
814
942
|
return (comments || []).find((comment) => typeof comment.body === "string" && comment.body.includes(marker)) || null;
|
|
815
943
|
}
|
|
@@ -945,6 +1073,24 @@ function extractLabelNames(labels) {
|
|
|
945
1073
|
.filter((label) => typeof label === "string" && label.length > 0);
|
|
946
1074
|
}
|
|
947
1075
|
|
|
1076
|
+
function mapTaskTypeToIssueType(taskType) {
|
|
1077
|
+
const mapping = {
|
|
1078
|
+
bug: "Bug",
|
|
1079
|
+
bugfix: "Bug",
|
|
1080
|
+
enhancement: "Feature",
|
|
1081
|
+
feature: "Feature",
|
|
1082
|
+
task: "Task",
|
|
1083
|
+
documentation: "Task",
|
|
1084
|
+
"dependency-upgrade": "Task",
|
|
1085
|
+
chore: "Task",
|
|
1086
|
+
docs: "Task",
|
|
1087
|
+
refactor: "Task",
|
|
1088
|
+
refactoring: "Task"
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
return mapping[taskType] || "Task";
|
|
1092
|
+
}
|
|
1093
|
+
|
|
948
1094
|
function arraysEqual(left, right) {
|
|
949
1095
|
if (left.length !== right.length) {
|
|
950
1096
|
return false;
|
|
@@ -992,6 +1138,28 @@ function resolveOwnerRepo(taskDir) {
|
|
|
992
1138
|
}
|
|
993
1139
|
|
|
994
1140
|
function ghJson(args, cwd) {
|
|
1141
|
+
const result = ghCommand(args, cwd);
|
|
1142
|
+
if (!result.ok) {
|
|
1143
|
+
return result;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
try {
|
|
1147
|
+
return { ok: true, value: JSON.parse(result.value || "null") };
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
return { ok: false, type: "network_error", message: `Invalid JSON from gh: ${error.message}` };
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function ghText(args, cwd) {
|
|
1154
|
+
const result = ghCommand(args, cwd);
|
|
1155
|
+
if (!result.ok) {
|
|
1156
|
+
return result;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
return { ok: true, value: String(result.value || "").trim() };
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function ghCommand(args, cwd) {
|
|
995
1163
|
const result = spawnSync("gh", args, {
|
|
996
1164
|
cwd,
|
|
997
1165
|
encoding: "utf8",
|
|
@@ -1004,11 +1172,7 @@ function ghJson(args, cwd) {
|
|
|
1004
1172
|
return { ok: false, type: classified.type, message: classified.message };
|
|
1005
1173
|
}
|
|
1006
1174
|
|
|
1007
|
-
|
|
1008
|
-
return { ok: true, value: JSON.parse(result.stdout || "null") };
|
|
1009
|
-
} catch (error) {
|
|
1010
|
-
return { ok: false, type: "network_error", message: `Invalid JSON from gh: ${error.message}` };
|
|
1011
|
-
}
|
|
1175
|
+
return { ok: true, value: result.stdout };
|
|
1012
1176
|
}
|
|
1013
1177
|
|
|
1014
1178
|
function ghPaginatedJson(args, cwd) {
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"expected_status_label": "status: pending-design-work",
|
|
36
36
|
"expected_comment_marker": "<!-- sync-issue:{task-id}:{artifact-stem} -->",
|
|
37
37
|
"verify_comment_content": true,
|
|
38
|
-
"verify_task_comment_content": true
|
|
38
|
+
"verify_task_comment_content": true,
|
|
39
|
+
"verify_issue_type": true,
|
|
40
|
+
"verify_milestone": true
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
}
|