@link-assistant/hive-mind 1.23.7 → 1.23.9
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 +21 -7
- package/package.json +1 -1
- package/src/config.lib.mjs +24 -2
- package/src/github-merge.lib.mjs +158 -0
- package/src/github.lib.mjs +15 -16
- package/src/option-suggestions.lib.mjs +1 -1
- package/src/solve.auto-continue.lib.mjs +14 -5
- package/src/solve.auto-merge.lib.mjs +24 -24
- package/src/solve.config.lib.mjs +2 -2
- package/src/solve.mjs +10 -7
- package/src/solve.restart-shared.lib.mjs +3 -3
- package/src/solve.results.lib.mjs +8 -8
- package/src/telegram-merge-queue.lib.mjs +71 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.23.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4456760: Fix merge queue to wait for target branch CI before merging (Issue #1307). The merge queue now checks for active CI runs on the target branch (main) before processing the first PR in the queue. This prevents cancelled workflows, incomplete releases, and failed post-merge checks when multiple PRs are merged in quick succession.
|
|
8
|
+
|
|
9
|
+
## 1.23.8
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Fix spelling: rename --auto-restart-until-mergable to --auto-restart-until-mergeable throughout the codebase. This includes CLI options, function names, variable names, documentation, and code comments to use the correct English spelling.
|
|
14
|
+
|
|
15
|
+
Increase limit reset buffer from 5 to 10 minutes and add random jitter (0-5 min) to avoid thundering herd problem when multiple instances wait for the same limit reset. Format reset time in PR comments with relative time and UTC timezone for better user understanding.
|
|
16
|
+
|
|
3
17
|
## 1.23.7
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
|
6
20
|
|
|
7
|
-
- d951635: Fix --auto-restart-until-
|
|
21
|
+
- d951635: Fix --auto-restart-until-mergeable false positive on empty CI checks
|
|
8
22
|
|
|
9
|
-
The `--auto-restart-until-
|
|
23
|
+
The `--auto-restart-until-mergeable` mode was incorrectly posting "Ready to merge" when CI checks hadn't started yet. This was caused by JavaScript's vacuous truth: `[].every(fn)` returns `true`, so an empty checks array would pass all validation.
|
|
10
24
|
|
|
11
25
|
Fix: Return `pending` status when no CI checks exist yet, instead of `success`.
|
|
12
26
|
|
|
@@ -137,7 +151,7 @@
|
|
|
137
151
|
### Patch Changes
|
|
138
152
|
|
|
139
153
|
- fdd8eaa: Fix auto-merge failure in fork mode with permission pre-check (Issue #1226)
|
|
140
|
-
- Add fork-mode guard in `
|
|
154
|
+
- Add fork-mode guard in `startAutoRestartUntilMergeable()` to detect when `--auto-merge` cannot work
|
|
141
155
|
- Add `checkMergePermissions()` function to verify write/push/admin/maintain access before merge attempts
|
|
142
156
|
- Add permission pre-check in `attemptAutoMerge()` to fail fast when user lacks write access
|
|
143
157
|
- Post "Ready to merge" comment to PR when auto-merge cannot be performed due to permissions
|
|
@@ -385,7 +399,7 @@
|
|
|
385
399
|
|
|
386
400
|
- 5723a93: fix: prevent early exit when --auto-merge flag is used
|
|
387
401
|
|
|
388
|
-
The `verifyResults()` function was calling `safeExit(0)` before the auto-merge logic could run. This caused the `--auto-merge` flag to be silently ignored. Now the exit condition properly checks for `argv.autoMerge` and `argv.
|
|
402
|
+
The `verifyResults()` function was calling `safeExit(0)` before the auto-merge logic could run. This caused the `--auto-merge` flag to be silently ignored. Now the exit condition properly checks for `argv.autoMerge|autoRestartUntilMergeable` and `argv.autoMerge|autoRestartUntilMergeable` flags.
|
|
389
403
|
|
|
390
404
|
## 1.15.1
|
|
391
405
|
|
|
@@ -468,11 +482,11 @@
|
|
|
468
482
|
|
|
469
483
|
### Minor Changes
|
|
470
484
|
|
|
471
|
-
- 03adcb6: Add --auto-merge and --auto-restart-until-
|
|
485
|
+
- 03adcb6: Add --auto-merge and --auto-restart-until-mergeable options for autonomous PR management
|
|
472
486
|
|
|
473
487
|
New CLI options:
|
|
474
|
-
- `--auto-merge`: Automatically merge the pull request when CI passes and PR is mergeable. Implies --auto-restart-until-
|
|
475
|
-
- `--auto-restart-until-
|
|
488
|
+
- `--auto-merge`: Automatically merge the pull request when CI passes and PR is mergeable. Implies --auto-restart-until-mergeable.
|
|
489
|
+
- `--auto-restart-until-mergeable`: Auto-restart the AI agent until PR becomes mergeable (no iteration limit). Restarts on new comments from non-bot users, CI failures, merge conflicts, or uncommitted changes. Does NOT auto-merge.
|
|
476
490
|
|
|
477
491
|
Features:
|
|
478
492
|
- Non-bot comment detection with configurable bot patterns
|
package/package.json
CHANGED
package/src/config.lib.mjs
CHANGED
|
@@ -61,10 +61,19 @@ export const autoContinue = {
|
|
|
61
61
|
|
|
62
62
|
// Auto-resume on limit reset configurations
|
|
63
63
|
// See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
64
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
64
65
|
export const limitReset = {
|
|
65
66
|
// Buffer time to wait after limit reset (in milliseconds)
|
|
66
|
-
// Default:
|
|
67
|
-
|
|
67
|
+
// Default: 10 minutes - accounts for server time differences and API propagation delays
|
|
68
|
+
// Increased from 5 to 10 minutes to reduce risk of hitting limits again immediately
|
|
69
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
70
|
+
bufferMs: parseIntWithDefault('HIVE_MIND_LIMIT_RESET_BUFFER_MS', 10 * 60 * 1000),
|
|
71
|
+
// Random jitter added to buffer to avoid thundering herd problem (in milliseconds)
|
|
72
|
+
// When multiple instances wait for the same limit reset, jitter distributes their
|
|
73
|
+
// resume times to reduce simultaneous API load
|
|
74
|
+
// Default: 5 minutes (0 to 5 minutes random) - total wait after reset: 10-15 minutes
|
|
75
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
76
|
+
jitterMs: parseIntWithDefault('HIVE_MIND_LIMIT_RESET_JITTER_MS', 5 * 60 * 1000),
|
|
68
77
|
};
|
|
69
78
|
|
|
70
79
|
// GitHub API limits
|
|
@@ -400,6 +409,7 @@ export const version = {
|
|
|
400
409
|
// Merge queue configurations
|
|
401
410
|
// See: https://github.com/link-assistant/hive-mind/issues/1143
|
|
402
411
|
// See: https://github.com/link-assistant/hive-mind/issues/1269
|
|
412
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1307
|
|
403
413
|
export const mergeQueue = {
|
|
404
414
|
// Maximum PRs to process in one merge session
|
|
405
415
|
// Default: 10 PRs per session
|
|
@@ -417,6 +427,18 @@ export const mergeQueue = {
|
|
|
417
427
|
// Issue #1269: gh pr merge requires explicit method when running non-interactively
|
|
418
428
|
// Default: 'merge' - creates a merge commit
|
|
419
429
|
mergeMethod: getenv('HIVE_MIND_MERGE_QUEUE_MERGE_METHOD', 'merge'),
|
|
430
|
+
// Issue #1307: Wait for main branch CI to complete before processing merge queue
|
|
431
|
+
// When enabled, the merge queue will wait for any active CI runs on the target branch
|
|
432
|
+
// (usually main) to complete before merging the first PR.
|
|
433
|
+
// Default: true - ensures all post-merge CI workflows complete before next merge
|
|
434
|
+
waitForTargetBranchCI: getenv('HIVE_MIND_MERGE_QUEUE_WAIT_FOR_TARGET_CI', 'true').toLowerCase() === 'true',
|
|
435
|
+
// Issue #1307: Timeout for waiting on target branch CI (in milliseconds)
|
|
436
|
+
// If active runs don't complete within this time, proceed with merge anyway
|
|
437
|
+
// Default: 45 minutes (2700000ms)
|
|
438
|
+
targetBranchCITimeoutMs: parseIntWithDefault('HIVE_MIND_MERGE_QUEUE_TARGET_CI_TIMEOUT_MS', 45 * 60 * 1000),
|
|
439
|
+
// Issue #1307: Polling interval for checking target branch CI status (in milliseconds)
|
|
440
|
+
// Default: 30 seconds (30000ms) - more frequent than PR CI polling since we're blocking
|
|
441
|
+
targetBranchCIPollIntervalMs: parseIntWithDefault('HIVE_MIND_MERGE_QUEUE_TARGET_CI_POLL_INTERVAL_MS', 30 * 1000),
|
|
420
442
|
};
|
|
421
443
|
|
|
422
444
|
// Helper function to validate configuration values
|
package/src/github-merge.lib.mjs
CHANGED
|
@@ -636,6 +636,160 @@ export function parseRepositoryUrl(url) {
|
|
|
636
636
|
};
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
+
/**
|
|
640
|
+
* Get active workflow runs on a specific branch
|
|
641
|
+
* Issue #1307: Used to check if there are any in-progress or queued runs on the target branch
|
|
642
|
+
* @param {string} owner - Repository owner
|
|
643
|
+
* @param {string} repo - Repository name
|
|
644
|
+
* @param {string} branch - Branch name (default: main)
|
|
645
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
646
|
+
* @returns {Promise<{runs: Array<Object>, hasActiveRuns: boolean, count: number}>}
|
|
647
|
+
*/
|
|
648
|
+
export async function getActiveBranchRuns(owner, repo, branch = 'main', verbose = false) {
|
|
649
|
+
try {
|
|
650
|
+
// Query for in_progress and queued runs on the specified branch
|
|
651
|
+
const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?branch=${branch}&per_page=10" --jq '[.workflow_runs[] | select(.status=="in_progress" or .status=="queued")] | map({id: .id, name: .name, status: .status, created_at: .created_at, html_url: .html_url})'`);
|
|
652
|
+
|
|
653
|
+
const runs = JSON.parse(stdout.trim() || '[]');
|
|
654
|
+
|
|
655
|
+
if (verbose) {
|
|
656
|
+
console.log(`[VERBOSE] /merge: Found ${runs.length} active runs on ${owner}/${repo} branch ${branch}`);
|
|
657
|
+
for (const run of runs) {
|
|
658
|
+
console.log(`[VERBOSE] /merge: - Run #${run.id}: ${run.name} (${run.status})`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
runs,
|
|
664
|
+
hasActiveRuns: runs.length > 0,
|
|
665
|
+
count: runs.length,
|
|
666
|
+
};
|
|
667
|
+
} catch (error) {
|
|
668
|
+
if (verbose) {
|
|
669
|
+
console.log(`[VERBOSE] /merge: Error checking active runs on ${branch}: ${error.message}`);
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
runs: [],
|
|
673
|
+
hasActiveRuns: false,
|
|
674
|
+
count: 0,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Wait for all active workflow runs on a branch to complete
|
|
681
|
+
* Issue #1307: Ensures all CI runs on target branch are complete before merging
|
|
682
|
+
* @param {string} owner - Repository owner
|
|
683
|
+
* @param {string} repo - Repository name
|
|
684
|
+
* @param {string} branch - Branch name (default: main)
|
|
685
|
+
* @param {Object} options - Wait options
|
|
686
|
+
* @param {number} options.timeout - Maximum wait time in ms (default: 45 minutes)
|
|
687
|
+
* @param {number} options.pollInterval - Polling interval in ms (default: 30 seconds)
|
|
688
|
+
* @param {Function} options.onStatusUpdate - Callback for status updates
|
|
689
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
690
|
+
* @returns {Promise<{success: boolean, waitedForRuns: boolean, completedRuns: number, error: string|null}>}
|
|
691
|
+
*/
|
|
692
|
+
export async function waitForBranchCI(owner, repo, branch = 'main', options = {}, verbose = false) {
|
|
693
|
+
const { timeout = 45 * 60 * 1000, pollInterval = 30 * 1000, onStatusUpdate = null } = options;
|
|
694
|
+
|
|
695
|
+
const startTime = Date.now();
|
|
696
|
+
let totalWaitedRuns = 0;
|
|
697
|
+
|
|
698
|
+
if (verbose) {
|
|
699
|
+
console.log(`[VERBOSE] /merge: Checking for active CI runs on ${owner}/${repo} branch ${branch}...`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
while (Date.now() - startTime < timeout) {
|
|
703
|
+
let activeRuns;
|
|
704
|
+
try {
|
|
705
|
+
activeRuns = await getActiveBranchRuns(owner, repo, branch, verbose);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
// Log and continue on errors
|
|
708
|
+
console.error(`[ERROR] /merge: Error checking branch CI: ${error.message}`);
|
|
709
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (onStatusUpdate) {
|
|
714
|
+
try {
|
|
715
|
+
await onStatusUpdate({
|
|
716
|
+
hasActiveRuns: activeRuns.hasActiveRuns,
|
|
717
|
+
count: activeRuns.count,
|
|
718
|
+
runs: activeRuns.runs,
|
|
719
|
+
elapsedMs: Date.now() - startTime,
|
|
720
|
+
});
|
|
721
|
+
} catch (callbackError) {
|
|
722
|
+
// Log callback errors but continue
|
|
723
|
+
console.error(`[ERROR] /merge: Status update callback failed: ${callbackError.message}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (!activeRuns.hasActiveRuns) {
|
|
728
|
+
if (verbose) {
|
|
729
|
+
console.log(`[VERBOSE] /merge: No active CI runs on ${branch} branch. Ready to proceed.`);
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
success: true,
|
|
733
|
+
waitedForRuns: totalWaitedRuns > 0,
|
|
734
|
+
completedRuns: totalWaitedRuns,
|
|
735
|
+
error: null,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
totalWaitedRuns = Math.max(totalWaitedRuns, activeRuns.count);
|
|
740
|
+
|
|
741
|
+
if (verbose) {
|
|
742
|
+
const elapsedSec = Math.round((Date.now() - startTime) / 1000);
|
|
743
|
+
console.log(`[VERBOSE] /merge: Waiting for ${activeRuns.count} active runs on ${branch}... (${elapsedSec}s elapsed)`);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Timeout reached
|
|
750
|
+
const finalCheck = await getActiveBranchRuns(owner, repo, branch, verbose);
|
|
751
|
+
if (finalCheck.hasActiveRuns) {
|
|
752
|
+
return {
|
|
753
|
+
success: false,
|
|
754
|
+
waitedForRuns: true,
|
|
755
|
+
completedRuns: totalWaitedRuns - finalCheck.count,
|
|
756
|
+
error: `Timeout waiting for ${finalCheck.count} CI runs on ${branch} branch`,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return {
|
|
761
|
+
success: true,
|
|
762
|
+
waitedForRuns: totalWaitedRuns > 0,
|
|
763
|
+
completedRuns: totalWaitedRuns,
|
|
764
|
+
error: null,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Get the default branch for a repository
|
|
770
|
+
* @param {string} owner - Repository owner
|
|
771
|
+
* @param {string} repo - Repository name
|
|
772
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
773
|
+
* @returns {Promise<string>} Default branch name (e.g., 'main' or 'master')
|
|
774
|
+
*/
|
|
775
|
+
export async function getDefaultBranch(owner, repo, verbose = false) {
|
|
776
|
+
try {
|
|
777
|
+
const { stdout } = await exec(`gh api repos/${owner}/${repo} --jq '.default_branch'`);
|
|
778
|
+
const branch = stdout.trim();
|
|
779
|
+
|
|
780
|
+
if (verbose) {
|
|
781
|
+
console.log(`[VERBOSE] /merge: Default branch for ${owner}/${repo}: ${branch}`);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return branch || 'main';
|
|
785
|
+
} catch (error) {
|
|
786
|
+
if (verbose) {
|
|
787
|
+
console.log(`[VERBOSE] /merge: Error getting default branch, falling back to 'main': ${error.message}`);
|
|
788
|
+
}
|
|
789
|
+
return 'main';
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
639
793
|
export default {
|
|
640
794
|
READY_LABEL,
|
|
641
795
|
checkReadyLabelExists,
|
|
@@ -651,4 +805,8 @@ export default {
|
|
|
651
805
|
mergePullRequest,
|
|
652
806
|
waitForCI,
|
|
653
807
|
parseRepositoryUrl,
|
|
808
|
+
// Issue #1307: New exports for target branch CI waiting
|
|
809
|
+
getActiveBranchRuns,
|
|
810
|
+
waitForBranchCI,
|
|
811
|
+
getDefaultBranch,
|
|
654
812
|
};
|
package/src/github.lib.mjs
CHANGED
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// GitHub-related utility functions
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (typeof globalThis.use === 'undefined') {
|
|
6
|
-
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
7
|
-
}
|
|
8
|
-
// Use command-stream for consistent $ behavior
|
|
9
|
-
const { $ } = await use('command-stream');
|
|
10
|
-
// Import log and maskToken from general lib
|
|
2
|
+
// GitHub-related utility functions. Check if use is already defined (when imported from solve.mjs), if not, fetch it (when running standalone)
|
|
3
|
+
if (typeof globalThis.use === 'undefined') globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
4
|
+
const { $ } = await use('command-stream'); // Use command-stream for consistent $ behavior
|
|
11
5
|
import { log, maskToken, cleanErrorMessage } from './lib.mjs';
|
|
12
6
|
import { reportError } from './sentry.lib.mjs';
|
|
13
7
|
import { githubLimits, timeouts } from './config.lib.mjs';
|
|
14
|
-
// Import batch operations from separate module
|
|
15
8
|
import { batchCheckPullRequestsForIssues as batchCheckPRs, batchCheckArchivedRepositories as batchCheckArchived } from './github.batch.lib.mjs';
|
|
16
|
-
// Import token sanitization from dedicated module (Issue #1037 fix)
|
|
17
9
|
import { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTokensFromCommand, sanitizeLogContent } from './token-sanitization.lib.mjs';
|
|
18
|
-
// Re-export
|
|
19
|
-
export { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTokensFromCommand, sanitizeLogContent };
|
|
20
|
-
// Import log upload function from separate module
|
|
10
|
+
export { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTokensFromCommand, sanitizeLogContent }; // Re-export for backward compatibility
|
|
21
11
|
import { uploadLogWithGhUploadLog } from './log-upload.lib.mjs';
|
|
12
|
+
import { formatResetTimeWithRelative } from './usage-limit.lib.mjs'; // See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
22
13
|
|
|
23
14
|
/**
|
|
24
15
|
* Build cost estimation string for log comments
|
|
@@ -488,7 +479,11 @@ The automated solution draft was interrupted because the ${toolName} usage limit
|
|
|
488
479
|
- **Limit Type**: Usage limit exceeded`;
|
|
489
480
|
|
|
490
481
|
if (limitResetTime) {
|
|
491
|
-
|
|
482
|
+
// Format reset time with relative time and UTC for better user understanding
|
|
483
|
+
// Shows "in 14m (Feb 6, 3:00 PM UTC)" instead of just "4:00 PM"
|
|
484
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
485
|
+
const formattedResetTime = formatResetTimeWithRelative(limitResetTime, global.limitTimezone || null) || limitResetTime;
|
|
486
|
+
logComment += `\n- **Reset Time**: ${formattedResetTime}`;
|
|
492
487
|
}
|
|
493
488
|
|
|
494
489
|
if (sessionId) {
|
|
@@ -681,7 +676,11 @@ The automated solution draft was interrupted because the ${toolName} usage limit
|
|
|
681
676
|
- **Limit Type**: Usage limit exceeded`;
|
|
682
677
|
|
|
683
678
|
if (limitResetTime) {
|
|
684
|
-
|
|
679
|
+
// Format reset time with relative time and UTC for better user understanding
|
|
680
|
+
// Shows "in 14m (Feb 6, 3:00 PM UTC)" instead of just "4:00 PM"
|
|
681
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
682
|
+
const formattedUploadResetTime = formatResetTimeWithRelative(limitResetTime, global.limitTimezone || null) || limitResetTime;
|
|
683
|
+
logUploadComment += `\n- **Reset Time**: ${formattedUploadResetTime}`;
|
|
685
684
|
}
|
|
686
685
|
|
|
687
686
|
if (sessionId) {
|
|
@@ -205,7 +205,7 @@ const KNOWN_OPTION_NAMES = [
|
|
|
205
205
|
'auto-continue-only-on-new-comments',
|
|
206
206
|
'auto-restart-on-limit-reset',
|
|
207
207
|
'auto-restart-on-non-updated-pull-request-description',
|
|
208
|
-
'auto-restart-until-
|
|
208
|
+
'auto-restart-until-mergeable',
|
|
209
209
|
'auto-merge',
|
|
210
210
|
'auto-gitkeep-file',
|
|
211
211
|
'playwright-mcp-auto-cleanup',
|
|
@@ -81,18 +81,27 @@ export const autoContinueWhenLimitResets = async (issueUrl, sessionId, argv, sho
|
|
|
81
81
|
const baseWaitMs = calculateWaitTime(resetTime);
|
|
82
82
|
|
|
83
83
|
// Add buffer time after limit reset to account for server time differences
|
|
84
|
-
// Default:
|
|
84
|
+
// Default: 10 minutes (configurable via HIVE_MIND_LIMIT_RESET_BUFFER_MS)
|
|
85
85
|
// See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
86
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
86
87
|
const bufferMs = limitReset.bufferMs;
|
|
87
|
-
|
|
88
|
+
|
|
89
|
+
// Add random jitter to avoid thundering herd problem when multiple instances
|
|
90
|
+
// wait for the same limit reset time and all resume simultaneously
|
|
91
|
+
// Default: random 0-5 minutes (configurable via HIVE_MIND_LIMIT_RESET_JITTER_MS)
|
|
92
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
93
|
+
const jitterMs = Math.floor(Math.random() * limitReset.jitterMs);
|
|
94
|
+
const totalBufferMs = bufferMs + jitterMs;
|
|
95
|
+
const waitMs = baseWaitMs + totalBufferMs;
|
|
88
96
|
const bufferMinutes = Math.round(bufferMs / 60000);
|
|
97
|
+
const jitterSeconds = Math.round(jitterMs / 1000);
|
|
89
98
|
|
|
90
99
|
// Format reset time with relative time and UTC for better user understanding
|
|
91
100
|
// See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
92
101
|
const formattedResetTime = formatResetTimeWithRelative(resetTime, timezone);
|
|
93
102
|
|
|
94
|
-
await log(`\n⏰ Waiting until ${formattedResetTime} + ${bufferMinutes} min buffer for limit to reset...`);
|
|
95
|
-
await log(` Wait time: ${formatWaitTime(waitMs)} (includes ${bufferMinutes} min buffer
|
|
103
|
+
await log(`\n⏰ Waiting until ${formattedResetTime} + ${bufferMinutes} min buffer + ${jitterSeconds}s jitter for limit to reset...`);
|
|
104
|
+
await log(` Wait time: ${formatWaitTime(waitMs)} (includes ${bufferMinutes} min buffer + ${jitterSeconds}s random jitter)`);
|
|
96
105
|
await log(` Current time: ${new Date().toLocaleTimeString()}`);
|
|
97
106
|
|
|
98
107
|
// Show countdown every 30 minutes for long waits, every minute for short waits
|
|
@@ -111,7 +120,7 @@ export const autoContinueWhenLimitResets = async (issueUrl, sessionId, argv, sho
|
|
|
111
120
|
clearInterval(countdownTimer);
|
|
112
121
|
|
|
113
122
|
const actionType = isRestart ? 'Restarting' : 'Resuming';
|
|
114
|
-
await log(`\n✅ Limit reset time reached (+ ${bufferMinutes} min buffer)! ${actionType} session...`);
|
|
123
|
+
await log(`\n✅ Limit reset time reached (+ ${bufferMinutes} min buffer + ${jitterSeconds}s jitter)! ${actionType} session...`);
|
|
115
124
|
await log(` Current time: ${new Date().toLocaleTimeString()}`);
|
|
116
125
|
|
|
117
126
|
// Recursively call the solve script
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Auto-merge and auto-restart-until-
|
|
4
|
+
* Auto-merge and auto-restart-until-mergeable module for solve.mjs
|
|
5
5
|
* Handles automatic merging of PRs and continuous restart until PR becomes mergeable
|
|
6
6
|
*
|
|
7
7
|
* Uses shared utilities from solve.restart-shared.lib.mjs for common functions.
|
|
@@ -25,7 +25,7 @@ const { log, cleanErrorMessage, formatAligned, getLogFile } = lib;
|
|
|
25
25
|
|
|
26
26
|
// Note: We don't use detectAndCountFeedback from solve.feedback.lib.mjs
|
|
27
27
|
// because we have our own non-bot comment detection logic that's more
|
|
28
|
-
// appropriate for auto-restart-until-
|
|
28
|
+
// appropriate for auto-restart-until-mergeable mode
|
|
29
29
|
|
|
30
30
|
// Import Sentry integration
|
|
31
31
|
const sentryLib = await import('./sentry.lib.mjs');
|
|
@@ -176,9 +176,9 @@ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
|
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
178
|
* Main function: Watch and restart until PR becomes mergeable
|
|
179
|
-
* This implements --auto-restart-until-
|
|
179
|
+
* This implements --auto-restart-until-mergeable functionality
|
|
180
180
|
*/
|
|
181
|
-
export const
|
|
181
|
+
export const watchUntilMergeable = async params => {
|
|
182
182
|
const { issueUrl, owner, repo, issueNumber, prNumber, prBranch, branchName, tempDir, argv } = params;
|
|
183
183
|
|
|
184
184
|
const watchInterval = argv.watchInterval || 60; // seconds
|
|
@@ -194,9 +194,9 @@ export const watchUntilMergable = async params => {
|
|
|
194
194
|
let currentBackoffSeconds = watchInterval;
|
|
195
195
|
|
|
196
196
|
await log('');
|
|
197
|
-
await log(formatAligned('🔄', 'AUTO-RESTART-UNTIL-
|
|
197
|
+
await log(formatAligned('🔄', 'AUTO-RESTART-UNTIL-MERGEABLE MODE ACTIVE', ''));
|
|
198
198
|
await log(formatAligned('', 'Monitoring PR:', `#${prNumber}`, 2));
|
|
199
|
-
await log(formatAligned('', 'Mode:', isAutoMerge ? 'Auto-merge (will merge when ready)' : 'Auto-restart-until-
|
|
199
|
+
await log(formatAligned('', 'Mode:', isAutoMerge ? 'Auto-merge (will merge when ready)' : 'Auto-restart-until-mergeable (will NOT auto-merge)', 2));
|
|
200
200
|
await log(formatAligned('', 'Checking interval:', `${watchInterval} seconds`, 2));
|
|
201
201
|
await log(formatAligned('', 'Stop conditions:', 'PR merged, PR closed, or becomes mergeable', 2));
|
|
202
202
|
await log(formatAligned('', 'Restart triggers:', 'New non-bot comments, CI failures, merge conflicts', 2));
|
|
@@ -215,7 +215,7 @@ export const watchUntilMergable = async params => {
|
|
|
215
215
|
const isMerged = await checkPRMerged(owner, repo, prNumber);
|
|
216
216
|
if (isMerged) {
|
|
217
217
|
await log('');
|
|
218
|
-
await log(formatAligned('🎉', 'PR MERGED!', 'Stopping auto-restart-until-
|
|
218
|
+
await log(formatAligned('🎉', 'PR MERGED!', 'Stopping auto-restart-until-mergeable mode'));
|
|
219
219
|
await log(formatAligned('', 'Pull request:', `#${prNumber} has been merged`, 2));
|
|
220
220
|
await log('');
|
|
221
221
|
return { success: true, reason: 'merged', latestSessionId, latestAnthropicCost };
|
|
@@ -225,7 +225,7 @@ export const watchUntilMergable = async params => {
|
|
|
225
225
|
const isClosed = await checkPRClosed(owner, repo, prNumber);
|
|
226
226
|
if (isClosed) {
|
|
227
227
|
await log('');
|
|
228
|
-
await log(formatAligned('🚫', 'PR CLOSED!', 'Stopping auto-restart-until-
|
|
228
|
+
await log(formatAligned('🚫', 'PR CLOSED!', 'Stopping auto-restart-until-mergeable mode'));
|
|
229
229
|
await log(formatAligned('', 'Pull request:', `#${prNumber} has been closed without merging`, 2));
|
|
230
230
|
await log('');
|
|
231
231
|
return { success: false, reason: 'closed', latestSessionId, latestAnthropicCost };
|
|
@@ -272,11 +272,11 @@ export const watchUntilMergable = async params => {
|
|
|
272
272
|
} else {
|
|
273
273
|
// Just report that PR is mergeable and exit
|
|
274
274
|
await log(formatAligned('', 'PR is ready to be merged manually', '', 2));
|
|
275
|
-
await log(formatAligned('', 'Exiting auto-restart-until-
|
|
275
|
+
await log(formatAligned('', 'Exiting auto-restart-until-mergeable mode', '', 2));
|
|
276
276
|
|
|
277
277
|
// Post success comment
|
|
278
278
|
try {
|
|
279
|
-
const commentBody = `## ✅ Ready to merge\n\nThis pull request is now ready to be merged:\n- All CI checks have passed\n- No merge conflicts\n- No pending changes\n\n---\n*Monitored by hive-mind with --auto-restart-until-
|
|
279
|
+
const commentBody = `## ✅ Ready to merge\n\nThis pull request is now ready to be merged:\n- All CI checks have passed\n- No merge conflicts\n- No pending changes\n\n---\n*Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
|
|
280
280
|
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
281
281
|
} catch {
|
|
282
282
|
// Don't fail if comment posting fails
|
|
@@ -345,7 +345,7 @@ export const watchUntilMergable = async params => {
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
if (shouldRestart) {
|
|
348
|
-
// Add standard instructions for auto-restart-until-
|
|
348
|
+
// Add standard instructions for auto-restart-until-mergeable mode using shared utility
|
|
349
349
|
feedbackLines.push(...buildAutoRestartInstructions());
|
|
350
350
|
|
|
351
351
|
await log(formatAligned('🔄', 'RESTART TRIGGERED:', restartReason));
|
|
@@ -353,7 +353,7 @@ export const watchUntilMergable = async params => {
|
|
|
353
353
|
|
|
354
354
|
// Post a comment to PR about the restart
|
|
355
355
|
try {
|
|
356
|
-
const commentBody = `## 🔄 Auto-restart triggered\n\n**Reason:** ${restartReason}\n\nStarting new session to address the issues.\n\n---\n*Auto-restart-until-
|
|
356
|
+
const commentBody = `## 🔄 Auto-restart triggered\n\n**Reason:** ${restartReason}\n\nStarting new session to address the issues.\n\n---\n*Auto-restart-until-mergeable mode is active. Will continue until PR becomes mergeable.*`;
|
|
357
357
|
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
358
358
|
await log(formatAligned('', '💬 Posted auto-restart notification to PR', '', 2));
|
|
359
359
|
} catch (commentError) {
|
|
@@ -428,7 +428,7 @@ export const watchUntilMergable = async params => {
|
|
|
428
428
|
try {
|
|
429
429
|
const logFile = getLogFile();
|
|
430
430
|
if (logFile) {
|
|
431
|
-
const customTitle = `🔄 Auto-restart-until-
|
|
431
|
+
const customTitle = `🔄 Auto-restart-until-mergeable Log (iteration ${iteration})`;
|
|
432
432
|
await attachLogToGitHub({
|
|
433
433
|
logFile,
|
|
434
434
|
targetType: 'pr',
|
|
@@ -478,7 +478,7 @@ export const watchUntilMergable = async params => {
|
|
|
478
478
|
lastCheckTime = currentTime;
|
|
479
479
|
} catch (error) {
|
|
480
480
|
reportError(error, {
|
|
481
|
-
context: '
|
|
481
|
+
context: 'watch_until_mergeable',
|
|
482
482
|
prNumber,
|
|
483
483
|
owner,
|
|
484
484
|
repo,
|
|
@@ -568,22 +568,22 @@ export const attemptAutoMerge = async params => {
|
|
|
568
568
|
};
|
|
569
569
|
|
|
570
570
|
/**
|
|
571
|
-
* Start auto-restart-until-
|
|
571
|
+
* Start auto-restart-until-mergeable mode
|
|
572
572
|
*/
|
|
573
|
-
export const
|
|
573
|
+
export const startAutoRestartUntilMergeable = async params => {
|
|
574
574
|
const { argv, owner, repo, prNumber } = params;
|
|
575
575
|
|
|
576
576
|
// Determine the mode
|
|
577
577
|
const isAutoMerge = argv.autoMerge || false;
|
|
578
|
-
const
|
|
578
|
+
const isAutoRestartUntilMergeable = argv.autoRestartUntilMergeable || false;
|
|
579
579
|
|
|
580
|
-
if (!isAutoMerge && !
|
|
580
|
+
if (!isAutoMerge && !isAutoRestartUntilMergeable) {
|
|
581
581
|
return null; // Neither mode enabled
|
|
582
582
|
}
|
|
583
583
|
|
|
584
584
|
if (!prNumber) {
|
|
585
585
|
await log('');
|
|
586
|
-
await log(formatAligned('⚠️', 'Auto-restart-until-
|
|
586
|
+
await log(formatAligned('⚠️', 'Auto-restart-until-mergeable:', 'Requires a pull request'));
|
|
587
587
|
await log(formatAligned('', 'Note:', 'This mode only works with existing PRs', 2));
|
|
588
588
|
return null;
|
|
589
589
|
}
|
|
@@ -632,18 +632,18 @@ export const startAutoRestartUntilMergable = async params => {
|
|
|
632
632
|
}
|
|
633
633
|
}
|
|
634
634
|
|
|
635
|
-
// If --auto-merge implies --auto-restart-until-
|
|
635
|
+
// If --auto-merge implies --auto-restart-until-mergeable
|
|
636
636
|
if (isAutoMerge) {
|
|
637
|
-
argv.
|
|
637
|
+
argv.autoRestartUntilMergeable = true;
|
|
638
638
|
}
|
|
639
639
|
|
|
640
640
|
// Start the watch loop
|
|
641
|
-
return await
|
|
641
|
+
return await watchUntilMergeable(params);
|
|
642
642
|
};
|
|
643
643
|
|
|
644
644
|
export default {
|
|
645
|
-
|
|
645
|
+
watchUntilMergeable,
|
|
646
646
|
attemptAutoMerge,
|
|
647
|
-
|
|
647
|
+
startAutoRestartUntilMergeable,
|
|
648
648
|
checkForNonBotComments,
|
|
649
649
|
};
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -173,10 +173,10 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
173
173
|
},
|
|
174
174
|
'auto-merge': {
|
|
175
175
|
type: 'boolean',
|
|
176
|
-
description: 'Automatically merge the pull request when the working session is finished and all CI/CD statuses pass and PR is mergeable. Implies --auto-restart-until-
|
|
176
|
+
description: 'Automatically merge the pull request when the working session is finished and all CI/CD statuses pass and PR is mergeable. Implies --auto-restart-until-mergeable.',
|
|
177
177
|
default: false,
|
|
178
178
|
},
|
|
179
|
-
'auto-restart-until-
|
|
179
|
+
'auto-restart-until-mergeable': {
|
|
180
180
|
type: 'boolean',
|
|
181
181
|
description: 'Auto-restart until PR becomes mergeable (no iteration limit). Restarts on new comments from non-bot users, CI failures, merge conflicts, or other issues. Does NOT auto-merge.',
|
|
182
182
|
default: false,
|
package/src/solve.mjs
CHANGED
|
@@ -74,7 +74,7 @@ const { createUncaughtExceptionHandler, createUnhandledRejectionHandler, handleM
|
|
|
74
74
|
const watchLib = await import('./solve.watch.lib.mjs');
|
|
75
75
|
const { startWatchMode } = watchLib;
|
|
76
76
|
const autoMergeLib = await import('./solve.auto-merge.lib.mjs');
|
|
77
|
-
const {
|
|
77
|
+
const { startAutoRestartUntilMergeable } = autoMergeLib;
|
|
78
78
|
const exitHandler = await import('./exit-handler.lib.mjs');
|
|
79
79
|
const { initializeExitHandler, installGlobalExitHandlers, safeExit } = exitHandler;
|
|
80
80
|
const getResourceSnapshot = memoryCheck.getResourceSnapshot;
|
|
@@ -1080,7 +1080,10 @@ try {
|
|
|
1080
1080
|
// See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
1081
1081
|
const continueModeName = limitContinueMode === 'restart' ? 'auto-restart' : 'auto-resume';
|
|
1082
1082
|
const continueDescription = limitContinueMode === 'restart' ? 'The session will automatically restart (fresh start) when the limit resets.' : 'The session will automatically resume (with context preserved) when the limit resets.';
|
|
1083
|
-
|
|
1083
|
+
// Format reset time with relative time and UTC for better user understanding
|
|
1084
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
1085
|
+
const waitingResetTimeFormatted = formatResetTimeWithRelative(global.limitResetTime, global.limitTimezone || null) || global.limitResetTime;
|
|
1086
|
+
const waitingComment = `⏳ **Usage Limit Reached - Waiting to ${limitContinueMode === 'restart' ? 'Restart' : 'Continue'}**\n\nThe AI tool has reached its usage limit. ${continueModeName} is enabled.\n\n**Reset time:** ${waitingResetTimeFormatted}\n**Wait time:** ${formatWaitTime(waitMs)} (days:hours:minutes:seconds)\n\n${continueDescription}\n\nSession ID: \`${sessionId}\``;
|
|
1084
1087
|
|
|
1085
1088
|
const commentResult = await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${waitingComment}`;
|
|
1086
1089
|
if (commentResult.code === 0) {
|
|
@@ -1403,11 +1406,11 @@ try {
|
|
|
1403
1406
|
}
|
|
1404
1407
|
}
|
|
1405
1408
|
|
|
1406
|
-
// Start auto-restart-until-
|
|
1409
|
+
// Start auto-restart-until-mergeable mode if enabled
|
|
1407
1410
|
// This runs after the normal watch mode completes (if any)
|
|
1408
|
-
// --auto-merge implies --auto-restart-until-
|
|
1409
|
-
if (argv.autoMerge || argv.
|
|
1410
|
-
const autoMergeResult = await
|
|
1411
|
+
// --auto-merge implies --auto-restart-until-mergeable
|
|
1412
|
+
if (argv.autoMerge || argv.autoRestartUntilMergeable) {
|
|
1413
|
+
const autoMergeResult = await startAutoRestartUntilMergeable({
|
|
1411
1414
|
issueUrl,
|
|
1412
1415
|
owner,
|
|
1413
1416
|
repo,
|
|
@@ -1425,7 +1428,7 @@ try {
|
|
|
1425
1428
|
anthropicTotalCostUSD = autoMergeResult.latestAnthropicCost;
|
|
1426
1429
|
if (argv.verbose) {
|
|
1427
1430
|
await log('');
|
|
1428
|
-
await log('📊 Updated session data from auto-restart-until-
|
|
1431
|
+
await log('📊 Updated session data from auto-restart-until-mergeable mode:', { verbose: true });
|
|
1429
1432
|
await log(` Session ID: ${sessionId}`, { verbose: true });
|
|
1430
1433
|
if (anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined) {
|
|
1431
1434
|
await log(` Anthropic cost: $${anthropicTotalCostUSD.toFixed(6)}`, { verbose: true });
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Shared utilities for watch mode and auto-restart-until-
|
|
4
|
+
* Shared utilities for watch mode and auto-restart-until-mergeable mode
|
|
5
5
|
*
|
|
6
6
|
* This module contains common functions used by both:
|
|
7
7
|
* - solve.watch.lib.mjs (--watch mode and temporary auto-restart)
|
|
8
|
-
* - solve.auto-merge.lib.mjs (--auto-merge and --auto-restart-until-
|
|
8
|
+
* - solve.auto-merge.lib.mjs (--auto-merge and --auto-restart-until-mergeable)
|
|
9
9
|
*
|
|
10
10
|
* Functions extracted to reduce duplication and ensure consistent behavior.
|
|
11
11
|
*
|
|
@@ -167,7 +167,7 @@ export const getUncommittedChangesDetails = async tempDir => {
|
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
169
|
* Execute the AI tool (Claude, OpenCode, Codex, Agent) for a restart iteration
|
|
170
|
-
* This is the shared tool execution logic used by both watch mode and auto-restart-until-
|
|
170
|
+
* This is the shared tool execution logic used by both watch mode and auto-restart-until-mergeable mode
|
|
171
171
|
* @param {Object} params - Execution parameters
|
|
172
172
|
* @returns {Promise<Object>} - Tool execution result
|
|
173
173
|
*/
|
|
@@ -688,12 +688,12 @@ Fixes ${issueRef}
|
|
|
688
688
|
await log('\n✨ Please review the pull request for the proposed solution draft.');
|
|
689
689
|
// Don't exit if watch mode is enabled OR if auto-restart is needed for uncommitted changes
|
|
690
690
|
// Also don't exit if auto-restart-on-non-updated-pull-request-description detected placeholders
|
|
691
|
-
// Issue #1219: Also don't exit if auto-merge or auto-restart-until-
|
|
691
|
+
// Issue #1219: Also don't exit if auto-merge or auto-restart-until-mergeable is enabled
|
|
692
692
|
const shouldAutoRestartForPlaceholder = argv.autoRestartOnNonUpdatedPullRequestDescription && (prTitleHasPlaceholder || prBodyHasPlaceholder);
|
|
693
693
|
if (shouldAutoRestartForPlaceholder) {
|
|
694
694
|
await log('\n🔄 Placeholder detected in PR title/description - auto-restart will be triggered');
|
|
695
695
|
}
|
|
696
|
-
const shouldWaitForAutoMerge = argv.autoMerge || argv.
|
|
696
|
+
const shouldWaitForAutoMerge = argv.autoMerge || argv.autoRestartUntilMergeable;
|
|
697
697
|
if (shouldWaitForAutoMerge) {
|
|
698
698
|
await log('\n🔄 Auto-merge mode enabled - will attempt to merge after verification');
|
|
699
699
|
}
|
|
@@ -764,8 +764,8 @@ Fixes ${issueRef}
|
|
|
764
764
|
}
|
|
765
765
|
await log('\n✨ A clarifying comment has been added to the issue.');
|
|
766
766
|
// Don't exit if watch mode is enabled OR if auto-restart is needed for uncommitted changes
|
|
767
|
-
// Issue #1219: Also don't exit if auto-merge or auto-restart-until-
|
|
768
|
-
const shouldWaitForAutoMergeComment = argv.autoMerge || argv.
|
|
767
|
+
// Issue #1219: Also don't exit if auto-merge or auto-restart-until-mergeable is enabled
|
|
768
|
+
const shouldWaitForAutoMergeComment = argv.autoMerge || argv.autoRestartUntilMergeable;
|
|
769
769
|
if (!argv.watch && !shouldRestart && !shouldWaitForAutoMergeComment) {
|
|
770
770
|
await safeExit(0, 'Process completed successfully');
|
|
771
771
|
}
|
|
@@ -785,8 +785,8 @@ Fixes ${issueRef}
|
|
|
785
785
|
const reviewLogPath = path.resolve(getLogFile());
|
|
786
786
|
await log(` ${reviewLogPath}`);
|
|
787
787
|
// Don't exit if watch mode is enabled - it needs to continue monitoring
|
|
788
|
-
// Issue #1219: Also don't exit if auto-merge or auto-restart-until-
|
|
789
|
-
const shouldWaitForAutoMergeNoAction = argv.autoMerge || argv.
|
|
788
|
+
// Issue #1219: Also don't exit if auto-merge or auto-restart-until-mergeable is enabled
|
|
789
|
+
const shouldWaitForAutoMergeNoAction = argv.autoMerge || argv.autoRestartUntilMergeable;
|
|
790
790
|
if (!argv.watch && !shouldWaitForAutoMergeNoAction) {
|
|
791
791
|
await safeExit(0, 'Process completed successfully');
|
|
792
792
|
}
|
|
@@ -804,8 +804,8 @@ Fixes ${issueRef}
|
|
|
804
804
|
const checkLogPath = path.resolve(getLogFile());
|
|
805
805
|
await log(` ${checkLogPath}`);
|
|
806
806
|
// Don't exit if watch mode is enabled - it needs to continue monitoring
|
|
807
|
-
// Issue #1219: Also don't exit if auto-merge or auto-restart-until-
|
|
808
|
-
const shouldWaitForAutoMergeError = argv.autoMerge || argv.
|
|
807
|
+
// Issue #1219: Also don't exit if auto-merge or auto-restart-until-mergeable is enabled
|
|
808
|
+
const shouldWaitForAutoMergeError = argv.autoMerge || argv.autoRestartUntilMergeable;
|
|
809
809
|
if (!argv.watch && !shouldWaitForAutoMergeError) {
|
|
810
810
|
await safeExit(0, 'Process completed successfully');
|
|
811
811
|
}
|
|
@@ -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 } from './github-merge.lib.mjs';
|
|
19
|
+
import { getAllReadyPRs, checkPRCIStatus, checkPRMergeable, mergePullRequest, waitForCI, ensureReadyLabel, waitForBranchCI, getDefaultBranch } 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
|
|
|
@@ -74,6 +74,11 @@ export const MERGE_QUEUE_CONFIG = {
|
|
|
74
74
|
// Merge method: 'merge', 'squash', or 'rebase' (Issue #1269)
|
|
75
75
|
// gh pr merge requires explicit method when running non-interactively
|
|
76
76
|
MERGE_METHOD: mergeQueueConfig.mergeMethod,
|
|
77
|
+
|
|
78
|
+
// Issue #1307: Wait for target branch CI before first merge
|
|
79
|
+
WAIT_FOR_TARGET_BRANCH_CI: mergeQueueConfig.waitForTargetBranchCI,
|
|
80
|
+
TARGET_BRANCH_CI_TIMEOUT_MS: mergeQueueConfig.targetBranchCITimeoutMs,
|
|
81
|
+
TARGET_BRANCH_CI_POLL_INTERVAL_MS: mergeQueueConfig.targetBranchCIPollIntervalMs,
|
|
77
82
|
};
|
|
78
83
|
|
|
79
84
|
/**
|
|
@@ -229,6 +234,12 @@ export class MergeQueueProcessor {
|
|
|
229
234
|
this.isCancelled = false;
|
|
230
235
|
|
|
231
236
|
try {
|
|
237
|
+
// Issue #1307: Wait for any active CI runs on the target branch before processing
|
|
238
|
+
// This prevents merging while post-merge CI from previous merges is still running
|
|
239
|
+
if (MERGE_QUEUE_CONFIG.WAIT_FOR_TARGET_BRANCH_CI) {
|
|
240
|
+
await this.waitForTargetBranchCI();
|
|
241
|
+
}
|
|
242
|
+
|
|
232
243
|
// Process each PR sequentially
|
|
233
244
|
for (this.currentIndex = 0; this.currentIndex < this.items.length; this.currentIndex++) {
|
|
234
245
|
if (this.isCancelled) {
|
|
@@ -383,6 +394,54 @@ export class MergeQueueProcessor {
|
|
|
383
394
|
this.log('Cancellation requested');
|
|
384
395
|
}
|
|
385
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Wait for any active CI runs on the target branch to complete
|
|
399
|
+
* Issue #1307: Prevents merging while post-merge CI from previous merges is still running
|
|
400
|
+
* @returns {Promise<void>}
|
|
401
|
+
*/
|
|
402
|
+
async waitForTargetBranchCI() {
|
|
403
|
+
// Track if we're waiting for CI (for progress updates)
|
|
404
|
+
this.waitingForTargetBranchCI = true;
|
|
405
|
+
this.targetBranchCIStatus = null;
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
// Get the default branch (usually 'main' or 'master')
|
|
409
|
+
const targetBranch = await getDefaultBranch(this.owner, this.repo, this.verbose);
|
|
410
|
+
this.log(`Checking for active CI runs on ${targetBranch} branch before processing queue...`);
|
|
411
|
+
|
|
412
|
+
const waitResult = await waitForBranchCI(
|
|
413
|
+
this.owner,
|
|
414
|
+
this.repo,
|
|
415
|
+
targetBranch,
|
|
416
|
+
{
|
|
417
|
+
timeout: MERGE_QUEUE_CONFIG.TARGET_BRANCH_CI_TIMEOUT_MS,
|
|
418
|
+
pollInterval: MERGE_QUEUE_CONFIG.TARGET_BRANCH_CI_POLL_INTERVAL_MS,
|
|
419
|
+
onStatusUpdate: async status => {
|
|
420
|
+
this.targetBranchCIStatus = status;
|
|
421
|
+
|
|
422
|
+
// Report progress while waiting
|
|
423
|
+
if (this.onProgress) {
|
|
424
|
+
await this.onProgress(this.getProgressUpdate());
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
this.verbose
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
if (!waitResult.success) {
|
|
432
|
+
// Log warning but don't fail - proceed with merge anyway
|
|
433
|
+
console.warn(`[WARN] /merge-queue: ${waitResult.error}. Proceeding with merge anyway.`);
|
|
434
|
+
} else if (waitResult.waitedForRuns) {
|
|
435
|
+
this.log(`Waited for ${waitResult.completedRuns} CI runs to complete on ${targetBranch} branch`);
|
|
436
|
+
} else {
|
|
437
|
+
this.log(`No active CI runs on ${targetBranch} branch. Ready to proceed.`);
|
|
438
|
+
}
|
|
439
|
+
} finally {
|
|
440
|
+
this.waitingForTargetBranchCI = false;
|
|
441
|
+
this.targetBranchCIStatus = null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
386
445
|
/**
|
|
387
446
|
* Get current progress update
|
|
388
447
|
*/
|
|
@@ -456,8 +515,18 @@ export class MergeQueueProcessor {
|
|
|
456
515
|
message += `⏭️ Skipped: ${update.stats.skipped} `;
|
|
457
516
|
message += `⏳ Pending: ${update.stats.total - update.progress.processed}\n\n`;
|
|
458
517
|
|
|
518
|
+
// Issue #1307: Show waiting status for target branch CI
|
|
519
|
+
if (this.waitingForTargetBranchCI && this.targetBranchCIStatus) {
|
|
520
|
+
const elapsedSec = Math.round(this.targetBranchCIStatus.elapsedMs / 1000);
|
|
521
|
+
const elapsedMin = Math.floor(elapsedSec / 60);
|
|
522
|
+
const elapsedSecRemainder = elapsedSec % 60;
|
|
523
|
+
message += `⏱️ Waiting for ${this.targetBranchCIStatus.count} CI run\\(s\\) on target branch to complete \\(${elapsedMin}m ${elapsedSecRemainder}s\\)\\.\\.\\.\\n\\n`;
|
|
524
|
+
} else if (this.waitingForTargetBranchCI) {
|
|
525
|
+
message += `⏱️ Checking for active CI runs on target branch\\.\\.\\.\n\n`;
|
|
526
|
+
}
|
|
527
|
+
|
|
459
528
|
// Current item being processed
|
|
460
|
-
if (update.current) {
|
|
529
|
+
if (update.current && !this.waitingForTargetBranchCI) {
|
|
461
530
|
const statusEmoji = update.currentStatus === MergeItemStatus.WAITING_CI ? '⏱️' : '🔄';
|
|
462
531
|
message += `${statusEmoji} ${update.current}\n\n`;
|
|
463
532
|
}
|