@link-assistant/hive-mind 1.37.1 → 1.37.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/solve.auto-merge.lib.mjs +100 -52
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.37.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f07ae29: fix false positive "Ready to merge" by cross-validating CI success status with GitHub Actions workflow runs API and removing unreliable commit-age-based grace period (Issue #1480)
|
|
8
|
+
|
|
3
9
|
## 1.37.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -33,7 +33,7 @@ const { reportError } = sentryLib;
|
|
|
33
33
|
|
|
34
34
|
// Import GitHub merge functions
|
|
35
35
|
const githubMergeLib = await import('./github-merge.lib.mjs');
|
|
36
|
-
const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, checkForBillingLimitError, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getActiveRepoWorkflows, getCommitDate,
|
|
36
|
+
const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, checkForBillingLimitError, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers } = githubMergeLib;
|
|
37
37
|
|
|
38
38
|
// Import GitHub functions for log attachment
|
|
39
39
|
const githubLib = await import('./github.lib.mjs');
|
|
@@ -187,7 +187,7 @@ const checkForNonBotComments = async (owner, repo, prNumber, issueNumber, lastCh
|
|
|
187
187
|
* - billing_limit: Billing/spending limit reached → stop (private) or wait (public)
|
|
188
188
|
* - no_checks: No CI checks yet (race condition) → wait
|
|
189
189
|
*/
|
|
190
|
-
const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
|
|
190
|
+
const getMergeBlockers = async (owner, repo, prNumber, verbose = false, checkCount = 1) => {
|
|
191
191
|
const blockers = [];
|
|
192
192
|
|
|
193
193
|
// Use detailed CI status to distinguish between all possible states
|
|
@@ -280,60 +280,54 @@ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
|
|
|
280
280
|
return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
if (
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
283
|
+
if (prTriggers.hasPRTriggers) {
|
|
284
|
+
// Issue #1480 (enhanced): Workflows have PR/push triggers but no runs yet.
|
|
285
|
+
// This is almost certainly a race condition — GitHub takes 30-120s to register
|
|
286
|
+
// workflow runs after a push. We MUST wait regardless of commit age, because
|
|
287
|
+
// commit date reflects authoring time, NOT push time.
|
|
288
|
+
//
|
|
289
|
+
// The commit may have been authored hours ago but pushed just now (rebased branches,
|
|
290
|
+
// amended commits, cherry-picks). Using commit age as a proxy for push age caused
|
|
291
|
+
// false positives in Case 1 of Issue #1480.
|
|
292
|
+
//
|
|
293
|
+
// Safety valve: after MAX_NO_RUNS_CHECKS consecutive checks (typically 5 × 60s = 5 min),
|
|
294
|
+
// conclude CI was not triggered. This handles cases like paths-ignore excluding all
|
|
295
|
+
// changed files, conditional workflows that don't match, etc.
|
|
296
|
+
const MAX_NO_RUNS_CHECKS = 5;
|
|
297
|
+
if (checkCount >= MAX_NO_RUNS_CHECKS) {
|
|
298
|
+
// We've waited long enough — CI was genuinely not triggered
|
|
291
299
|
if (verbose) {
|
|
292
|
-
console.log(`[VERBOSE] /merge:
|
|
300
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has no workflow runs after ${checkCount} consecutive checks — concluding CI was not triggered despite PR triggers existing`);
|
|
293
301
|
}
|
|
294
|
-
blockers
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
});
|
|
302
|
+
return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (verbose) {
|
|
306
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has no workflow runs for SHA ${ciStatus.sha.substring(0, 7)}, but workflows have PR/push triggers (${prTriggers.workflows.map(w => w.name).join(', ')}) — waiting for workflow runs to appear (check ${checkCount}/${MAX_NO_RUNS_CHECKS}, commit age: ${commitInfo.ageSeconds ?? 'unknown'}s)`);
|
|
307
|
+
}
|
|
308
|
+
blockers.push({
|
|
309
|
+
type: 'ci_pending',
|
|
310
|
+
message: `CI/CD workflow runs have not appeared yet — workflows have PR/push triggers (${prTriggers.workflows.map(w => w.name).join(', ')}), waiting for GitHub to register workflow runs (check ${checkCount}/${MAX_NO_RUNS_CHECKS})`,
|
|
311
|
+
details: prTriggers.workflows.map(w => w.name),
|
|
312
|
+
});
|
|
313
|
+
} else if (commitInfo.ageSeconds !== null && commitInfo.ageSeconds < WORKFLOW_RUN_GRACE_PERIOD_SECONDS) {
|
|
314
|
+
// No PR triggers found in workflow files, but commit is still recent — be safe and wait
|
|
315
|
+
if (verbose) {
|
|
316
|
+
console.log(`[VERBOSE] /merge: No PR/push triggers found in workflow files, but commit is only ${commitInfo.ageSeconds}s old — waiting to be safe`);
|
|
309
317
|
}
|
|
318
|
+
blockers.push({
|
|
319
|
+
type: 'ci_pending',
|
|
320
|
+
message: `CI/CD workflow runs have not appeared yet — commit is ${commitInfo.ageSeconds}s old, waiting for GitHub to register workflow runs (grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s)`,
|
|
321
|
+
details: [],
|
|
322
|
+
});
|
|
310
323
|
} else {
|
|
311
|
-
//
|
|
312
|
-
// Issue #
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (previousCI.hadPreviousCI && prTriggers.hasPRTriggers) {
|
|
317
|
-
// Previous commits had CI AND workflow files have PR triggers — something is wrong,
|
|
318
|
-
// this could be a GitHub API glitch or delayed registration beyond the grace period.
|
|
319
|
-
// Wait one more cycle to be safe.
|
|
320
|
-
if (verbose) {
|
|
321
|
-
console.log(`[VERBOSE] /merge: PR #${prNumber} previous commits had CI (${previousCI.previousCommitsWithCI}/${previousCI.totalPreviousCommits}) and workflows have PR triggers, but HEAD has no runs — waiting as safety measure`);
|
|
322
|
-
}
|
|
323
|
-
blockers.push({
|
|
324
|
-
type: 'ci_pending',
|
|
325
|
-
message: `CI/CD workflow runs missing for HEAD — previous PR commits had CI (${previousCI.previousCommitsWithCI} of ${previousCI.totalPreviousCommits}), workflows have PR triggers, possible API delay`,
|
|
326
|
-
details: prTriggers.workflows.map(w => w.name),
|
|
327
|
-
});
|
|
328
|
-
} else {
|
|
329
|
-
// CI was definitively NOT triggered
|
|
330
|
-
// Issue #1442: Fork PRs needing maintainer approval, paths-ignore filtering,
|
|
331
|
-
// workflow conditions not matching, etc. all result in zero workflow runs.
|
|
332
|
-
if (verbose) {
|
|
333
|
-
console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks and no workflow runs for SHA ${ciStatus.sha.substring(0, 7)} (commit age: ${commitInfo.ageSeconds ?? 'unknown'}s, grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s elapsed, previous CI: ${previousCI.hadPreviousCI}, PR triggers: ${prTriggers.hasPRTriggers}) — CI was not triggered`);
|
|
334
|
-
}
|
|
335
|
-
return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
|
|
324
|
+
// No PR triggers AND commit is old enough — CI was definitively NOT triggered
|
|
325
|
+
// Issue #1442: Fork PRs needing maintainer approval, paths-ignore filtering,
|
|
326
|
+
// workflow conditions not matching, etc. all result in zero workflow runs.
|
|
327
|
+
if (verbose) {
|
|
328
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks and no workflow runs for SHA ${ciStatus.sha.substring(0, 7)} (commit age: ${commitInfo.ageSeconds ?? 'unknown'}s, no PR/push triggers in workflow files) — CI was not triggered`);
|
|
336
329
|
}
|
|
330
|
+
return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
|
|
337
331
|
}
|
|
338
332
|
}
|
|
339
333
|
} else {
|
|
@@ -355,6 +349,60 @@ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
|
|
|
355
349
|
details: [],
|
|
356
350
|
});
|
|
357
351
|
}
|
|
352
|
+
} else if (ciStatus.status === 'success') {
|
|
353
|
+
// Issue #1480: Cross-validate "success" with workflow runs API.
|
|
354
|
+
// A fast external check (e.g., CodeFactor) can register and pass before the main CI
|
|
355
|
+
// pipeline starts, causing getDetailedCIStatus to return 'success' prematurely.
|
|
356
|
+
// We must verify that all expected workflow runs have actually completed.
|
|
357
|
+
const workflowRuns = await getWorkflowRunsForSha(owner, repo, ciStatus.sha, verbose);
|
|
358
|
+
|
|
359
|
+
if (workflowRuns.length > 0) {
|
|
360
|
+
// Workflow runs exist — check if any are still running
|
|
361
|
+
const incompleteRuns = workflowRuns.filter(r => r.status !== 'completed');
|
|
362
|
+
if (incompleteRuns.length > 0) {
|
|
363
|
+
// Some workflow runs are still in progress — more check-runs may appear
|
|
364
|
+
if (verbose) {
|
|
365
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} CI status is 'success' (${ciStatus.passedChecks.length} checks passed), but ${incompleteRuns.length} workflow run(s) still in progress — waiting for completion`);
|
|
366
|
+
}
|
|
367
|
+
blockers.push({
|
|
368
|
+
type: 'ci_pending',
|
|
369
|
+
message: `CI checks show success (${ciStatus.passedChecks.length} passed) but ${incompleteRuns.length} workflow run(s) still in progress — waiting for all to complete`,
|
|
370
|
+
details: incompleteRuns.map(r => r.name),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
// All workflow runs completed — the check-runs we see are the final set, trust the 'success' status
|
|
374
|
+
} else {
|
|
375
|
+
// No workflow runs for this SHA — the passed checks are from external services only
|
|
376
|
+
// (e.g., CodeFactor, Codecov). Check if the repo has workflows that should produce runs.
|
|
377
|
+
const repoWorkflows = await getActiveRepoWorkflows(owner, repo, verbose);
|
|
378
|
+
if (repoWorkflows.hasWorkflows) {
|
|
379
|
+
const prTriggers = await checkWorkflowsHavePRTriggers(owner, repo, verbose);
|
|
380
|
+
if (prTriggers.hasPRTriggers) {
|
|
381
|
+
// Repo has workflows with PR triggers but no runs yet — CI hasn't started
|
|
382
|
+
// This is the exact scenario from Case 2 of Issue #1480
|
|
383
|
+
//
|
|
384
|
+
// Safety valve: after MAX_NO_RUNS_CHECKS consecutive checks, trust the external checks
|
|
385
|
+
const MAX_NO_RUNS_CHECKS = 5;
|
|
386
|
+
if (checkCount >= MAX_NO_RUNS_CHECKS) {
|
|
387
|
+
if (verbose) {
|
|
388
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} CI 'success' with ${ciStatus.passedChecks.length} external checks, no workflow runs after ${checkCount} checks — trusting external checks`);
|
|
389
|
+
}
|
|
390
|
+
// Fall through — trust the success status from external checks
|
|
391
|
+
} else {
|
|
392
|
+
if (verbose) {
|
|
393
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} CI status is 'success' (${ciStatus.passedChecks.length} external checks), but repo has PR-triggered workflows with 0 workflow runs — likely race condition (check ${checkCount}/${MAX_NO_RUNS_CHECKS})`);
|
|
394
|
+
}
|
|
395
|
+
// Wait for GitHub Actions to register workflow runs
|
|
396
|
+
blockers.push({
|
|
397
|
+
type: 'ci_pending',
|
|
398
|
+
message: `CI shows ${ciStatus.passedChecks.length} passed check(s) from external services, but repo has PR-triggered workflows that haven't started yet — waiting for GitHub Actions to register (check ${checkCount}/${MAX_NO_RUNS_CHECKS})`,
|
|
399
|
+
details: prTriggers.workflows.map(w => w.name),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// No repo workflows → external checks are the only CI, trust the 'success' status
|
|
405
|
+
}
|
|
358
406
|
} else if (ciStatus.status === 'pending') {
|
|
359
407
|
// CI is still running or queued - wait for completion
|
|
360
408
|
const pendingNames = [...ciStatus.pendingChecks, ...ciStatus.queuedChecks].map(c => c.name);
|
|
@@ -507,7 +555,7 @@ export const watchUntilMergeable = async params => {
|
|
|
507
555
|
|
|
508
556
|
try {
|
|
509
557
|
// Get merge blockers
|
|
510
|
-
const { blockers, noCiConfigured, noCiTriggered, workflowRunConclusions } = await getMergeBlockers(owner, repo, prNumber, argv.verbose);
|
|
558
|
+
const { blockers, noCiConfigured, noCiTriggered, workflowRunConclusions } = await getMergeBlockers(owner, repo, prNumber, argv.verbose, iteration);
|
|
511
559
|
|
|
512
560
|
// Check for new comments from non-bot users
|
|
513
561
|
const { hasNewComments, comments } = await checkForNonBotComments(owner, repo, prNumber, issueNumber, lastCheckTime, argv.verbose);
|