@link-assistant/hive-mind 1.24.6 → 1.25.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 CHANGED
@@ -1,5 +1,54 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.25.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 2a87d56: tests: expand unit tests for token accumulation logic (Issue #1313)
8
+
9
+ Added comprehensive unit tests for the token accumulation fix (Issue #1250)
10
+ that resolved the "Token usage: 0 input, 0 output" bug reported in Issue #1313.
11
+
12
+ New test coverage includes:
13
+ - End-to-end token display pipeline (accumulation → display format)
14
+ - Large token count handling (millions of tokens across many steps)
15
+ - NDJSON boundary cases (CRLF line endings, arrays, extra fields)
16
+ - Accumulator state isolation (independent accumulators)
17
+ - Exact reproduction of the Issue #1313 bug scenario
18
+ - Demonstration of why the streaming fix was necessary (concatenated JSON)
19
+
20
+ Total: 44 tests covering both `parseAgentTokenUsage` and streaming accumulation.
21
+
22
+ ## 1.25.0
23
+
24
+ ### Minor Changes
25
+
26
+ - cbac3dd: feat: wait for post-merge CI to complete before merging next PR (Issue #1341)
27
+
28
+ This change ensures that the /merge command waits for GitHub Actions to complete after each merge before processing the next PR in the queue.
29
+
30
+ **Problem:**
31
+ - Merge queue was merging PRs too quickly (70 seconds apart)
32
+ - Workflow runs were being cancelled (superseded by new commits)
33
+ - Only one version published instead of multiple
34
+
35
+ **Solution:**
36
+ 1. Check branch CI health before starting the queue
37
+ 2. Wait for post-merge CI after each successful merge
38
+ 3. Stop queue on CI failure (configurable)
39
+
40
+ **New configuration options:**
41
+ - `HIVE_MIND_MERGE_QUEUE_WAIT_FOR_POST_MERGE_CI` (default: true)
42
+ - `HIVE_MIND_MERGE_QUEUE_STOP_ON_CI_FAILURE` (default: true)
43
+ - `HIVE_MIND_MERGE_QUEUE_CHECK_BRANCH_HEALTH` (default: true)
44
+ - `HIVE_MIND_MERGE_QUEUE_POST_MERGE_CI_TIMEOUT_MS` (default: 60 minutes)
45
+ - `HIVE_MIND_MERGE_QUEUE_POST_MERGE_CI_POLL_INTERVAL_MS` (default: 30 seconds)
46
+
47
+ **New API functions:**
48
+ - `waitForCommitCI()` - Wait for workflow runs on a commit
49
+ - `checkBranchCIHealth()` - Check for failed CI on a branch
50
+ - `getMergeCommitSha()` - Get merge commit SHA for a PR
51
+
3
52
  ## 1.24.6
4
53
 
5
54
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.24.6",
3
+ "version": "1.25.1",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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
+ };
@@ -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
  };
@@ -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 before processing next PR to allow CI to stabilize
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
- this.log(`Waiting ${MERGE_QUEUE_CONFIG.POST_MERGE_WAIT_MS / 1000}s before next PR...`);
261
- await this.sleep(MERGE_QUEUE_CONFIG.POST_MERGE_WAIT_MS);
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`;