@fitlab-ai/agent-infra 0.8.3 → 0.8.5
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.zh-CN.md +10 -0
- package/bin/cli.ts +1 -1
- package/dist/bin/cli.js +1 -1
- package/dist/lib/cp.js +56 -15
- package/dist/lib/defaults.json +9 -0
- package/dist/lib/init.js +5 -0
- package/dist/lib/sandbox/capture.js +5 -3
- package/dist/lib/sandbox/clipboard/bridge.js +12 -9
- package/dist/lib/sandbox/clipboard/inbox.js +82 -0
- package/dist/lib/sandbox/commands/create.js +68 -26
- package/dist/lib/sandbox/engine.js +33 -3
- package/dist/lib/sandbox/engines/colima.js +2 -2
- package/dist/lib/sandbox/runtimes/base.dockerfile +7 -0
- package/dist/lib/sandbox/shell.js +9 -1
- package/dist/lib/sandbox/tools.js +2 -2
- package/dist/lib/server/daemon.js +9 -0
- package/dist/lib/server/process-control.js +66 -73
- package/dist/lib/server/process-state.js +168 -0
- package/dist/lib/update.js +2 -0
- package/lib/cp.ts +56 -15
- package/lib/defaults.json +9 -0
- package/lib/init.ts +9 -0
- package/lib/sandbox/capture.ts +5 -3
- package/lib/sandbox/clipboard/bridge.ts +14 -12
- package/lib/sandbox/clipboard/inbox.ts +83 -0
- package/lib/sandbox/commands/create.ts +80 -27
- package/lib/sandbox/engine.ts +37 -3
- package/lib/sandbox/engines/colima.ts +2 -2
- package/lib/sandbox/runtimes/base.dockerfile +7 -0
- package/lib/sandbox/shell.ts +11 -2
- package/lib/sandbox/tools.ts +5 -5
- package/lib/server/daemon.ts +9 -0
- package/lib/server/process-control.ts +70 -71
- package/lib/server/process-state.ts +183 -0
- package/lib/update.ts +6 -0
- package/package.json +2 -1
- package/runtime/platform-adapters/platform-sync.github.js +1501 -0
- package/templates/.agents/README.en.md +5 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/rules/create-issue.github.en.md +1 -1
- package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +5 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +5 -4
- package/templates/.agents/rules/testing-discipline.en.md +3 -41
- package/templates/.agents/rules/testing-discipline.zh-CN.md +3 -41
- package/templates/.agents/scripts/lib/agent-infra-package.js +149 -0
- package/templates/.agents/scripts/lib/post-review-commit.js +5 -0
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +25 -1459
- package/templates/.agents/scripts/review-diff-fingerprint.js +95 -24
- package/templates/.agents/scripts/validate-artifact.js +144 -38
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +9 -9
- package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +6 -4
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +35 -8
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +35 -8
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +27 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +27 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +5 -4
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +6 -5
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +170 -45
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/SKILL.en.md +8 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +6 -3
- package/templates/.agents/skills/review-code/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -2
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/test/SKILL.zh-CN.md +26 -8
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +203 -142
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +8 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +8 -1
|
@@ -1,1474 +1,40 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import process from "node:process";
|
|
4
|
-
import spawn from "cross-spawn";
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
priority: "Priority",
|
|
11
|
-
effort: "Effort",
|
|
12
|
-
start_date: "Start date",
|
|
13
|
-
target_date: "Target date"
|
|
14
|
-
};
|
|
15
|
-
const OPTION_LOCALIZATION = {
|
|
16
|
-
"紧急": "Urgent",
|
|
17
|
-
"高": "High",
|
|
18
|
-
"中": "Medium",
|
|
19
|
-
"低": "Low"
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
let activeShared = null;
|
|
23
|
-
let repoRoot = "";
|
|
24
|
-
|
|
25
|
-
export function getDefaults() {
|
|
26
|
-
return {
|
|
27
|
-
statusLabels: {
|
|
28
|
-
pendingDesignWork: "status: pending-design-work",
|
|
29
|
-
inProgress: "status: in-progress",
|
|
30
|
-
blocked: "status: blocked",
|
|
31
|
-
completed: "status: completed",
|
|
32
|
-
waitingForTriage: "status: waiting-for-triage"
|
|
33
|
-
},
|
|
34
|
-
markers: {
|
|
35
|
-
task: "<!-- sync-issue:{task-id}:task -->",
|
|
36
|
-
artifact: "<!-- sync-issue:{task-id}:{artifact-stem} -->",
|
|
37
|
-
artifactChunk: "<!-- sync-issue:{task-id}:{artifact-stem}:{part}/{total} -->",
|
|
38
|
-
summary: "<!-- sync-issue:{task-id}:summary -->",
|
|
39
|
-
cancel: "<!-- sync-issue:{task-id}:cancel -->",
|
|
40
|
-
prSummary: "<!-- sync-pr:{task-id}:summary -->"
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function getShared() {
|
|
46
|
-
if (!activeShared) {
|
|
47
|
-
throw new Error("platform-sync adapter shared utilities are unavailable");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return activeShared;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function loadTask(...args) {
|
|
54
|
-
return getShared().loadTask(...args);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function getCheckedRequirements(...args) {
|
|
58
|
-
return getShared().getCheckedRequirements(...args);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function normalizeContent(...args) {
|
|
62
|
-
return getShared().normalizeContent(...args);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function isBlank(...args) {
|
|
66
|
-
return getShared().isBlank(...args);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function escapeRegExp(...args) {
|
|
70
|
-
return getShared().escapeRegExp(...args);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function passResult(...args) {
|
|
74
|
-
return getShared().passResult(...args);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function failResult(...args) {
|
|
78
|
-
return getShared().failResult(...args);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function blockedResult(...args) {
|
|
82
|
-
return getShared().blockedResult(...args);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function safeStat(...args) {
|
|
86
|
-
return getShared().safeStat(...args);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function parseIssueNumber(...args) {
|
|
90
|
-
return getShared().parseIssueNumber(...args);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function parsePrNumber(...args) {
|
|
94
|
-
return getShared().parsePrNumber(...args);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function check({ taskDir, config, artifactFile }, shared) {
|
|
98
|
-
activeShared = shared;
|
|
99
|
-
repoRoot = shared.repoRoot;
|
|
100
|
-
const context = buildSyncContext({ taskDir, config, artifactFile });
|
|
101
|
-
if (context.earlyReturn) {
|
|
102
|
-
return context.earlyReturn;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const remoteData = fetchRemoteData(context);
|
|
106
|
-
if (remoteData.earlyReturn) {
|
|
107
|
-
return remoteData.earlyReturn;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const subChecks = [
|
|
111
|
-
checkStatusLabel,
|
|
112
|
-
checkCommentMarker,
|
|
113
|
-
checkPrCommentMarker,
|
|
114
|
-
checkPrCommentLastCommit,
|
|
115
|
-
checkPrCommentRequiredPatterns,
|
|
116
|
-
checkCommentContent,
|
|
117
|
-
checkTaskCommentContent,
|
|
118
|
-
checkInLabelsComputed,
|
|
119
|
-
checkPrTypeLabel,
|
|
120
|
-
checkInLabelsMatchPr,
|
|
121
|
-
checkPrAssignee,
|
|
122
|
-
checkSyncedRequirements,
|
|
123
|
-
checkIssueType,
|
|
124
|
-
checkIssueFields,
|
|
125
|
-
checkMilestone
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
for (const subCheck of subChecks) {
|
|
129
|
-
const result = subCheck(context, remoteData);
|
|
130
|
-
if (result) {
|
|
131
|
-
return result;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return passResult(CHECK_TYPE, `GitHub sync checks passed for Issue #${context.issueNumber}`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function buildSyncContext({ taskDir, config, artifactFile }) {
|
|
139
|
-
const task = loadTask(taskDir);
|
|
140
|
-
if (!task.ok) {
|
|
141
|
-
return { earlyReturn: failResult(CHECK_TYPE, task.message) };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const issueNumber = parseIssueNumber(task.metadata.issue_number);
|
|
145
|
-
const prNumber = parsePrNumber(task.metadata.pr_number);
|
|
146
|
-
if (config.when === "issue_number_exists" && !issueNumber) {
|
|
147
|
-
return { earlyReturn: passResult(CHECK_TYPE, "Skipped: task has no issue_number") };
|
|
148
|
-
}
|
|
149
|
-
if (config.when === "pr_number_exists" && !prNumber) {
|
|
150
|
-
return { earlyReturn: passResult(CHECK_TYPE, "Skipped: task has no pr_number") };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!issueNumber) {
|
|
154
|
-
return { earlyReturn: passResult(CHECK_TYPE, "Skipped: platform-sync not required for this task") };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const upstreamRepo = resolveUpstreamRepo(taskDir);
|
|
158
|
-
if (!upstreamRepo.ok) {
|
|
159
|
-
return { earlyReturn: blockedResult(CHECK_TYPE, upstreamRepo.message, "network_error") };
|
|
160
|
-
}
|
|
161
|
-
const permissions = detectPermissions(upstreamRepo.value, taskDir);
|
|
162
|
-
const repoOwnerType = detectRepoOwnerType(upstreamRepo.value, taskDir);
|
|
163
|
-
const expectedValues = resolveExpectedValues(config);
|
|
164
|
-
if (!expectedValues.ok) {
|
|
165
|
-
return { earlyReturn: failResult(CHECK_TYPE, expectedValues.message, "check_failed") };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const marker = expectedValues.commentMarker
|
|
169
|
-
? interpolate(expectedValues.commentMarker, taskDir, artifactFile)
|
|
170
|
-
: null;
|
|
171
|
-
const prMarker = expectedValues.prCommentMarker
|
|
172
|
-
? interpolate(expectedValues.prCommentMarker, taskDir, artifactFile)
|
|
173
|
-
: null;
|
|
174
|
-
const artifactPath = artifactFile ? path.join(taskDir, artifactFile) : null;
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
task,
|
|
178
|
-
taskDir,
|
|
179
|
-
config,
|
|
180
|
-
artifactFile,
|
|
181
|
-
artifactPath,
|
|
182
|
-
issueNumber,
|
|
183
|
-
prNumber,
|
|
184
|
-
upstreamRepo: upstreamRepo.value,
|
|
185
|
-
repoOwnerType,
|
|
186
|
-
hasTriage: permissions.hasTriage,
|
|
187
|
-
hasPush: permissions.hasPush,
|
|
188
|
-
expectedStatusLabel: expectedValues.statusLabel,
|
|
189
|
-
marker,
|
|
190
|
-
prMarker
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function resolveExpectedValues(config) {
|
|
195
|
-
const defaults = getDefaults();
|
|
196
|
-
const statusLabel = resolveDefaultValue({
|
|
197
|
-
collection: defaults.statusLabels,
|
|
198
|
-
key: config.expected_status_label_key,
|
|
199
|
-
value: config.expected_status_label,
|
|
200
|
-
configKey: "expected_status_label_key"
|
|
201
|
-
});
|
|
202
|
-
if (!statusLabel.ok) {
|
|
203
|
-
return statusLabel;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const commentMarker = resolveDefaultValue({
|
|
207
|
-
collection: defaults.markers,
|
|
208
|
-
key: config.expected_comment_marker_key,
|
|
209
|
-
value: config.expected_comment_marker,
|
|
210
|
-
configKey: "expected_comment_marker_key"
|
|
211
|
-
});
|
|
212
|
-
if (!commentMarker.ok) {
|
|
213
|
-
return commentMarker;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const prCommentMarker = resolveDefaultValue({
|
|
217
|
-
collection: defaults.markers,
|
|
218
|
-
key: config.expected_pr_comment_marker_key,
|
|
219
|
-
value: config.expected_pr_comment_marker,
|
|
220
|
-
configKey: "expected_pr_comment_marker_key"
|
|
221
|
-
});
|
|
222
|
-
if (!prCommentMarker.ok) {
|
|
223
|
-
return prCommentMarker;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
ok: true,
|
|
228
|
-
statusLabel: statusLabel.value,
|
|
229
|
-
commentMarker: commentMarker.value,
|
|
230
|
-
prCommentMarker: prCommentMarker.value
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function resolveDefaultValue({ collection, key, value, configKey }) {
|
|
235
|
-
if (!key) {
|
|
236
|
-
return { ok: true, value: value || null };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const resolvedValue = collection[key];
|
|
240
|
-
if (!resolvedValue) {
|
|
241
|
-
return { ok: false, message: `Unknown ${configKey}: ${key}` };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return { ok: true, value: resolvedValue };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function fetchRemoteData(context) {
|
|
248
|
-
let issueResult = withRetry(() => ghJson([
|
|
249
|
-
"issue",
|
|
250
|
-
"view",
|
|
251
|
-
String(context.issueNumber),
|
|
252
|
-
"-R",
|
|
253
|
-
context.upstreamRepo,
|
|
254
|
-
"--json",
|
|
255
|
-
"state,labels,body,milestone"
|
|
256
|
-
], context.taskDir));
|
|
257
|
-
if (!issueResult.ok && issueResult.type !== "check_failed") {
|
|
258
|
-
const fallbackIssueResult = withRetry(() => ghJson([
|
|
259
|
-
"api",
|
|
260
|
-
`repos/${context.upstreamRepo}/issues/${context.issueNumber}`
|
|
261
|
-
], context.taskDir));
|
|
262
|
-
if (fallbackIssueResult.ok) {
|
|
263
|
-
issueResult = {
|
|
264
|
-
ok: true,
|
|
265
|
-
value: normalizeIssuePayload(fallbackIssueResult.value)
|
|
266
|
-
};
|
|
267
|
-
} else {
|
|
268
|
-
issueResult = fallbackIssueResult;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
if (!issueResult.ok) {
|
|
272
|
-
return {
|
|
273
|
-
earlyReturn: issueResult.type === "check_failed"
|
|
274
|
-
? failResult(CHECK_TYPE, issueResult.message, issueResult.type)
|
|
275
|
-
: blockedResult(CHECK_TYPE, issueResult.message, issueResult.type)
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const issue = issueResult.value;
|
|
280
|
-
if (context.config.issue_must_exist !== false && !issue) {
|
|
281
|
-
return {
|
|
282
|
-
earlyReturn: failResult(CHECK_TYPE, `Issue #${context.issueNumber} not found`, "check_failed")
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
let comments = null;
|
|
287
|
-
if (shouldFetchComments(context.config)) {
|
|
288
|
-
const commentsResult = withRetry(() => ghPaginatedJson([
|
|
289
|
-
"api",
|
|
290
|
-
"--paginate",
|
|
291
|
-
"--slurp",
|
|
292
|
-
`repos/${context.upstreamRepo}/issues/${context.issueNumber}/comments?per_page=100`
|
|
293
|
-
], context.taskDir));
|
|
294
|
-
|
|
295
|
-
if (!commentsResult.ok) {
|
|
296
|
-
return {
|
|
297
|
-
earlyReturn: commentsResult.type === "check_failed"
|
|
298
|
-
? failResult(CHECK_TYPE, commentsResult.message, commentsResult.type)
|
|
299
|
-
: blockedResult(CHECK_TYPE, commentsResult.message, commentsResult.type)
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
comments = flattenComments(commentsResult.value);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
let prComments = null;
|
|
307
|
-
if (context.prMarker) {
|
|
308
|
-
if (!context.prNumber) {
|
|
309
|
-
return {
|
|
310
|
-
earlyReturn: failResult(CHECK_TYPE, "Expected a valid pr_number for PR comment verification", "check_failed")
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const prCommentsResult = withRetry(() => ghPaginatedJson([
|
|
315
|
-
"api",
|
|
316
|
-
"--paginate",
|
|
317
|
-
"--slurp",
|
|
318
|
-
`repos/${context.upstreamRepo}/issues/${context.prNumber}/comments?per_page=100`
|
|
319
|
-
], context.taskDir));
|
|
320
|
-
|
|
321
|
-
if (!prCommentsResult.ok) {
|
|
322
|
-
return {
|
|
323
|
-
earlyReturn: prCommentsResult.type === "check_failed"
|
|
324
|
-
? failResult(CHECK_TYPE, prCommentsResult.message, prCommentsResult.type)
|
|
325
|
-
: blockedResult(CHECK_TYPE, prCommentsResult.message, prCommentsResult.type)
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
prComments = flattenComments(prCommentsResult.value);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
let issueType;
|
|
333
|
-
if (context.config.verify_issue_type && context.hasPush) {
|
|
334
|
-
const issueTypeResult = withRetry(() => ghText([
|
|
335
|
-
"api",
|
|
336
|
-
`repos/${context.upstreamRepo}/issues/${context.issueNumber}`,
|
|
337
|
-
"--jq",
|
|
338
|
-
".type.name // empty"
|
|
339
|
-
], context.taskDir));
|
|
340
|
-
|
|
341
|
-
if (issueTypeResult.ok) {
|
|
342
|
-
issueType = issueTypeResult.value || null;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
let issueFields;
|
|
347
|
-
if (context.config.verify_issue_fields && context.hasPush) {
|
|
348
|
-
const [owner, name] = context.upstreamRepo.split("/");
|
|
349
|
-
const issueFieldsResult = withRetry(() => ghJson([
|
|
350
|
-
"api",
|
|
351
|
-
"graphql",
|
|
352
|
-
"-f",
|
|
353
|
-
`query=${ISSUE_FIELDS_QUERY}`,
|
|
354
|
-
"-F",
|
|
355
|
-
`owner=${owner}`,
|
|
356
|
-
"-F",
|
|
357
|
-
`name=${name}`,
|
|
358
|
-
"-F",
|
|
359
|
-
`number=${context.issueNumber}`
|
|
360
|
-
], context.taskDir));
|
|
361
|
-
|
|
362
|
-
if (issueFieldsResult.ok) {
|
|
363
|
-
issueFields = normalizeIssueFields(issueFieldsResult.value);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
let prLabels = null;
|
|
368
|
-
let prMilestone;
|
|
369
|
-
let prAssignees;
|
|
370
|
-
if (((context.config.verify_in_labels_match_pr && context.hasTriage)
|
|
371
|
-
|| (context.config.verify_pr_type_label && context.hasTriage)
|
|
372
|
-
|| (context.config.verify_milestone && context.hasTriage)
|
|
373
|
-
|| (context.config.verify_pr_assignee && context.hasPush)) && context.prNumber) {
|
|
374
|
-
const prFields = [];
|
|
375
|
-
if (context.config.verify_in_labels_match_pr || context.config.verify_pr_type_label) {
|
|
376
|
-
prFields.push("labels");
|
|
377
|
-
}
|
|
378
|
-
if (context.config.verify_milestone) {
|
|
379
|
-
prFields.push("milestone");
|
|
380
|
-
}
|
|
381
|
-
if (context.config.verify_pr_assignee) {
|
|
382
|
-
prFields.push("assignees");
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const prResult = withRetry(() => ghJson([
|
|
386
|
-
"pr",
|
|
387
|
-
"view",
|
|
388
|
-
String(context.prNumber),
|
|
389
|
-
"--json",
|
|
390
|
-
prFields.join(",")
|
|
391
|
-
], context.taskDir));
|
|
392
|
-
|
|
393
|
-
if (!prResult.ok) {
|
|
394
|
-
return {
|
|
395
|
-
earlyReturn: prResult.type === "check_failed"
|
|
396
|
-
? failResult(CHECK_TYPE, prResult.message, prResult.type)
|
|
397
|
-
: blockedResult(CHECK_TYPE, prResult.message, prResult.type)
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
prLabels = (context.config.verify_in_labels_match_pr || context.config.verify_pr_type_label)
|
|
402
|
-
? extractLabelNames(prResult.value?.labels)
|
|
403
|
-
: null;
|
|
404
|
-
prMilestone = context.config.verify_milestone
|
|
405
|
-
? prResult.value?.milestone ?? null
|
|
406
|
-
: undefined;
|
|
407
|
-
prAssignees = context.config.verify_pr_assignee
|
|
408
|
-
? (prResult.value?.assignees || []).map((a) => a.login).filter(Boolean)
|
|
409
|
-
: undefined;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
return {
|
|
413
|
-
issue,
|
|
414
|
-
comments,
|
|
415
|
-
prComments,
|
|
416
|
-
prLabels,
|
|
417
|
-
issueType,
|
|
418
|
-
issueFields,
|
|
419
|
-
prMilestone,
|
|
420
|
-
prAssignees
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function mapTaskTypeToLabel(taskType) {
|
|
425
|
-
const mapping = {
|
|
426
|
-
bug: "type: bug",
|
|
427
|
-
bugfix: "type: bug",
|
|
428
|
-
feature: "type: feature",
|
|
429
|
-
enhancement: "type: enhancement",
|
|
430
|
-
refactor: "type: enhancement",
|
|
431
|
-
refactoring: "type: enhancement",
|
|
432
|
-
documentation: "type: documentation",
|
|
433
|
-
"dependency-upgrade": "type: dependency-upgrade",
|
|
434
|
-
task: "type: task"
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
return mapping[taskType] || null;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function shouldFetchComments(config) {
|
|
441
|
-
return Boolean(
|
|
442
|
-
config.expected_comment_marker
|
|
443
|
-
|| config.expected_comment_marker_key
|
|
444
|
-
|| config.expected_pr_comment_marker
|
|
445
|
-
|| config.expected_pr_comment_marker_key
|
|
446
|
-
|| config.verify_pr_comment_last_commit_matches_head
|
|
447
|
-
|| config.verify_comment_content
|
|
448
|
-
|| config.verify_task_comment_content
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function flattenComments(value) {
|
|
453
|
-
if (!Array.isArray(value)) {
|
|
454
|
-
return [];
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return value.flatMap((page) => Array.isArray(page) ? page : []);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function checkStatusLabel(context, remoteData) {
|
|
461
|
-
if (!context.expectedStatusLabel || !context.hasTriage) {
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if (String(remoteData.issue.state || "").toUpperCase() !== "OPEN") {
|
|
466
|
-
return null;
|
|
467
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
formatAgentInfraPackageError,
|
|
5
|
+
resolveAgentInfraPackage
|
|
6
|
+
} from "../lib/agent-infra-package.js";
|
|
468
7
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
return failResult(CHECK_TYPE,
|
|
475
|
-
`Expected label '${context.expectedStatusLabel}' not found on Issue #${context.issueNumber}`,
|
|
476
|
-
"check_failed"
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function checkCommentMarker(context, remoteData) {
|
|
481
|
-
if (!context.marker) {
|
|
482
|
-
return null;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const comment = findCommentByMarker(remoteData.comments, context.marker);
|
|
486
|
-
if (comment) {
|
|
487
|
-
return null;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
return failResult(CHECK_TYPE,
|
|
491
|
-
`Expected comment marker '${context.marker}' not found on Issue #${context.issueNumber}`,
|
|
492
|
-
"check_failed"
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function checkPrCommentMarker(context, remoteData) {
|
|
497
|
-
if (!context.prMarker) {
|
|
498
|
-
return null;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
|
|
502
|
-
if (comment) {
|
|
503
|
-
return null;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
return failResult(CHECK_TYPE,
|
|
507
|
-
`Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
|
|
508
|
-
"check_failed"
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function checkPrCommentLastCommit(context, remoteData) {
|
|
513
|
-
if (!context.config.verify_pr_comment_last_commit_matches_head) {
|
|
514
|
-
return null;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (!context.prMarker) {
|
|
518
|
-
return failResult(CHECK_TYPE,
|
|
519
|
-
"verify_pr_comment_last_commit_matches_head requires expected_pr_comment_marker",
|
|
520
|
-
"check_failed"
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
|
|
525
|
-
if (!comment) {
|
|
526
|
-
return failResult(CHECK_TYPE,
|
|
527
|
-
`Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
|
|
528
|
-
"check_failed"
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const match = String(comment.body || "").match(/<!--\s*last-commit:\s*([0-9a-f]{7,40})\s*-->/i);
|
|
533
|
-
if (!match) {
|
|
534
|
-
return failResult(CHECK_TYPE,
|
|
535
|
-
`PR #${context.prNumber} summary comment is missing '<!-- last-commit: <sha> -->' metadata`,
|
|
536
|
-
"check_failed"
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const headResult = resolvePrHeadSha(context);
|
|
541
|
-
if (!headResult.ok) {
|
|
542
|
-
return headResult.type === "check_failed"
|
|
543
|
-
? failResult(CHECK_TYPE, headResult.message, headResult.type)
|
|
544
|
-
: blockedResult(CHECK_TYPE, headResult.message, headResult.type);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const expectedHead = String(headResult.value || "").trim();
|
|
548
|
-
const actualHead = match[1].trim();
|
|
549
|
-
if (expectedHead === actualHead) {
|
|
550
|
-
return null;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return failResult(CHECK_TYPE,
|
|
554
|
-
`PR #${context.prNumber} summary comment last-commit metadata mismatch: expected ${expectedHead}, got ${actualHead}`,
|
|
555
|
-
"check_failed"
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function checkPrCommentRequiredPatterns(context, remoteData) {
|
|
560
|
-
const patterns = context.config.expected_pr_comment_required_patterns || [];
|
|
561
|
-
if (!Array.isArray(patterns) || patterns.length === 0) {
|
|
562
|
-
return null;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (!context.prMarker) {
|
|
566
|
-
return failResult(CHECK_TYPE,
|
|
567
|
-
"expected_pr_comment_required_patterns requires expected_pr_comment_marker",
|
|
568
|
-
"check_failed"
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
|
|
573
|
-
if (!comment) {
|
|
574
|
-
return failResult(CHECK_TYPE,
|
|
575
|
-
`Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
|
|
576
|
-
"check_failed"
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const body = String(comment.body || "");
|
|
581
|
-
for (const pattern of patterns) {
|
|
582
|
-
const regex = new RegExp(pattern, "m");
|
|
583
|
-
if (!regex.test(body)) {
|
|
584
|
-
return failResult(CHECK_TYPE,
|
|
585
|
-
`PR #${context.prNumber} summary comment is missing required pattern: ${pattern}`,
|
|
586
|
-
"check_failed"
|
|
587
|
-
);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function checkCommentContent(context, remoteData) {
|
|
595
|
-
if (!context.config.verify_comment_content) {
|
|
596
|
-
return null;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
if (!context.marker) {
|
|
600
|
-
return failResult(CHECK_TYPE, "verify_comment_content requires expected_comment_marker", "check_failed");
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if (!context.artifactPath || !safeStat(context.artifactPath)) {
|
|
604
|
-
return failResult(CHECK_TYPE,
|
|
605
|
-
`Artifact not found for comment verification: ${context.artifactFile || "(missing artifactFile)"}`,
|
|
606
|
-
"check_failed"
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const comment = findCommentByMarker(remoteData.comments, context.marker);
|
|
611
|
-
const localContent = normalizeContent(fs.readFileSync(context.artifactPath, "utf8"));
|
|
612
|
-
const commentContent = normalizeContent(extractCommentBody(comment?.body || ""));
|
|
613
|
-
|
|
614
|
-
if (localContent === commentContent) {
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
return failResult(CHECK_TYPE,
|
|
619
|
-
buildCommentContentMismatchMessage(
|
|
620
|
-
path.basename(context.artifactPath, path.extname(context.artifactPath)),
|
|
621
|
-
context.issueNumber,
|
|
622
|
-
localContent,
|
|
623
|
-
commentContent
|
|
624
|
-
),
|
|
625
|
-
"check_failed"
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function checkTaskCommentContent(context, remoteData) {
|
|
630
|
-
if (!context.config.verify_task_comment_content) {
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
|
|
635
|
-
const comment = findCommentByMarker(remoteData.comments, taskMarker);
|
|
636
|
-
if (!comment) {
|
|
637
|
-
return failResult(CHECK_TYPE,
|
|
638
|
-
`Expected comment marker '${taskMarker}' not found on Issue #${context.issueNumber}`,
|
|
639
|
-
"check_failed"
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const expectedBody = normalizeContent(buildExpectedTaskBody(context.task.content));
|
|
644
|
-
const commentBody = normalizeContent(extractCommentBody(comment.body || ""));
|
|
645
|
-
|
|
646
|
-
if (expectedBody === commentBody) {
|
|
647
|
-
return null;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
return failResult(CHECK_TYPE,
|
|
651
|
-
buildCommentContentMismatchMessage("task", context.issueNumber, expectedBody, commentBody),
|
|
652
|
-
"check_failed"
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function checkPrTypeLabel(context, remoteData) {
|
|
657
|
-
if (!context.config.verify_pr_type_label || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
|
|
658
|
-
return null;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
const expectedLabel = mapTaskTypeToLabel(context.task.metadata.type);
|
|
662
|
-
if (!expectedLabel) {
|
|
663
|
-
return null;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (remoteData.prLabels.includes(expectedLabel)) {
|
|
667
|
-
return null;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
return failResult(CHECK_TYPE,
|
|
671
|
-
`Expected type label '${expectedLabel}' not found on PR #${context.prNumber}`,
|
|
672
|
-
"check_failed"
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function checkInLabelsMatchPr(context, remoteData) {
|
|
677
|
-
if (!context.config.verify_in_labels_match_pr || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
|
|
678
|
-
return null;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
const issueInLabels = extractLabelNames(remoteData.issue.labels)
|
|
682
|
-
.filter((label) => label.startsWith("in:"))
|
|
683
|
-
.sort();
|
|
684
|
-
const prInLabels = remoteData.prLabels
|
|
685
|
-
.filter((label) => label.startsWith("in:"))
|
|
686
|
-
.sort();
|
|
687
|
-
|
|
688
|
-
if (arraysEqual(issueInLabels, prInLabels)) {
|
|
689
|
-
return null;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
return failResult(CHECK_TYPE,
|
|
693
|
-
`in: labels mismatch — PR #${context.prNumber} has [${formatLabelList(prInLabels)}], Issue #${context.issueNumber} has [${formatLabelList(issueInLabels)}]`,
|
|
694
|
-
"check_failed"
|
|
695
|
-
);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function checkInLabelsComputed(context, remoteData) {
|
|
699
|
-
if (!context.config.verify_in_labels_computed || !context.hasTriage) {
|
|
700
|
-
return null;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const expectedInLabels = computeExpectedInLabels(context.taskDir);
|
|
704
|
-
if (!expectedInLabels.ok) {
|
|
705
|
-
return expectedInLabels.type === "check_failed"
|
|
706
|
-
? failResult(CHECK_TYPE, expectedInLabels.message, expectedInLabels.type)
|
|
707
|
-
: blockedResult(CHECK_TYPE, expectedInLabels.message, expectedInLabels.type);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
if (expectedInLabels.mode === "skipped") {
|
|
711
|
-
return null;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
const actualInLabels = extractLabelNames(remoteData.issue.labels)
|
|
715
|
-
.filter((label) => label.startsWith("in:"))
|
|
716
|
-
.sort();
|
|
717
|
-
|
|
718
|
-
if (arraysEqual(expectedInLabels.labels, actualInLabels)) {
|
|
719
|
-
return null;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
return failResult(
|
|
723
|
-
CHECK_TYPE,
|
|
724
|
-
`Issue #${context.issueNumber} in: labels do not match committed changes: expected [${formatLabelList(expectedInLabels.labels)}], got [${formatLabelList(actualInLabels)}]`,
|
|
725
|
-
"check_failed"
|
|
726
|
-
);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function checkSyncedRequirements(context, remoteData) {
|
|
730
|
-
if (!context.config.sync_checked_requirements || !context.hasTriage) {
|
|
731
|
-
return null;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const checkedRequirements = getCheckedRequirements(context.task.content);
|
|
735
|
-
if (checkedRequirements.length === 0) {
|
|
736
|
-
return null;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
const issueBody = remoteData.issue.body || "";
|
|
740
|
-
const missingRequirements = checkedRequirements.filter(
|
|
741
|
-
(item) => !new RegExp(`^- \\[x\\] ${escapeRegExp(item)}$`, "m").test(issueBody)
|
|
742
|
-
);
|
|
743
|
-
if (missingRequirements.length === 0) {
|
|
744
|
-
return null;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
return failResult(CHECK_TYPE,
|
|
748
|
-
`Issue body is missing checked requirements: ${missingRequirements.join(", ")}`,
|
|
749
|
-
"check_failed"
|
|
750
|
-
);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function checkIssueType(context, remoteData) {
|
|
754
|
-
if (!context.config.verify_issue_type || !context.hasPush) {
|
|
755
|
-
return null;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
if (remoteData.issueType === undefined) {
|
|
759
|
-
return null;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
if (!remoteData.issueType) {
|
|
763
|
-
if (context.repoOwnerType === "User") {
|
|
764
|
-
return null;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
return failResult(CHECK_TYPE,
|
|
768
|
-
`Issue #${context.issueNumber} has no Issue Type set`,
|
|
769
|
-
"check_failed"
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
const expectedType = mapTaskTypeToIssueType(context.task.metadata.type);
|
|
774
|
-
if (expectedType && remoteData.issueType !== expectedType) {
|
|
775
|
-
return failResult(CHECK_TYPE,
|
|
776
|
-
`Issue #${context.issueNumber} has type '${remoteData.issueType}', expected '${expectedType}' (from task type '${context.task.metadata.type}')`,
|
|
777
|
-
"check_failed"
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
return null;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
function checkIssueFields(context, remoteData) {
|
|
785
|
-
if (!context.config.verify_issue_fields || !context.hasPush) {
|
|
786
|
-
return null;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
if (remoteData.issueFields === undefined) {
|
|
790
|
-
return null;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
for (const [metadataKey, fieldName] of Object.entries(FRONTMATTER_FIELD_MAP)) {
|
|
794
|
-
const expectedRaw = context.task.metadata[metadataKey];
|
|
795
|
-
if (isBlank(expectedRaw) || !remoteData.issueFields.pinnedNames.has(fieldName)) {
|
|
796
|
-
continue;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
const actual = remoteData.issueFields.values.get(fieldName);
|
|
800
|
-
const expected = normalizeExpectedIssueField(metadataKey, expectedRaw);
|
|
801
|
-
if (!expected) {
|
|
802
|
-
continue;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
if (!actual) {
|
|
806
|
-
return failResult(CHECK_TYPE,
|
|
807
|
-
`Issue #${context.issueNumber} field '${fieldName}' is missing, expected '${expected.value}'`,
|
|
808
|
-
"check_failed"
|
|
809
|
-
);
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
if (actual.kind !== expected.kind || actual.value !== expected.value) {
|
|
813
|
-
return failResult(CHECK_TYPE,
|
|
814
|
-
`Issue #${context.issueNumber} field '${fieldName}' is '${actual.value}', expected '${expected.value}'`,
|
|
815
|
-
"check_failed"
|
|
816
|
-
);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
return null;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function checkPrAssignee(context, remoteData) {
|
|
824
|
-
if (!context.config.verify_pr_assignee || !context.hasPush || !context.prNumber) {
|
|
825
|
-
return null;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
if (!remoteData.prAssignees || remoteData.prAssignees.length === 0) {
|
|
829
|
-
return failResult(CHECK_TYPE,
|
|
830
|
-
`PR #${context.prNumber} has no assignee`,
|
|
831
|
-
"check_failed"
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
return null;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
function checkMilestone(context, remoteData) {
|
|
839
|
-
if (!context.config.verify_milestone || !context.hasTriage) {
|
|
840
|
-
return null;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
if (!remoteData.issue?.milestone?.title) {
|
|
844
|
-
return failResult(CHECK_TYPE,
|
|
845
|
-
`Issue #${context.issueNumber} has no milestone set`,
|
|
846
|
-
"check_failed"
|
|
847
|
-
);
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
if (context.prNumber && remoteData.prMilestone !== undefined && !remoteData.prMilestone?.title) {
|
|
851
|
-
return failResult(CHECK_TYPE,
|
|
852
|
-
`PR #${context.prNumber} has no milestone set`,
|
|
853
|
-
"check_failed"
|
|
854
|
-
);
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
if (context.config.verify_milestone_specific) {
|
|
858
|
-
const issueTitle = remoteData.issue.milestone.title;
|
|
859
|
-
if (VERSION_LINE_REGEX.test(issueTitle)) {
|
|
860
|
-
return failResult(CHECK_TYPE,
|
|
861
|
-
`Issue #${context.issueNumber} milestone '${issueTitle}' is a release line; narrow to a specific version (e.g. ${issueTitle.replace(/\.x$/, ".N")}) before continuing`,
|
|
862
|
-
"check_failed"
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
if (context.prNumber && remoteData.prMilestone?.title && VERSION_LINE_REGEX.test(remoteData.prMilestone.title)) {
|
|
866
|
-
return failResult(CHECK_TYPE,
|
|
867
|
-
`PR #${context.prNumber} milestone '${remoteData.prMilestone.title}' is a release line; narrow to a specific version before continuing`,
|
|
868
|
-
"check_failed"
|
|
869
|
-
);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
return null;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
function findCommentByMarker(comments, marker) {
|
|
877
|
-
return (comments || []).find((comment) => typeof comment.body === "string" && comment.body.includes(marker)) || null;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
function extractCommentBody(commentBody) {
|
|
881
|
-
const lines = String(commentBody || "").split(/\r?\n/);
|
|
882
|
-
|
|
883
|
-
let start = 0;
|
|
884
|
-
while (start < lines.length && (lines[start].trim() === "" || /^<!--.*-->$/.test(lines[start].trim()))) {
|
|
885
|
-
start += 1;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
if (start < lines.length && lines[start].startsWith("## ")) {
|
|
889
|
-
start += 1;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
while (start < lines.length && lines[start].trim() === "") {
|
|
893
|
-
start += 1;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
if (start < lines.length && /^> \*\*.+\*\* · .+$/.test(lines[start].trim())) {
|
|
897
|
-
start += 1;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
while (start < lines.length && lines[start].trim() === "") {
|
|
901
|
-
start += 1;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
let end = lines.length;
|
|
905
|
-
for (let index = lines.length - 1; index >= start; index -= 1) {
|
|
906
|
-
const trimmed = lines[index].trim();
|
|
907
|
-
if (trimmed === "") {
|
|
908
|
-
continue;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
if (/^\*.*\*$/.test(trimmed)) {
|
|
912
|
-
end = index;
|
|
913
|
-
if (end > start && lines[end - 1].trim() === "---") {
|
|
914
|
-
end -= 1;
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
break;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
return lines.slice(start, end).join("\n");
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
function buildExpectedTaskBody(taskContent) {
|
|
924
|
-
const frontmatterMatch = taskContent.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
925
|
-
if (!frontmatterMatch) {
|
|
926
|
-
return taskContent.trim();
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
const body = taskContent.slice(frontmatterMatch[0].length).trim();
|
|
930
|
-
return [
|
|
931
|
-
buildTaskFrontmatterSummary(),
|
|
932
|
-
"",
|
|
933
|
-
"```yaml",
|
|
934
|
-
frontmatterMatch[0].trim(),
|
|
935
|
-
"```",
|
|
936
|
-
"",
|
|
937
|
-
"</details>",
|
|
938
|
-
"",
|
|
939
|
-
body
|
|
940
|
-
].join("\n").trim();
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
function buildTaskFrontmatterSummary() {
|
|
944
|
-
const language = loadProjectLanguage();
|
|
945
|
-
if (language === "en" || language === "en-US") {
|
|
946
|
-
return "<details><summary>Metadata (frontmatter)</summary>";
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
return "<details><summary>元数据 (frontmatter)</summary>";
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function loadProjectLanguage() {
|
|
953
|
-
const override = process.env.VALIDATE_ARTIFACT_LANGUAGE;
|
|
954
|
-
if (!isBlank(override)) {
|
|
955
|
-
return String(override).trim();
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
const configPath = path.join(repoRoot, ".agents", ".airc.json");
|
|
959
|
-
if (!fs.existsSync(configPath)) {
|
|
960
|
-
return "";
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
try {
|
|
964
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
965
|
-
return String(config.language || "").trim();
|
|
966
|
-
} catch {
|
|
967
|
-
return "";
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
function buildCommentContentMismatchMessage(fileStem, issueNumber, localContent, commentContent) {
|
|
972
|
-
const diffIndex = firstDifferenceIndex(localContent, commentContent);
|
|
973
|
-
const position = indexToLineColumn(localContent, diffIndex);
|
|
974
|
-
|
|
975
|
-
return `Comment content mismatch for '${fileStem}' on Issue #${issueNumber}: local file has ${localContent.length} chars, comment body has ${commentContent.length} chars (first difference near char ${diffIndex + 1}, line ${position.line}, column ${position.column})`;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
function firstDifferenceIndex(left, right) {
|
|
979
|
-
const limit = Math.max(left.length, right.length);
|
|
980
|
-
for (let index = 0; index < limit; index += 1) {
|
|
981
|
-
if (left[index] !== right[index]) {
|
|
982
|
-
return index;
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
return limit;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
function indexToLineColumn(text, index) {
|
|
990
|
-
const prefix = text.slice(0, Math.min(index, text.length));
|
|
991
|
-
const lines = prefix.split("\n");
|
|
992
|
-
return {
|
|
993
|
-
line: lines.length,
|
|
994
|
-
column: (lines.at(-1) || "").length + 1
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
function extractLabelNames(labels) {
|
|
999
|
-
return (labels || [])
|
|
1000
|
-
.map((label) => typeof label === "string" ? label : label?.name)
|
|
1001
|
-
.filter((label) => typeof label === "string" && label.length > 0);
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
function mapTaskTypeToIssueType(taskType) {
|
|
1005
|
-
const mapping = {
|
|
1006
|
-
bug: "Bug",
|
|
1007
|
-
bugfix: "Bug",
|
|
1008
|
-
enhancement: "Feature",
|
|
1009
|
-
feature: "Feature",
|
|
1010
|
-
task: "Task",
|
|
1011
|
-
documentation: "Task",
|
|
1012
|
-
"dependency-upgrade": "Task",
|
|
1013
|
-
chore: "Task",
|
|
1014
|
-
docs: "Task",
|
|
1015
|
-
refactor: "Task",
|
|
1016
|
-
refactoring: "Task"
|
|
1017
|
-
};
|
|
1018
|
-
|
|
1019
|
-
return mapping[taskType] || "Task";
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
const ISSUE_FIELDS_QUERY = `query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){issue(number:$number){issueType{name pinnedFields{__typename ... on IssueFieldSingleSelect{id name} ... on IssueFieldDate{id name} ... on IssueFieldText{id name} ... on IssueFieldNumber{id name}}} issueFieldValues(first:50){nodes{__typename ... on IssueFieldSingleSelectValue{name optionId field{... on IssueFieldSingleSelect{name}}} ... on IssueFieldDateValue{value field{... on IssueFieldDate{name}}} ... on IssueFieldTextValue{value field{... on IssueFieldText{name}}} ... on IssueFieldNumberValue{value field{... on IssueFieldNumber{name}}}}}}}}`;
|
|
1023
|
-
|
|
1024
|
-
function normalizeIssueFields(payload) {
|
|
1025
|
-
const issue = payload?.data?.repository?.issue;
|
|
1026
|
-
const pinnedFields = Array.isArray(issue?.issueType?.pinnedFields)
|
|
1027
|
-
? issue.issueType.pinnedFields
|
|
1028
|
-
: [];
|
|
1029
|
-
const values = Array.isArray(issue?.issueFieldValues?.nodes)
|
|
1030
|
-
? issue.issueFieldValues.nodes
|
|
1031
|
-
: [];
|
|
1032
|
-
const pinnedNames = new Set(
|
|
1033
|
-
pinnedFields
|
|
1034
|
-
.map((field) => typeof field?.name === "string" ? field.name : "")
|
|
1035
|
-
.filter(Boolean)
|
|
1036
|
-
);
|
|
1037
|
-
const normalizedValues = new Map();
|
|
1038
|
-
|
|
1039
|
-
for (const value of values) {
|
|
1040
|
-
const fieldName = value?.field?.name;
|
|
1041
|
-
if (!fieldName) {
|
|
1042
|
-
continue;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
if (value.__typename === "IssueFieldSingleSelectValue") {
|
|
1046
|
-
normalizedValues.set(fieldName, {
|
|
1047
|
-
kind: "single-select",
|
|
1048
|
-
value: normalizeOptionName(value.name)
|
|
1049
|
-
});
|
|
1050
|
-
} else if (value.__typename === "IssueFieldDateValue") {
|
|
1051
|
-
normalizedValues.set(fieldName, {
|
|
1052
|
-
kind: "date",
|
|
1053
|
-
value: normalizeDateValue(value.value)
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
return { pinnedNames, values: normalizedValues };
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
function normalizeExpectedIssueField(metadataKey, rawValue) {
|
|
1062
|
-
const value = String(rawValue || "").trim();
|
|
1063
|
-
if (!value) {
|
|
1064
|
-
return null;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
if (metadataKey === "start_date" || metadataKey === "target_date") {
|
|
1068
|
-
return { kind: "date", value: normalizeDateValue(value) };
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
return { kind: "single-select", value: normalizeOptionName(value) };
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
function normalizeOptionName(value) {
|
|
1075
|
-
const normalized = String(value || "").trim();
|
|
1076
|
-
return OPTION_LOCALIZATION[normalized] || normalized;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
function normalizeDateValue(value) {
|
|
1080
|
-
const normalized = String(value || "").trim();
|
|
1081
|
-
const match = normalized.match(/^\d{4}-\d{2}-\d{2}/);
|
|
1082
|
-
return match ? match[0] : normalized;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function arraysEqual(left, right) {
|
|
1086
|
-
if (left.length !== right.length) {
|
|
1087
|
-
return false;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
return left.every((value, index) => value === right[index]);
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
function formatLabelList(labels) {
|
|
1094
|
-
return labels.length > 0 ? labels.join(", ") : "none";
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
function computeExpectedInLabels(taskDir) {
|
|
1098
|
-
const changedFilesResult = gitText(["diff", "main...HEAD", "--name-only"], taskDir);
|
|
1099
|
-
if (!changedFilesResult.ok) {
|
|
1100
|
-
return changedFilesResult;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
const changedFiles = String(changedFilesResult.value || "")
|
|
1104
|
-
.split(/\r?\n/)
|
|
1105
|
-
.map((value) => value.trim())
|
|
1106
|
-
.filter(Boolean);
|
|
1107
|
-
|
|
1108
|
-
const mapping = loadInLabelMapping();
|
|
1109
|
-
if (!mapping.ok) {
|
|
1110
|
-
return mapping;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
if (Object.keys(mapping.value).length > 0) {
|
|
1114
|
-
const labels = new Set();
|
|
1115
|
-
|
|
1116
|
-
for (const file of changedFiles) {
|
|
1117
|
-
for (const [label, prefixes] of Object.entries(mapping.value)) {
|
|
1118
|
-
if (prefixes.some((prefix) => file.startsWith(prefix))) {
|
|
1119
|
-
labels.add(`in: ${label}`);
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
return { ok: true, labels: Array.from(labels).sort(), mode: "mapped" };
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
const repoLabelsResult = withRetry(() => ghJson([
|
|
1128
|
-
"label",
|
|
1129
|
-
"list",
|
|
1130
|
-
"--limit",
|
|
1131
|
-
"200",
|
|
1132
|
-
"--json",
|
|
1133
|
-
"name"
|
|
1134
|
-
], taskDir));
|
|
1135
|
-
if (!repoLabelsResult.ok) {
|
|
1136
|
-
return repoLabelsResult;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
const repoInLabels = new Set(
|
|
1140
|
-
extractLabelNames(repoLabelsResult.value)
|
|
1141
|
-
.filter((label) => label.startsWith("in:"))
|
|
1142
|
-
);
|
|
1143
|
-
|
|
1144
|
-
if (repoInLabels.size === 0) {
|
|
1145
|
-
return { ok: true, labels: [], mode: "fallback" };
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
const labels = new Set();
|
|
1149
|
-
for (const file of changedFiles) {
|
|
1150
|
-
const topLevel = file.split("/")[0];
|
|
1151
|
-
if (!topLevel) {
|
|
1152
|
-
continue;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
const candidate = `in: ${topLevel}`;
|
|
1156
|
-
if (repoInLabels.has(candidate)) {
|
|
1157
|
-
labels.add(candidate);
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
return { ok: true, labels: Array.from(labels).sort(), mode: "fallback" };
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
function loadInLabelMapping() {
|
|
1165
|
-
const configPath = path.join(repoRoot, ".agents", ".airc.json");
|
|
1166
|
-
if (!fs.existsSync(configPath)) {
|
|
1167
|
-
return { ok: true, value: {} };
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
try {
|
|
1171
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
1172
|
-
const mapping = config?.labels?.in;
|
|
1173
|
-
if (!mapping || typeof mapping !== "object" || Array.isArray(mapping)) {
|
|
1174
|
-
return { ok: true, value: {} };
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
const normalized = {};
|
|
1178
|
-
for (const [label, prefixes] of Object.entries(mapping)) {
|
|
1179
|
-
if (!Array.isArray(prefixes)) {
|
|
1180
|
-
continue;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
const cleaned = prefixes
|
|
1184
|
-
.map((value) => String(value || "").trim())
|
|
1185
|
-
.filter(Boolean);
|
|
1186
|
-
if (cleaned.length > 0) {
|
|
1187
|
-
normalized[label] = cleaned;
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
return { ok: true, value: normalized };
|
|
1192
|
-
} catch (error) {
|
|
1193
|
-
return { ok: false, type: "check_failed", message: `Unable to parse .agents/.airc.json: ${error.message}` };
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
// === GitHub API ===
|
|
1198
|
-
|
|
1199
|
-
function normalizeIssuePayload(payload) {
|
|
1200
|
-
return {
|
|
1201
|
-
state: String(payload?.state || "").toUpperCase(),
|
|
1202
|
-
labels: Array.isArray(payload?.labels) ? payload.labels : [],
|
|
1203
|
-
body: typeof payload?.body === "string" ? payload.body : "",
|
|
1204
|
-
milestone: payload?.milestone ?? null
|
|
1205
|
-
};
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
function resolveUpstreamRepo(taskDir) {
|
|
1209
|
-
const ownerRepo = resolveOwnerRepo(taskDir);
|
|
1210
|
-
if (!ownerRepo.ok) {
|
|
1211
|
-
return ownerRepo;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
const repoResult = withRetry(() => ghJson([
|
|
1215
|
-
"api",
|
|
1216
|
-
`repos/${ownerRepo.value}`
|
|
1217
|
-
], taskDir));
|
|
1218
|
-
|
|
1219
|
-
if (!repoResult.ok) {
|
|
1220
|
-
return repoResult;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
const repo = repoResult.value && typeof repoResult.value === "object" ? repoResult.value : {};
|
|
1224
|
-
const upstreamRepo = repo.fork ? repo.parent?.full_name : repo.full_name;
|
|
1225
|
-
if (isBlank(upstreamRepo)) {
|
|
1226
|
-
return { ok: false, message: "Unable to resolve upstream repository" };
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
return { ok: true, value: upstreamRepo };
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
function resolveOwnerRepo(taskDir) {
|
|
1233
|
-
const gitResult = spawn.sync("git", ["remote", "get-url", "origin"], {
|
|
1234
|
-
cwd: taskDir,
|
|
1235
|
-
encoding: "utf8"
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
if (gitResult.status !== 0) {
|
|
1239
|
-
return { ok: false, message: `Unable to resolve git remote: ${gitResult.stderr.trim() || gitResult.stdout.trim()}` };
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
const remote = gitResult.stdout.trim();
|
|
1243
|
-
const sshMatch = remote.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
1244
|
-
if (!sshMatch) {
|
|
1245
|
-
return { ok: false, message: `Unable to parse owner/repo from remote '${remote}'` };
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
return { ok: true, value: sshMatch[1] };
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
function detectPermissions(upstreamRepo, taskDir) {
|
|
1252
|
-
const permissionsResult = withRetry(() => ghJson([
|
|
1253
|
-
"api",
|
|
1254
|
-
`repos/${upstreamRepo}`,
|
|
1255
|
-
"--jq",
|
|
1256
|
-
".permissions"
|
|
1257
|
-
], taskDir));
|
|
1258
|
-
|
|
1259
|
-
if (!permissionsResult.ok) {
|
|
1260
|
-
return { hasTriage: false, hasPush: false };
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
const permissions = permissionsResult.value && typeof permissionsResult.value === "object"
|
|
1264
|
-
? permissionsResult.value
|
|
1265
|
-
: {};
|
|
1266
|
-
|
|
1267
|
-
return {
|
|
1268
|
-
hasTriage: permissions.triage === true,
|
|
1269
|
-
hasPush: permissions.push === true
|
|
1270
|
-
};
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
function detectRepoOwnerType(upstreamRepo, taskDir) {
|
|
1274
|
-
const ownerTypeResult = withRetry(() => ghText([
|
|
1275
|
-
"api",
|
|
1276
|
-
`repos/${upstreamRepo}`,
|
|
1277
|
-
"--jq",
|
|
1278
|
-
".owner.type // empty"
|
|
1279
|
-
], taskDir));
|
|
1280
|
-
|
|
1281
|
-
if (!ownerTypeResult.ok) {
|
|
1282
|
-
return "unknown";
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
return ownerTypeResult.value || "unknown";
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
function ghJson(args, cwd) {
|
|
1289
|
-
const result = ghCommand(args, cwd);
|
|
1290
|
-
if (!result.ok) {
|
|
1291
|
-
return result;
|
|
1292
|
-
}
|
|
8
|
+
const CHECK_TYPE = "platform-sync";
|
|
9
|
+
const packageResolution = resolveAgentInfraPackage();
|
|
10
|
+
let runtime = null;
|
|
11
|
+
let loadError = null;
|
|
1293
12
|
|
|
13
|
+
if (packageResolution.runtimePath) {
|
|
1294
14
|
try {
|
|
1295
|
-
|
|
15
|
+
runtime = await import(pathToFileURL(packageResolution.runtimePath).href);
|
|
1296
16
|
} catch (error) {
|
|
1297
|
-
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
function ghText(args, cwd) {
|
|
1302
|
-
const result = ghCommand(args, cwd);
|
|
1303
|
-
if (!result.ok) {
|
|
1304
|
-
return result;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
return { ok: true, value: String(result.value || "").trim() };
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
function ghCommand(args, cwd) {
|
|
1311
|
-
const gh = resolveGhCommand();
|
|
1312
|
-
const result = spawn.sync(gh.command, [...gh.preArgs, ...args], {
|
|
1313
|
-
cwd,
|
|
1314
|
-
encoding: "utf8",
|
|
1315
|
-
env: process.env
|
|
1316
|
-
});
|
|
1317
|
-
|
|
1318
|
-
if (result.status !== 0) {
|
|
1319
|
-
const stderr = `${result.stderr || ""}${result.stdout || ""}`.trim();
|
|
1320
|
-
const classified = classifyGhFailure(stderr, args);
|
|
1321
|
-
return { ok: false, type: classified.type, message: classified.message };
|
|
17
|
+
loadError = error;
|
|
1322
18
|
}
|
|
1323
|
-
|
|
1324
|
-
return { ok: true, value: result.stdout };
|
|
1325
19
|
}
|
|
1326
20
|
|
|
1327
|
-
function
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
try {
|
|
1335
|
-
const preArgs = JSON.parse(rawPreArgs);
|
|
1336
|
-
if (Array.isArray(preArgs) && preArgs.every((arg) => typeof arg === "string")) {
|
|
1337
|
-
return { command, preArgs };
|
|
1338
|
-
}
|
|
1339
|
-
} catch {
|
|
1340
|
-
return { command, preArgs: [] };
|
|
21
|
+
function resolutionError() {
|
|
22
|
+
if (loadError) {
|
|
23
|
+
return [
|
|
24
|
+
formatAgentInfraPackageError(packageResolution),
|
|
25
|
+
`The package runtime could not be loaded: ${loadError instanceof Error ? loadError.message : String(loadError)}`
|
|
26
|
+
].join("\n");
|
|
1341
27
|
}
|
|
1342
|
-
|
|
1343
|
-
return { command, preArgs: [] };
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
function ghPaginatedJson(args, cwd) {
|
|
1347
|
-
return ghJson(args, cwd);
|
|
28
|
+
return formatAgentInfraPackageError(packageResolution);
|
|
1348
29
|
}
|
|
1349
30
|
|
|
1350
|
-
function
|
|
1351
|
-
|
|
1352
|
-
cwd,
|
|
1353
|
-
encoding: "utf8",
|
|
1354
|
-
env: process.env
|
|
1355
|
-
});
|
|
1356
|
-
|
|
1357
|
-
if (result.status !== 0) {
|
|
1358
|
-
const stderr = `${result.stderr || ""}${result.stdout || ""}`.trim();
|
|
1359
|
-
return {
|
|
1360
|
-
ok: false,
|
|
1361
|
-
type: "check_failed",
|
|
1362
|
-
message: stderr || `git ${args.join(" ")} failed`
|
|
1363
|
-
};
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
return { ok: true, value: String(result.stdout || "").trim() };
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
function resolvePrHeadSha(context) {
|
|
1370
|
-
const fallback = () => withRetry(() => gitText(["rev-parse", "HEAD"], context.taskDir));
|
|
1371
|
-
const branch = String(context.task?.metadata?.branch || "").trim();
|
|
1372
|
-
if (!branch) {
|
|
1373
|
-
return fallback();
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
const worktreeList = withRetry(() => gitText(["worktree", "list", "--porcelain"], context.taskDir));
|
|
1377
|
-
if (!worktreeList.ok) {
|
|
1378
|
-
return fallback();
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
const matchedWorktree = findWorktreeForBranch(worktreeList.value, branch);
|
|
1382
|
-
if (!matchedWorktree) {
|
|
1383
|
-
return fallback();
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
const headInWorktree = withRetry(() => gitText(["rev-parse", "HEAD"], matchedWorktree));
|
|
1387
|
-
if (!headInWorktree.ok) {
|
|
1388
|
-
return fallback();
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
return headInWorktree;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
function findWorktreeForBranch(porcelainOutput, branch) {
|
|
1395
|
-
let currentWorktree = "";
|
|
1396
|
-
for (const rawLine of String(porcelainOutput || "").split("\n")) {
|
|
1397
|
-
const line = rawLine.trimEnd();
|
|
1398
|
-
if (line.startsWith("worktree ")) {
|
|
1399
|
-
currentWorktree = line.slice("worktree ".length).trim();
|
|
1400
|
-
continue;
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
if (line.startsWith("branch refs/heads/")) {
|
|
1404
|
-
const usedBranch = line.slice("branch refs/heads/".length).trim();
|
|
1405
|
-
if (usedBranch === branch && currentWorktree) {
|
|
1406
|
-
return currentWorktree;
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
return null;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
function withRetry(operation) {
|
|
1415
|
-
const delays = getRetryDelays();
|
|
1416
|
-
let lastFailure = null;
|
|
1417
|
-
|
|
1418
|
-
for (let attempt = 0; attempt <= delays.length; attempt += 1) {
|
|
1419
|
-
const result = operation();
|
|
1420
|
-
if (result.ok) {
|
|
1421
|
-
return result;
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
lastFailure = result;
|
|
1425
|
-
if (result.type === "check_failed") {
|
|
1426
|
-
return result;
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
if (attempt < delays.length) {
|
|
1430
|
-
sleep(delays[attempt]);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
return lastFailure || { ok: false, type: "network_error", message: "Unknown GitHub sync failure" };
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
function classifyGhFailure(stderr, args) {
|
|
1438
|
-
const message = stderr || `gh ${args.join(" ")} failed`;
|
|
1439
|
-
|
|
1440
|
-
if (/not found|could not resolve to an issue|http 404/i.test(message)) {
|
|
1441
|
-
return { type: "check_failed", message };
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
return { type: "network_error", message };
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
function getRetryDelays() {
|
|
1448
|
-
const override = process.env.VALIDATE_ARTIFACT_RETRY_DELAYS_MS;
|
|
1449
|
-
if (!override) {
|
|
1450
|
-
return DEFAULT_RETRY_DELAYS_MS;
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
const parsed = override
|
|
1454
|
-
.split(",")
|
|
1455
|
-
.map((value) => Number(value.trim()))
|
|
1456
|
-
.filter((value) => Number.isFinite(value) && value >= 0);
|
|
1457
|
-
|
|
1458
|
-
return parsed.length > 0 ? parsed : DEFAULT_RETRY_DELAYS_MS;
|
|
31
|
+
export function getDefaults() {
|
|
32
|
+
return runtime?.getDefaults?.() || { statusLabels: {}, markers: {} };
|
|
1459
33
|
}
|
|
1460
34
|
|
|
1461
|
-
function
|
|
1462
|
-
if (
|
|
1463
|
-
return;
|
|
35
|
+
export function check(context, shared) {
|
|
36
|
+
if (!runtime?.check) {
|
|
37
|
+
return shared.blockedResult(CHECK_TYPE, resolutionError(), "dependency_error");
|
|
1464
38
|
}
|
|
1465
|
-
|
|
1466
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMs);
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
function interpolate(template, taskDir, artifactFile) {
|
|
1470
|
-
const artifactStem = artifactFile ? path.basename(artifactFile, path.extname(artifactFile)) : "";
|
|
1471
|
-
return template
|
|
1472
|
-
.replace(/\{task-id\}/g, path.basename(taskDir))
|
|
1473
|
-
.replace(/\{artifact-stem\}/g, artifactStem);
|
|
39
|
+
return runtime.check(context, shared);
|
|
1474
40
|
}
|