@fitlab-ai/agent-infra 0.11.0 → 0.11.1
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/dist/lib/internal/manual-validation.js +6 -2
- package/dist/lib/platform/issues.js +3 -4
- package/dist/lib/platform/verification-sync.js +142 -154
- package/dist/lib/sandbox/control/workflow-executor.js +7 -1
- package/dist/lib/task/artifact-lifecycle.js +15 -52
- package/dist/lib/task/artifact-operations.js +27 -1
- package/dist/lib/task/capabilities.js +0 -2
- package/dist/lib/task/events.js +60 -43
- package/dist/lib/task/gate-policy.js +94 -0
- package/dist/lib/task/local-artifact-finalization.js +7 -4
- package/dist/lib/task/orchestration.js +8 -6
- package/dist/lib/task/review-artifacts.js +12 -6
- package/dist/lib/task/review-finalization.js +3 -9
- package/dist/lib/task/verification-engine.js +28 -17
- package/dist/lib/task/verification.js +25 -9
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/manual-validation.ts +6 -2
- package/lib/platform/issues.ts +3 -4
- package/lib/platform/verification-sync.ts +159 -177
- package/lib/sandbox/control/workflow-executor.ts +11 -1
- package/lib/task/artifact-lifecycle.ts +12 -49
- package/lib/task/artifact-operations.ts +27 -1
- package/lib/task/capabilities.ts +0 -1
- package/lib/task/events.ts +49 -32
- package/lib/task/gate-policy.ts +133 -0
- package/lib/task/local-artifact-finalization.ts +6 -4
- package/lib/task/orchestration.ts +12 -7
- package/lib/task/review-artifacts.ts +8 -2
- package/lib/task/review-finalization.ts +3 -9
- package/lib/task/verification-engine.ts +24 -14
- package/lib/task/verification-types.ts +7 -0
- package/lib/task/verification.ts +25 -9
- package/package.json +1 -1
- package/templates/.agents/rules/issue-sync.en.md +2 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +5 -8
- package/templates/.agents/rules/local-artifact-repair.en.md +11 -3
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +11 -3
- package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/block-task/config/verify.json +1 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
- package/templates/.agents/skills/commit/config/verify.json +1 -5
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
- package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
- package/templates/.agents/skills/create-pr/config/verify.json +1 -10
- package/templates/.agents/skills/create-task/config/verify.json +1 -4
- package/templates/.agents/skills/import-issue/config/verify.json +1 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
|
@@ -23,6 +23,7 @@ function printFailure(format, error) {
|
|
|
23
23
|
process.exitCode = 1;
|
|
24
24
|
}
|
|
25
25
|
const MANUAL_VALIDATION_STATUS_SECTION = /^###\s+(?:⚠️\s+(?:需人工校验|Manual Validation Required)|✅\s+(?:人工验证已通过|无需人工校验|Manual Validation Passed|No Manual Validation Required)|⏳\s+(?:人工验证待收尾|Manual Validation Pending))\s*$[\s\S]*?(?=^#{1,3}\s|(?![\s\S]))/gmu;
|
|
26
|
+
const TRAILING_CANONICAL_REPORT_PLACEHOLDER = /\n*<!--\s*canonical-pr-change-report\s*-->\s*$/u;
|
|
26
27
|
function manualSummaryBody(body, phase, transactionId, receiptDigest = '', evidenceDigest = '', prHeadSha = '') {
|
|
27
28
|
const chinese = /###\s+(?:⚠️\s+需人工校验|✅\s+(?:人工验证已通过|无需人工校验)|⏳\s+人工验证待收尾)\s*$/mu.test(body);
|
|
28
29
|
const section = phase === 'pending'
|
|
@@ -33,11 +34,14 @@ function manualSummaryBody(body, phase, transactionId, receiptDigest = '', evide
|
|
|
33
34
|
? `### ✅ 人工验证已通过\n\n人工验证已通过;transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`
|
|
34
35
|
: `### ✅ Manual Validation Passed\n\nManual validation passed; transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`;
|
|
35
36
|
let inserted = false;
|
|
36
|
-
const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, () => {
|
|
37
|
+
const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, (matched) => {
|
|
37
38
|
if (inserted)
|
|
38
39
|
return '';
|
|
39
40
|
inserted = true;
|
|
40
|
-
|
|
41
|
+
const trailingPlaceholder = TRAILING_CANONICAL_REPORT_PLACEHOLDER.exec(matched)?.[0];
|
|
42
|
+
return trailingPlaceholder
|
|
43
|
+
? `${trailingPlaceholder.trim()}\n\n${section}\n\n`
|
|
44
|
+
: `${section}\n\n`;
|
|
41
45
|
});
|
|
42
46
|
return inserted ? `${updated.replace(/\s+$/u, '')}\n` : `${body.replace(/\s+$/u, '')}\n\n${section}\n`;
|
|
43
47
|
}
|
|
@@ -538,11 +538,10 @@ async function syncPlatformIssue(taskRef, options) {
|
|
|
538
538
|
if (failure)
|
|
539
539
|
return result('failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: { code: failure.reasonCode || 'ISSUE_SYNC_FAILED', message: `Operation ${failure.name} failed`, retryable: false } });
|
|
540
540
|
const planned = plan.operations.filter((operation) => operation.status === 'planned');
|
|
541
|
-
const skipped = plan.operations.some((operation) => operation.status === 'skipped');
|
|
542
541
|
if (options.dryRun)
|
|
543
|
-
return result(planned.length ? 'planned' :
|
|
542
|
+
return result(planned.length ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
|
|
544
543
|
if (planned.length === 0)
|
|
545
|
-
return result(
|
|
544
|
+
return result('no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
|
|
546
545
|
const payload = applyRestOperations(snapshot, planned);
|
|
547
546
|
const updated = base.provider.issues?.update
|
|
548
547
|
? await base.provider.issues.update({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity, currentLabels: snapshot.labels, patch: payload, mutation: { idempotencyKey: `issue:update:${base.resolved.taskId}` } })
|
|
@@ -577,7 +576,7 @@ async function syncPlatformIssue(taskRef, options) {
|
|
|
577
576
|
error: { code: 'IN_LABEL_SYNC_PARTIAL', message: 'Issue labels did not converge after update', retryable: true }
|
|
578
577
|
});
|
|
579
578
|
}
|
|
580
|
-
return result(
|
|
579
|
+
return result('applied', base.resolved.taskId, base.issueNumber, {
|
|
581
580
|
changed: updated.value.changed, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
582
581
|
resource: { kind: 'issue', number: base.issueNumber, identity: base.issueIdentity }, issue: finalIssue,
|
|
583
582
|
operations: plan.operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeVerificationRecord, platformAuditPolicy } from "../task/gate-policy.js";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import process from "node:process";
|
|
@@ -49,8 +50,21 @@ function sanitizeCommentContent(content) {
|
|
|
49
50
|
const result = sanitizeMarkdownDocument(content, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
|
|
50
51
|
return result.ok ? result.value : null;
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
function infoResult(message) {
|
|
54
|
+
return {
|
|
55
|
+
type: CHECK_TYPE,
|
|
56
|
+
status: 'pass',
|
|
57
|
+
message,
|
|
58
|
+
classification: 'info',
|
|
59
|
+
reason: 'NOT_APPLICABLE',
|
|
60
|
+
action: 'No action required'
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function hardResult(result) {
|
|
64
|
+
return { ...result, classification: 'hard' };
|
|
65
|
+
}
|
|
66
|
+
export async function check({ taskDir, config, artifactFile, skillName }, shared) {
|
|
67
|
+
const context = await buildSyncContext({ taskDir, config, artifactFile, skillName }, shared);
|
|
54
68
|
if (context.earlyReturn) {
|
|
55
69
|
return context.earlyReturn;
|
|
56
70
|
}
|
|
@@ -59,32 +73,35 @@ export async function check({ taskDir, config, artifactFile }, shared) {
|
|
|
59
73
|
return remoteData.earlyReturn;
|
|
60
74
|
}
|
|
61
75
|
const subChecks = [
|
|
62
|
-
checkClosedIssueStatusLabels,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
checkInLabelsComputed,
|
|
71
|
-
checkPrTypeLabel,
|
|
72
|
-
checkInLabelsMatchPr,
|
|
73
|
-
checkPrAssignee,
|
|
74
|
-
checkSyncedRequirements,
|
|
75
|
-
checkIssueType,
|
|
76
|
-
checkIssueFields,
|
|
77
|
-
checkMilestone
|
|
76
|
+
['closed-status-labels', checkClosedIssueStatusLabels], ['status-label', checkStatusLabel],
|
|
77
|
+
['comment-marker', checkCommentMarker], ['pr-comment-marker', checkPrCommentMarker],
|
|
78
|
+
['pr-comment-last-commit', checkPrCommentLastCommit], ['pr-comment-content', checkPrCommentRequiredPatterns],
|
|
79
|
+
['comment-content', checkCommentContent], ['task-comment-content', checkTaskCommentContent],
|
|
80
|
+
['in-labels-computed', checkInLabelsComputed], ['pr-type-label', checkPrTypeLabel],
|
|
81
|
+
['in-labels-match-pr', checkInLabelsMatchPr], ['pr-assignee', checkPrAssignee],
|
|
82
|
+
['requirements', checkSyncedRequirements], ['issue-type', checkIssueType],
|
|
83
|
+
['issue-fields', checkIssueFields], ['milestone', checkMilestone]
|
|
78
84
|
];
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return
|
|
85
|
+
const results = subChecks.map(([id, subCheck]) => {
|
|
86
|
+
const audit = platformAuditPolicy(id, context);
|
|
87
|
+
const result = subCheck({ ...context, audit }, remoteData, shared) ?? infoResult(`Platform audit '${id}' was not evaluated`);
|
|
88
|
+
return { ...result, checkId: `platform.${id}`, classification: result.classification ?? audit.classification };
|
|
89
|
+
});
|
|
90
|
+
const blocking = results.map(normalizeVerificationRecord).find((result) => result.effectiveStatus !== 'pass');
|
|
91
|
+
return {
|
|
92
|
+
...(blocking
|
|
93
|
+
? {
|
|
94
|
+
type: CHECK_TYPE,
|
|
95
|
+
status: blocking.effectiveStatus,
|
|
96
|
+
message: blocking.message,
|
|
97
|
+
...(blocking.fail_type ? { fail_type: blocking.fail_type } : {}),
|
|
98
|
+
classification: 'hard'
|
|
99
|
+
}
|
|
100
|
+
: shared.passResult(CHECK_TYPE, `Platform sync audits completed for Issue ${context.issueNumber || "identity"}`)),
|
|
101
|
+
subchecks: results
|
|
102
|
+
};
|
|
86
103
|
}
|
|
87
|
-
async function buildSyncContext({ taskDir, config, artifactFile }, shared) {
|
|
104
|
+
async function buildSyncContext({ taskDir, config, artifactFile, skillName }, shared) {
|
|
88
105
|
const task = shared.loadTask(taskDir);
|
|
89
106
|
if (!task.ok) {
|
|
90
107
|
return { earlyReturn: shared.failResult(CHECK_TYPE, task.message) };
|
|
@@ -120,7 +137,8 @@ async function buildSyncContext({ taskDir, config, artifactFile }, shared) {
|
|
|
120
137
|
}
|
|
121
138
|
return { earlyReturn: shared.passResult(CHECK_TYPE, `Skipped: ${platformContext.error?.message || "platform unavailable"}`) };
|
|
122
139
|
}
|
|
123
|
-
const
|
|
140
|
+
const effectiveSkillName = skillName || config.skillName || "code-task";
|
|
141
|
+
const expectedValues = resolveExpectedValues(effectiveSkillName, artifactFile);
|
|
124
142
|
if (!expectedValues.ok) {
|
|
125
143
|
return { earlyReturn: shared.failResult(CHECK_TYPE, expectedValues.message, "check_failed") };
|
|
126
144
|
}
|
|
@@ -135,6 +153,7 @@ async function buildSyncContext({ taskDir, config, artifactFile }, shared) {
|
|
|
135
153
|
task,
|
|
136
154
|
taskDir,
|
|
137
155
|
config,
|
|
156
|
+
skillName: effectiveSkillName,
|
|
138
157
|
artifactFile,
|
|
139
158
|
artifactPath,
|
|
140
159
|
issueIdentity,
|
|
@@ -152,52 +171,20 @@ async function buildSyncContext({ taskDir, config, artifactFile }, shared) {
|
|
|
152
171
|
loadedContext: loaded.ok ? loaded.value : null
|
|
153
172
|
};
|
|
154
173
|
}
|
|
155
|
-
function resolveExpectedValues(
|
|
174
|
+
function resolveExpectedValues(skillName, artifactFile) {
|
|
156
175
|
const defaults = getDefaults();
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
});
|
|
163
|
-
if (!statusLabel.ok) {
|
|
164
|
-
return statusLabel;
|
|
165
|
-
}
|
|
166
|
-
const commentMarker = resolveDefaultValue({
|
|
167
|
-
collection: defaults.markers,
|
|
168
|
-
key: config.expected_comment_marker_key,
|
|
169
|
-
value: config.expected_comment_marker,
|
|
170
|
-
configKey: "expected_comment_marker_key"
|
|
171
|
-
});
|
|
172
|
-
if (!commentMarker.ok) {
|
|
173
|
-
return commentMarker;
|
|
174
|
-
}
|
|
175
|
-
const prCommentMarker = resolveDefaultValue({
|
|
176
|
-
collection: defaults.markers,
|
|
177
|
-
key: config.expected_pr_comment_marker_key,
|
|
178
|
-
value: config.expected_pr_comment_marker,
|
|
179
|
-
configKey: "expected_pr_comment_marker_key"
|
|
180
|
-
});
|
|
181
|
-
if (!prCommentMarker.ok) {
|
|
182
|
-
return prCommentMarker;
|
|
183
|
-
}
|
|
176
|
+
const statusPolicy = platformAuditPolicy('status-label', { skillName, artifactFile });
|
|
177
|
+
const commentPolicy = platformAuditPolicy('comment-marker', { skillName, artifactFile });
|
|
178
|
+
const prCommentPolicy = ['pr-comment-marker', 'pr-comment-last-commit', 'pr-comment-content']
|
|
179
|
+
.map((id) => platformAuditPolicy(id, { skillName, artifactFile }))
|
|
180
|
+
.find((policy) => policy.enabled && policy.expectedPrCommentMarkerKey);
|
|
184
181
|
return {
|
|
185
182
|
ok: true,
|
|
186
|
-
statusLabel:
|
|
187
|
-
commentMarker:
|
|
188
|
-
prCommentMarker:
|
|
183
|
+
statusLabel: statusPolicy.enabled ? statusPolicy.expectedStatusLabel || null : null,
|
|
184
|
+
commentMarker: commentPolicy.enabled && commentPolicy.expectedCommentMarkerKey ? defaults.markers[commentPolicy.expectedCommentMarkerKey] : null,
|
|
185
|
+
prCommentMarker: prCommentPolicy?.expectedPrCommentMarkerKey ? defaults.markers[prCommentPolicy.expectedPrCommentMarkerKey] : null
|
|
189
186
|
};
|
|
190
187
|
}
|
|
191
|
-
function resolveDefaultValue({ collection, key, value, configKey }) {
|
|
192
|
-
if (!key) {
|
|
193
|
-
return { ok: true, value: value || null };
|
|
194
|
-
}
|
|
195
|
-
const resolvedValue = collection[key];
|
|
196
|
-
if (!resolvedValue) {
|
|
197
|
-
return { ok: false, message: `Unknown ${configKey}: ${key}` };
|
|
198
|
-
}
|
|
199
|
-
return { ok: true, value: resolvedValue };
|
|
200
|
-
}
|
|
201
188
|
async function fetchRemoteData(context, shared) {
|
|
202
189
|
const provider = context.provider;
|
|
203
190
|
const operationContext = providerOperationContext(context.loadedContext, context.taskDir);
|
|
@@ -207,8 +194,8 @@ async function fetchRemoteData(context, shared) {
|
|
|
207
194
|
taskId: context.task?.metadata?.id || context.task?.id || "",
|
|
208
195
|
...(context.issueIdentity ? { issue: context.issueIdentity } : {}),
|
|
209
196
|
...(context.prIdentity ? { changeRequest: context.prIdentity } : {}),
|
|
210
|
-
includeComments:
|
|
211
|
-
includeFields:
|
|
197
|
+
includeComments: shouldFetchIssueComments(context),
|
|
198
|
+
includeFields: true
|
|
212
199
|
})
|
|
213
200
|
: unsupportedProviderOperation(provider, "verification.fetchRemoteFacts");
|
|
214
201
|
if (!facts.ok) {
|
|
@@ -228,7 +215,7 @@ async function fetchRemoteData(context, shared) {
|
|
|
228
215
|
}
|
|
229
216
|
: null;
|
|
230
217
|
let issueFields;
|
|
231
|
-
if (
|
|
218
|
+
if (issueSnapshot?.issueType) {
|
|
232
219
|
const fieldKinds = new Map(issueSnapshot.issueType.fields.map((field) => [field.name, field.kind]));
|
|
233
220
|
issueFields = {
|
|
234
221
|
pinnedNames: new Set(issueSnapshot.issueType.fields.map((field) => field.name)),
|
|
@@ -236,7 +223,7 @@ async function fetchRemoteData(context, shared) {
|
|
|
236
223
|
};
|
|
237
224
|
}
|
|
238
225
|
let prComments = null;
|
|
239
|
-
if (context.prMarker && context.prIdentity &&
|
|
226
|
+
if (context.prMarker && context.prIdentity && shouldFetchPrComments(context) && provider?.comments?.list) {
|
|
240
227
|
const listed = await provider.comments.list({ context: operationContext, parent: context.prIdentity });
|
|
241
228
|
if (!listed.ok)
|
|
242
229
|
return {
|
|
@@ -249,7 +236,7 @@ async function fetchRemoteData(context, shared) {
|
|
|
249
236
|
const changeRequest = facts.value.changeRequest;
|
|
250
237
|
let inLabelMapping = {};
|
|
251
238
|
let repositoryLabels = [];
|
|
252
|
-
if (context.
|
|
239
|
+
if (platformAuditPolicy('in-labels-computed', context).enabled && context.hasTriage) {
|
|
253
240
|
const mapping = loadInLabelMapping(shared);
|
|
254
241
|
if (!mapping.ok)
|
|
255
242
|
return { earlyReturn: shared.failResult(CHECK_TYPE, mapping.message, "check_failed") };
|
|
@@ -290,116 +277,117 @@ async function fetchRemoteData(context, shared) {
|
|
|
290
277
|
function mapTaskTypeToLabel(taskType) {
|
|
291
278
|
return taskTypeLabel(taskType);
|
|
292
279
|
}
|
|
293
|
-
function
|
|
294
|
-
return
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
280
|
+
function hasEnabledAudit(context, auditIds) {
|
|
281
|
+
return auditIds
|
|
282
|
+
.some((id) => platformAuditPolicy(id, context).enabled);
|
|
283
|
+
}
|
|
284
|
+
function shouldFetchIssueComments(context) {
|
|
285
|
+
return hasEnabledAudit(context, ['comment-marker', 'comment-content', 'task-comment-content']);
|
|
286
|
+
}
|
|
287
|
+
function shouldFetchPrComments(context) {
|
|
288
|
+
return hasEnabledAudit(context, ['pr-comment-marker', 'pr-comment-last-commit', 'pr-comment-content']);
|
|
301
289
|
}
|
|
302
290
|
function checkStatusLabel(context, remoteData, shared) {
|
|
303
291
|
if (!context.expectedStatusLabel || !context.hasTriage) {
|
|
304
|
-
return
|
|
292
|
+
return infoResult('Status label audit is not applicable because no expected label or triage capability is available');
|
|
305
293
|
}
|
|
306
294
|
if (String(remoteData.issue.state || "").toUpperCase() !== "OPEN") {
|
|
307
|
-
return
|
|
295
|
+
return infoResult('Status label audit is not applicable because the Issue is closed');
|
|
308
296
|
}
|
|
309
297
|
const labels = extractLabelNames(remoteData.issue.labels);
|
|
310
298
|
if (labels.includes(context.expectedStatusLabel)) {
|
|
311
|
-
return
|
|
299
|
+
return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has expected status label '${context.expectedStatusLabel}'`);
|
|
312
300
|
}
|
|
313
301
|
return shared.failResult(CHECK_TYPE, `Expected label '${context.expectedStatusLabel}' not found on Issue #${context.issueNumber}`, "check_failed");
|
|
314
302
|
}
|
|
315
303
|
function checkClosedIssueStatusLabels(context, remoteData, shared) {
|
|
316
|
-
if (!context.
|
|
317
|
-
return
|
|
304
|
+
if (!context.audit.enabled) {
|
|
305
|
+
return infoResult('Closed Issue status-label audit is not enabled for this workflow');
|
|
318
306
|
}
|
|
319
307
|
if (String(remoteData.issue.state || "").toUpperCase() !== "CLOSED") {
|
|
320
|
-
return
|
|
308
|
+
return infoResult('Closed Issue status-label audit is not applicable because the Issue is open');
|
|
321
309
|
}
|
|
322
310
|
const statusLabels = extractLabelNames(remoteData.issue.labels)
|
|
323
311
|
.filter((label) => label.startsWith("status:"));
|
|
324
312
|
if (statusLabels.length === 0) {
|
|
325
|
-
return
|
|
313
|
+
return shared.passResult(CHECK_TYPE, `Closed Issue #${context.issueNumber} has no status labels`);
|
|
326
314
|
}
|
|
327
315
|
return shared.failResult(CHECK_TYPE, `Closed Issue #${context.issueNumber} retains status labels: ${statusLabels.join(", ")}`, "check_failed");
|
|
328
316
|
}
|
|
329
317
|
function checkCommentMarker(context, remoteData, shared) {
|
|
330
|
-
if (!context.marker) {
|
|
331
|
-
return
|
|
318
|
+
if (!context.audit.enabled || !context.marker) {
|
|
319
|
+
return infoResult('Issue comment-marker audit is not applicable because no marker is configured');
|
|
332
320
|
}
|
|
333
321
|
const comment = findCommentByMarker(remoteData.comments, context.marker);
|
|
334
322
|
if (comment) {
|
|
335
|
-
return
|
|
323
|
+
return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has expected comment marker`);
|
|
336
324
|
}
|
|
337
325
|
return shared.failResult(CHECK_TYPE, `Expected comment marker '${context.marker}' not found on Issue #${context.issueNumber}`, "check_failed");
|
|
338
326
|
}
|
|
339
327
|
function checkPrCommentMarker(context, remoteData, shared) {
|
|
340
|
-
if (!context.prMarker) {
|
|
341
|
-
return
|
|
328
|
+
if (!context.audit.enabled || !context.prMarker) {
|
|
329
|
+
return infoResult('PR comment-marker audit is not applicable because no marker is configured');
|
|
342
330
|
}
|
|
343
331
|
const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
|
|
344
332
|
if (comment) {
|
|
345
|
-
return
|
|
333
|
+
return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has expected comment marker`);
|
|
346
334
|
}
|
|
347
335
|
return shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed");
|
|
348
336
|
}
|
|
349
337
|
function checkPrCommentLastCommit(context, remoteData, shared) {
|
|
350
|
-
if (!context.
|
|
351
|
-
return
|
|
338
|
+
if (!context.audit.enabled) {
|
|
339
|
+
return infoResult('PR last-commit audit is not enabled for this workflow');
|
|
352
340
|
}
|
|
353
341
|
if (!context.prMarker) {
|
|
354
|
-
return shared.failResult(CHECK_TYPE, "verify_pr_comment_last_commit_matches_head requires expected_pr_comment_marker", "check_failed");
|
|
342
|
+
return hardResult(shared.failResult(CHECK_TYPE, "verify_pr_comment_last_commit_matches_head requires expected_pr_comment_marker", "check_failed"));
|
|
355
343
|
}
|
|
356
344
|
const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
|
|
357
345
|
if (!comment) {
|
|
358
|
-
return shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed");
|
|
346
|
+
return hardResult(shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed"));
|
|
359
347
|
}
|
|
360
348
|
const match = String(comment.body || "").match(/<!--\s*last-commit:\s*([0-9a-f]{7,40})\s*-->/i);
|
|
361
349
|
if (!match) {
|
|
362
|
-
return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment is missing '<!-- last-commit: <sha> -->' metadata`, "check_failed");
|
|
350
|
+
return hardResult(shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment is missing '<!-- last-commit: <sha> -->' metadata`, "check_failed"));
|
|
363
351
|
}
|
|
364
352
|
const expectedHead = String(remoteData.prHeadSha || "").trim();
|
|
365
353
|
if (!expectedHead)
|
|
366
|
-
return shared.blockedResult(CHECK_TYPE, "Unable to resolve the PR head SHA", "network_error");
|
|
354
|
+
return hardResult(shared.blockedResult(CHECK_TYPE, "Unable to resolve the PR head SHA", "network_error"));
|
|
367
355
|
const actualHead = match[1].trim();
|
|
368
356
|
if (expectedHead === actualHead) {
|
|
369
|
-
return
|
|
357
|
+
return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} summary comment last-commit matches HEAD`);
|
|
370
358
|
}
|
|
371
|
-
return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment last-commit metadata mismatch: expected ${expectedHead}, got ${actualHead}`, "check_failed");
|
|
359
|
+
return hardResult(shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment last-commit metadata mismatch: expected ${expectedHead}, got ${actualHead}`, "check_failed"));
|
|
372
360
|
}
|
|
373
361
|
function checkPrCommentRequiredPatterns(context, remoteData, shared) {
|
|
374
|
-
const patterns = context.config.expected_pr_comment_required_patterns || [];
|
|
362
|
+
const patterns = context.audit.enabled ? context.config.expected_pr_comment_required_patterns || [] : [];
|
|
375
363
|
if (!Array.isArray(patterns) || patterns.length === 0) {
|
|
376
|
-
return
|
|
364
|
+
return infoResult('PR comment-content audit is not enabled for this workflow');
|
|
377
365
|
}
|
|
378
366
|
if (!context.prMarker) {
|
|
379
|
-
return shared.failResult(CHECK_TYPE, "expected_pr_comment_required_patterns requires expected_pr_comment_marker", "check_failed");
|
|
367
|
+
return hardResult(shared.failResult(CHECK_TYPE, "expected_pr_comment_required_patterns requires expected_pr_comment_marker", "check_failed"));
|
|
380
368
|
}
|
|
381
369
|
const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
|
|
382
370
|
if (!comment) {
|
|
383
|
-
return shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed");
|
|
371
|
+
return hardResult(shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed"));
|
|
384
372
|
}
|
|
385
373
|
const body = String(comment.body || "");
|
|
386
374
|
for (const pattern of patterns) {
|
|
387
375
|
const regex = new RegExp(pattern, "m");
|
|
388
376
|
if (!regex.test(body)) {
|
|
389
|
-
return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment is missing required pattern: ${pattern}`, "check_failed");
|
|
377
|
+
return hardResult(shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment is missing required pattern: ${pattern}`, "check_failed"));
|
|
390
378
|
}
|
|
391
379
|
}
|
|
392
|
-
return
|
|
380
|
+
return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} summary comment has all required patterns`);
|
|
393
381
|
}
|
|
394
382
|
function checkCommentContent(context, remoteData, shared) {
|
|
395
|
-
if (!context.
|
|
396
|
-
return
|
|
383
|
+
if (!context.audit.enabled) {
|
|
384
|
+
return infoResult('Artifact comment-content audit is not enabled for this workflow');
|
|
397
385
|
}
|
|
398
386
|
if (!context.marker) {
|
|
399
|
-
return shared.failResult(CHECK_TYPE, "
|
|
387
|
+
return hardResult(shared.failResult(CHECK_TYPE, "Artifact comment-content audit requires an expected comment marker", "check_failed"));
|
|
400
388
|
}
|
|
401
389
|
if (!context.artifactPath || !shared.safeStat(context.artifactPath)) {
|
|
402
|
-
return shared.failResult(CHECK_TYPE, `Artifact not found for comment verification: ${context.artifactFile || "(missing artifactFile)"}`, "check_failed");
|
|
390
|
+
return hardResult(shared.failResult(CHECK_TYPE, `Artifact not found for comment verification: ${context.artifactFile || "(missing artifactFile)"}`, "check_failed"));
|
|
403
391
|
}
|
|
404
392
|
const comment = findCommentByMarker(remoteData.comments, context.marker);
|
|
405
393
|
let localContent;
|
|
@@ -408,25 +396,25 @@ function checkCommentContent(context, remoteData, shared) {
|
|
|
408
396
|
const localCanonical = canonicalizeCommentBody(fs.readFileSync(context.artifactPath, "utf8"));
|
|
409
397
|
const commentCanonical = canonicalizeCommentBody(extractCommentBody(comment?.body || ""));
|
|
410
398
|
if (!localCanonical.ok) {
|
|
411
|
-
return shared.failResult(CHECK_TYPE, `Comment content cannot be canonicalized for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${localCanonical.error.message}`, "check_failed");
|
|
399
|
+
return hardResult(shared.failResult(CHECK_TYPE, `Comment content cannot be canonicalized for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${localCanonical.error.message}`, "check_failed"));
|
|
412
400
|
}
|
|
413
401
|
if (!commentCanonical.ok) {
|
|
414
|
-
return shared.failResult(CHECK_TYPE, `Comment content cannot be canonicalized for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${commentCanonical.error.message}`, "check_failed");
|
|
402
|
+
return hardResult(shared.failResult(CHECK_TYPE, `Comment content cannot be canonicalized for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${commentCanonical.error.message}`, "check_failed"));
|
|
415
403
|
}
|
|
416
404
|
localContent = shared.normalizeContent(localCanonical.value);
|
|
417
405
|
commentContent = shared.normalizeContent(commentCanonical.value);
|
|
418
406
|
}
|
|
419
407
|
catch (error) {
|
|
420
|
-
return shared.failResult(CHECK_TYPE, `Comment content cannot be read for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${error instanceof Error ? error.message : String(error)}`, "check_failed");
|
|
408
|
+
return hardResult(shared.failResult(CHECK_TYPE, `Comment content cannot be read for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${error instanceof Error ? error.message : String(error)}`, "check_failed"));
|
|
421
409
|
}
|
|
422
410
|
if (localContent === commentContent) {
|
|
423
|
-
return
|
|
411
|
+
return shared.passResult(CHECK_TYPE, `Artifact comment content matches Issue #${context.issueNumber}`);
|
|
424
412
|
}
|
|
425
413
|
return shared.failResult(CHECK_TYPE, buildCommentContentMismatchMessage(path.basename(context.artifactPath, path.extname(context.artifactPath)), context.issueNumber, localContent, commentContent), "check_failed");
|
|
426
414
|
}
|
|
427
415
|
function checkTaskCommentContent(context, remoteData, shared) {
|
|
428
|
-
if (!context.
|
|
429
|
-
return
|
|
416
|
+
if (!context.audit.enabled) {
|
|
417
|
+
return infoResult('Task comment-content audit is not enabled for this workflow');
|
|
430
418
|
}
|
|
431
419
|
const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
|
|
432
420
|
const comment = findCommentByMarker(remoteData.comments, taskMarker);
|
|
@@ -438,34 +426,34 @@ function checkTaskCommentContent(context, remoteData, shared) {
|
|
|
438
426
|
renderedTask = renderTaskCommentResult(context.task.content, context.task.metadata.id, loadProjectLanguage(shared));
|
|
439
427
|
}
|
|
440
428
|
catch {
|
|
441
|
-
return shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed");
|
|
429
|
+
return hardResult(shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed"));
|
|
442
430
|
}
|
|
443
431
|
if (renderedTask.byteLength > 60_000) {
|
|
444
|
-
return shared.failResult(CHECK_TYPE, "Task comment exceeds the platform byte limit", "check_failed");
|
|
432
|
+
return hardResult(shared.failResult(CHECK_TYPE, "Task comment exceeds the platform byte limit", "check_failed"));
|
|
445
433
|
}
|
|
446
434
|
const expectedBody = shared.normalizeContent(extractCommentBody(renderedTask.body));
|
|
447
435
|
const commentBody = shared.normalizeContent(extractCommentBody(comment.body || ""));
|
|
448
436
|
if (expectedBody === commentBody) {
|
|
449
|
-
return
|
|
437
|
+
return shared.passResult(CHECK_TYPE, `Task comment content matches Issue #${context.issueNumber}`);
|
|
450
438
|
}
|
|
451
439
|
return shared.failResult(CHECK_TYPE, buildCommentContentMismatchMessage("task", context.issueNumber, expectedBody, commentBody), "check_failed");
|
|
452
440
|
}
|
|
453
441
|
function checkPrTypeLabel(context, remoteData, shared) {
|
|
454
|
-
if (!context.
|
|
455
|
-
return
|
|
442
|
+
if (!context.audit.enabled || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
|
|
443
|
+
return infoResult('PR type-label audit is not applicable because the required PR metadata or capability is unavailable');
|
|
456
444
|
}
|
|
457
445
|
const expectedLabel = mapTaskTypeToLabel(context.task.metadata.type);
|
|
458
446
|
if (!expectedLabel) {
|
|
459
|
-
return
|
|
447
|
+
return infoResult('PR type-label audit is not applicable because the task type has no label mapping');
|
|
460
448
|
}
|
|
461
449
|
if (remoteData.prLabels.includes(expectedLabel)) {
|
|
462
|
-
return
|
|
450
|
+
return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has expected type label '${expectedLabel}'`);
|
|
463
451
|
}
|
|
464
452
|
return shared.failResult(CHECK_TYPE, `Expected type label '${expectedLabel}' not found on PR #${context.prNumber}`, "check_failed");
|
|
465
453
|
}
|
|
466
454
|
function checkInLabelsMatchPr(context, remoteData, shared) {
|
|
467
|
-
if (!context.
|
|
468
|
-
return
|
|
455
|
+
if (!context.audit.enabled || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
|
|
456
|
+
return infoResult('PR in: label audit is not applicable because the required PR metadata or capability is unavailable');
|
|
469
457
|
}
|
|
470
458
|
const issueInLabels = extractLabelNames(remoteData.issue.labels)
|
|
471
459
|
.filter((label) => label.startsWith("in:"))
|
|
@@ -474,13 +462,13 @@ function checkInLabelsMatchPr(context, remoteData, shared) {
|
|
|
474
462
|
.filter((label) => label.startsWith("in:"))
|
|
475
463
|
.sort();
|
|
476
464
|
if (arraysEqual(issueInLabels, prInLabels)) {
|
|
477
|
-
return
|
|
465
|
+
return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} in: labels match Issue #${context.issueNumber}`);
|
|
478
466
|
}
|
|
479
467
|
return shared.failResult(CHECK_TYPE, `in: labels mismatch — PR #${context.prNumber} has [${formatLabelList(prInLabels)}], Issue #${context.issueNumber} has [${formatLabelList(issueInLabels)}]`, "check_failed");
|
|
480
468
|
}
|
|
481
469
|
function checkInLabelsComputed(context, remoteData, shared) {
|
|
482
|
-
if (!context.
|
|
483
|
-
return
|
|
470
|
+
if (!context.audit.enabled || !context.hasTriage) {
|
|
471
|
+
return infoResult('Computed in: label audit is not applicable because it is disabled or triage capability is unavailable');
|
|
484
472
|
}
|
|
485
473
|
const expectedInLabels = computeExpectedInLabels(context.taskDir, remoteData.repositoryLabels, remoteData.inLabelMapping, shared);
|
|
486
474
|
if (!expectedInLabels.ok) {
|
|
@@ -489,49 +477,49 @@ function checkInLabelsComputed(context, remoteData, shared) {
|
|
|
489
477
|
: shared.blockedResult(CHECK_TYPE, expectedInLabels.message, expectedInLabels.type);
|
|
490
478
|
}
|
|
491
479
|
if (expectedInLabels.mode === "skipped") {
|
|
492
|
-
return
|
|
480
|
+
return infoResult('Computed in: label audit is not applicable because no mapped changed path exists');
|
|
493
481
|
}
|
|
494
482
|
const actualInLabels = extractLabelNames(remoteData.issue.labels)
|
|
495
483
|
.filter((label) => label.startsWith("in:"))
|
|
496
484
|
.sort();
|
|
497
485
|
if (arraysEqual(expectedInLabels.labels, actualInLabels)) {
|
|
498
|
-
return
|
|
486
|
+
return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} in: labels match committed changes`);
|
|
499
487
|
}
|
|
500
488
|
return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} in: labels do not match committed changes: expected [${formatLabelList(expectedInLabels.labels)}], got [${formatLabelList(actualInLabels)}]`, "check_failed");
|
|
501
489
|
}
|
|
502
490
|
function checkSyncedRequirements(context, remoteData, shared) {
|
|
503
|
-
if (!context.
|
|
504
|
-
return
|
|
491
|
+
if (!context.audit.enabled || !context.hasTriage) {
|
|
492
|
+
return infoResult('Requirements audit is not applicable because it is disabled or triage capability is unavailable');
|
|
505
493
|
}
|
|
506
494
|
const checkedRequirements = shared.getCheckedRequirements(context.task.content);
|
|
507
495
|
if (checkedRequirements.length === 0) {
|
|
508
|
-
return
|
|
496
|
+
return infoResult('Requirements audit is not applicable because the task has no checked requirements');
|
|
509
497
|
}
|
|
510
498
|
const issueBody = remoteData.issue.body || "";
|
|
511
499
|
const resolution = resolveRequirementSection(issueBody, requirementSectionAnchors(shared.repoRoot, context.task.metadata.type || "task"));
|
|
512
500
|
if (resolution.status === "missing") {
|
|
513
|
-
return
|
|
501
|
+
return infoResult('Requirements audit is not applicable because the Issue has no requirements section');
|
|
514
502
|
}
|
|
515
503
|
if (resolution.status === "ambiguous") {
|
|
516
|
-
return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} requirements section is ambiguous`, "check_failed");
|
|
504
|
+
return hardResult(shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} requirements section is ambiguous`, "check_failed"));
|
|
517
505
|
}
|
|
518
506
|
const requirementBody = issueBody.slice(resolution.bodyStart, resolution.bodyEnd);
|
|
519
507
|
const missingRequirements = checkedRequirements.filter((item) => !hasCheckedRequirement(requirementBody, item));
|
|
520
508
|
if (missingRequirements.length === 0) {
|
|
521
|
-
return
|
|
509
|
+
return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} contains all checked requirements`);
|
|
522
510
|
}
|
|
523
511
|
return shared.failResult(CHECK_TYPE, `Issue body is missing checked requirements: ${missingRequirements.join(", ")}`, "check_failed");
|
|
524
512
|
}
|
|
525
513
|
function checkIssueType(context, remoteData, shared) {
|
|
526
|
-
if (!context.
|
|
527
|
-
return
|
|
514
|
+
if (!context.hasPush) {
|
|
515
|
+
return infoResult('Issue Type audit is not applicable because push capability is unavailable');
|
|
528
516
|
}
|
|
529
517
|
if (remoteData.issueType === undefined) {
|
|
530
|
-
return
|
|
518
|
+
return infoResult('Issue Type audit is not applicable because the provider does not expose Issue Type');
|
|
531
519
|
}
|
|
532
520
|
if (!remoteData.issueType) {
|
|
533
521
|
if (context.repoOwnerType === "User") {
|
|
534
|
-
return
|
|
522
|
+
return infoResult('Issue Type audit is not applicable for a user-owned repository');
|
|
535
523
|
}
|
|
536
524
|
return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has no Issue Type set`, "check_failed");
|
|
537
525
|
}
|
|
@@ -539,14 +527,14 @@ function checkIssueType(context, remoteData, shared) {
|
|
|
539
527
|
if (expectedType && remoteData.issueType !== expectedType) {
|
|
540
528
|
return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has type '${remoteData.issueType}', expected '${expectedType}' (from task type '${context.task.metadata.type}')`, "check_failed");
|
|
541
529
|
}
|
|
542
|
-
return
|
|
530
|
+
return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has the expected Issue Type`);
|
|
543
531
|
}
|
|
544
532
|
function checkIssueFields(context, remoteData, shared) {
|
|
545
|
-
if (!context.
|
|
546
|
-
return
|
|
533
|
+
if (!context.hasPush) {
|
|
534
|
+
return infoResult('Issue field audit is not applicable because push capability is unavailable');
|
|
547
535
|
}
|
|
548
536
|
if (remoteData.issueFields === undefined) {
|
|
549
|
-
return
|
|
537
|
+
return infoResult('Issue field audit is not applicable because the provider does not expose Issue fields');
|
|
550
538
|
}
|
|
551
539
|
for (const [metadataKey, fieldName] of Object.entries(FRONTMATTER_FIELD_MAP)) {
|
|
552
540
|
const expectedRaw = context.task.metadata[metadataKey];
|
|
@@ -565,20 +553,20 @@ function checkIssueFields(context, remoteData, shared) {
|
|
|
565
553
|
return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} field '${fieldName}' is '${actual.value}', expected '${expected.value}'`, "check_failed");
|
|
566
554
|
}
|
|
567
555
|
}
|
|
568
|
-
return
|
|
556
|
+
return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} fields match task metadata`);
|
|
569
557
|
}
|
|
570
558
|
function checkPrAssignee(context, remoteData, shared) {
|
|
571
|
-
if (!context.
|
|
572
|
-
return
|
|
559
|
+
if (!context.audit.enabled || !context.hasPush || !context.prNumber) {
|
|
560
|
+
return infoResult('PR assignee audit is not applicable because it is disabled or required PR metadata/capability is unavailable');
|
|
573
561
|
}
|
|
574
562
|
if (!remoteData.prAssignees || remoteData.prAssignees.length === 0) {
|
|
575
563
|
return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} has no assignee`, "check_failed");
|
|
576
564
|
}
|
|
577
|
-
return
|
|
565
|
+
return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has an assignee`);
|
|
578
566
|
}
|
|
579
567
|
function checkMilestone(context, remoteData, shared) {
|
|
580
|
-
if (!context.
|
|
581
|
-
return
|
|
568
|
+
if (!context.hasTriage) {
|
|
569
|
+
return infoResult('Milestone audit is not applicable because triage capability is unavailable');
|
|
582
570
|
}
|
|
583
571
|
if (!remoteData.issue?.milestone?.title) {
|
|
584
572
|
return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has no milestone set`, "check_failed");
|
|
@@ -586,7 +574,7 @@ function checkMilestone(context, remoteData, shared) {
|
|
|
586
574
|
if (context.prNumber && remoteData.prMilestone !== undefined && !remoteData.prMilestone?.title) {
|
|
587
575
|
return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} has no milestone set`, "check_failed");
|
|
588
576
|
}
|
|
589
|
-
if (context.
|
|
577
|
+
if (context.audit.requireSpecificMilestone) {
|
|
590
578
|
const issueTitle = remoteData.issue.milestone.title;
|
|
591
579
|
if (VERSION_LINE_REGEX.test(issueTitle)) {
|
|
592
580
|
return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} milestone '${issueTitle}' is a release line; narrow to a specific version (e.g. ${issueTitle.replace(/\.x$/, ".N")}) before continuing`, "check_failed");
|
|
@@ -595,7 +583,7 @@ function checkMilestone(context, remoteData, shared) {
|
|
|
595
583
|
return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} milestone '${remoteData.prMilestone.title}' is a release line; narrow to a specific version before continuing`, "check_failed");
|
|
596
584
|
}
|
|
597
585
|
}
|
|
598
|
-
return
|
|
586
|
+
return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} milestone satisfies the configured policy`);
|
|
599
587
|
}
|
|
600
588
|
function findCommentByMarker(comments, marker) {
|
|
601
589
|
return (comments || []).find((comment) => typeof comment.body === "string" && comment.body.includes(marker)) || null;
|