@link-assistant/hive-mind 1.71.0 → 1.71.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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/config.lib.mjs +9 -0
- package/src/github-merge-ci.lib.mjs +34 -0
- package/src/github-merge.lib.mjs +4 -2
- package/src/telegram-merge-queue.lib.mjs +232 -9
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/config.lib.mjs
CHANGED
|
@@ -662,6 +662,15 @@ export const mergeQueue = {
|
|
|
662
662
|
// Issue #1341: Polling interval for post-merge CI status (in milliseconds)
|
|
663
663
|
// Default: 30 seconds (30000ms) - balance between responsiveness and API rate limits
|
|
664
664
|
postMergeCIPollIntervalMs: parseIntWithDefault('HIVE_MIND_MERGE_QUEUE_POST_MERGE_CI_POLL_INTERVAL_MS', 30 * 1000),
|
|
665
|
+
// Issue #1807: Timeout (ms) the sequential auto-resolve pass will wait for
|
|
666
|
+
// a single `/solve <pr> --auto-merge` session to land its PR. Conflict-
|
|
667
|
+
// resolution sessions can be long-running because Claude has to recompute
|
|
668
|
+
// merges and re-run CI; default is 4 hours.
|
|
669
|
+
autoResolveWaitTimeoutMs: parseIntWithDefault('HIVE_MIND_MERGE_QUEUE_AUTO_RESOLVE_WAIT_TIMEOUT_MS', 4 * 60 * 60 * 1000),
|
|
670
|
+
// Issue #1807: Polling interval (ms) for `gh pr view` lifecycle checks
|
|
671
|
+
// during the auto-resolve wait. 60 seconds balances responsiveness with
|
|
672
|
+
// GitHub API rate limits over the timeout window above.
|
|
673
|
+
autoResolvePollIntervalMs: parseIntWithDefault('HIVE_MIND_MERGE_QUEUE_AUTO_RESOLVE_POLL_INTERVAL_MS', 60 * 1000),
|
|
665
674
|
};
|
|
666
675
|
|
|
667
676
|
// Helper function to validate configuration values
|
|
@@ -289,8 +289,42 @@ export async function getMergeCommitSha(owner, repo, prNumber, verbose = false)
|
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Get the lifecycle state of a pull request (OPEN / CLOSED / MERGED) along
|
|
294
|
+
* with its mergeability state. Used by the sequential auto-resolve pass
|
|
295
|
+
* (issue #1807) to poll until a `/solve <pr> --auto-merge` session either
|
|
296
|
+
* lands the PR or fails.
|
|
297
|
+
*
|
|
298
|
+
* @param {string} owner - Repository owner
|
|
299
|
+
* @param {string} repo - Repository name
|
|
300
|
+
* @param {number} prNumber - Pull request number
|
|
301
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
302
|
+
* @returns {Promise<{state: string|null, mergeStateStatus: string|null, mergeable: string|null, error: string|null}>}
|
|
303
|
+
*/
|
|
304
|
+
export async function getPRStatus(owner, repo, prNumber, verbose = false) {
|
|
305
|
+
try {
|
|
306
|
+
const { stdout } = await exec(`gh pr view ${prNumber} --repo ${owner}/${repo} --json state,mergeStateStatus,mergeable`);
|
|
307
|
+
const pr = JSON.parse(stdout.trim());
|
|
308
|
+
if (verbose) {
|
|
309
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} state=${pr.state}, mergeStateStatus=${pr.mergeStateStatus}, mergeable=${pr.mergeable}`);
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
state: pr.state || null,
|
|
313
|
+
mergeStateStatus: pr.mergeStateStatus || null,
|
|
314
|
+
mergeable: pr.mergeable || null,
|
|
315
|
+
error: null,
|
|
316
|
+
};
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (verbose) {
|
|
319
|
+
console.log(`[VERBOSE] /merge: Error getting PR #${prNumber} status: ${error.message}`);
|
|
320
|
+
}
|
|
321
|
+
return { state: null, mergeStateStatus: null, mergeable: null, error: error.message };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
292
325
|
export default {
|
|
293
326
|
waitForCommitCI,
|
|
294
327
|
checkBranchCIHealth,
|
|
295
328
|
getMergeCommitSha,
|
|
329
|
+
getPRStatus,
|
|
296
330
|
};
|
package/src/github-merge.lib.mjs
CHANGED
|
@@ -1389,8 +1389,9 @@ import { getCommitDate, checkPreviousPRCommitsHadCI, checkWorkflowsHavePRTrigger
|
|
|
1389
1389
|
export { getCommitDate, checkPreviousPRCommitsHadCI, checkWorkflowsHavePRTriggers };
|
|
1390
1390
|
|
|
1391
1391
|
// Issue #1341: Re-export post-merge CI functions from separate module
|
|
1392
|
-
|
|
1393
|
-
|
|
1392
|
+
// Issue #1807: getPRStatus is used by the sequential auto-resolve pass to poll PR lifecycle state.
|
|
1393
|
+
import { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha, getPRStatus } from './github-merge-ci.lib.mjs';
|
|
1394
|
+
export { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha, getPRStatus };
|
|
1394
1395
|
|
|
1395
1396
|
import { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas, getActivePRWorkflowRuns } from './github-merge-repo-actions.lib.mjs'; // Issue #1503, #1712
|
|
1396
1397
|
export { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas, getActivePRWorkflowRuns };
|
|
@@ -1427,6 +1428,7 @@ export default {
|
|
|
1427
1428
|
waitForCommitCI,
|
|
1428
1429
|
checkBranchCIHealth,
|
|
1429
1430
|
getMergeCommitSha,
|
|
1431
|
+
getPRStatus, // Issue #1807: sequential auto-resolve PR lifecycle polling
|
|
1430
1432
|
getActiveRepoWorkflows,
|
|
1431
1433
|
getCommitDate,
|
|
1432
1434
|
checkPreviousPRCommitsHadCI,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* @see https://github.com/link-assistant/hive-mind/issues/1143
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { getAllReadyPRs, checkPRCIStatus, checkPRMergeable, mergePullRequest, waitForCI, ensureReadyLabel, waitForBranchCI, getDefaultBranch, waitForCommitCI, checkBranchCIHealth, getMergeCommitSha, syncReadyTags } from './github-merge.lib.mjs';
|
|
19
|
+
import { getAllReadyPRs, checkPRCIStatus, checkPRMergeable, mergePullRequest, waitForCI, ensureReadyLabel, waitForBranchCI, getDefaultBranch, waitForCommitCI, checkBranchCIHealth, getMergeCommitSha, getPRStatus, syncReadyTags } from './github-merge.lib.mjs';
|
|
20
20
|
import { mergeQueue as mergeQueueConfig } from './config.lib.mjs';
|
|
21
21
|
import { getProgressBar } from './limits.lib.mjs';
|
|
22
22
|
|
|
@@ -102,6 +102,12 @@ export const MERGE_QUEUE_CONFIG = {
|
|
|
102
102
|
CHECK_BRANCH_CI_HEALTH_BEFORE_START: mergeQueueConfig.checkBranchCIHealthBeforeStart,
|
|
103
103
|
POST_MERGE_CI_TIMEOUT_MS: mergeQueueConfig.postMergeCITimeoutMs,
|
|
104
104
|
POST_MERGE_CI_POLL_INTERVAL_MS: mergeQueueConfig.postMergeCIPollIntervalMs,
|
|
105
|
+
|
|
106
|
+
// Issue #1807: Sequential auto-resolve — wait for each `/solve --auto-merge`
|
|
107
|
+
// session to land its PR (or fail) before spawning the next one. These
|
|
108
|
+
// timeouts apply to the polling loop in `waitForAutoResolveCompletion`.
|
|
109
|
+
AUTO_RESOLVE_WAIT_TIMEOUT_MS: mergeQueueConfig.autoResolveWaitTimeoutMs,
|
|
110
|
+
AUTO_RESOLVE_POLL_INTERVAL_MS: mergeQueueConfig.autoResolvePollIntervalMs,
|
|
105
111
|
};
|
|
106
112
|
|
|
107
113
|
/**
|
|
@@ -191,6 +197,17 @@ export class MergeQueueProcessor {
|
|
|
191
197
|
// Issue #1805: track auto-resolve progress so the renderer can surface it.
|
|
192
198
|
this.autoResolveActive = false;
|
|
193
199
|
this.autoResolveCurrent = null;
|
|
200
|
+
// Issue #1807: sequential auto-resolve — track which wait phase is active
|
|
201
|
+
// for the current auto-resolve item so the progress message can render
|
|
202
|
+
// distinct "spawning…", "waiting for merge…", and "waiting for CI…" lines.
|
|
203
|
+
// Values: null | 'spawning' | 'awaiting-resolution' | 'awaiting-ci'.
|
|
204
|
+
this.autoResolvePhase = null;
|
|
205
|
+
this.autoResolveWaitStartedAt = null;
|
|
206
|
+
// For dependency injection in tests (issue #1807) — when set, the
|
|
207
|
+
// sequential auto-resolve pass uses this in place of `getPRStatus()`.
|
|
208
|
+
this.getPRStatus = typeof options.getPRStatus === 'function' ? options.getPRStatus : getPRStatus;
|
|
209
|
+
// Same idea for `getMergeCommitSha` so tests don't need to stub gh.
|
|
210
|
+
this.getMergeCommitSha = typeof options.getMergeCommitSha === 'function' ? options.getMergeCommitSha : getMergeCommitSha;
|
|
194
211
|
|
|
195
212
|
// Statistics
|
|
196
213
|
this.stats = {
|
|
@@ -539,11 +556,17 @@ export class MergeQueueProcessor {
|
|
|
539
556
|
}
|
|
540
557
|
|
|
541
558
|
/**
|
|
542
|
-
* Issue #1805: Iterate every conflict-skipped item and hand it off
|
|
543
|
-
* `/solve <pr-url> --auto-merge` session via the injected
|
|
544
|
-
* `spawnSolveSession` callback.
|
|
545
|
-
*
|
|
546
|
-
*
|
|
559
|
+
* Issue #1805 / #1807: Iterate every conflict-skipped item and hand it off
|
|
560
|
+
* to a `/solve <pr-url> --auto-merge` session via the injected
|
|
561
|
+
* `spawnSolveSession` callback. Sessions are processed STRICTLY
|
|
562
|
+
* sequentially — for each PR we:
|
|
563
|
+
* 1. Spawn the solve session and confirm the spawn succeeded.
|
|
564
|
+
* 2. Poll the PR until it becomes MERGED or CLOSED, or until
|
|
565
|
+
* `AUTO_RESOLVE_WAIT_TIMEOUT_MS` elapses.
|
|
566
|
+
* 3. If MERGED, await post-merge CI via `waitForPostMergeCI()` so the
|
|
567
|
+
* release pipeline drains before the next resolution starts.
|
|
568
|
+
* This back-pressure is what keeps the conflict pass from spinning up
|
|
569
|
+
* eight parallel Claude sessions (issue #1807).
|
|
547
570
|
*
|
|
548
571
|
* @returns {Promise<void>}
|
|
549
572
|
*/
|
|
@@ -571,7 +594,7 @@ export class MergeQueueProcessor {
|
|
|
571
594
|
}
|
|
572
595
|
|
|
573
596
|
this.autoResolveActive = true;
|
|
574
|
-
this.log(`Auto-resolve: dispatching ${conflicted.length} conflict PR(s) to /solve --auto-merge`);
|
|
597
|
+
this.log(`Auto-resolve: dispatching ${conflicted.length} conflict PR(s) sequentially to /solve --auto-merge`);
|
|
575
598
|
try {
|
|
576
599
|
for (const item of conflicted) {
|
|
577
600
|
if (this.isCancelled) {
|
|
@@ -581,10 +604,14 @@ export class MergeQueueProcessor {
|
|
|
581
604
|
|
|
582
605
|
item.status = MergeItemStatus.RESOLVING;
|
|
583
606
|
this.autoResolveCurrent = item.pr.number;
|
|
607
|
+
this.autoResolvePhase = 'spawning';
|
|
608
|
+
this.autoResolveWaitStartedAt = new Date();
|
|
584
609
|
if (this.onProgress) {
|
|
585
610
|
await this.onProgress(this.getProgressUpdate());
|
|
586
611
|
}
|
|
587
612
|
|
|
613
|
+
// Step 1 — spawn the solve session.
|
|
614
|
+
let spawned = false;
|
|
588
615
|
try {
|
|
589
616
|
const result = await this.spawnSolveSession({
|
|
590
617
|
url: item.pr.url,
|
|
@@ -596,8 +623,8 @@ export class MergeQueueProcessor {
|
|
|
596
623
|
|
|
597
624
|
if (result && result.success) {
|
|
598
625
|
item.autoResolveSession = result.sessionName || result.session || null;
|
|
599
|
-
this.stats.autoResolved++;
|
|
600
626
|
this.log(`Auto-resolve: spawned solve session for PR #${item.pr.number}${item.autoResolveSession ? ` (session ${item.autoResolveSession})` : ''}`);
|
|
627
|
+
spawned = true;
|
|
601
628
|
} else {
|
|
602
629
|
item.status = MergeItemStatus.RESOLVE_FAILED;
|
|
603
630
|
item.autoResolveError = (result && (result.error || result.warning)) || 'spawn failed';
|
|
@@ -611,6 +638,97 @@ export class MergeQueueProcessor {
|
|
|
611
638
|
console.error(`[ERROR] /merge-queue: auto-resolve error for PR #${item.pr.number}: ${item.autoResolveError}`);
|
|
612
639
|
}
|
|
613
640
|
|
|
641
|
+
if (!spawned) {
|
|
642
|
+
this.autoResolvePhase = null;
|
|
643
|
+
this.autoResolveWaitStartedAt = null;
|
|
644
|
+
if (this.onProgress) {
|
|
645
|
+
await this.onProgress(this.getProgressUpdate());
|
|
646
|
+
}
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Step 2 — wait for the spawned session to actually land (or fail)
|
|
651
|
+
// before starting the next one. This is the heart of issue #1807.
|
|
652
|
+
this.autoResolvePhase = 'awaiting-resolution';
|
|
653
|
+
this.autoResolveWaitStartedAt = new Date();
|
|
654
|
+
if (this.onProgress) {
|
|
655
|
+
await this.onProgress(this.getProgressUpdate());
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const waitResult = await this.waitForAutoResolveCompletion(item);
|
|
659
|
+
|
|
660
|
+
if (waitResult.outcome === 'merged') {
|
|
661
|
+
// Treat the PR as merged for accounting purposes. We bump the
|
|
662
|
+
// dedicated `autoResolved` counter (kept for backwards-compat with
|
|
663
|
+
// issue #1805 reporting) and also fold the merge into `stats.merged`
|
|
664
|
+
// so the final report's success percentage reflects what the queue
|
|
665
|
+
// ultimately accomplished.
|
|
666
|
+
item.status = MergeItemStatus.MERGED;
|
|
667
|
+
item.completedAt = new Date();
|
|
668
|
+
this.stats.autoResolved++;
|
|
669
|
+
this.stats.merged++;
|
|
670
|
+
// The PR previously sat in `skipped` because of the merge conflict;
|
|
671
|
+
// now that it's merged via auto-resolve, decrement that counter so
|
|
672
|
+
// we don't double-count it.
|
|
673
|
+
if (this.stats.skipped > 0) this.stats.skipped--;
|
|
674
|
+
this.log(`Auto-resolve: PR #${item.pr.number} merged by solve session`);
|
|
675
|
+
|
|
676
|
+
// Best-effort: capture the merge commit SHA so post-merge CI wait
|
|
677
|
+
// has something to poll on.
|
|
678
|
+
try {
|
|
679
|
+
await this.sleep(5000);
|
|
680
|
+
const sha = await this.getMergeCommitSha(this.owner, this.repo, item.pr.number, this.verbose);
|
|
681
|
+
if (sha && sha.sha) {
|
|
682
|
+
item.mergeCommitSha = sha.sha;
|
|
683
|
+
}
|
|
684
|
+
} catch (error) {
|
|
685
|
+
this.log(`Auto-resolve: could not get merge commit SHA for PR #${item.pr.number}: ${error.message}`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Step 3 — drain the merged PR's CI before continuing. We reuse
|
|
689
|
+
// the same `waitForPostMergeCI` path the main loop already uses so
|
|
690
|
+
// release workflows finish before the next resolution starts.
|
|
691
|
+
if (MERGE_QUEUE_CONFIG.WAIT_FOR_POST_MERGE_CI && item.mergeCommitSha && !this.isCancelled) {
|
|
692
|
+
this.autoResolvePhase = 'awaiting-ci';
|
|
693
|
+
this.autoResolveWaitStartedAt = new Date();
|
|
694
|
+
if (this.onProgress) {
|
|
695
|
+
await this.onProgress(this.getProgressUpdate());
|
|
696
|
+
}
|
|
697
|
+
const postCi = await this.waitForPostMergeCI(item);
|
|
698
|
+
if (!postCi.success && MERGE_QUEUE_CONFIG.STOP_ON_POST_MERGE_CI_FAILURE) {
|
|
699
|
+
// Stop the auto-resolve pass on CI failure so humans can
|
|
700
|
+
// investigate before more resolutions run on a broken branch.
|
|
701
|
+
// Mirrors the main loop's behaviour for issue #1341.
|
|
702
|
+
this.postMergeCIFailedRuns = postCi.failedRuns;
|
|
703
|
+
this.error = postCi.error;
|
|
704
|
+
this.log(`Auto-resolve: stopping pass after post-merge CI failure for PR #${item.pr.number}`);
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
} else if (waitResult.outcome === 'closed') {
|
|
709
|
+
item.status = MergeItemStatus.RESOLVE_FAILED;
|
|
710
|
+
item.autoResolveError = 'PR was closed without merging';
|
|
711
|
+
this.stats.autoResolveFailed++;
|
|
712
|
+
this.log(`Auto-resolve: PR #${item.pr.number} was closed without merging`);
|
|
713
|
+
} else if (waitResult.outcome === 'cancelled') {
|
|
714
|
+
this.log(`Auto-resolve: cancelled while waiting for PR #${item.pr.number}`);
|
|
715
|
+
// Don't downgrade the item status — the user can resume later.
|
|
716
|
+
break;
|
|
717
|
+
} else if (waitResult.outcome === 'timeout') {
|
|
718
|
+
item.status = MergeItemStatus.RESOLVE_FAILED;
|
|
719
|
+
item.autoResolveError = `timed out after ${Math.round((MERGE_QUEUE_CONFIG.AUTO_RESOLVE_WAIT_TIMEOUT_MS || 0) / 60000)}m waiting for resolution`;
|
|
720
|
+
this.stats.autoResolveFailed++;
|
|
721
|
+
this.log(`Auto-resolve: timed out waiting for PR #${item.pr.number}`);
|
|
722
|
+
} else {
|
|
723
|
+
// 'error' — surface the cause but don't halt the whole pass.
|
|
724
|
+
item.status = MergeItemStatus.RESOLVE_FAILED;
|
|
725
|
+
item.autoResolveError = waitResult.error || 'unknown error while waiting';
|
|
726
|
+
this.stats.autoResolveFailed++;
|
|
727
|
+
this.log(`Auto-resolve: error waiting for PR #${item.pr.number}: ${item.autoResolveError}`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
this.autoResolvePhase = null;
|
|
731
|
+
this.autoResolveWaitStartedAt = null;
|
|
614
732
|
if (this.onProgress) {
|
|
615
733
|
await this.onProgress(this.getProgressUpdate());
|
|
616
734
|
}
|
|
@@ -618,6 +736,89 @@ export class MergeQueueProcessor {
|
|
|
618
736
|
} finally {
|
|
619
737
|
this.autoResolveActive = false;
|
|
620
738
|
this.autoResolveCurrent = null;
|
|
739
|
+
this.autoResolvePhase = null;
|
|
740
|
+
this.autoResolveWaitStartedAt = null;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Issue #1807: Poll the PR's lifecycle state until the spawned solve
|
|
746
|
+
* session either lands (MERGED), gives up (CLOSED without merge), or the
|
|
747
|
+
* caller hits a configured timeout. The polling cadence and overall
|
|
748
|
+
* timeout come from `MERGE_QUEUE_CONFIG`. Cancellation is checked between
|
|
749
|
+
* polls so the user can abort a long resolution wait via the inline
|
|
750
|
+
* cancel button.
|
|
751
|
+
*
|
|
752
|
+
* Implementation note: we deliberately use `gh pr view --json
|
|
753
|
+
* state,mergeStateStatus` rather than tracking the screen session
|
|
754
|
+
* itself. `start-screen` keeps the screen alive after `solve` exits
|
|
755
|
+
* (via `exec bash`), so the screen lifecycle is not a reliable
|
|
756
|
+
* completion signal. The PR's lifecycle, on the other hand, is the
|
|
757
|
+
* authoritative source of truth for "did the resolution succeed?".
|
|
758
|
+
*
|
|
759
|
+
* @param {MergeQueueItem} item
|
|
760
|
+
* @returns {Promise<{outcome: 'merged'|'closed'|'cancelled'|'timeout'|'error', error?: string}>}
|
|
761
|
+
*/
|
|
762
|
+
async waitForAutoResolveCompletion(item) {
|
|
763
|
+
const timeout = MERGE_QUEUE_CONFIG.AUTO_RESOLVE_WAIT_TIMEOUT_MS;
|
|
764
|
+
const pollInterval = MERGE_QUEUE_CONFIG.AUTO_RESOLVE_POLL_INTERVAL_MS;
|
|
765
|
+
const startTime = Date.now();
|
|
766
|
+
let consecutiveErrors = 0;
|
|
767
|
+
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
768
|
+
|
|
769
|
+
this.log(`Auto-resolve: polling PR #${item.pr.number} until merged/closed (timeout=${Math.round(timeout / 60000)}m, poll=${Math.round(pollInterval / 1000)}s)`);
|
|
770
|
+
|
|
771
|
+
while (Date.now() - startTime < timeout) {
|
|
772
|
+
if (this.isCancelled) {
|
|
773
|
+
return { outcome: 'cancelled' };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
let status;
|
|
777
|
+
try {
|
|
778
|
+
status = await this.getPRStatus(this.owner, this.repo, item.pr.number, this.verbose);
|
|
779
|
+
} catch (error) {
|
|
780
|
+
consecutiveErrors++;
|
|
781
|
+
this.log(`Auto-resolve: error polling PR #${item.pr.number} (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}): ${error.message}`);
|
|
782
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
783
|
+
return { outcome: 'error', error: error.message };
|
|
784
|
+
}
|
|
785
|
+
await this.cancellableSleep(pollInterval);
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (status && !status.error) {
|
|
790
|
+
consecutiveErrors = 0;
|
|
791
|
+
if (status.state === 'MERGED') {
|
|
792
|
+
return { outcome: 'merged' };
|
|
793
|
+
}
|
|
794
|
+
if (status.state === 'CLOSED') {
|
|
795
|
+
return { outcome: 'closed' };
|
|
796
|
+
}
|
|
797
|
+
} else if (status && status.error) {
|
|
798
|
+
consecutiveErrors++;
|
|
799
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
800
|
+
return { outcome: 'error', error: status.error };
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
await this.cancellableSleep(pollInterval);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return { outcome: 'timeout' };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Issue #1807: Sleep helper that bails out as soon as cancellation is
|
|
812
|
+
* requested. Used by the auto-resolve poll loop so a `cancel()` call
|
|
813
|
+
* doesn't have to wait a full polling interval before taking effect.
|
|
814
|
+
*/
|
|
815
|
+
async cancellableSleep(ms) {
|
|
816
|
+
const step = Math.min(ms, 1000);
|
|
817
|
+
const deadline = Date.now() + ms;
|
|
818
|
+
while (Date.now() < deadline) {
|
|
819
|
+
if (this.isCancelled) return;
|
|
820
|
+
const remaining = deadline - Date.now();
|
|
821
|
+
await this.sleep(Math.min(step, remaining));
|
|
621
822
|
}
|
|
622
823
|
}
|
|
623
824
|
|
|
@@ -802,6 +1003,11 @@ export class MergeQueueProcessor {
|
|
|
802
1003
|
enabled: this.autoResolve,
|
|
803
1004
|
active: this.autoResolveActive,
|
|
804
1005
|
currentPrNumber: this.autoResolveCurrent,
|
|
1006
|
+
// Issue #1807: expose the sequential wait phase so the progress
|
|
1007
|
+
// renderer (and tests) can show whether we're spawning, waiting on
|
|
1008
|
+
// resolution, or waiting on post-merge CI.
|
|
1009
|
+
phase: this.autoResolvePhase,
|
|
1010
|
+
waitElapsedMs: this.autoResolveWaitStartedAt ? Date.now() - this.autoResolveWaitStartedAt.getTime() : 0,
|
|
805
1011
|
},
|
|
806
1012
|
progress: {
|
|
807
1013
|
processed,
|
|
@@ -917,7 +1123,24 @@ export class MergeQueueProcessor {
|
|
|
917
1123
|
if (update.autoResolve && update.autoResolve.active && update.autoResolve.currentPrNumber) {
|
|
918
1124
|
const activeItem = update.items.find(it => it.prNumber === update.autoResolve.currentPrNumber);
|
|
919
1125
|
const link = activeItem ? this.formatPrLink(activeItem.prNumber, activeItem.title, activeItem.prUrl) : `\\#${update.autoResolve.currentPrNumber}`;
|
|
920
|
-
|
|
1126
|
+
// Issue #1807: differentiate the wait phases so the user can tell at
|
|
1127
|
+
// a glance whether we're still spawning, polling for merge, or
|
|
1128
|
+
// waiting on post-merge CI to drain.
|
|
1129
|
+
const phase = update.autoResolve.phase;
|
|
1130
|
+
const elapsedMs = update.autoResolve.waitElapsedMs || 0;
|
|
1131
|
+
const elapsedSec = Math.round(elapsedMs / 1000);
|
|
1132
|
+
const elapsedMin = Math.floor(elapsedSec / 60);
|
|
1133
|
+
const elapsedSecRemainder = elapsedSec % 60;
|
|
1134
|
+
const elapsed = elapsedMs > 0 ? ` \\(${elapsedMin}m ${elapsedSecRemainder}s\\)` : '';
|
|
1135
|
+
if (phase === 'awaiting-resolution') {
|
|
1136
|
+
message += `🛠️ Auto\\-resolving ${link}: waiting for resolution${elapsed}\\.\\.\\.\n\n`;
|
|
1137
|
+
} else if (phase === 'awaiting-ci') {
|
|
1138
|
+
message += `🛠️ Auto\\-resolving ${link}: waiting for post\\-merge CI${elapsed}\\.\\.\\.\n\n`;
|
|
1139
|
+
} else if (phase === 'spawning') {
|
|
1140
|
+
message += `🛠️ Auto\\-resolving ${link}: dispatching solve session${elapsed}\\.\\.\\.\n\n`;
|
|
1141
|
+
} else {
|
|
1142
|
+
message += `🛠️ Auto\\-resolving ${link}\n\n`;
|
|
1143
|
+
}
|
|
921
1144
|
} else if (update.current && !this.waitingForTargetBranchCI && !this.waitingForPostMergeCI) {
|
|
922
1145
|
// Current item being processed
|
|
923
1146
|
const statusEmoji = update.currentStatus === MergeItemStatus.WAITING_CI ? '⏱️' : '🔄';
|