@fitlab-ai/agent-infra 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/lib/merge.js +22 -7
- package/lib/sandbox/commands/rm.js +1 -1
- package/lib/sandbox/runtimes/base.dockerfile +17 -1
- package/package.json +1 -1
- package/templates/.agents/rules/issue-pr-commands.github.en.md +25 -9
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +25 -9
- package/templates/.agents/rules/issue-sync.github.en.md +111 -23
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +105 -17
- package/templates/.agents/rules/milestone-inference.github.en.md +13 -6
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +13 -6
- package/templates/.agents/rules/pr-sync.github.en.md +3 -1
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +3 -1
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +1080 -0
- package/templates/.agents/scripts/validate-artifact.js +54 -805
- package/templates/.agents/skills/analyze-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/analyze-task/config/verify.json +1 -1
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/block-task/config/verify.json +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +18 -18
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +18 -18
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/commit/SKILL.en.md +15 -3
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +15 -3
- package/templates/.agents/skills/commit/config/verify.json +2 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +23 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +23 -0
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -2
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +13 -13
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +13 -13
- package/templates/.agents/skills/complete-task/config/verify.json +1 -1
- package/templates/.agents/skills/create-issue/SKILL.en.md +4 -2
- package/templates/.agents/skills/create-issue/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/create-issue/config/verify.json +1 -1
- package/templates/.agents/skills/create-issue/reference/label-and-type.en.md +6 -1
- package/templates/.agents/skills/create-issue/reference/label-and-type.zh-CN.md +6 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +5 -5
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/create-pr/config/verify.json +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +9 -5
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +9 -5
- package/templates/.agents/skills/create-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/config/verify.json +1 -1
- package/templates/.agents/skills/implement-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/implement-task/config/verify.json +1 -2
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/import-codescan/config/verify.json +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/import-dependabot/config/verify.json +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +5 -5
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/import-issue/config/verify.json +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/plan-task/config/verify.json +1 -1
- package/templates/.agents/skills/refine-task/SKILL.en.md +4 -6
- package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +4 -6
- package/templates/.agents/skills/refine-task/config/verify.json +1 -2
- package/templates/.agents/skills/refine-title/SKILL.en.md +5 -1
- package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/config/verify.json +1 -1
- package/templates/.agents/skills/review-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-task/config/verify.json +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +1 -1
- package/templates/.agents/templates/task.en.md +3 -3
- package/templates/.agents/templates/task.zh-CN.md +3 -3
- package/templates/.github/workflows/metadata-sync.yml +127 -0
- package/templates/.github/workflows/pr-label.yml +75 -0
- package/templates/.github/workflows/status-label.yml +12 -8
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
5
|
|
|
7
6
|
const EXIT_CODE = {
|
|
@@ -27,15 +26,47 @@ const DEFAULT_REQUIRED_FIELDS = [
|
|
|
27
26
|
"assigned_to"
|
|
28
27
|
];
|
|
29
28
|
|
|
30
|
-
const DEFAULT_RETRY_DELAYS_MS = [3000, 10000];
|
|
31
29
|
const DEFAULT_FRESHNESS_MINUTES = 30;
|
|
32
|
-
const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}
|
|
33
|
-
const ACTIVITY_LOG_PATTERN = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.+)$/;
|
|
30
|
+
const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[+-]\d{2}:\d{2})?$/;
|
|
31
|
+
const ACTIVITY_LOG_PATTERN = /^- (\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[+-]\d{2}:\d{2})?) — \*\*(.+?)\*\* by (.+?) — (.+)$/;
|
|
34
32
|
const BRANCH_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
35
33
|
|
|
36
34
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
37
35
|
const repoRoot = path.resolve(path.dirname(scriptPath), "..", "..");
|
|
38
36
|
|
|
37
|
+
const PLATFORM_ADAPTERS = {};
|
|
38
|
+
const OPTIONAL_PLATFORM_CHECKS = new Set(["platform-sync"]);
|
|
39
|
+
const adaptersDir = path.join(path.dirname(scriptPath), "platform-adapters");
|
|
40
|
+
|
|
41
|
+
if (fs.existsSync(adaptersDir)) {
|
|
42
|
+
for (const file of fs.readdirSync(adaptersDir)) {
|
|
43
|
+
if (!file.endsWith(".js")) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const adapterName = path.basename(file, ".js");
|
|
48
|
+
const mod = await import(new URL(`./platform-adapters/${file}`, import.meta.url));
|
|
49
|
+
if (typeof mod.check === "function") {
|
|
50
|
+
PLATFORM_ADAPTERS[adapterName] = mod.check;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sharedUtils = {
|
|
56
|
+
loadTask,
|
|
57
|
+
getCheckedRequirements,
|
|
58
|
+
normalizeContent,
|
|
59
|
+
isBlank,
|
|
60
|
+
escapeRegExp,
|
|
61
|
+
passResult,
|
|
62
|
+
failResult,
|
|
63
|
+
blockedResult,
|
|
64
|
+
safeStat,
|
|
65
|
+
parseIssueNumber,
|
|
66
|
+
parsePrNumber,
|
|
67
|
+
repoRoot
|
|
68
|
+
};
|
|
69
|
+
|
|
39
70
|
// === CLI Entry ===
|
|
40
71
|
|
|
41
72
|
function main(argv) {
|
|
@@ -155,10 +186,18 @@ function runCheck(type, context) {
|
|
|
155
186
|
return checkActivityLog(context);
|
|
156
187
|
case "completion-checklist":
|
|
157
188
|
return checkCompletionChecklist(context);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
189
|
+
default: {
|
|
190
|
+
const adapter = PLATFORM_ADAPTERS[type];
|
|
191
|
+
if (!adapter) {
|
|
192
|
+
if (OPTIONAL_PLATFORM_CHECKS.has(type)) {
|
|
193
|
+
return passResult(type, `Skipped: no platform adapter registered for '${type}'`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return failResult(type, `Unsupported check type '${type}'.`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return adapter(context, sharedUtils);
|
|
200
|
+
}
|
|
162
201
|
}
|
|
163
202
|
}
|
|
164
203
|
|
|
@@ -419,40 +458,6 @@ function checkCompletionChecklist({ taskDir, config }) {
|
|
|
419
458
|
return passResult("completion-checklist", `Completion Checklist valid (${items.length} items checked)`);
|
|
420
459
|
}
|
|
421
460
|
|
|
422
|
-
function checkGithubSync({ taskDir, config, artifactFile }) {
|
|
423
|
-
const context = buildSyncContext({ taskDir, config, artifactFile });
|
|
424
|
-
if (context.earlyReturn) {
|
|
425
|
-
return context.earlyReturn;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const remoteData = fetchRemoteData(context);
|
|
429
|
-
if (remoteData.earlyReturn) {
|
|
430
|
-
return remoteData.earlyReturn;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const subChecks = [
|
|
434
|
-
checkStatusLabel,
|
|
435
|
-
checkCommentMarker,
|
|
436
|
-
checkPrCommentMarker,
|
|
437
|
-
checkPrCommentLastCommit,
|
|
438
|
-
checkCommentContent,
|
|
439
|
-
checkTaskCommentContent,
|
|
440
|
-
checkInLabelsMatchPr,
|
|
441
|
-
checkSyncedRequirements,
|
|
442
|
-
checkIssueType,
|
|
443
|
-
checkMilestone
|
|
444
|
-
];
|
|
445
|
-
|
|
446
|
-
for (const subCheck of subChecks) {
|
|
447
|
-
const result = subCheck(context, remoteData);
|
|
448
|
-
if (result) {
|
|
449
|
-
return result;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return passResult("github-sync", `GitHub sync checks passed for Issue #${context.issueNumber}`);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
461
|
// === File & Config Loaders ===
|
|
457
462
|
|
|
458
463
|
function loadVerifyConfig(skillName) {
|
|
@@ -572,634 +577,6 @@ function getCheckedRequirements(content) {
|
|
|
572
577
|
.map((match) => match[1].trim());
|
|
573
578
|
}
|
|
574
579
|
|
|
575
|
-
function buildSyncContext({ taskDir, config, artifactFile }) {
|
|
576
|
-
const task = loadTask(taskDir);
|
|
577
|
-
if (!task.ok) {
|
|
578
|
-
return { earlyReturn: failResult("github-sync", task.message) };
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const issueNumber = parseIssueNumber(task.metadata.issue_number);
|
|
582
|
-
const prNumber = parsePrNumber(task.metadata.pr_number);
|
|
583
|
-
if (config.when === "issue_number_exists" && !issueNumber) {
|
|
584
|
-
return { earlyReturn: passResult("github-sync", "Skipped: task has no issue_number") };
|
|
585
|
-
}
|
|
586
|
-
if (config.when === "pr_number_exists" && !prNumber) {
|
|
587
|
-
return { earlyReturn: passResult("github-sync", "Skipped: task has no pr_number") };
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (!issueNumber) {
|
|
591
|
-
return { earlyReturn: passResult("github-sync", "Skipped: github-sync not required for this task") };
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const ownerRepo = resolveOwnerRepo(taskDir);
|
|
595
|
-
if (!ownerRepo.ok) {
|
|
596
|
-
return { earlyReturn: blockedResult("github-sync", ownerRepo.message, "network_error") };
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const marker = config.expected_comment_marker
|
|
600
|
-
? interpolate(config.expected_comment_marker, taskDir, artifactFile)
|
|
601
|
-
: null;
|
|
602
|
-
const prMarker = config.expected_pr_comment_marker
|
|
603
|
-
? interpolate(config.expected_pr_comment_marker, taskDir, artifactFile)
|
|
604
|
-
: null;
|
|
605
|
-
const artifactPath = artifactFile ? path.join(taskDir, artifactFile) : null;
|
|
606
|
-
|
|
607
|
-
return {
|
|
608
|
-
task,
|
|
609
|
-
taskDir,
|
|
610
|
-
config,
|
|
611
|
-
artifactFile,
|
|
612
|
-
artifactPath,
|
|
613
|
-
issueNumber,
|
|
614
|
-
prNumber,
|
|
615
|
-
ownerRepo: ownerRepo.value,
|
|
616
|
-
marker,
|
|
617
|
-
prMarker
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function fetchRemoteData(context) {
|
|
622
|
-
const issueResult = withRetry(() => ghJson([
|
|
623
|
-
"issue",
|
|
624
|
-
"view",
|
|
625
|
-
String(context.issueNumber),
|
|
626
|
-
"--json",
|
|
627
|
-
"state,labels,body,milestone"
|
|
628
|
-
], context.taskDir));
|
|
629
|
-
if (!issueResult.ok) {
|
|
630
|
-
return {
|
|
631
|
-
earlyReturn: issueResult.type === "check_failed"
|
|
632
|
-
? failResult("github-sync", issueResult.message, issueResult.type)
|
|
633
|
-
: blockedResult("github-sync", issueResult.message, issueResult.type)
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
const issue = issueResult.value;
|
|
638
|
-
if (context.config.issue_must_exist !== false && !issue) {
|
|
639
|
-
return {
|
|
640
|
-
earlyReturn: failResult("github-sync", `Issue #${context.issueNumber} not found`, "check_failed")
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
let comments = null;
|
|
645
|
-
if (shouldFetchComments(context.config)) {
|
|
646
|
-
const commentsResult = withRetry(() => ghPaginatedJson([
|
|
647
|
-
"api",
|
|
648
|
-
"--paginate",
|
|
649
|
-
"--slurp",
|
|
650
|
-
`repos/${context.ownerRepo}/issues/${context.issueNumber}/comments?per_page=100`
|
|
651
|
-
], context.taskDir));
|
|
652
|
-
|
|
653
|
-
if (!commentsResult.ok) {
|
|
654
|
-
return {
|
|
655
|
-
earlyReturn: commentsResult.type === "check_failed"
|
|
656
|
-
? failResult("github-sync", commentsResult.message, commentsResult.type)
|
|
657
|
-
: blockedResult("github-sync", commentsResult.message, commentsResult.type)
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
comments = flattenComments(commentsResult.value);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
let prComments = null;
|
|
665
|
-
if (context.config.expected_pr_comment_marker) {
|
|
666
|
-
if (!context.prNumber) {
|
|
667
|
-
return {
|
|
668
|
-
earlyReturn: failResult("github-sync", "Expected a valid pr_number for PR comment verification", "check_failed")
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const prCommentsResult = withRetry(() => ghPaginatedJson([
|
|
673
|
-
"api",
|
|
674
|
-
"--paginate",
|
|
675
|
-
"--slurp",
|
|
676
|
-
`repos/${context.ownerRepo}/issues/${context.prNumber}/comments?per_page=100`
|
|
677
|
-
], context.taskDir));
|
|
678
|
-
|
|
679
|
-
if (!prCommentsResult.ok) {
|
|
680
|
-
return {
|
|
681
|
-
earlyReturn: prCommentsResult.type === "check_failed"
|
|
682
|
-
? failResult("github-sync", prCommentsResult.message, prCommentsResult.type)
|
|
683
|
-
: blockedResult("github-sync", prCommentsResult.message, prCommentsResult.type)
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
prComments = flattenComments(prCommentsResult.value);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
let issueType;
|
|
691
|
-
if (context.config.verify_issue_type) {
|
|
692
|
-
const issueTypeResult = withRetry(() => ghText([
|
|
693
|
-
"api",
|
|
694
|
-
`repos/${context.ownerRepo}/issues/${context.issueNumber}`,
|
|
695
|
-
"--jq",
|
|
696
|
-
".type.name // empty"
|
|
697
|
-
], context.taskDir));
|
|
698
|
-
|
|
699
|
-
if (issueTypeResult.ok) {
|
|
700
|
-
issueType = issueTypeResult.value || null;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
let prLabels = null;
|
|
705
|
-
let prMilestone;
|
|
706
|
-
if ((context.config.verify_in_labels_match_pr || context.config.verify_milestone) && context.prNumber) {
|
|
707
|
-
const prFields = [];
|
|
708
|
-
if (context.config.verify_in_labels_match_pr) {
|
|
709
|
-
prFields.push("labels");
|
|
710
|
-
}
|
|
711
|
-
if (context.config.verify_milestone) {
|
|
712
|
-
prFields.push("milestone");
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
const prResult = withRetry(() => ghJson([
|
|
716
|
-
"pr",
|
|
717
|
-
"view",
|
|
718
|
-
String(context.prNumber),
|
|
719
|
-
"--json",
|
|
720
|
-
prFields.join(",")
|
|
721
|
-
], context.taskDir));
|
|
722
|
-
|
|
723
|
-
if (!prResult.ok) {
|
|
724
|
-
return {
|
|
725
|
-
earlyReturn: prResult.type === "check_failed"
|
|
726
|
-
? failResult("github-sync", prResult.message, prResult.type)
|
|
727
|
-
: blockedResult("github-sync", prResult.message, prResult.type)
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
prLabels = context.config.verify_in_labels_match_pr
|
|
732
|
-
? extractLabelNames(prResult.value?.labels)
|
|
733
|
-
: null;
|
|
734
|
-
prMilestone = context.config.verify_milestone
|
|
735
|
-
? prResult.value?.milestone ?? null
|
|
736
|
-
: undefined;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
return {
|
|
740
|
-
issue,
|
|
741
|
-
comments,
|
|
742
|
-
prComments,
|
|
743
|
-
prLabels,
|
|
744
|
-
issueType,
|
|
745
|
-
prMilestone
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
function shouldFetchComments(config) {
|
|
750
|
-
return Boolean(
|
|
751
|
-
config.expected_comment_marker
|
|
752
|
-
|| config.expected_pr_comment_marker
|
|
753
|
-
|| config.verify_pr_comment_last_commit_matches_head
|
|
754
|
-
|| config.verify_comment_content
|
|
755
|
-
|| config.verify_task_comment_content
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function flattenComments(value) {
|
|
760
|
-
if (!Array.isArray(value)) {
|
|
761
|
-
return [];
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
return value.flatMap((page) => Array.isArray(page) ? page : []);
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function checkStatusLabel(context, remoteData) {
|
|
768
|
-
if (!context.config.expected_status_label || remoteData.issue.state !== "OPEN") {
|
|
769
|
-
return null;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
const labels = extractLabelNames(remoteData.issue.labels);
|
|
773
|
-
if (labels.includes(context.config.expected_status_label)) {
|
|
774
|
-
return null;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
return failResult(
|
|
778
|
-
"github-sync",
|
|
779
|
-
`Expected label '${context.config.expected_status_label}' not found on Issue #${context.issueNumber}`,
|
|
780
|
-
"check_failed"
|
|
781
|
-
);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
function checkCommentMarker(context, remoteData) {
|
|
785
|
-
if (!context.marker) {
|
|
786
|
-
return null;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
const comment = findCommentByMarker(remoteData.comments, context.marker);
|
|
790
|
-
if (comment) {
|
|
791
|
-
return null;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
return failResult(
|
|
795
|
-
"github-sync",
|
|
796
|
-
`Expected comment marker '${context.marker}' not found on Issue #${context.issueNumber}`,
|
|
797
|
-
"check_failed"
|
|
798
|
-
);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
function checkPrCommentMarker(context, remoteData) {
|
|
802
|
-
if (!context.prMarker) {
|
|
803
|
-
return null;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
|
|
807
|
-
if (comment) {
|
|
808
|
-
return null;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
return failResult(
|
|
812
|
-
"github-sync",
|
|
813
|
-
`Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
|
|
814
|
-
"check_failed"
|
|
815
|
-
);
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function checkPrCommentLastCommit(context, remoteData) {
|
|
819
|
-
if (!context.config.verify_pr_comment_last_commit_matches_head) {
|
|
820
|
-
return null;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if (!context.prMarker) {
|
|
824
|
-
return failResult(
|
|
825
|
-
"github-sync",
|
|
826
|
-
"verify_pr_comment_last_commit_matches_head requires expected_pr_comment_marker",
|
|
827
|
-
"check_failed"
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
|
|
832
|
-
if (!comment) {
|
|
833
|
-
return failResult(
|
|
834
|
-
"github-sync",
|
|
835
|
-
`Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
|
|
836
|
-
"check_failed"
|
|
837
|
-
);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
const match = String(comment.body || "").match(/<!--\s*last-commit:\s*([0-9a-f]{7,40})\s*-->/i);
|
|
841
|
-
if (!match) {
|
|
842
|
-
return failResult(
|
|
843
|
-
"github-sync",
|
|
844
|
-
`PR #${context.prNumber} summary comment is missing '<!-- last-commit: <sha> -->' metadata`,
|
|
845
|
-
"check_failed"
|
|
846
|
-
);
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
const headResult = withRetry(() => gitText(["rev-parse", "HEAD"], context.taskDir));
|
|
850
|
-
if (!headResult.ok) {
|
|
851
|
-
return headResult.type === "check_failed"
|
|
852
|
-
? failResult("github-sync", headResult.message, headResult.type)
|
|
853
|
-
: blockedResult("github-sync", headResult.message, headResult.type);
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
const expectedHead = String(headResult.value || "").trim();
|
|
857
|
-
const actualHead = match[1].trim();
|
|
858
|
-
if (expectedHead === actualHead) {
|
|
859
|
-
return null;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
return failResult(
|
|
863
|
-
"github-sync",
|
|
864
|
-
`PR #${context.prNumber} summary comment last-commit metadata mismatch: expected ${expectedHead}, got ${actualHead}`,
|
|
865
|
-
"check_failed"
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
function checkCommentContent(context, remoteData) {
|
|
870
|
-
if (!context.config.verify_comment_content) {
|
|
871
|
-
return null;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
if (!context.marker) {
|
|
875
|
-
return failResult("github-sync", "verify_comment_content requires expected_comment_marker", "check_failed");
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
if (!context.artifactPath || !safeStat(context.artifactPath)) {
|
|
879
|
-
return failResult(
|
|
880
|
-
"github-sync",
|
|
881
|
-
`Artifact not found for comment verification: ${context.artifactFile || "(missing artifactFile)"}`,
|
|
882
|
-
"check_failed"
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
const comment = findCommentByMarker(remoteData.comments, context.marker);
|
|
887
|
-
const localContent = normalizeContent(fs.readFileSync(context.artifactPath, "utf8"));
|
|
888
|
-
const commentContent = normalizeContent(extractCommentBody(comment?.body || ""));
|
|
889
|
-
|
|
890
|
-
if (localContent === commentContent) {
|
|
891
|
-
return null;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
return failResult(
|
|
895
|
-
"github-sync",
|
|
896
|
-
buildCommentContentMismatchMessage(
|
|
897
|
-
path.basename(context.artifactPath, path.extname(context.artifactPath)),
|
|
898
|
-
context.issueNumber,
|
|
899
|
-
localContent,
|
|
900
|
-
commentContent
|
|
901
|
-
),
|
|
902
|
-
"check_failed"
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
function checkTaskCommentContent(context, remoteData) {
|
|
907
|
-
if (!context.config.verify_task_comment_content) {
|
|
908
|
-
return null;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
|
|
912
|
-
const comment = findCommentByMarker(remoteData.comments, taskMarker);
|
|
913
|
-
if (!comment) {
|
|
914
|
-
return failResult(
|
|
915
|
-
"github-sync",
|
|
916
|
-
`Expected comment marker '${taskMarker}' not found on Issue #${context.issueNumber}`,
|
|
917
|
-
"check_failed"
|
|
918
|
-
);
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
const expectedBody = normalizeContent(buildExpectedTaskBody(context.task.content));
|
|
922
|
-
const commentBody = normalizeContent(extractCommentBody(comment.body || ""));
|
|
923
|
-
|
|
924
|
-
if (expectedBody === commentBody) {
|
|
925
|
-
return null;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
return failResult(
|
|
929
|
-
"github-sync",
|
|
930
|
-
buildCommentContentMismatchMessage("task", context.issueNumber, expectedBody, commentBody),
|
|
931
|
-
"check_failed"
|
|
932
|
-
);
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
function checkInLabelsMatchPr(context, remoteData) {
|
|
936
|
-
if (!context.config.verify_in_labels_match_pr || !context.prNumber || !remoteData.prLabels) {
|
|
937
|
-
return null;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
const issueInLabels = extractLabelNames(remoteData.issue.labels)
|
|
941
|
-
.filter((label) => label.startsWith("in:"))
|
|
942
|
-
.sort();
|
|
943
|
-
const prInLabels = remoteData.prLabels
|
|
944
|
-
.filter((label) => label.startsWith("in:"))
|
|
945
|
-
.sort();
|
|
946
|
-
|
|
947
|
-
if (arraysEqual(issueInLabels, prInLabels)) {
|
|
948
|
-
return null;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
return failResult(
|
|
952
|
-
"github-sync",
|
|
953
|
-
`in: labels mismatch — PR #${context.prNumber} has [${formatLabelList(prInLabels)}], Issue #${context.issueNumber} has [${formatLabelList(issueInLabels)}]`,
|
|
954
|
-
"check_failed"
|
|
955
|
-
);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function checkSyncedRequirements(context, remoteData) {
|
|
959
|
-
if (!context.config.sync_checked_requirements) {
|
|
960
|
-
return null;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
const checkedRequirements = getCheckedRequirements(context.task.content);
|
|
964
|
-
if (checkedRequirements.length === 0) {
|
|
965
|
-
return null;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
const issueBody = remoteData.issue.body || "";
|
|
969
|
-
const missingRequirements = checkedRequirements.filter(
|
|
970
|
-
(item) => !new RegExp(`^- \\[x\\] ${escapeRegExp(item)}$`, "m").test(issueBody)
|
|
971
|
-
);
|
|
972
|
-
if (missingRequirements.length === 0) {
|
|
973
|
-
return null;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
return failResult(
|
|
977
|
-
"github-sync",
|
|
978
|
-
`Issue body is missing checked requirements: ${missingRequirements.join(", ")}`,
|
|
979
|
-
"check_failed"
|
|
980
|
-
);
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
function checkIssueType(context, remoteData) {
|
|
984
|
-
if (!context.config.verify_issue_type) {
|
|
985
|
-
return null;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
if (remoteData.issueType === undefined) {
|
|
989
|
-
return null;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
if (!remoteData.issueType) {
|
|
993
|
-
return failResult(
|
|
994
|
-
"github-sync",
|
|
995
|
-
`Issue #${context.issueNumber} has no Issue Type set`,
|
|
996
|
-
"check_failed"
|
|
997
|
-
);
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
const expectedType = mapTaskTypeToIssueType(context.task.metadata.type);
|
|
1001
|
-
if (expectedType && remoteData.issueType !== expectedType) {
|
|
1002
|
-
return failResult(
|
|
1003
|
-
"github-sync",
|
|
1004
|
-
`Issue #${context.issueNumber} has type '${remoteData.issueType}', expected '${expectedType}' (from task type '${context.task.metadata.type}')`,
|
|
1005
|
-
"check_failed"
|
|
1006
|
-
);
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
return null;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
function checkMilestone(context, remoteData) {
|
|
1013
|
-
if (!context.config.verify_milestone) {
|
|
1014
|
-
return null;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
if (!remoteData.issue?.milestone?.title) {
|
|
1018
|
-
return failResult(
|
|
1019
|
-
"github-sync",
|
|
1020
|
-
`Issue #${context.issueNumber} has no milestone set`,
|
|
1021
|
-
"check_failed"
|
|
1022
|
-
);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
if (context.prNumber && remoteData.prMilestone !== undefined && !remoteData.prMilestone?.title) {
|
|
1026
|
-
return failResult(
|
|
1027
|
-
"github-sync",
|
|
1028
|
-
`PR #${context.prNumber} has no milestone set`,
|
|
1029
|
-
"check_failed"
|
|
1030
|
-
);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
return null;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
function findCommentByMarker(comments, marker) {
|
|
1037
|
-
return (comments || []).find((comment) => typeof comment.body === "string" && comment.body.includes(marker)) || null;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
function extractCommentBody(commentBody) {
|
|
1041
|
-
const lines = String(commentBody || "").split(/\r?\n/);
|
|
1042
|
-
|
|
1043
|
-
let start = 0;
|
|
1044
|
-
while (start < lines.length && (lines[start].trim() === "" || /^<!--.*-->$/.test(lines[start].trim()))) {
|
|
1045
|
-
start += 1;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
if (start < lines.length && lines[start].startsWith("## ")) {
|
|
1049
|
-
start += 1;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
while (start < lines.length && lines[start].trim() === "") {
|
|
1053
|
-
start += 1;
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
if (start < lines.length && /^> \*\*.+\*\* · .+$/.test(lines[start].trim())) {
|
|
1057
|
-
start += 1;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
while (start < lines.length && lines[start].trim() === "") {
|
|
1061
|
-
start += 1;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
let end = lines.length;
|
|
1065
|
-
for (let index = lines.length - 1; index >= start; index -= 1) {
|
|
1066
|
-
const trimmed = lines[index].trim();
|
|
1067
|
-
if (trimmed === "") {
|
|
1068
|
-
continue;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
if (/^\*.*\*$/.test(trimmed)) {
|
|
1072
|
-
end = index;
|
|
1073
|
-
if (end > start && lines[end - 1].trim() === "---") {
|
|
1074
|
-
end -= 1;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
break;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
return lines.slice(start, end).join("\n");
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
function buildExpectedTaskBody(taskContent) {
|
|
1084
|
-
const frontmatterMatch = taskContent.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
1085
|
-
if (!frontmatterMatch) {
|
|
1086
|
-
return taskContent.trim();
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
const body = taskContent.slice(frontmatterMatch[0].length).trim();
|
|
1090
|
-
return [
|
|
1091
|
-
buildTaskFrontmatterSummary(),
|
|
1092
|
-
"",
|
|
1093
|
-
"```yaml",
|
|
1094
|
-
frontmatterMatch[0].trim(),
|
|
1095
|
-
"```",
|
|
1096
|
-
"",
|
|
1097
|
-
"</details>",
|
|
1098
|
-
"",
|
|
1099
|
-
body
|
|
1100
|
-
].join("\n").trim();
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
function buildTaskFrontmatterSummary() {
|
|
1104
|
-
const language = loadProjectLanguage();
|
|
1105
|
-
if (language === "en" || language === "en-US") {
|
|
1106
|
-
return "<details><summary>Metadata (frontmatter)</summary>";
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
return "<details><summary>元数据 (frontmatter)</summary>";
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
function loadProjectLanguage() {
|
|
1113
|
-
const override = process.env.VALIDATE_ARTIFACT_LANGUAGE;
|
|
1114
|
-
if (!isBlank(override)) {
|
|
1115
|
-
return String(override).trim();
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
const configPath = path.join(repoRoot, ".agents", ".airc.json");
|
|
1119
|
-
if (!fs.existsSync(configPath)) {
|
|
1120
|
-
return "";
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
try {
|
|
1124
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
1125
|
-
return String(config.language || "").trim();
|
|
1126
|
-
} catch {
|
|
1127
|
-
return "";
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
function normalizeContent(text) {
|
|
1132
|
-
return String(text || "")
|
|
1133
|
-
.replace(/\r\n/g, "\n")
|
|
1134
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
1135
|
-
.trim();
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
function buildCommentContentMismatchMessage(fileStem, issueNumber, localContent, commentContent) {
|
|
1139
|
-
const diffIndex = firstDifferenceIndex(localContent, commentContent);
|
|
1140
|
-
const position = indexToLineColumn(localContent, diffIndex);
|
|
1141
|
-
|
|
1142
|
-
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})`;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
function firstDifferenceIndex(left, right) {
|
|
1146
|
-
const limit = Math.max(left.length, right.length);
|
|
1147
|
-
for (let index = 0; index < limit; index += 1) {
|
|
1148
|
-
if (left[index] !== right[index]) {
|
|
1149
|
-
return index;
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
return limit;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
function indexToLineColumn(text, index) {
|
|
1157
|
-
const prefix = text.slice(0, Math.min(index, text.length));
|
|
1158
|
-
const lines = prefix.split("\n");
|
|
1159
|
-
return {
|
|
1160
|
-
line: lines.length,
|
|
1161
|
-
column: (lines.at(-1) || "").length + 1
|
|
1162
|
-
};
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
function extractLabelNames(labels) {
|
|
1166
|
-
return (labels || [])
|
|
1167
|
-
.map((label) => typeof label === "string" ? label : label?.name)
|
|
1168
|
-
.filter((label) => typeof label === "string" && label.length > 0);
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
function mapTaskTypeToIssueType(taskType) {
|
|
1172
|
-
const mapping = {
|
|
1173
|
-
bug: "Bug",
|
|
1174
|
-
bugfix: "Bug",
|
|
1175
|
-
enhancement: "Feature",
|
|
1176
|
-
feature: "Feature",
|
|
1177
|
-
task: "Task",
|
|
1178
|
-
documentation: "Task",
|
|
1179
|
-
"dependency-upgrade": "Task",
|
|
1180
|
-
chore: "Task",
|
|
1181
|
-
docs: "Task",
|
|
1182
|
-
refactor: "Task",
|
|
1183
|
-
refactoring: "Task"
|
|
1184
|
-
};
|
|
1185
|
-
|
|
1186
|
-
return mapping[taskType] || "Task";
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
function arraysEqual(left, right) {
|
|
1190
|
-
if (left.length !== right.length) {
|
|
1191
|
-
return false;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
return left.every((value, index) => value === right[index]);
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
function formatLabelList(labels) {
|
|
1198
|
-
return labels.length > 0 ? labels.join(", ") : "none";
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
// === GitHub API ===
|
|
1202
|
-
|
|
1203
580
|
function parseIssueNumber(value) {
|
|
1204
581
|
if (isBlank(value) || value === "N/A") {
|
|
1205
582
|
return null;
|
|
@@ -1213,145 +590,17 @@ function parsePrNumber(value) {
|
|
|
1213
590
|
return parseIssueNumber(value);
|
|
1214
591
|
}
|
|
1215
592
|
|
|
1216
|
-
|
|
1217
|
-
const gitResult = spawnSync("git", ["remote", "get-url", "origin"], {
|
|
1218
|
-
cwd: taskDir,
|
|
1219
|
-
encoding: "utf8"
|
|
1220
|
-
});
|
|
1221
|
-
|
|
1222
|
-
if (gitResult.status !== 0) {
|
|
1223
|
-
return { ok: false, message: `Unable to resolve git remote: ${gitResult.stderr.trim() || gitResult.stdout.trim()}` };
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
const remote = gitResult.stdout.trim();
|
|
1227
|
-
const sshMatch = remote.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
1228
|
-
if (!sshMatch) {
|
|
1229
|
-
return { ok: false, message: `Unable to parse owner/repo from remote '${remote}'` };
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
return { ok: true, value: sshMatch[1] };
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
function ghJson(args, cwd) {
|
|
1236
|
-
const result = ghCommand(args, cwd);
|
|
1237
|
-
if (!result.ok) {
|
|
1238
|
-
return result;
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
try {
|
|
1242
|
-
return { ok: true, value: JSON.parse(result.value || "null") };
|
|
1243
|
-
} catch (error) {
|
|
1244
|
-
return { ok: false, type: "network_error", message: `Invalid JSON from gh: ${error.message}` };
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
function ghText(args, cwd) {
|
|
1249
|
-
const result = ghCommand(args, cwd);
|
|
1250
|
-
if (!result.ok) {
|
|
1251
|
-
return result;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
return { ok: true, value: String(result.value || "").trim() };
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
function ghCommand(args, cwd) {
|
|
1258
|
-
const result = spawnSync("gh", args, {
|
|
1259
|
-
cwd,
|
|
1260
|
-
encoding: "utf8",
|
|
1261
|
-
env: process.env
|
|
1262
|
-
});
|
|
1263
|
-
|
|
1264
|
-
if (result.status !== 0) {
|
|
1265
|
-
const stderr = `${result.stderr || ""}${result.stdout || ""}`.trim();
|
|
1266
|
-
const classified = classifyGhFailure(stderr, args);
|
|
1267
|
-
return { ok: false, type: classified.type, message: classified.message };
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
return { ok: true, value: result.stdout };
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
function ghPaginatedJson(args, cwd) {
|
|
1274
|
-
return ghJson(args, cwd);
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
function gitText(args, cwd) {
|
|
1278
|
-
const result = spawnSync("git", args, {
|
|
1279
|
-
cwd,
|
|
1280
|
-
encoding: "utf8",
|
|
1281
|
-
env: process.env
|
|
1282
|
-
});
|
|
1283
|
-
|
|
1284
|
-
if (result.status !== 0) {
|
|
1285
|
-
const stderr = `${result.stderr || ""}${result.stdout || ""}`.trim();
|
|
1286
|
-
return {
|
|
1287
|
-
ok: false,
|
|
1288
|
-
type: "check_failed",
|
|
1289
|
-
message: stderr || `git ${args.join(" ")} failed`
|
|
1290
|
-
};
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
return { ok: true, value: String(result.stdout || "").trim() };
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
function withRetry(operation) {
|
|
1297
|
-
const delays = getRetryDelays();
|
|
1298
|
-
let lastFailure = null;
|
|
1299
|
-
|
|
1300
|
-
for (let attempt = 0; attempt <= delays.length; attempt += 1) {
|
|
1301
|
-
const result = operation();
|
|
1302
|
-
if (result.ok) {
|
|
1303
|
-
return result;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
lastFailure = result;
|
|
1307
|
-
if (result.type === "check_failed") {
|
|
1308
|
-
return result;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
if (attempt < delays.length) {
|
|
1312
|
-
sleep(delays[attempt]);
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
return lastFailure || { ok: false, type: "network_error", message: "Unknown GitHub sync failure" };
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
function classifyGhFailure(stderr, args) {
|
|
1320
|
-
const message = stderr || `gh ${args.join(" ")} failed`;
|
|
1321
|
-
|
|
1322
|
-
if (/not found|could not resolve to an issue|http 404/i.test(message)) {
|
|
1323
|
-
return { type: "check_failed", message };
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
return { type: "network_error", message };
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
function getRetryDelays() {
|
|
1330
|
-
const override = process.env.VALIDATE_ARTIFACT_RETRY_DELAYS_MS;
|
|
1331
|
-
if (!override) {
|
|
1332
|
-
return DEFAULT_RETRY_DELAYS_MS;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
const parsed = override
|
|
1336
|
-
.split(",")
|
|
1337
|
-
.map((value) => Number(value.trim()))
|
|
1338
|
-
.filter((value) => Number.isFinite(value) && value >= 0);
|
|
1339
|
-
|
|
1340
|
-
return parsed.length > 0 ? parsed : DEFAULT_RETRY_DELAYS_MS;
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
function sleep(delayMs) {
|
|
1344
|
-
if (delayMs <= 0) {
|
|
1345
|
-
return;
|
|
1346
|
-
}
|
|
593
|
+
// === Utilities ===
|
|
1347
594
|
|
|
1348
|
-
|
|
595
|
+
function normalizeContent(text) {
|
|
596
|
+
return String(text || "")
|
|
597
|
+
.replace(/\r\n/g, "\n")
|
|
598
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
599
|
+
.trim();
|
|
1349
600
|
}
|
|
1350
601
|
|
|
1351
|
-
// === Utilities ===
|
|
1352
|
-
|
|
1353
602
|
function minutesSinceTimestamp(timestamp) {
|
|
1354
|
-
const normalized = timestamp.replace(" ", "T");
|
|
603
|
+
const normalized = timestamp.includes("T") ? timestamp : timestamp.replace(" ", "T");
|
|
1355
604
|
const parsed = Date.parse(normalized);
|
|
1356
605
|
if (Number.isNaN(parsed)) {
|
|
1357
606
|
return Number.POSITIVE_INFINITY;
|