@yemi33/minions 0.1.1648 → 0.1.1649
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/CHANGELOG.md +8 -0
- package/dashboard/js/render-prs.js +2 -1
- package/engine/ado-status.js +5 -1
- package/engine/ado.js +261 -102
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +6 -0
- package/engine.js +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,7 @@ function prRow(pr) {
|
|
|
16
16
|
const reviewTitle = reviewEscalated ? 'Review/re-review and review-fix automation stopped after evalMaxIterations; build-fix and conflict-fix automation may still run.' : '';
|
|
17
17
|
const buildClass = pr.buildFixEscalated ? 'build-escalated' : pr._buildStatusStale ? 'build-stale' : pr.buildStatus === 'passing' ? 'build-pass' : pr.buildStatus === 'failing' ? 'build-fail' : pr.buildStatus === 'running' ? 'building' : 'no-build';
|
|
18
18
|
const buildLabel = pr.buildFixEscalated ? 'escalated (' + (pr.buildFixAttempts || '?') + ' fixes)' : (pr.buildStatus || 'none') + (pr._buildStatusStale ? ' (stale)' : '');
|
|
19
|
+
const buildTitle = pr._buildStatusDetail || '';
|
|
19
20
|
const statusClass = pr.status === 'merged' ? 'merged' : pr.status === 'abandoned' ? 'rejected' : pr.status === 'active' ? 'active' : 'draft';
|
|
20
21
|
const statusLabel = pr.status || 'active';
|
|
21
22
|
const branchError = pr._branchResolutionError?.reason || '';
|
|
@@ -34,7 +35,7 @@ function prRow(pr) {
|
|
|
34
35
|
'<td><span class="' + branchClass + '"' + (branchError ? ' title="' + escapeHtml(branchError) + '"' : '') + '>' + escapeHtml(branchLabel) + '</span>' + pendingReasonHtml + '</td>' +
|
|
35
36
|
'<td><span class="pr-badge ' + reviewClass + '"' + (reviewTitle ? ' title="' + escapeHtml(reviewTitle) + '"' : '') + '>' + escapeHtml(reviewLabel) + '</span></td>' +
|
|
36
37
|
'<td>' + (sq.reviewer && sq.status !== 'waiting' ? '<span class="pr-agent" title="' + escapeHtml(sq.note || '') + '">' + escapeHtml(sq.reviewer) + '</span>' : sq.reviewer && sq.status === 'waiting' ? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escapeHtml(sq.reviewer) + '…</span>' : pr.reviewedBy && pr.reviewedBy.length ? '<span class="pr-agent">' + escapeHtml(pr.reviewedBy.join(', ')) + '</span>' : '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
|
|
37
|
-
'<td><span class="pr-badge ' + buildClass + '">' + escapeHtml(buildLabel) + '</span></td>' +
|
|
38
|
+
'<td><span class="pr-badge ' + buildClass + '"' + (buildTitle ? ' title="' + escapeHtml(buildTitle) + '"' : '') + '>' + escapeHtml(buildLabel) + '</span></td>' +
|
|
38
39
|
'<td><span class="pr-badge ' + statusClass + '">' + escapeHtml(statusLabel) + '</span></td>' +
|
|
39
40
|
'<td><span class="pr-date">' + escapeHtml((pr.created || '—').slice(0, 16).replace('T', ' ')) + '</span></td>' +
|
|
40
41
|
'<td><button class="pr-pager-btn" style="font-size:9px;padding:1px 5px;color:var(--red);border-color:var(--red)" data-pr-id="' + escapeHtml(String(prId)) + '" onclick="event.stopPropagation();unlinkPr(this.dataset.prId)" title="Remove from tracking">x</button></td>' +
|
package/engine/ado-status.js
CHANGED
|
@@ -42,7 +42,7 @@ if (!prNumberArg) {
|
|
|
42
42
|
' --project <name> Scope search to one project (optional)',
|
|
43
43
|
'',
|
|
44
44
|
'Output: JSON with fields: prNumber, title, branch, status, reviewStatus,',
|
|
45
|
-
' buildStatus, buildErrorLog (if failing), mergeConflict, url, project, source',
|
|
45
|
+
' buildStatus, buildStatusStale/detail (if stale), buildErrorLog (if failing), mergeConflict, url, project, source',
|
|
46
46
|
'',
|
|
47
47
|
'buildStatus values: passing | failing | running | none',
|
|
48
48
|
'reviewStatus values: approved | changes-requested | waiting | pending',
|
|
@@ -73,6 +73,10 @@ function findInCache(projects) {
|
|
|
73
73
|
source: 'cached',
|
|
74
74
|
};
|
|
75
75
|
if (pr.buildErrorLog) out.buildErrorLog = pr.buildErrorLog;
|
|
76
|
+
if (pr._buildStatusStale) {
|
|
77
|
+
out.buildStatusStale = true;
|
|
78
|
+
if (pr._buildStatusDetail) out.buildStatusDetail = pr._buildStatusDetail;
|
|
79
|
+
}
|
|
76
80
|
if (pr._mergeConflict) out.mergeConflict = true;
|
|
77
81
|
return out;
|
|
78
82
|
}
|
package/engine/ado.js
CHANGED
|
@@ -26,17 +26,31 @@ const getAdoPrUrl = (project, prNumber) => {
|
|
|
26
26
|
const repoPath = encodeURIComponent(project.repoName || project.repositoryId || '');
|
|
27
27
|
return `https://dev.azure.com/${project.adoOrg}/${project.adoProject}/_git/${repoPath}/pullrequest/${prNumber}`;
|
|
28
28
|
};
|
|
29
|
+
const ADO_GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
29
30
|
|
|
30
31
|
function isGitHubProject(project) {
|
|
31
32
|
return String(project?.repoHost || '').toLowerCase() === 'github';
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
function
|
|
35
|
+
function isAdoGuid(value) {
|
|
36
|
+
return ADO_GUID_RE.test(String(value || '').trim());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getAdoRepositoryLookupKey(project) {
|
|
35
40
|
const repositoryId = String(project?.repositoryId || '').trim();
|
|
36
41
|
if (repositoryId) return repositoryId;
|
|
37
42
|
return String(project?.repoName || '').trim();
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
function getAdoRepositoryId(project) {
|
|
46
|
+
return getAdoRepositoryLookupKey(project);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getConfiguredAdoRepositoryGuid(project) {
|
|
50
|
+
const repositoryId = String(project?.repositoryId || '').trim();
|
|
51
|
+
return isAdoGuid(repositoryId) ? repositoryId : '';
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
function getAdoProjectLabel(project) {
|
|
41
55
|
return project?.name || project?.repoName || `${project?.adoOrg || 'unknown-org'}/${project?.adoProject || 'unknown-project'}`;
|
|
42
56
|
}
|
|
@@ -45,6 +59,85 @@ function logMissingAdoRepository(project, purpose) {
|
|
|
45
59
|
log('error', `${purpose} disabled for project ${getAdoProjectLabel(project)}: missing project.repositoryId and project.repoName; configure one so Azure DevOps repository API calls can target the repo`);
|
|
46
60
|
}
|
|
47
61
|
|
|
62
|
+
function adoConfigPath() {
|
|
63
|
+
return path.join(shared.MINIONS_DIR, 'config.json');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sameAdoProject(a, b) {
|
|
67
|
+
if (!a || !b) return false;
|
|
68
|
+
if (a.name && b.name && a.name === b.name) return true;
|
|
69
|
+
if (a.localPath && b.localPath && path.resolve(a.localPath) === path.resolve(b.localPath)) return true;
|
|
70
|
+
return String(a.adoOrg || '') === String(b.adoOrg || '')
|
|
71
|
+
&& String(a.adoProject || '') === String(b.adoProject || '')
|
|
72
|
+
&& String(a.repoName || '') === String(b.repoName || '')
|
|
73
|
+
&& String(a.repoHost || 'ado').toLowerCase() === String(b.repoHost || 'ado').toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function persistAdoRepositoryGuid(project, guid, repoName) {
|
|
77
|
+
if (!isAdoGuid(guid)) return;
|
|
78
|
+
const previous = String(project?.repositoryId || '').trim();
|
|
79
|
+
project.repositoryId = guid;
|
|
80
|
+
if (!project.repoName && repoName) project.repoName = repoName;
|
|
81
|
+
if (previous === guid) return;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
let persisted = false;
|
|
85
|
+
mutateJsonFileLocked(adoConfigPath(), (config) => {
|
|
86
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) return config;
|
|
87
|
+
if (!Array.isArray(config.projects)) return config;
|
|
88
|
+
const target = config.projects.find(p => sameAdoProject(p, project));
|
|
89
|
+
if (!target) return config;
|
|
90
|
+
target.repositoryId = guid;
|
|
91
|
+
if (!target.repoName && repoName) target.repoName = repoName;
|
|
92
|
+
persisted = true;
|
|
93
|
+
return config;
|
|
94
|
+
}, { defaultValue: { projects: [] }, skipWriteIfUnchanged: true });
|
|
95
|
+
if (persisted) {
|
|
96
|
+
log('info', `Resolved ADO repository GUID for ${getAdoProjectLabel(project)}: ${previous || project.repoName || 'unknown'} → ${guid}`);
|
|
97
|
+
} else {
|
|
98
|
+
log('warn', `Resolved ADO repository GUID for ${getAdoProjectLabel(project)} but could not find the project in config.json to persist it`);
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
log('warn', `Resolved ADO repository GUID for ${getAdoProjectLabel(project)} but failed to persist it: ${e.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function resolveAdoBuildRepositoryGuid(project, token, orgBase, purpose, opts = {}) {
|
|
106
|
+
const configured = getConfiguredAdoRepositoryGuid(project);
|
|
107
|
+
if (configured) return configured;
|
|
108
|
+
|
|
109
|
+
const lookupKey = getAdoRepositoryLookupKey(project);
|
|
110
|
+
if (!lookupKey) {
|
|
111
|
+
logMissingAdoRepository(project, purpose);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const repoUrl = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodeURIComponent(lookupKey)}?api-version=7.1`;
|
|
117
|
+
const repoData = await adoFetch(repoUrl, token, opts);
|
|
118
|
+
const guid = String(repoData?.id || '').trim();
|
|
119
|
+
if (!isAdoGuid(guid)) {
|
|
120
|
+
log('error', `${purpose} disabled for project ${getAdoProjectLabel(project)}: ADO repository lookup for "${lookupKey}" did not return a repository GUID`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
persistAdoRepositoryGuid(project, guid, repoData?.name || project.repoName || '');
|
|
124
|
+
return guid;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
log('warn', `${purpose} could not resolve repository GUID for project ${getAdoProjectLabel(project)} from "${lookupKey}": ${e.message}`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function markBuildStatusStale(pr, detail) {
|
|
132
|
+
pr._buildStatusStale = true;
|
|
133
|
+
if (detail) pr._buildStatusDetail = detail;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function clearBuildStatusStale(pr) {
|
|
137
|
+
if (pr._buildStatusStale) delete pr._buildStatusStale;
|
|
138
|
+
if (pr._buildStatusDetail) delete pr._buildStatusDetail;
|
|
139
|
+
}
|
|
140
|
+
|
|
48
141
|
// ── Build/Review Status Helpers ───────────────────────────────────────────────
|
|
49
142
|
|
|
50
143
|
/** Classify an array of ADO build records into a single status string. */
|
|
@@ -353,9 +446,6 @@ async function pollPrStatus(config) {
|
|
|
353
446
|
const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodedRepoId}/pullrequests/${prNum}`;
|
|
354
447
|
let updated = false;
|
|
355
448
|
|
|
356
|
-
// Clear stale flag — we're attempting a fresh poll
|
|
357
|
-
if (pr._buildStatusStale) { delete pr._buildStatusStale; updated = true; }
|
|
358
|
-
|
|
359
449
|
const prData = await adoFetch(`${repoBase}?api-version=7.1`, token);
|
|
360
450
|
|
|
361
451
|
const sourceBranch = stripRefsHeads(prData.sourceRefName);
|
|
@@ -387,6 +477,8 @@ async function pollPrStatus(config) {
|
|
|
387
477
|
delete pr.buildFailReason;
|
|
388
478
|
delete pr.buildErrorLog;
|
|
389
479
|
delete pr._buildFailNotified;
|
|
480
|
+
delete pr._buildStatusStale;
|
|
481
|
+
delete pr._buildStatusDetail;
|
|
390
482
|
delete pr.buildFixAttempts;
|
|
391
483
|
delete pr.buildFixEscalated;
|
|
392
484
|
}
|
|
@@ -485,42 +577,59 @@ async function pollPrStatus(config) {
|
|
|
485
577
|
// merge commit (same ref accumulates builds across all prior pushes to the PR).
|
|
486
578
|
const prNumber = pr.prNumber;
|
|
487
579
|
const mergeCommitId = prData.lastMergeCommit?.commitId;
|
|
488
|
-
let buildStatus = 'none';
|
|
489
|
-
let buildFailReason = '';
|
|
580
|
+
let buildStatus = pr.buildStatus || 'none';
|
|
581
|
+
let buildFailReason = pr.buildFailReason || '';
|
|
490
582
|
let buildStatuses = []; // for error log fetching
|
|
583
|
+
let buildStatusResolved = true;
|
|
584
|
+
let buildStatusStaleDetail = '';
|
|
491
585
|
|
|
492
586
|
if (prNumber && mergeCommitId) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
587
|
+
const buildRepositoryGuid = await resolveAdoBuildRepositoryGuid(project, token, orgBase, 'ADO build polling');
|
|
588
|
+
if (!buildRepositoryGuid) {
|
|
589
|
+
buildStatusResolved = false;
|
|
590
|
+
buildStatusStaleDetail = 'ADO Builds API requires a repository GUID; repository GUID could not be resolved from project.repositoryId/project.repoName';
|
|
591
|
+
} else {
|
|
592
|
+
try {
|
|
593
|
+
const mergeRef = encodeURIComponent(`refs/pull/${prNumber}/merge`);
|
|
594
|
+
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${encodeURIComponent(buildRepositoryGuid)}&repositoryType=TfsGit&$top=25&api-version=7.1`;
|
|
595
|
+
const buildsData = await adoFetch(buildsUrl, token);
|
|
596
|
+
const allBuilds = buildsData?.value || [];
|
|
597
|
+
const prBuilds = allBuilds.filter(b => b.sourceVersion === mergeCommitId);
|
|
598
|
+
buildStatus = 'none';
|
|
599
|
+
buildFailReason = '';
|
|
600
|
+
|
|
601
|
+
if (prBuilds.length > 0) {
|
|
602
|
+
buildStatus = classifyBuildStatus(prBuilds);
|
|
603
|
+
if (buildStatus === 'failing') {
|
|
604
|
+
const failed = prBuilds.find(b => b.result === 'failed');
|
|
605
|
+
buildFailReason = failed?.definition?.name || 'Build failed';
|
|
606
|
+
// Build fake status objects for error log fetching
|
|
607
|
+
buildStatuses = prBuilds.filter(b => b.result === 'failed').map(b => ({
|
|
608
|
+
state: 'failed', targetUrl: `${orgBase}/${project.adoProject}/_build/results?buildId=${b.id}`,
|
|
609
|
+
_buildId: String(b.id),
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
612
|
+
} else if (allBuilds.length > 0 && pr.buildStatus) {
|
|
613
|
+
// Stale merge-commit fallback — ADO returned builds for this PR's merge ref
|
|
614
|
+
// but none target the current `mergeCommitId`. Most likely the target branch
|
|
615
|
+
// moved, ADO recomputed the merge commit, but no new source-side changes
|
|
616
|
+
// triggered a rebuild. Preserve the previous `pr.buildStatus` so the tracker
|
|
617
|
+
// reflects the last known truth instead of flipping to a spurious 'none'.
|
|
618
|
+
// Also log a warn so stale states are detectable in engine logs. Issue #1233.
|
|
619
|
+
const sampleSv = (allBuilds[0]?.sourceVersion || '').slice(0, 8);
|
|
620
|
+
log('warn', `PR ${pr.id} build: merge-commit mismatch — ${allBuilds.length} build(s) on merge ref, none target current merge commit ${String(mergeCommitId).slice(0, 8)} (sample sourceVersion ${sampleSv}); preserving previous buildStatus '${pr.buildStatus}'`);
|
|
621
|
+
buildStatus = pr.buildStatus;
|
|
622
|
+
if (pr.buildFailReason) buildFailReason = pr.buildFailReason;
|
|
510
623
|
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
// triggered a rebuild. Preserve the previous `pr.buildStatus` so the tracker
|
|
516
|
-
// reflects the last known truth instead of flipping to a spurious 'none'.
|
|
517
|
-
// Also log a warn so stale states are detectable in engine logs. Issue #1233.
|
|
518
|
-
const sampleSv = (allBuilds[0]?.sourceVersion || '').slice(0, 8);
|
|
519
|
-
log('warn', `PR ${pr.id} build: merge-commit mismatch — ${allBuilds.length} build(s) on merge ref, none target current merge commit ${String(mergeCommitId).slice(0, 8)} (sample sourceVersion ${sampleSv}); preserving previous buildStatus '${pr.buildStatus}'`);
|
|
520
|
-
buildStatus = pr.buildStatus;
|
|
521
|
-
if (pr.buildFailReason) buildFailReason = pr.buildFailReason;
|
|
624
|
+
} catch (e) {
|
|
625
|
+
buildStatusResolved = false;
|
|
626
|
+
buildStatusStaleDetail = `ADO build query failed: ${e.message}`;
|
|
627
|
+
log('warn', `ADO build query for ${pr.id}: ${e.message}; preserving previous buildStatus '${pr.buildStatus || 'none'}'`);
|
|
522
628
|
}
|
|
523
|
-
}
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
buildStatus = 'none';
|
|
632
|
+
buildFailReason = '';
|
|
524
633
|
}
|
|
525
634
|
|
|
526
635
|
// Record actual poll time — makes lastBuildCheck reflect when the engine last
|
|
@@ -528,50 +637,64 @@ async function pollPrStatus(config) {
|
|
|
528
637
|
pr.lastBuildCheck = ts();
|
|
529
638
|
updated = true;
|
|
530
639
|
|
|
531
|
-
if (
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
else delete pr.buildFailReason;
|
|
536
|
-
// Build transitioned — clear grace period and auto-complete flag
|
|
537
|
-
delete pr._buildFixPushedAt;
|
|
538
|
-
if (buildStatus === 'failing') delete pr._autoCompleted;
|
|
539
|
-
if (buildStatus !== 'failing') {
|
|
540
|
-
delete pr._buildFailNotified;
|
|
541
|
-
// Preserve buildErrorLog + buildFixAttempts through transient 'none'/'running'
|
|
542
|
-
// transitions — only clear on confirmed 'passing' recovery. Issue #1232: 'none'
|
|
543
|
-
// can also occur when ADO recomputes the merge commit after a target-branch
|
|
544
|
-
// update but no new builds have been triggered yet (filter by sourceVersion
|
|
545
|
-
// returns []), which previously wiped the last known error log and caused
|
|
546
|
-
// fix agents to be dispatched blind.
|
|
547
|
-
if (buildStatus === 'passing') {
|
|
548
|
-
delete pr.buildErrorLog;
|
|
549
|
-
// Reset build fix retry counter on recovery — allows fresh auto-fix cycles if build breaks again
|
|
550
|
-
if (pr.buildFixAttempts) { delete pr.buildFixAttempts; delete pr.buildFixEscalated; }
|
|
551
|
-
}
|
|
640
|
+
if (buildStatusResolved) {
|
|
641
|
+
if (pr._buildStatusStale || pr._buildStatusDetail) {
|
|
642
|
+
clearBuildStatusStale(pr);
|
|
643
|
+
updated = true;
|
|
552
644
|
}
|
|
645
|
+
} else {
|
|
646
|
+
markBuildStatusStale(pr, buildStatusStaleDetail);
|
|
553
647
|
updated = true;
|
|
648
|
+
}
|
|
554
649
|
|
|
555
|
-
|
|
556
|
-
if (buildStatus
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if (
|
|
565
|
-
pr.
|
|
566
|
-
|
|
650
|
+
if (buildStatusResolved) {
|
|
651
|
+
if (pr.buildStatus !== buildStatus) {
|
|
652
|
+
log('info', `PR ${pr.id} build: ${pr.buildStatus || 'none'} → ${buildStatus}${buildFailReason ? ' (' + buildFailReason + ')' : ''}`);
|
|
653
|
+
pr.buildStatus = buildStatus;
|
|
654
|
+
if (buildFailReason) pr.buildFailReason = buildFailReason;
|
|
655
|
+
else delete pr.buildFailReason;
|
|
656
|
+
// Build transitioned — clear grace period and auto-complete flag
|
|
657
|
+
delete pr._buildFixPushedAt;
|
|
658
|
+
if (buildStatus === 'failing') delete pr._autoCompleted;
|
|
659
|
+
if (buildStatus !== 'failing') {
|
|
660
|
+
delete pr._buildFailNotified;
|
|
661
|
+
delete pr._buildStatusStale;
|
|
662
|
+
delete pr._buildStatusDetail;
|
|
663
|
+
// Preserve buildErrorLog + buildFixAttempts through transient 'none'/'running'
|
|
664
|
+
// transitions — only clear on confirmed 'passing' recovery. Issue #1232: 'none'
|
|
665
|
+
// can also occur when ADO recomputes the merge commit after a target-branch
|
|
666
|
+
// update but no new builds have been triggered yet (filter by sourceVersion
|
|
667
|
+
// returns []), which previously wiped the last known error log and caused
|
|
668
|
+
// fix agents to be dispatched blind.
|
|
669
|
+
if (buildStatus === 'passing') {
|
|
670
|
+
delete pr.buildErrorLog;
|
|
671
|
+
// Reset build fix retry counter on recovery — allows fresh auto-fix cycles if build breaks again
|
|
672
|
+
if (pr.buildFixAttempts) { delete pr.buildFixAttempts; delete pr.buildFixEscalated; }
|
|
673
|
+
}
|
|
567
674
|
}
|
|
675
|
+
updated = true;
|
|
568
676
|
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
677
|
+
// Fetch actual compiler/build error logs when transitioning to failing
|
|
678
|
+
if (buildStatus === 'failing') {
|
|
679
|
+
const failedStatusObjs = buildStatuses.filter(s => s.state === 'failed' || s.state === 'error').slice(0, 10);
|
|
680
|
+
const logParts = [];
|
|
681
|
+
const seenBuildIds = new Set();
|
|
682
|
+
for (const failedStatusObj of failedStatusObjs) {
|
|
683
|
+
const errorLog = await fetchAdoBuildErrorLog(orgBase, project, failedStatusObj, token, pr, seenBuildIds);
|
|
684
|
+
if (errorLog) logParts.push(errorLog);
|
|
685
|
+
}
|
|
686
|
+
if (logParts.length > 0) {
|
|
687
|
+
pr.buildErrorLog = logParts.join('\n\n');
|
|
688
|
+
log('info', `PR ${pr.id}: fetched error logs from ${logParts.length} failing pipeline(s)`);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Teams notification for build failure — non-blocking
|
|
692
|
+
try {
|
|
693
|
+
const teams = require('./teams');
|
|
694
|
+
const prFilePath = shared.projectPrPath(project);
|
|
695
|
+
teams.teamsNotifyPrEvent(pr, 'build-failed', project, prFilePath).catch(() => {});
|
|
696
|
+
} catch {}
|
|
697
|
+
}
|
|
575
698
|
}
|
|
576
699
|
}
|
|
577
700
|
|
|
@@ -618,7 +741,7 @@ async function pollPrStatus(config) {
|
|
|
618
741
|
// Auth errors → mark build status stale so dashboard shows uncertainty
|
|
619
742
|
// and engine re-polls on next tick instead of waiting 6 ticks
|
|
620
743
|
if (isAdoAuthError(err)) {
|
|
621
|
-
pr
|
|
744
|
+
markBuildStatusStale(pr, `ADO auth error: ${err.message}`);
|
|
622
745
|
_adoPollHadAuthFailure = true;
|
|
623
746
|
log('warn', `PR ${pr.id}: build status marked stale (auth error: ${err.message})`);
|
|
624
747
|
return true; // count as updated to persist the stale flag
|
|
@@ -923,6 +1046,8 @@ async function checkLiveReviewStatus(pr, project) {
|
|
|
923
1046
|
* {
|
|
924
1047
|
* buildStatus: 'failing' | 'passing' | 'running' | 'none' | null,
|
|
925
1048
|
* mergeConflict: boolean,
|
|
1049
|
+
* buildStatusStale?: boolean,
|
|
1050
|
+
* buildStatusDetail?: string,
|
|
926
1051
|
* }
|
|
927
1052
|
*
|
|
928
1053
|
* `buildStatus` is null when ADO has builds on the merge ref but none target the
|
|
@@ -937,12 +1062,12 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
937
1062
|
const orgBase = shared.getAdoOrgBase(project);
|
|
938
1063
|
const prNum = shared.getPrNumber(pr);
|
|
939
1064
|
if (!prNum) return null;
|
|
940
|
-
const
|
|
941
|
-
if (!
|
|
1065
|
+
const adoRepositoryLookupKey = getAdoRepositoryLookupKey(project);
|
|
1066
|
+
if (!adoRepositoryLookupKey) {
|
|
942
1067
|
logMissingAdoRepository(project, 'ADO live build/conflict check');
|
|
943
1068
|
return null;
|
|
944
1069
|
}
|
|
945
|
-
const encodedRepoId = encodeURIComponent(
|
|
1070
|
+
const encodedRepoId = encodeURIComponent(adoRepositoryLookupKey);
|
|
946
1071
|
const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodedRepoId}`;
|
|
947
1072
|
const prUrl = `${repoBase}/pullrequests/${prNum}?api-version=7.1`;
|
|
948
1073
|
// 4s timeout — same budget as checkLiveReviewStatus. This is a pre-dispatch
|
|
@@ -959,23 +1084,35 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
959
1084
|
// pollPrStatus's narrowing logic so the live check and the cached poll
|
|
960
1085
|
// agree on what 'failing' / 'passing' / 'running' / 'none' mean.
|
|
961
1086
|
let buildStatus = null;
|
|
1087
|
+
let buildStatusStale = false;
|
|
1088
|
+
let buildStatusDetail = '';
|
|
962
1089
|
if (prData.status === 'active') {
|
|
963
1090
|
const mergeCommitId = prData.lastMergeCommit?.commitId;
|
|
964
1091
|
if (mergeCommitId) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1092
|
+
const buildRepositoryGuid = await resolveAdoBuildRepositoryGuid(project, token, orgBase, 'ADO live build check', { timeout: 4000 });
|
|
1093
|
+
if (!buildRepositoryGuid) {
|
|
1094
|
+
buildStatusStale = true;
|
|
1095
|
+
buildStatusDetail = 'ADO Builds API requires a repository GUID; repository GUID could not be resolved from project.repositoryId/project.repoName';
|
|
1096
|
+
} else {
|
|
1097
|
+
try {
|
|
1098
|
+
const mergeRef = encodeURIComponent(`refs/pull/${prNum}/merge`);
|
|
1099
|
+
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${encodeURIComponent(buildRepositoryGuid)}&repositoryType=TfsGit&$top=25&api-version=7.1`;
|
|
1100
|
+
const buildsData = await adoFetch(buildsUrl, token, { timeout: 4000 });
|
|
1101
|
+
const allBuilds = buildsData?.value || [];
|
|
1102
|
+
const prBuilds = allBuilds.filter(b => b.sourceVersion === mergeCommitId);
|
|
1103
|
+
if (prBuilds.length > 0) {
|
|
1104
|
+
buildStatus = classifyBuildStatus(prBuilds);
|
|
1105
|
+
} else if (allBuilds.length === 0) {
|
|
1106
|
+
buildStatus = 'none';
|
|
1107
|
+
}
|
|
1108
|
+
// else: merge-commit mismatch — leave buildStatus null so caller
|
|
1109
|
+
// falls back to cached state (issue #1233).
|
|
1110
|
+
} catch (e) {
|
|
1111
|
+
buildStatusStale = true;
|
|
1112
|
+
buildStatusDetail = `ADO live build query failed: ${e.message}`;
|
|
1113
|
+
log('warn', `Live build check builds query for ${pr.id}: ${e.message}`);
|
|
975
1114
|
}
|
|
976
|
-
|
|
977
|
-
// falls back to cached state (issue #1233).
|
|
978
|
-
} catch (e) { log('warn', `Live build check builds query for ${pr.id}: ${e.message}`); }
|
|
1115
|
+
}
|
|
979
1116
|
} else {
|
|
980
1117
|
// No merge commit yet — likely conflict or fresh PR. Treat as 'none'
|
|
981
1118
|
// so a stale 'failing' cache can be cleared by the caller.
|
|
@@ -983,7 +1120,11 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
983
1120
|
}
|
|
984
1121
|
}
|
|
985
1122
|
|
|
986
|
-
return {
|
|
1123
|
+
return {
|
|
1124
|
+
buildStatus,
|
|
1125
|
+
mergeConflict,
|
|
1126
|
+
...(buildStatusStale ? { buildStatusStale, buildStatusDetail } : {}),
|
|
1127
|
+
};
|
|
987
1128
|
} catch (e) {
|
|
988
1129
|
log('warn', `Live build/conflict check for ${pr.id}: ${e.message}`);
|
|
989
1130
|
return null;
|
|
@@ -1007,7 +1148,8 @@ async function fetchAdoPrMetadata(prNum, adoOrg, adoProj, adoRepo) {
|
|
|
1007
1148
|
/**
|
|
1008
1149
|
* Fetch live PR and build status for a single PR number.
|
|
1009
1150
|
* Used by engine/ado-status.js so agents can check CI without raw curl calls.
|
|
1010
|
-
* Returns { prNumber, title, branch, status, reviewStatus, buildStatus,
|
|
1151
|
+
* Returns { prNumber, title, branch, status, reviewStatus, buildStatus,
|
|
1152
|
+
* buildStatusStale?, buildStatusDetail?, buildErrorLog?,
|
|
1011
1153
|
* mergeConflict, url, project } or null on auth failure.
|
|
1012
1154
|
*/
|
|
1013
1155
|
async function fetchSinglePrBuildStatus(project, prNumber) {
|
|
@@ -1015,29 +1157,45 @@ async function fetchSinglePrBuildStatus(project, prNumber) {
|
|
|
1015
1157
|
if (!token) return null;
|
|
1016
1158
|
|
|
1017
1159
|
const orgBase = getAdoOrgBase(project);
|
|
1018
|
-
const
|
|
1019
|
-
if (!
|
|
1160
|
+
const adoRepositoryLookupKey = getAdoRepositoryLookupKey(project);
|
|
1161
|
+
if (!adoRepositoryLookupKey) {
|
|
1020
1162
|
logMissingAdoRepository(project, 'ADO single PR status fetch');
|
|
1021
1163
|
return null;
|
|
1022
1164
|
}
|
|
1023
|
-
const encodedRepoId = encodeURIComponent(
|
|
1165
|
+
const encodedRepoId = encodeURIComponent(adoRepositoryLookupKey);
|
|
1024
1166
|
const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodedRepoId}`;
|
|
1025
|
-
const mergeRef = encodeURIComponent(`refs/pull/${prNumber}/merge`);
|
|
1026
|
-
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${encodedRepoId}&repositoryType=TfsGit&$top=25&api-version=7.1`;
|
|
1027
1167
|
|
|
1028
|
-
// Fetch PR metadata and
|
|
1029
|
-
const [prData,
|
|
1168
|
+
// Fetch PR metadata and resolve the Builds API repository GUID in parallel.
|
|
1169
|
+
const [prData, buildRepositoryGuid] = await Promise.all([
|
|
1030
1170
|
adoFetch(`${repoBase}/pullrequests/${prNumber}?api-version=7.1`, token),
|
|
1031
|
-
|
|
1171
|
+
resolveAdoBuildRepositoryGuid(project, token, orgBase, 'ADO single PR status fetch'),
|
|
1032
1172
|
]);
|
|
1033
1173
|
if (!prData) return null;
|
|
1034
1174
|
|
|
1175
|
+
let buildsData = null;
|
|
1176
|
+
let buildStatusStale = false;
|
|
1177
|
+
let buildStatusDetail = '';
|
|
1178
|
+
if (buildRepositoryGuid) {
|
|
1179
|
+
try {
|
|
1180
|
+
const mergeRef = encodeURIComponent(`refs/pull/${prNumber}/merge`);
|
|
1181
|
+
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${encodeURIComponent(buildRepositoryGuid)}&repositoryType=TfsGit&$top=25&api-version=7.1`;
|
|
1182
|
+
buildsData = await adoFetch(buildsUrl, token);
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
buildStatusStale = true;
|
|
1185
|
+
buildStatusDetail = `ADO build query failed: ${e.message}`;
|
|
1186
|
+
log('warn', `fetchSinglePrBuildStatus builds query for PR #${prNumber}: ${e.message}`);
|
|
1187
|
+
}
|
|
1188
|
+
} else {
|
|
1189
|
+
buildStatusStale = true;
|
|
1190
|
+
buildStatusDetail = 'ADO Builds API requires a repository GUID; repository GUID could not be resolved from project.repositoryId/project.repoName';
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1035
1193
|
const mergeCommitId = prData.lastMergeCommit?.commitId;
|
|
1036
1194
|
const prBuilds = mergeCommitId
|
|
1037
1195
|
? (buildsData?.value || []).filter(b => b.sourceVersion === mergeCommitId)
|
|
1038
1196
|
: [];
|
|
1039
1197
|
|
|
1040
|
-
let buildStatus = classifyBuildStatus(prBuilds);
|
|
1198
|
+
let buildStatus = buildStatusStale ? null : classifyBuildStatus(prBuilds);
|
|
1041
1199
|
let buildErrorLog = null;
|
|
1042
1200
|
|
|
1043
1201
|
if (buildStatus === 'failing') {
|
|
@@ -1067,6 +1225,7 @@ async function fetchSinglePrBuildStatus(project, prNumber) {
|
|
|
1067
1225
|
status: prData.status || 'unknown',
|
|
1068
1226
|
reviewStatus: votesToReviewStatus(votes),
|
|
1069
1227
|
buildStatus,
|
|
1228
|
+
...(buildStatusStale ? { buildStatusStale, buildStatusDetail } : {}),
|
|
1070
1229
|
...(buildErrorLog ? { buildErrorLog } : {}),
|
|
1071
1230
|
mergeConflict: prData.mergeStatus === 'conflicts',
|
|
1072
1231
|
url: prUrl,
|
package/engine/lifecycle.js
CHANGED
|
@@ -787,9 +787,15 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
787
787
|
if (!output || !String(output).trim()) {
|
|
788
788
|
const today = dateStamp();
|
|
789
789
|
const inboxFiles = getInboxFiles().filter(f => f.includes(agentId) && f.includes(today));
|
|
790
|
+
const currentItemId = meta?.item?.id ? String(meta.item.id) : '';
|
|
791
|
+
function isCurrentItemInboxNote(fileName, content) {
|
|
792
|
+
if (!currentItemId) return true;
|
|
793
|
+
return String(fileName || '').includes(currentItemId) || String(content || '').includes(currentItemId);
|
|
794
|
+
}
|
|
790
795
|
for (const f of inboxFiles) {
|
|
791
796
|
const content = safeRead(path.join(INBOX_DIR, f));
|
|
792
797
|
if (!content) continue;
|
|
798
|
+
if (!isCurrentItemInboxNote(f, content)) continue;
|
|
793
799
|
const prHeaderPattern = /(?:^|\n)\s*\*{0,2}(?:PR|Pull\s+Request|E2E\s+PR)\s+(?:created|opened|submitted)\*{0,2}\s*[:\-]\s*([^\n]+)/gi;
|
|
794
800
|
while ((match = prHeaderPattern.exec(content)) !== null) {
|
|
795
801
|
addPrUrlEvidence(match[1]);
|
package/engine.js
CHANGED
|
@@ -2383,6 +2383,16 @@ async function discoverFromPrs(config, project) {
|
|
|
2383
2383
|
try {
|
|
2384
2384
|
const checkBcFn = project.repoHost === 'github' ? ghCheckLiveBuildAndConflict : adoCheckLiveBuildAndConflict;
|
|
2385
2385
|
const live = await checkBcFn(pr, project);
|
|
2386
|
+
if (live?.buildStatusStale) {
|
|
2387
|
+
try {
|
|
2388
|
+
mutatePullRequests(projectPrPath(project), prs => {
|
|
2389
|
+
const target = shared.findPrRecord(prs, pr, project);
|
|
2390
|
+
if (!target) return;
|
|
2391
|
+
target._buildStatusStale = true;
|
|
2392
|
+
if (live.buildStatusDetail) target._buildStatusDetail = live.buildStatusDetail;
|
|
2393
|
+
});
|
|
2394
|
+
} catch {}
|
|
2395
|
+
}
|
|
2386
2396
|
if (live && live.buildStatus && live.buildStatus !== 'failing') {
|
|
2387
2397
|
log('info', `Pre-dispatch build check: ${pr.id} build is ${live.buildStatus} (cached was failing) — skipping build-fix`);
|
|
2388
2398
|
// Persist the fresh status so subsequent ticks don't re-check on every pass
|
|
@@ -2391,6 +2401,8 @@ async function discoverFromPrs(config, project) {
|
|
|
2391
2401
|
const target = shared.findPrRecord(prs, pr, project);
|
|
2392
2402
|
if (!target) return;
|
|
2393
2403
|
target.buildStatus = live.buildStatus;
|
|
2404
|
+
delete target._buildStatusStale;
|
|
2405
|
+
delete target._buildStatusDetail;
|
|
2394
2406
|
if (live.buildStatus === 'passing') {
|
|
2395
2407
|
delete target.buildErrorLog;
|
|
2396
2408
|
delete target.buildFailReason;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1649",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|