@link-assistant/hive-mind 1.24.5 → 1.25.0
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 +44 -0
- package/README.md +0 -4
- package/package.json +1 -1
- package/src/agent.lib.mjs +12 -0
- package/src/config.lib.mjs +25 -0
- package/src/github-merge-ci.lib.mjs +242 -0
- package/src/github-merge.lib.mjs +11 -2
- package/src/hive.config.lib.mjs +1 -1
- package/src/solve.config.lib.mjs +1 -1
- package/src/telegram-merge-queue.lib.mjs +215 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.25.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- cbac3dd: feat: wait for post-merge CI to complete before merging next PR (Issue #1341)
|
|
8
|
+
|
|
9
|
+
This change ensures that the /merge command waits for GitHub Actions to complete after each merge before processing the next PR in the queue.
|
|
10
|
+
|
|
11
|
+
**Problem:**
|
|
12
|
+
- Merge queue was merging PRs too quickly (70 seconds apart)
|
|
13
|
+
- Workflow runs were being cancelled (superseded by new commits)
|
|
14
|
+
- Only one version published instead of multiple
|
|
15
|
+
|
|
16
|
+
**Solution:**
|
|
17
|
+
1. Check branch CI health before starting the queue
|
|
18
|
+
2. Wait for post-merge CI after each successful merge
|
|
19
|
+
3. Stop queue on CI failure (configurable)
|
|
20
|
+
|
|
21
|
+
**New configuration options:**
|
|
22
|
+
- `HIVE_MIND_MERGE_QUEUE_WAIT_FOR_POST_MERGE_CI` (default: true)
|
|
23
|
+
- `HIVE_MIND_MERGE_QUEUE_STOP_ON_CI_FAILURE` (default: true)
|
|
24
|
+
- `HIVE_MIND_MERGE_QUEUE_CHECK_BRANCH_HEALTH` (default: true)
|
|
25
|
+
- `HIVE_MIND_MERGE_QUEUE_POST_MERGE_CI_TIMEOUT_MS` (default: 60 minutes)
|
|
26
|
+
- `HIVE_MIND_MERGE_QUEUE_POST_MERGE_CI_POLL_INTERVAL_MS` (default: 30 seconds)
|
|
27
|
+
|
|
28
|
+
**New API functions:**
|
|
29
|
+
- `waitForCommitCI()` - Wait for workflow runs on a commit
|
|
30
|
+
- `checkBranchCIHealth()` - Check for failed CI on a branch
|
|
31
|
+
- `getMergeCommitSha()` - Get merge commit SHA for a PR
|
|
32
|
+
|
|
33
|
+
## 1.24.6
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- Make `--auto-resume-on-limit-reset` enabled by default to improve user experience when hitting API rate limits. Previously defaulted to `false`, now defaults to `true` for both `solve` and `hive` commands. Users can explicitly disable with `--no-auto-resume-on-limit-reset` if needed.
|
|
38
|
+
|
|
39
|
+
Fix false positive error detection for step_finish with reason stop
|
|
40
|
+
|
|
41
|
+
When an agent encounters a timeout error during execution but successfully recovers and completes (indicated by `step_finish` with `reason: "stop"`), the error detection was incorrectly flagging this as a failure due to fallback pattern matching.
|
|
42
|
+
|
|
43
|
+
The `agentCompletedSuccessfully` flag was only being set for `session.idle` and `"exiting loop"` log messages (Issue #1276), but not for the more common `step_finish` event with `reason: "stop"`. This meant the fallback pattern matching would still trigger and detect error patterns in the full output, even when the agent had clearly completed successfully.
|
|
44
|
+
|
|
45
|
+
Fix: Add `step_finish` with `reason: "stop"` as a success marker in both stdout and stderr processing loops in `src/agent.lib.mjs`.
|
|
46
|
+
|
|
3
47
|
## 1.24.5
|
|
4
48
|
|
|
5
49
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -268,12 +268,10 @@ See [docs/HELM.md](./docs/HELM.md) for detailed Helm configuration options.
|
|
|
268
268
|
--attach-logs
|
|
269
269
|
--verbose
|
|
270
270
|
--no-tool-check
|
|
271
|
-
--auto-resume-on-limit-reset
|
|
272
271
|
TELEGRAM_SOLVE_OVERRIDES:
|
|
273
272
|
--attach-logs
|
|
274
273
|
--verbose
|
|
275
274
|
--no-tool-check
|
|
276
|
-
--auto-resume-on-limit-reset
|
|
277
275
|
TELEGRAM_BOT_VERBOSE: true
|
|
278
276
|
"
|
|
279
277
|
|
|
@@ -295,12 +293,10 @@ See [docs/HELM.md](./docs/HELM.md) for detailed Helm configuration options.
|
|
|
295
293
|
--attach-logs
|
|
296
294
|
--verbose
|
|
297
295
|
--no-tool-check
|
|
298
|
-
--auto-resume-on-limit-reset
|
|
299
296
|
)" --solve-overrides "(
|
|
300
297
|
--attach-logs
|
|
301
298
|
--verbose
|
|
302
299
|
--no-tool-check
|
|
303
|
-
--auto-resume-on-limit-reset
|
|
304
300
|
)" --verbose
|
|
305
301
|
|
|
306
302
|
# Press CTRL + A + D for detach from screen
|
package/package.json
CHANGED
package/src/agent.lib.mjs
CHANGED
|
@@ -665,6 +665,13 @@ export const executeAgentCommand = async params => {
|
|
|
665
665
|
if (data.type === 'session.idle' || (data.type === 'log' && data.message === 'exiting loop')) {
|
|
666
666
|
agentCompletedSuccessfully = true;
|
|
667
667
|
}
|
|
668
|
+
// Issue #1296: Detect step_finish with reason "stop" as successful completion
|
|
669
|
+
// This is a clear marker of success - agent finished normally, not due to error or limit
|
|
670
|
+
// When this event appears, we should ignore any error events that appeared earlier in the stream
|
|
671
|
+
// (e.g., timeout errors that were recovered from via retry logic)
|
|
672
|
+
if (data.type === 'step_finish' && data.part?.reason === 'stop') {
|
|
673
|
+
agentCompletedSuccessfully = true;
|
|
674
|
+
}
|
|
668
675
|
} catch {
|
|
669
676
|
// Not JSON - log as plain text
|
|
670
677
|
await log(line);
|
|
@@ -727,6 +734,11 @@ export const executeAgentCommand = async params => {
|
|
|
727
734
|
if (stderrData.type === 'session.idle' || (stderrData.type === 'log' && stderrData.message === 'exiting loop')) {
|
|
728
735
|
agentCompletedSuccessfully = true;
|
|
729
736
|
}
|
|
737
|
+
// Issue #1296: Detect step_finish with reason "stop" as successful completion (stderr)
|
|
738
|
+
// This is a clear marker of success - agent finished normally, not due to error or limit
|
|
739
|
+
if (stderrData.type === 'step_finish' && stderrData.part?.reason === 'stop') {
|
|
740
|
+
agentCompletedSuccessfully = true;
|
|
741
|
+
}
|
|
730
742
|
} catch {
|
|
731
743
|
// Not JSON - log as plain text
|
|
732
744
|
await log(stderrLine);
|
package/src/config.lib.mjs
CHANGED
|
@@ -443,6 +443,31 @@ export const mergeQueue = {
|
|
|
443
443
|
// Issue #1307: Polling interval for checking target branch CI status (in milliseconds)
|
|
444
444
|
// Default: 30 seconds (30000ms) - more frequent than PR CI polling since we're blocking
|
|
445
445
|
targetBranchCIPollIntervalMs: parseIntWithDefault('HIVE_MIND_MERGE_QUEUE_TARGET_CI_POLL_INTERVAL_MS', 30 * 1000),
|
|
446
|
+
// Issue #1341: Wait for post-merge CI to complete before merging next PR
|
|
447
|
+
// When enabled, the merge queue will wait for all CI runs triggered by a merge
|
|
448
|
+
// to complete before processing the next PR. This ensures each merge gets its own
|
|
449
|
+
// release/publish cycle.
|
|
450
|
+
// Default: true - ensures post-merge CI (including release workflows) completes
|
|
451
|
+
waitForPostMergeCI: getenv('HIVE_MIND_MERGE_QUEUE_WAIT_FOR_POST_MERGE_CI', 'true').toLowerCase() === 'true',
|
|
452
|
+
// Issue #1341: Stop the queue if post-merge CI fails
|
|
453
|
+
// When enabled, the merge queue will stop processing if any post-merge CI run fails
|
|
454
|
+
// This prevents cascading failures and allows humans to investigate
|
|
455
|
+
// Default: true - stop on failure to prevent problems from multiplying
|
|
456
|
+
stopOnPostMergeCIFailure: getenv('HIVE_MIND_MERGE_QUEUE_STOP_ON_CI_FAILURE', 'true').toLowerCase() === 'true',
|
|
457
|
+
// Issue #1341: Check for existing CI failures before starting the queue
|
|
458
|
+
// When enabled, the merge queue will check if there are any failed CI runs on
|
|
459
|
+
// the default branch before starting to process PRs. If failures exist, it will
|
|
460
|
+
// report them and stop.
|
|
461
|
+
// Default: true - ensure a healthy branch before merging
|
|
462
|
+
checkBranchCIHealthBeforeStart: getenv('HIVE_MIND_MERGE_QUEUE_CHECK_BRANCH_HEALTH', 'true').toLowerCase() === 'true',
|
|
463
|
+
// Issue #1341: Timeout for waiting on post-merge CI (in milliseconds)
|
|
464
|
+
// This is per-merge, not total. If a single merge's CI doesn't complete within
|
|
465
|
+
// this time, the queue will fail with a timeout error.
|
|
466
|
+
// Default: 60 minutes (3600000ms) - typical CI/CD pipelines take 15-45 minutes
|
|
467
|
+
postMergeCITimeoutMs: parseIntWithDefault('HIVE_MIND_MERGE_QUEUE_POST_MERGE_CI_TIMEOUT_MS', 60 * 60 * 1000),
|
|
468
|
+
// Issue #1341: Polling interval for post-merge CI status (in milliseconds)
|
|
469
|
+
// Default: 30 seconds (30000ms) - balance between responsiveness and API rate limits
|
|
470
|
+
postMergeCIPollIntervalMs: parseIntWithDefault('HIVE_MIND_MERGE_QUEUE_POST_MERGE_CI_POLL_INTERVAL_MS', 30 * 1000),
|
|
446
471
|
};
|
|
447
472
|
|
|
448
473
|
// Helper function to validate configuration values
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Merge Queue CI Monitoring Library
|
|
4
|
+
*
|
|
5
|
+
* Provides utilities for monitoring CI/CD status on commits and branches.
|
|
6
|
+
* Split from github-merge.lib.mjs to maintain file size limits.
|
|
7
|
+
*
|
|
8
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1341
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getWorkflowRunsForSha } from './github-merge.lib.mjs';
|
|
12
|
+
import { promisify } from 'util';
|
|
13
|
+
import { exec as execCallback } from 'child_process';
|
|
14
|
+
|
|
15
|
+
const exec = promisify(execCallback);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wait for all workflow runs triggered by a specific commit to complete
|
|
19
|
+
* Issue #1341: Used to wait for post-merge CI before processing the next PR
|
|
20
|
+
*
|
|
21
|
+
* @param {string} owner - Repository owner
|
|
22
|
+
* @param {string} repo - Repository name
|
|
23
|
+
* @param {string} sha - Commit SHA to monitor
|
|
24
|
+
* @param {Object} options - Wait options
|
|
25
|
+
* @param {number} options.timeout - Maximum wait time in ms (default: 60 minutes)
|
|
26
|
+
* @param {number} options.pollInterval - Polling interval in ms (default: 30 seconds)
|
|
27
|
+
* @param {Function} options.onStatusUpdate - Callback for status updates
|
|
28
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
29
|
+
* @returns {Promise<{success: boolean, status: string, runs: Array, failedRuns: Array, error: string|null}>}
|
|
30
|
+
*/
|
|
31
|
+
export async function waitForCommitCI(owner, repo, sha, options = {}, verbose = false) {
|
|
32
|
+
const { timeout = 60 * 60 * 1000, pollInterval = 30 * 1000, onStatusUpdate = null } = options;
|
|
33
|
+
|
|
34
|
+
const startTime = Date.now();
|
|
35
|
+
let noRunsIterations = 0;
|
|
36
|
+
const MAX_NO_RUNS_ITERATIONS = 10; // Wait up to ~5 minutes for runs to appear
|
|
37
|
+
|
|
38
|
+
if (verbose) {
|
|
39
|
+
console.log(`[VERBOSE] /merge: Waiting for CI runs on commit ${sha.substring(0, 7)} to complete...`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
while (Date.now() - startTime < timeout) {
|
|
43
|
+
let runs;
|
|
44
|
+
try {
|
|
45
|
+
runs = await getWorkflowRunsForSha(owner, repo, sha, verbose);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(`[ERROR] /merge: Error checking commit CI: ${error.message}`);
|
|
48
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle case where no runs exist yet (CI hasn't started)
|
|
53
|
+
if (runs.length === 0) {
|
|
54
|
+
noRunsIterations++;
|
|
55
|
+
if (noRunsIterations >= MAX_NO_RUNS_ITERATIONS) {
|
|
56
|
+
// No CI runs after waiting - assume no CI is configured or it's optional
|
|
57
|
+
if (verbose) {
|
|
58
|
+
console.log(`[VERBOSE] /merge: No CI runs found for commit ${sha.substring(0, 7)} after ${MAX_NO_RUNS_ITERATIONS} checks. Proceeding.`);
|
|
59
|
+
}
|
|
60
|
+
return { success: true, status: 'no_runs', runs: [], failedRuns: [], error: null };
|
|
61
|
+
}
|
|
62
|
+
if (verbose) {
|
|
63
|
+
console.log(`[VERBOSE] /merge: No CI runs yet for commit ${sha.substring(0, 7)} (attempt ${noRunsIterations}/${MAX_NO_RUNS_ITERATIONS}). Waiting...`);
|
|
64
|
+
}
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Reset counter when runs appear
|
|
70
|
+
noRunsIterations = 0;
|
|
71
|
+
|
|
72
|
+
// Check run statuses
|
|
73
|
+
const completedRuns = runs.filter(r => r.status === 'completed');
|
|
74
|
+
const inProgressRuns = runs.filter(r => r.status === 'in_progress' || r.status === 'queued' || r.status === 'waiting' || r.status === 'requested' || r.status === 'pending');
|
|
75
|
+
const failedRuns = completedRuns.filter(r => r.conclusion === 'failure' || r.conclusion === 'timed_out' || r.conclusion === 'cancelled');
|
|
76
|
+
const successRuns = completedRuns.filter(r => r.conclusion === 'success' || r.conclusion === 'skipped' || r.conclusion === 'neutral');
|
|
77
|
+
|
|
78
|
+
// Report status
|
|
79
|
+
if (onStatusUpdate) {
|
|
80
|
+
try {
|
|
81
|
+
await onStatusUpdate({
|
|
82
|
+
sha,
|
|
83
|
+
totalRuns: runs.length,
|
|
84
|
+
completedRuns: completedRuns.length,
|
|
85
|
+
inProgressRuns: inProgressRuns.length,
|
|
86
|
+
failedRuns: failedRuns.length,
|
|
87
|
+
successRuns: successRuns.length,
|
|
88
|
+
runs,
|
|
89
|
+
elapsedMs: Date.now() - startTime,
|
|
90
|
+
});
|
|
91
|
+
} catch (callbackError) {
|
|
92
|
+
console.error(`[ERROR] /merge: Status update callback failed: ${callbackError.message}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// All runs completed
|
|
97
|
+
if (inProgressRuns.length === 0) {
|
|
98
|
+
// Check for failures
|
|
99
|
+
if (failedRuns.length > 0) {
|
|
100
|
+
if (verbose) {
|
|
101
|
+
console.log(`[VERBOSE] /merge: CI completed with ${failedRuns.length} failure(s) for commit ${sha.substring(0, 7)}`);
|
|
102
|
+
for (const run of failedRuns) {
|
|
103
|
+
console.log(`[VERBOSE] /merge: - FAILED: ${run.name} (${run.conclusion}): ${run.html_url}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
status: 'failure',
|
|
109
|
+
runs,
|
|
110
|
+
failedRuns,
|
|
111
|
+
error: `${failedRuns.length} CI run(s) failed: ${failedRuns.map(r => r.name).join(', ')}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// All passed
|
|
116
|
+
if (verbose) {
|
|
117
|
+
console.log(`[VERBOSE] /merge: All ${completedRuns.length} CI runs passed for commit ${sha.substring(0, 7)}`);
|
|
118
|
+
}
|
|
119
|
+
return { success: true, status: 'success', runs, failedRuns: [], error: null };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Still waiting
|
|
123
|
+
if (verbose) {
|
|
124
|
+
const elapsedSec = Math.round((Date.now() - startTime) / 1000);
|
|
125
|
+
console.log(`[VERBOSE] /merge: Waiting for ${inProgressRuns.length} CI run(s) to complete... (${elapsedSec}s elapsed)`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Timeout reached
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
status: 'timeout',
|
|
135
|
+
runs: await getWorkflowRunsForSha(owner, repo, sha, verbose),
|
|
136
|
+
failedRuns: [],
|
|
137
|
+
error: `Timeout waiting for CI runs on commit ${sha.substring(0, 7)}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if the default branch has any recent failed CI runs
|
|
143
|
+
* Issue #1341: Used to detect pre-existing failures before starting the merge queue
|
|
144
|
+
*
|
|
145
|
+
* @param {string} owner - Repository owner
|
|
146
|
+
* @param {string} repo - Repository name
|
|
147
|
+
* @param {string} branch - Branch name (usually 'main' or 'master')
|
|
148
|
+
* @param {Object} options - Check options
|
|
149
|
+
* @param {number} options.lookbackCount - Number of recent runs to check (default: 5)
|
|
150
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
151
|
+
* @returns {Promise<{healthy: boolean, failedRuns: Array, error: string|null}>}
|
|
152
|
+
*/
|
|
153
|
+
export async function checkBranchCIHealth(owner, repo, branch = 'main', options = {}, verbose = false) {
|
|
154
|
+
const { lookbackCount = 5 } = options;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Get recent completed workflow runs on the branch
|
|
158
|
+
const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?branch=${branch}&status=completed&per_page=${lookbackCount}" --jq '[.workflow_runs[] | {id: .id, name: .name, status: .status, conclusion: .conclusion, html_url: .html_url, head_sha: .head_sha, created_at: .created_at}]'`);
|
|
159
|
+
const runs = JSON.parse(stdout.trim() || '[]');
|
|
160
|
+
|
|
161
|
+
if (verbose) {
|
|
162
|
+
console.log(`[VERBOSE] /merge: Checking ${runs.length} recent CI runs on ${owner}/${repo} branch ${branch}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (runs.length === 0) {
|
|
166
|
+
// No recent runs - assume healthy
|
|
167
|
+
return { healthy: true, failedRuns: [], error: null };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check for failures in the most recent run(s)
|
|
171
|
+
const latestSha = runs[0].head_sha;
|
|
172
|
+
const latestRuns = runs.filter(r => r.head_sha === latestSha);
|
|
173
|
+
const failedRuns = latestRuns.filter(r => r.conclusion === 'failure' || r.conclusion === 'timed_out');
|
|
174
|
+
|
|
175
|
+
if (failedRuns.length > 0) {
|
|
176
|
+
if (verbose) {
|
|
177
|
+
console.log(`[VERBOSE] /merge: Found ${failedRuns.length} failed CI run(s) on ${branch}:`);
|
|
178
|
+
for (const run of failedRuns) {
|
|
179
|
+
console.log(`[VERBOSE] /merge: - ${run.name}: ${run.conclusion} (${run.html_url})`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
healthy: false,
|
|
184
|
+
failedRuns,
|
|
185
|
+
error: `${failedRuns.length} CI run(s) failed on ${branch}: ${failedRuns.map(r => r.name).join(', ')}`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (verbose) {
|
|
190
|
+
console.log(`[VERBOSE] /merge: Branch ${branch} CI is healthy (${latestRuns.length} runs checked)`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { healthy: true, failedRuns: [], error: null };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (verbose) {
|
|
196
|
+
console.log(`[VERBOSE] /merge: Error checking branch CI health: ${error.message}`);
|
|
197
|
+
}
|
|
198
|
+
// On error, assume healthy to avoid blocking merges due to API issues
|
|
199
|
+
return { healthy: true, failedRuns: [], error: null };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get the merge commit SHA for a merged pull request
|
|
205
|
+
* Issue #1341: Used to track the commit that triggers post-merge CI
|
|
206
|
+
*
|
|
207
|
+
* @param {string} owner - Repository owner
|
|
208
|
+
* @param {string} repo - Repository name
|
|
209
|
+
* @param {number} prNumber - Pull request number
|
|
210
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
211
|
+
* @returns {Promise<{sha: string|null, error: string|null}>}
|
|
212
|
+
*/
|
|
213
|
+
export async function getMergeCommitSha(owner, repo, prNumber, verbose = false) {
|
|
214
|
+
try {
|
|
215
|
+
const { stdout } = await exec(`gh pr view ${prNumber} --repo ${owner}/${repo} --json mergeCommit --jq '.mergeCommit.oid'`);
|
|
216
|
+
const sha = stdout.trim();
|
|
217
|
+
|
|
218
|
+
if (!sha || sha === 'null') {
|
|
219
|
+
if (verbose) {
|
|
220
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has no merge commit (may not be merged yet)`);
|
|
221
|
+
}
|
|
222
|
+
return { sha: null, error: 'PR is not merged or merge commit not available' };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (verbose) {
|
|
226
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} merge commit: ${sha.substring(0, 7)}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { sha, error: null };
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (verbose) {
|
|
232
|
+
console.log(`[VERBOSE] /merge: Error getting merge commit for PR #${prNumber}: ${error.message}`);
|
|
233
|
+
}
|
|
234
|
+
return { sha: null, error: error.message };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export default {
|
|
239
|
+
waitForCommitCI,
|
|
240
|
+
checkBranchCIHealth,
|
|
241
|
+
getMergeCommitSha,
|
|
242
|
+
};
|
package/src/github-merge.lib.mjs
CHANGED
|
@@ -1204,11 +1204,11 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
|
|
|
1204
1204
|
* @param {string} repo - Repository name
|
|
1205
1205
|
* @param {string} sha - Commit SHA
|
|
1206
1206
|
* @param {boolean} verbose - Whether to log verbose output
|
|
1207
|
-
* @returns {Promise<Array<{id: number, status: string, conclusion: string|null, name: string}>>}
|
|
1207
|
+
* @returns {Promise<Array<{id: number, status: string, conclusion: string|null, name: string, html_url: string}>>}
|
|
1208
1208
|
*/
|
|
1209
1209
|
export async function getWorkflowRunsForSha(owner, repo, sha, verbose = false) {
|
|
1210
1210
|
try {
|
|
1211
|
-
const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=20" --jq '[.workflow_runs[] | {id: .id, status: .status, conclusion: .conclusion, name: .name}]'`);
|
|
1211
|
+
const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=20" --jq '[.workflow_runs[] | {id: .id, status: .status, conclusion: .conclusion, name: .name, html_url: .html_url}]'`);
|
|
1212
1212
|
const runs = JSON.parse(stdout.trim() || '[]');
|
|
1213
1213
|
|
|
1214
1214
|
if (verbose) {
|
|
@@ -1227,6 +1227,11 @@ export async function getWorkflowRunsForSha(owner, repo, sha, verbose = false) {
|
|
|
1227
1227
|
}
|
|
1228
1228
|
}
|
|
1229
1229
|
|
|
1230
|
+
// Issue #1341: Import and re-export post-merge CI functions from separate module
|
|
1231
|
+
// to keep this file under the 1500 line limit
|
|
1232
|
+
import { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha } from './github-merge-ci.lib.mjs';
|
|
1233
|
+
export { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha };
|
|
1234
|
+
|
|
1230
1235
|
export default {
|
|
1231
1236
|
READY_LABEL,
|
|
1232
1237
|
checkReadyLabelExists,
|
|
@@ -1256,4 +1261,8 @@ export default {
|
|
|
1256
1261
|
rerunWorkflowRun,
|
|
1257
1262
|
rerunFailedJobs,
|
|
1258
1263
|
getWorkflowRunsForSha,
|
|
1264
|
+
// Issue #1341: Post-merge CI waiting and branch health checking
|
|
1265
|
+
waitForCommitCI,
|
|
1266
|
+
checkBranchCIHealth,
|
|
1267
|
+
getMergeCommitSha,
|
|
1259
1268
|
};
|
package/src/hive.config.lib.mjs
CHANGED
|
@@ -36,7 +36,7 @@ const HIVE_CUSTOM_SOLVE_OPTIONS = {
|
|
|
36
36
|
'auto-resume-on-limit-reset': {
|
|
37
37
|
type: 'boolean',
|
|
38
38
|
description: 'Automatically resume when AI tool limit resets (calculates reset time and waits). Passed to solve command.',
|
|
39
|
-
default:
|
|
39
|
+
default: true,
|
|
40
40
|
},
|
|
41
41
|
'auto-cleanup': {
|
|
42
42
|
type: 'boolean',
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -132,7 +132,7 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
132
132
|
'auto-resume-on-limit-reset': {
|
|
133
133
|
type: 'boolean',
|
|
134
134
|
description: 'Automatically resume when AI tool limit resets (maintains session context with --resume flag)',
|
|
135
|
-
default:
|
|
135
|
+
default: true,
|
|
136
136
|
},
|
|
137
137
|
'auto-restart-on-limit-reset': {
|
|
138
138
|
type: 'boolean',
|
|
@@ -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 } from './github-merge.lib.mjs';
|
|
19
|
+
import { getAllReadyPRs, checkPRCIStatus, checkPRMergeable, mergePullRequest, waitForCI, ensureReadyLabel, waitForBranchCI, getDefaultBranch, waitForCommitCI, checkBranchCIHealth, getMergeCommitSha } 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
|
|
|
@@ -79,6 +79,13 @@ export const MERGE_QUEUE_CONFIG = {
|
|
|
79
79
|
WAIT_FOR_TARGET_BRANCH_CI: mergeQueueConfig.waitForTargetBranchCI,
|
|
80
80
|
TARGET_BRANCH_CI_TIMEOUT_MS: mergeQueueConfig.targetBranchCITimeoutMs,
|
|
81
81
|
TARGET_BRANCH_CI_POLL_INTERVAL_MS: mergeQueueConfig.targetBranchCIPollIntervalMs,
|
|
82
|
+
|
|
83
|
+
// Issue #1341: Wait for post-merge CI to complete before merging next PR
|
|
84
|
+
WAIT_FOR_POST_MERGE_CI: mergeQueueConfig.waitForPostMergeCI,
|
|
85
|
+
STOP_ON_POST_MERGE_CI_FAILURE: mergeQueueConfig.stopOnPostMergeCIFailure,
|
|
86
|
+
CHECK_BRANCH_CI_HEALTH_BEFORE_START: mergeQueueConfig.checkBranchCIHealthBeforeStart,
|
|
87
|
+
POST_MERGE_CI_TIMEOUT_MS: mergeQueueConfig.postMergeCITimeoutMs,
|
|
88
|
+
POST_MERGE_CI_POLL_INTERVAL_MS: mergeQueueConfig.postMergeCIPollIntervalMs,
|
|
82
89
|
};
|
|
83
90
|
|
|
84
91
|
/**
|
|
@@ -94,6 +101,8 @@ class MergeQueueItem {
|
|
|
94
101
|
this.ciStatus = null;
|
|
95
102
|
this.startedAt = null;
|
|
96
103
|
this.completedAt = null;
|
|
104
|
+
// Issue #1341: Track merge commit SHA for post-merge CI waiting
|
|
105
|
+
this.mergeCommitSha = null;
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
/**
|
|
@@ -234,6 +243,29 @@ export class MergeQueueProcessor {
|
|
|
234
243
|
this.isCancelled = false;
|
|
235
244
|
|
|
236
245
|
try {
|
|
246
|
+
// Issue #1341: Check if the default branch has any failed CI runs before starting
|
|
247
|
+
// This prevents merging on top of a broken branch
|
|
248
|
+
if (MERGE_QUEUE_CONFIG.CHECK_BRANCH_CI_HEALTH_BEFORE_START) {
|
|
249
|
+
const healthCheckResult = await this.checkBranchCIHealthBeforeStart();
|
|
250
|
+
if (!healthCheckResult.healthy) {
|
|
251
|
+
this.status = MergeStatus.FAILED;
|
|
252
|
+
this.error = healthCheckResult.error;
|
|
253
|
+
this.completedAt = new Date();
|
|
254
|
+
// Store the failed runs for the error report
|
|
255
|
+
this.branchCIFailedRuns = healthCheckResult.failedRuns;
|
|
256
|
+
|
|
257
|
+
if (this.onError) {
|
|
258
|
+
await this.onError(new Error(healthCheckResult.error));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
success: false,
|
|
263
|
+
stats: this.stats,
|
|
264
|
+
error: healthCheckResult.error,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
237
269
|
// Issue #1307: Wait for any active CI runs on the target branch before processing
|
|
238
270
|
// This prevents merging while post-merge CI from previous merges is still running
|
|
239
271
|
if (MERGE_QUEUE_CONFIG.WAIT_FOR_TARGET_BRANCH_CI) {
|
|
@@ -255,10 +287,35 @@ export class MergeQueueProcessor {
|
|
|
255
287
|
await this.onProgress(this.getProgressUpdate());
|
|
256
288
|
}
|
|
257
289
|
|
|
258
|
-
// If merged, wait
|
|
290
|
+
// Issue #1341: If merged successfully, wait for post-merge CI to complete
|
|
291
|
+
// This ensures each PR's CI completes (including releases) before merging the next
|
|
259
292
|
if (item.status === MergeItemStatus.MERGED && this.currentIndex < this.items.length - 1) {
|
|
260
|
-
|
|
261
|
-
|
|
293
|
+
if (MERGE_QUEUE_CONFIG.WAIT_FOR_POST_MERGE_CI && item.mergeCommitSha) {
|
|
294
|
+
const postMergeCIResult = await this.waitForPostMergeCI(item);
|
|
295
|
+
|
|
296
|
+
// Issue #1341: Stop the queue if post-merge CI failed
|
|
297
|
+
if (!postMergeCIResult.success && MERGE_QUEUE_CONFIG.STOP_ON_POST_MERGE_CI_FAILURE) {
|
|
298
|
+
this.status = MergeStatus.FAILED;
|
|
299
|
+
this.error = postMergeCIResult.error;
|
|
300
|
+
this.completedAt = new Date();
|
|
301
|
+
// Store the failed runs for the error report
|
|
302
|
+
this.postMergeCIFailedRuns = postMergeCIResult.failedRuns;
|
|
303
|
+
|
|
304
|
+
if (this.onError) {
|
|
305
|
+
await this.onError(new Error(postMergeCIResult.error));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
success: false,
|
|
310
|
+
stats: this.stats,
|
|
311
|
+
error: postMergeCIResult.error,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
// Fallback: short wait before processing next PR
|
|
316
|
+
this.log(`Waiting ${MERGE_QUEUE_CONFIG.POST_MERGE_WAIT_MS / 1000}s before next PR...`);
|
|
317
|
+
await this.sleep(MERGE_QUEUE_CONFIG.POST_MERGE_WAIT_MS);
|
|
318
|
+
}
|
|
262
319
|
}
|
|
263
320
|
}
|
|
264
321
|
|
|
@@ -372,6 +429,17 @@ export class MergeQueueProcessor {
|
|
|
372
429
|
item.completedAt = new Date();
|
|
373
430
|
this.stats.merged++;
|
|
374
431
|
this.log(`Successfully merged PR #${item.pr.number}`);
|
|
432
|
+
|
|
433
|
+
// Issue #1341: Get the merge commit SHA for post-merge CI tracking
|
|
434
|
+
// Need a small delay to allow GitHub to update the PR state
|
|
435
|
+
await this.sleep(5000);
|
|
436
|
+
const mergeCommitResult = await getMergeCommitSha(this.owner, this.repo, item.pr.number, this.verbose);
|
|
437
|
+
if (mergeCommitResult.sha) {
|
|
438
|
+
item.mergeCommitSha = mergeCommitResult.sha;
|
|
439
|
+
this.log(`PR #${item.pr.number} merge commit: ${mergeCommitResult.sha.substring(0, 7)}`);
|
|
440
|
+
} else {
|
|
441
|
+
this.log(`Could not get merge commit SHA for PR #${item.pr.number}: ${mergeCommitResult.error}`);
|
|
442
|
+
}
|
|
375
443
|
} catch (error) {
|
|
376
444
|
item.status = MergeItemStatus.FAILED;
|
|
377
445
|
item.error = error.message;
|
|
@@ -442,6 +510,105 @@ export class MergeQueueProcessor {
|
|
|
442
510
|
}
|
|
443
511
|
}
|
|
444
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Check if the default branch has any failed CI runs before starting the queue
|
|
515
|
+
* Issue #1341: Prevents merging on top of a broken branch
|
|
516
|
+
* @returns {Promise<{healthy: boolean, failedRuns: Array, error: string|null}>}
|
|
517
|
+
*/
|
|
518
|
+
async checkBranchCIHealthBeforeStart() {
|
|
519
|
+
try {
|
|
520
|
+
const targetBranch = await getDefaultBranch(this.owner, this.repo, this.verbose);
|
|
521
|
+
this.log(`Checking CI health on ${targetBranch} branch before starting queue...`);
|
|
522
|
+
|
|
523
|
+
const healthResult = await checkBranchCIHealth(this.owner, this.repo, targetBranch, {}, this.verbose);
|
|
524
|
+
|
|
525
|
+
if (!healthResult.healthy) {
|
|
526
|
+
this.log(`Branch ${targetBranch} has ${healthResult.failedRuns.length} failed CI run(s)`);
|
|
527
|
+
return {
|
|
528
|
+
healthy: false,
|
|
529
|
+
failedRuns: healthResult.failedRuns,
|
|
530
|
+
error: `Cannot start merge queue: ${healthResult.error}. Please fix the CI failures first.`,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
this.log(`Branch ${targetBranch} CI is healthy. Ready to proceed.`);
|
|
535
|
+
return {
|
|
536
|
+
healthy: true,
|
|
537
|
+
failedRuns: [],
|
|
538
|
+
error: null,
|
|
539
|
+
};
|
|
540
|
+
} catch (error) {
|
|
541
|
+
// On error, assume healthy to avoid blocking merges due to API issues
|
|
542
|
+
console.warn(`[WARN] /merge-queue: Error checking branch CI health: ${error.message}. Proceeding anyway.`);
|
|
543
|
+
return {
|
|
544
|
+
healthy: true,
|
|
545
|
+
failedRuns: [],
|
|
546
|
+
error: null,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Wait for post-merge CI to complete for a merged PR
|
|
553
|
+
* Issue #1341: Ensures each PR's CI completes (including releases) before merging the next
|
|
554
|
+
* @param {MergeQueueItem} item - The merged PR item
|
|
555
|
+
* @returns {Promise<{success: boolean, failedRuns: Array, error: string|null}>}
|
|
556
|
+
*/
|
|
557
|
+
async waitForPostMergeCI(item) {
|
|
558
|
+
if (!item.mergeCommitSha) {
|
|
559
|
+
this.log(`No merge commit SHA available for PR #${item.pr.number}, skipping post-merge CI wait`);
|
|
560
|
+
return { success: true, failedRuns: [], error: null };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Track that we're waiting for post-merge CI (for progress updates)
|
|
564
|
+
this.waitingForPostMergeCI = true;
|
|
565
|
+
this.postMergeCIStatus = null;
|
|
566
|
+
this.currentPostMergePR = item.pr.number;
|
|
567
|
+
|
|
568
|
+
try {
|
|
569
|
+
this.log(`Waiting for post-merge CI on commit ${item.mergeCommitSha.substring(0, 7)} (PR #${item.pr.number})...`);
|
|
570
|
+
|
|
571
|
+
const waitResult = await waitForCommitCI(
|
|
572
|
+
this.owner,
|
|
573
|
+
this.repo,
|
|
574
|
+
item.mergeCommitSha,
|
|
575
|
+
{
|
|
576
|
+
timeout: MERGE_QUEUE_CONFIG.POST_MERGE_CI_TIMEOUT_MS,
|
|
577
|
+
pollInterval: MERGE_QUEUE_CONFIG.POST_MERGE_CI_POLL_INTERVAL_MS,
|
|
578
|
+
onStatusUpdate: async status => {
|
|
579
|
+
this.postMergeCIStatus = status;
|
|
580
|
+
|
|
581
|
+
// Report progress while waiting
|
|
582
|
+
if (this.onProgress) {
|
|
583
|
+
await this.onProgress(this.getProgressUpdate());
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
this.verbose
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
if (waitResult.success) {
|
|
591
|
+
if (waitResult.status === 'no_runs') {
|
|
592
|
+
this.log(`No CI runs found for merge commit ${item.mergeCommitSha.substring(0, 7)}. Proceeding.`);
|
|
593
|
+
} else {
|
|
594
|
+
this.log(`Post-merge CI completed successfully for PR #${item.pr.number}`);
|
|
595
|
+
}
|
|
596
|
+
return { success: true, failedRuns: [], error: null };
|
|
597
|
+
} else {
|
|
598
|
+
this.log(`Post-merge CI failed for PR #${item.pr.number}: ${waitResult.error}`);
|
|
599
|
+
return {
|
|
600
|
+
success: false,
|
|
601
|
+
failedRuns: waitResult.failedRuns || [],
|
|
602
|
+
error: waitResult.error,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
} finally {
|
|
606
|
+
this.waitingForPostMergeCI = false;
|
|
607
|
+
this.postMergeCIStatus = null;
|
|
608
|
+
this.currentPostMergePR = null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
445
612
|
/**
|
|
446
613
|
* Get current progress update
|
|
447
614
|
*/
|
|
@@ -525,8 +692,21 @@ export class MergeQueueProcessor {
|
|
|
525
692
|
message += `⏱️ Checking for active CI runs on target branch\\.\\.\\.\n\n`;
|
|
526
693
|
}
|
|
527
694
|
|
|
695
|
+
// Issue #1341: Show waiting status for post-merge CI
|
|
696
|
+
if (this.waitingForPostMergeCI && this.postMergeCIStatus) {
|
|
697
|
+
const elapsedSec = Math.round(this.postMergeCIStatus.elapsedMs / 1000);
|
|
698
|
+
const elapsedMin = Math.floor(elapsedSec / 60);
|
|
699
|
+
const elapsedSecRemainder = elapsedSec % 60;
|
|
700
|
+
const completed = this.postMergeCIStatus.completedRuns || 0;
|
|
701
|
+
const total = this.postMergeCIStatus.totalRuns || 0;
|
|
702
|
+
const inProgress = total - completed;
|
|
703
|
+
message += `⏱️ Waiting for post\\-merge CI \\(PR \\#${this.currentPostMergePR}\\): ${inProgress} in progress, ${completed}/${total} completed \\(${elapsedMin}m ${elapsedSecRemainder}s\\)\\.\\.\\.\n\n`;
|
|
704
|
+
} else if (this.waitingForPostMergeCI) {
|
|
705
|
+
message += `⏱️ Waiting for post\\-merge CI \\(PR \\#${this.currentPostMergePR}\\)\\.\\.\\.\n\n`;
|
|
706
|
+
}
|
|
707
|
+
|
|
528
708
|
// Current item being processed
|
|
529
|
-
if (update.current && !this.waitingForTargetBranchCI) {
|
|
709
|
+
if (update.current && !this.waitingForTargetBranchCI && !this.waitingForPostMergeCI) {
|
|
530
710
|
const statusEmoji = update.currentStatus === MergeItemStatus.WAITING_CI ? '⏱️' : '🔄';
|
|
531
711
|
// Issue #1339: escape the current item description for MarkdownV2
|
|
532
712
|
message += `${statusEmoji} ${this.escapeMarkdown(update.current)}\n\n`;
|
|
@@ -609,6 +789,36 @@ export class MergeQueueProcessor {
|
|
|
609
789
|
message += `⏭️ Skipped: ${report.stats.skipped} `;
|
|
610
790
|
message += `📋 Total: ${report.stats.total}\n\n`;
|
|
611
791
|
|
|
792
|
+
// Issue #1341: Show branch CI health failure details if applicable
|
|
793
|
+
if (this.branchCIFailedRuns && this.branchCIFailedRuns.length > 0) {
|
|
794
|
+
message += `⚠️ *Branch CI Failures \\(blocked queue\\):*\n`;
|
|
795
|
+
for (const run of this.branchCIFailedRuns.slice(0, 3)) {
|
|
796
|
+
const runName = this.escapeMarkdown(run.name);
|
|
797
|
+
// Format the URL for MarkdownV2 - need to escape special characters
|
|
798
|
+
const runUrl = run.html_url ? `[View](${run.html_url.replace(/[)]/g, '\\)')})` : '';
|
|
799
|
+
message += ` ❌ ${runName} ${runUrl}\n`;
|
|
800
|
+
}
|
|
801
|
+
if (this.branchCIFailedRuns.length > 3) {
|
|
802
|
+
message += ` _\\.\\.\\.and ${this.branchCIFailedRuns.length - 3} more_\n`;
|
|
803
|
+
}
|
|
804
|
+
message += '\n';
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Issue #1341: Show post-merge CI failure details if applicable
|
|
808
|
+
if (this.postMergeCIFailedRuns && this.postMergeCIFailedRuns.length > 0) {
|
|
809
|
+
message += `⚠️ *Post\\-Merge CI Failures \\(stopped queue\\):*\n`;
|
|
810
|
+
for (const run of this.postMergeCIFailedRuns.slice(0, 3)) {
|
|
811
|
+
const runName = this.escapeMarkdown(run.name);
|
|
812
|
+
// Format the URL for MarkdownV2 - need to escape special characters
|
|
813
|
+
const runUrl = run.html_url ? `[View](${run.html_url.replace(/[)]/g, '\\)')})` : '';
|
|
814
|
+
message += ` ❌ ${runName} ${runUrl}\n`;
|
|
815
|
+
}
|
|
816
|
+
if (this.postMergeCIFailedRuns.length > 3) {
|
|
817
|
+
message += ` _\\.\\.\\.and ${this.postMergeCIFailedRuns.length - 3} more_\n`;
|
|
818
|
+
}
|
|
819
|
+
message += '\n';
|
|
820
|
+
}
|
|
821
|
+
|
|
612
822
|
// Details
|
|
613
823
|
if (report.items.length > 0) {
|
|
614
824
|
message += `*Results:*\n`;
|