@link-assistant/hive-mind 1.59.0 → 1.59.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 +78 -0
- package/package.json +1 -1
- package/src/github-merge-repo-actions.lib.mjs +54 -0
- package/src/github-merge.lib.mjs +24 -6
- package/src/queue-config.lib.mjs +7 -2
- package/src/solve.auto-merge-helpers.lib.mjs +89 -7
- package/src/solve.auto-merge.lib.mjs +22 -2
- package/src/telegram-bot.mjs +3 -1
- package/src/use-with-retry.lib.mjs +20 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.59.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9e96635: Fix Telegram `/solve` repo-not-accessible message still suggesting `--auto-accept-invite` even when that flag is already active (issue #1714). After issue #1694 flipped `--auto-accept-invite` to default-on, `src/telegram-bot.mjs` was passing `autoAcceptInvite: args.some(a => a === '--auto-accept-invite')` to `validateGitHubEntityExistence()` — but the literal flag is no longer present in the typical default-on invocation, so the suppression added by issue #1692 silently regressed. The call now reads `parsedSolveArgs?.autoAcceptInvite` (matching the auto-accept pre-check two lines above), so the hint is suppressed when the flag is active and only shown when the user explicitly opts out with `--no-auto-accept-invite`. Adds `tests/test-issue-1714-auto-accept-invite-hint.mjs` covering the parsed-argv contract and a source-level guard against the `args.some(...)` form returning, plus a case study under `docs/case-studies/issue-1714/`.
|
|
8
|
+
|
|
9
|
+
## 1.59.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 65d7b99: Fix misleading `/merge` verbose logs that read as "no CI configured" when CI was actually
|
|
14
|
+
running — addresses issue [#1712](https://github.com/link-assistant/hive-mind/issues/1712)
|
|
15
|
+
where a user mistakenly Ctrl+C'd the auto-restart-until-mergeable watcher after seeing:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
[VERBOSE] /merge: PR #83 has no CI checks yet - treating as no_checks
|
|
19
|
+
[VERBOSE] /merge: PR #83 has no CI check-runs yet, but 1 workflow run(s) were triggered ...
|
|
20
|
+
⏳ Waiting for CI: Build and Release Docker Image
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The classification logic was correct — `/merge` was waiting on the legitimate 30-120s gap
|
|
24
|
+
between GitHub registering a `workflow_run` and publishing the corresponding `check_runs`.
|
|
25
|
+
The wording was the bug: "no CI checks yet" is parseable as "this repo has no CI", and the
|
|
26
|
+
listing showed run IDs without URLs, so the user couldn't quickly verify what `/merge` was
|
|
27
|
+
watching.
|
|
28
|
+
|
|
29
|
+
Changes:
|
|
30
|
+
- **`src/github-merge.lib.mjs`** — `getDetailedCIStatus` and `checkPRCIStatus` reword the
|
|
31
|
+
`no_checks` verbose lines to "has no check-runs or commit statuses registered yet",
|
|
32
|
+
including the short SHA. `getWorkflowRunsForSha` now appends `run.html_url` to every
|
|
33
|
+
entry. Normalized check-run / commit-status entries carry an `html_url` field
|
|
34
|
+
(falling back to `details_url` / `target_url`).
|
|
35
|
+
- **`src/solve.auto-merge-helpers.lib.mjs::getMergeBlockers`** — the `no_checks`,
|
|
36
|
+
`pending`, and `cancelled` branches now produce blocker `details` strings of the form
|
|
37
|
+
`"<name> [<status>] — <html_url>"`. The user-facing `⏳ Waiting for CI: …` line in
|
|
38
|
+
`solve.auto-merge.lib.mjs` (which joins `details` with commas) automatically picks up
|
|
39
|
+
the URLs, so the user can click through to the run.
|
|
40
|
+
- **`tests/test-misleading-merge-logs-1712.mjs`** — 13 unit tests covering the wording
|
|
41
|
+
guard, blocker enrichment for the no_checks / pending / cancelled paths, regression
|
|
42
|
+
guard for #1466, and the joined user-facing line format.
|
|
43
|
+
- **`docs/case-studies/issue-1712/README.md`** — full case study with raw logs, timeline,
|
|
44
|
+
root cause, fix description, and verification on the original PR
|
|
45
|
+
[link-foundation/box#83](https://github.com/link-foundation/box/pull/83) (which CI
|
|
46
|
+
passed for, after the user killed the watcher prematurely).
|
|
47
|
+
|
|
48
|
+
Also extends the `useWithRetry` helper (originally added in #1710 to recover from corrupt
|
|
49
|
+
hosted-CI npm-install state) with a third failure mode: `ERR_INVALID_PACKAGE_CONFIG` —
|
|
50
|
+
seen in this branch's own CI run when Node refused to parse a truncated
|
|
51
|
+
`getenv-v-latest/package.json`. `src/queue-config.lib.mjs` now loads `getenv` and
|
|
52
|
+
`links-notation` through the retry wrapper, matching `config.lib.mjs` and `lino.lib.mjs`.
|
|
53
|
+
Three new unit tests in `tests/test-use-with-retry.mjs` cover the new mode.
|
|
54
|
+
|
|
55
|
+
No upstream issue is needed — the bug was entirely in `link-assistant/hive-mind`. The
|
|
56
|
+
external workflow finished successfully (`check-runs-dfc4c14.json` shows `total_count: 22`).
|
|
57
|
+
|
|
58
|
+
**Follow-up round** (after review feedback in
|
|
59
|
+
[PR #1713 comment](https://github.com/link-assistant/hive-mind/pull/1713#issuecomment-4342387674)):
|
|
60
|
+
- **List active runs across ALL PR commits, not just HEAD.** New
|
|
61
|
+
`getActivePRWorkflowRuns()` in `src/github-merge-repo-actions.lib.mjs` walks every
|
|
62
|
+
commit on the PR (`/repos/.../pulls/N/commits`), dedupes by `run.id`, returns groups
|
|
63
|
+
marked `head` / `older`. The verbose log now lists active runs on older commits under
|
|
64
|
+
per-commit URL headers, so the GitHub Actions tab (which shows yellow dots for older
|
|
65
|
+
commits) reconciles with the log.
|
|
66
|
+
- **Eliminate duplicate logging.** `getWorkflowRunsForSha(verbose=true)` already prints
|
|
67
|
+
every run; the no_checks branch no longer re-iterates `workflowRuns`, just emits a
|
|
68
|
+
single explanatory summary line.
|
|
69
|
+
- **Commit URLs instead of short SHAs.** Verbose lines that referenced
|
|
70
|
+
`${sha.substring(0, 7)}` now use `https://github.com/${owner}/${repo}/commit/${sha}`
|
|
71
|
+
(or `/pull/N/commits/${sha}` where the PR context matters).
|
|
72
|
+
- **Inline plain-English explanations.** New `STATUS_HINTS` / `CONCLUSION_HINTS`
|
|
73
|
+
dictionaries plus `explainStatus()` helper — verbose lines read
|
|
74
|
+
`[in_progress] (currently executing)` instead of bare `in_progress`.
|
|
75
|
+
- **Multi-line user-facing waiting message.** The `⏳ Waiting for CI:` line is now
|
|
76
|
+
rendered by `renderBlocker()` — single-line for the common case (one run), but each
|
|
77
|
+
detail on its own indented line when there are multiple.
|
|
78
|
+
- 8 new tests added to `tests/test-misleading-merge-logs-1712.mjs` (Groups 5–8); 21
|
|
79
|
+
total. #1480 (31/31) and #1466 (14/14) regression suites still pass.
|
|
80
|
+
|
|
3
81
|
## 1.59.0
|
|
4
82
|
|
|
5
83
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -91,6 +91,60 @@ export async function getPRCommitShas(owner, repo, prNumber, verbose = false) {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Issue #1712: Collect every active (in_progress / pending / queued / waiting / requested)
|
|
96
|
+
* workflow run on the PR branch — across ALL commits, not only the head SHA.
|
|
97
|
+
*
|
|
98
|
+
* Why this exists: when the user watches `/merge`, the GitHub Actions tab shows yellow
|
|
99
|
+
* dots for every commit that ever had a run, including older commits whose runs were
|
|
100
|
+
* automatically cancelled by GitHub's concurrency group. The verbose log used to list
|
|
101
|
+
* only the head-SHA runs, so a user comparing the log to the GitHub UI would see
|
|
102
|
+
* "1 workflow run" in the log but two yellow dots on screen — looking like a bug.
|
|
103
|
+
*
|
|
104
|
+
* Returns runs grouped by SHA, deduplicated by run.id (a single run can be associated
|
|
105
|
+
* with one SHA, but the same workflow file can produce runs on multiple SHAs).
|
|
106
|
+
*
|
|
107
|
+
* @param {string} owner - Repository owner
|
|
108
|
+
* @param {string} repo - Repository name
|
|
109
|
+
* @param {number} prNumber - Pull request number
|
|
110
|
+
* @param {string} headSha - The PR head SHA (used to mark which group is "current")
|
|
111
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
112
|
+
* @param {Function} getWorkflowRunsForSha - Function to get workflow runs for a SHA
|
|
113
|
+
* @returns {Promise<{groups: Array<{sha: string, isHead: boolean, runs: Array}>, totalActive: number, headActive: number, otherActive: number}>}
|
|
114
|
+
*/
|
|
115
|
+
export async function getActivePRWorkflowRuns(owner, repo, prNumber, headSha, verbose, getWorkflowRunsForSha) {
|
|
116
|
+
const shas = await getPRCommitShas(owner, repo, prNumber, false);
|
|
117
|
+
if (shas.length === 0) {
|
|
118
|
+
return { groups: [], totalActive: 0, headActive: 0, otherActive: 0 };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const ACTIVE_STATUSES = new Set(['in_progress', 'pending', 'queued', 'waiting', 'requested']);
|
|
122
|
+
const groups = [];
|
|
123
|
+
const seenRunIds = new Set();
|
|
124
|
+
let totalActive = 0;
|
|
125
|
+
let headActive = 0;
|
|
126
|
+
let otherActive = 0;
|
|
127
|
+
|
|
128
|
+
for (const sha of shas) {
|
|
129
|
+
const runs = await getWorkflowRunsForSha(owner, repo, sha, false);
|
|
130
|
+
const activeRuns = runs.filter(r => ACTIVE_STATUSES.has(r.status) && !seenRunIds.has(r.id));
|
|
131
|
+
for (const r of activeRuns) seenRunIds.add(r.id);
|
|
132
|
+
if (activeRuns.length === 0) continue;
|
|
133
|
+
|
|
134
|
+
const isHead = sha === headSha;
|
|
135
|
+
groups.push({ sha, isHead, runs: activeRuns });
|
|
136
|
+
totalActive += activeRuns.length;
|
|
137
|
+
if (isHead) headActive += activeRuns.length;
|
|
138
|
+
else otherActive += activeRuns.length;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (verbose && totalActive > 0) {
|
|
142
|
+
console.log(`[VERBOSE] pr-commits: ${totalActive} active workflow run(s) across ${groups.length} commit(s) on PR #${prNumber} (${headActive} on HEAD, ${otherActive} on older commits)`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { groups, totalActive, headActive, otherActive };
|
|
146
|
+
}
|
|
147
|
+
|
|
94
148
|
/**
|
|
95
149
|
* Check that workflow runs for ALL commits on the PR branch have completed.
|
|
96
150
|
* @param {string} owner - Repository owner
|
package/src/github-merge.lib.mjs
CHANGED
|
@@ -342,7 +342,9 @@ export async function checkPRCIStatus(owner, repo, prNumber, verbose = false) {
|
|
|
342
342
|
// ([].every(fn) returns true for any fn).
|
|
343
343
|
if (allChecks.length === 0) {
|
|
344
344
|
if (verbose) {
|
|
345
|
-
|
|
345
|
+
// Issue #1712: Reword for clarity — empty check-runs API response, not "no CI configured".
|
|
346
|
+
const commitUrl = `https://github.com/${owner}/${repo}/commit/${sha}`;
|
|
347
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} HEAD ${commitUrl} has no check-runs/statuses registered yet — treating as pending`);
|
|
346
348
|
}
|
|
347
349
|
return {
|
|
348
350
|
status: 'pending',
|
|
@@ -1070,6 +1072,8 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
|
|
|
1070
1072
|
const statuses = JSON.parse(statusJson.trim() || '[]');
|
|
1071
1073
|
|
|
1072
1074
|
// Build detailed checks list
|
|
1075
|
+
// Issue #1712: Include html_url so the user-facing waiting message can include
|
|
1076
|
+
// a clickable link for each pending/failing check.
|
|
1073
1077
|
const allChecks = [
|
|
1074
1078
|
...checkRuns.map(check => ({
|
|
1075
1079
|
name: check.name,
|
|
@@ -1077,6 +1081,7 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
|
|
|
1077
1081
|
conclusion: check.conclusion, // success, failure, cancelled, timed_out, skipped, neutral, action_required, stale, null
|
|
1078
1082
|
type: 'check_run',
|
|
1079
1083
|
id: check.id,
|
|
1084
|
+
html_url: check.html_url || check.details_url || null,
|
|
1080
1085
|
})),
|
|
1081
1086
|
...statuses.map(status => ({
|
|
1082
1087
|
name: status.context,
|
|
@@ -1084,13 +1089,20 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
|
|
|
1084
1089
|
conclusion: status.state === 'pending' ? null : status.state === 'success' ? 'success' : status.state === 'failure' ? 'failure' : status.state,
|
|
1085
1090
|
type: 'status',
|
|
1086
1091
|
id: null,
|
|
1092
|
+
html_url: status.target_url || null,
|
|
1087
1093
|
})),
|
|
1088
1094
|
];
|
|
1089
1095
|
|
|
1090
1096
|
// No checks yet
|
|
1091
1097
|
if (allChecks.length === 0) {
|
|
1092
1098
|
if (verbose) {
|
|
1093
|
-
|
|
1099
|
+
// Issue #1712: Reword to avoid "no CI checks" sounding like "no CI configured".
|
|
1100
|
+
// We are inspecting the GitHub `/commits/{sha}/check-runs` and `/commits/{sha}/status` endpoints;
|
|
1101
|
+
// an empty result means no check-runs/statuses are registered yet for this commit, NOT that the
|
|
1102
|
+
// repository has no CI/CD workflows. The caller (`getMergeBlockers`) decides between race
|
|
1103
|
+
// condition vs. "no CI configured" based on additional API calls.
|
|
1104
|
+
const commitUrl = `https://github.com/${owner}/${repo}/commit/${sha}`;
|
|
1105
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} HEAD ${commitUrl} has no check-runs or commit statuses registered yet (status=no_checks; race vs. no-CI distinction is decided downstream)`);
|
|
1094
1106
|
}
|
|
1095
1107
|
return {
|
|
1096
1108
|
status: 'no_checks',
|
|
@@ -1213,9 +1225,14 @@ export async function getWorkflowRunsForSha(owner, repo, sha, verbose = false) {
|
|
|
1213
1225
|
.map(run => ({ id: run.id, status: run.status, conclusion: run.conclusion, name: run.name, html_url: run.html_url, path: run.path }));
|
|
1214
1226
|
|
|
1215
1227
|
if (verbose) {
|
|
1216
|
-
|
|
1228
|
+
// Issue #1712: Include the commit URL (not just SHA) and the workflow run html_url
|
|
1229
|
+
// so the user can click through to GitHub instead of pasting IDs into a URL by hand.
|
|
1230
|
+
// The run html_url already encodes the run ID, so we don't repeat it as a separate field.
|
|
1231
|
+
const commitUrl = `https://github.com/${owner}/${repo}/commit/${sha}`;
|
|
1232
|
+
console.log(`[VERBOSE] /merge: Found ${runs.length} workflow run(s) for ${commitUrl}`);
|
|
1217
1233
|
for (const run of runs) {
|
|
1218
|
-
|
|
1234
|
+
const concPart = run.conclusion ? `/${run.conclusion}` : '';
|
|
1235
|
+
console.log(`[VERBOSE] /merge: - ${run.name} [${run.status}${concPart}]: ${run.html_url}`);
|
|
1219
1236
|
}
|
|
1220
1237
|
}
|
|
1221
1238
|
|
|
@@ -1333,8 +1350,8 @@ export { getCommitDate, checkPreviousPRCommitsHadCI, checkWorkflowsHavePRTrigger
|
|
|
1333
1350
|
import { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha } from './github-merge-ci.lib.mjs';
|
|
1334
1351
|
export { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha };
|
|
1335
1352
|
|
|
1336
|
-
import { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas } from './github-merge-repo-actions.lib.mjs'; // Issue #1503
|
|
1337
|
-
export { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas };
|
|
1353
|
+
import { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas, getActivePRWorkflowRuns } from './github-merge-repo-actions.lib.mjs'; // Issue #1503, #1712
|
|
1354
|
+
export { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas, getActivePRWorkflowRuns };
|
|
1338
1355
|
|
|
1339
1356
|
export default {
|
|
1340
1357
|
READY_LABEL,
|
|
@@ -1376,4 +1393,5 @@ export default {
|
|
|
1376
1393
|
getAllActiveRepoRuns,
|
|
1377
1394
|
waitForAllRepoActions,
|
|
1378
1395
|
checkCIConsensus, // Issue #1503
|
|
1396
|
+
getActivePRWorkflowRuns, // Issue #1712: list active runs across ALL PR commits
|
|
1379
1397
|
};
|
package/src/queue-config.lib.mjs
CHANGED
|
@@ -36,10 +36,15 @@ if (typeof globalThis.use === 'undefined') {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// Issue #1710 / #1712: use-m occasionally hands back a truncated/corrupt global
|
|
40
|
+
// package on hosted CI (npm install -g flake — manifests as ERR_INVALID_PACKAGE_CONFIG,
|
|
41
|
+
// "Failed to resolve the path", or SyntaxError mid-import). useWithRetry deletes
|
|
42
|
+
// the broken install dir and re-fetches.
|
|
43
|
+
const { useWithRetry } = await import('./use-with-retry.lib.mjs');
|
|
44
|
+
const getenvModule = await useWithRetry(globalThis.use, 'getenv');
|
|
40
45
|
// Node 24 CJS/ESM interop may return the whole module object instead of the function directly
|
|
41
46
|
const getenv = typeof getenvModule === 'function' ? getenvModule : getenvModule.default || getenvModule;
|
|
42
|
-
const linoModule = await use
|
|
47
|
+
const linoModule = await useWithRetry(globalThis.use, 'links-notation');
|
|
43
48
|
const LinoParser = linoModule.Parser || linoModule.default?.Parser;
|
|
44
49
|
|
|
45
50
|
/**
|
|
@@ -33,7 +33,43 @@ const { reportError } = sentryLib;
|
|
|
33
33
|
|
|
34
34
|
// Import GitHub merge functions
|
|
35
35
|
const githubMergeLib = await import('./github-merge.lib.mjs');
|
|
36
|
-
const { checkPRMergeable, checkForBillingLimitError, getDetailedCIStatus, getWorkflowRunsForSha, getWorkflowRunJobsCount, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers, checkPreviousPRCommitsHadCI } = githubMergeLib;
|
|
36
|
+
const { checkPRMergeable, checkForBillingLimitError, getDetailedCIStatus, getWorkflowRunsForSha, getWorkflowRunJobsCount, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers, checkPreviousPRCommitsHadCI, getActivePRWorkflowRuns } = githubMergeLib;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Issue #1712: Plain-English meaning of GitHub Actions / check-run statuses, so the
|
|
40
|
+
* verbose log explains itself instead of forcing the user to look up GitHub docs.
|
|
41
|
+
* Returns the same status string suffixed with a parenthetical hint, e.g.
|
|
42
|
+
* "in_progress (currently executing)". Unknown statuses are returned unchanged.
|
|
43
|
+
*/
|
|
44
|
+
const STATUS_HINTS = {
|
|
45
|
+
in_progress: 'currently executing',
|
|
46
|
+
queued: 'waiting for a runner',
|
|
47
|
+
pending: 'waiting to start',
|
|
48
|
+
waiting: 'blocked on a deployment / approval gate',
|
|
49
|
+
requested: 'requested but not yet picked up',
|
|
50
|
+
completed: 'finished',
|
|
51
|
+
};
|
|
52
|
+
const CONCLUSION_HINTS = {
|
|
53
|
+
success: 'passed',
|
|
54
|
+
failure: 'failed',
|
|
55
|
+
cancelled: 'cancelled (will be re-triggered if applicable)',
|
|
56
|
+
timed_out: 'timed out',
|
|
57
|
+
skipped: 'skipped (e.g. paths-ignore matched)',
|
|
58
|
+
neutral: 'neutral / informational',
|
|
59
|
+
action_required: 'manual approval required',
|
|
60
|
+
stale: 'stale — superseded by a newer run',
|
|
61
|
+
startup_failure: 'workflow failed to start (likely invalid YAML)',
|
|
62
|
+
};
|
|
63
|
+
const explainStatus = (status, conclusion) => {
|
|
64
|
+
const statusPart = status ? `${status}${STATUS_HINTS[status] ? ` (${STATUS_HINTS[status]})` : ''}` : 'unknown';
|
|
65
|
+
if (!conclusion) return statusPart;
|
|
66
|
+
const concPart = `${conclusion}${CONCLUSION_HINTS[conclusion] ? ` (${CONCLUSION_HINTS[conclusion]})` : ''}`;
|
|
67
|
+
return `${statusPart} → ${concPart}`;
|
|
68
|
+
};
|
|
69
|
+
const formatRunLine = run => {
|
|
70
|
+
const status = run.conclusion ? `${run.status}/${run.conclusion}` : run.status;
|
|
71
|
+
return `${run.name} [${status}] — ${run.html_url}`;
|
|
72
|
+
};
|
|
37
73
|
|
|
38
74
|
// Issue #1625: Import centralized session-ending markers so the duplicate-
|
|
39
75
|
// search scope for checkForExistingComment() stays in lock-step with the
|
|
@@ -343,13 +379,38 @@ export const getMergeBlockers = async (owner, repo, prNumber, verbose = false, c
|
|
|
343
379
|
}
|
|
344
380
|
|
|
345
381
|
// Some workflow runs are still in progress or produced results — genuine race condition
|
|
382
|
+
// Issue #1712: User-facing blocker `details` carry the run URL + status so the
|
|
383
|
+
// top-level "⏳ Waiting for CI:" line is self-explanatory. Verbose listing is
|
|
384
|
+
// produced by `getWorkflowRunsForSha(..., verbose=true)` above — do NOT print the
|
|
385
|
+
// same run list twice. Here we only emit the one-line summary that explains
|
|
386
|
+
// *why* we're still waiting (race vs. real run).
|
|
387
|
+
const commitUrl = `https://github.com/${owner}/${repo}/pull/${prNumber}/commits/${ciStatus.sha}`;
|
|
346
388
|
if (verbose) {
|
|
347
|
-
await log(`[VERBOSE] /merge:
|
|
389
|
+
await log(`[VERBOSE] /merge: ${workflowRuns.length} workflow run(s) registered for PR #${prNumber} HEAD ${commitUrl} — waiting for them to publish check-runs (race condition between workflow_run and check_runs APIs, typically ~30–120 s)`);
|
|
348
390
|
}
|
|
391
|
+
|
|
392
|
+
// Also surface any active workflow runs on OLDER PR commits, so the user's view of
|
|
393
|
+
// the GitHub Actions tab (which shows yellow dots for every commit) reconciles
|
|
394
|
+
// with the log. These are NOT blockers — GitHub's concurrency group cancels them
|
|
395
|
+
// when a new commit is pushed — but listing them stops the user from worrying that
|
|
396
|
+
// the watcher is missing them.
|
|
397
|
+
const activeAcrossCommits = await getActivePRWorkflowRuns(owner, repo, prNumber, ciStatus.sha, verbose, getWorkflowRunsForSha);
|
|
398
|
+
if (verbose && activeAcrossCommits.otherActive > 0) {
|
|
399
|
+
await log(`[VERBOSE] /merge: ${activeAcrossCommits.otherActive} additional active workflow run(s) on older commits of PR #${prNumber} (these are not blockers — GitHub's concurrency group will cancel them):`);
|
|
400
|
+
for (const group of activeAcrossCommits.groups) {
|
|
401
|
+
if (group.isHead) continue;
|
|
402
|
+
const olderCommitUrl = `https://github.com/${owner}/${repo}/pull/${prNumber}/commits/${group.sha}`;
|
|
403
|
+
await log(`[VERBOSE] /merge: on ${olderCommitUrl}:`);
|
|
404
|
+
for (const run of group.runs) {
|
|
405
|
+
await log(`[VERBOSE] /merge: - ${formatRunLine(run)} — ${explainStatus(run.status, run.conclusion)}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
349
410
|
blockers.push({
|
|
350
411
|
type: 'ci_pending',
|
|
351
|
-
message: `
|
|
352
|
-
details: workflowRuns.map(
|
|
412
|
+
message: `Waiting for ${workflowRuns.length} workflow run(s) on HEAD ${commitUrl} to publish check-runs`,
|
|
413
|
+
details: workflowRuns.map(formatRunLine),
|
|
353
414
|
});
|
|
354
415
|
} else {
|
|
355
416
|
// No workflow runs for this SHA — but this could be a race condition!
|
|
@@ -519,11 +580,27 @@ export const getMergeBlockers = async (owner, repo, prNumber, verbose = false, c
|
|
|
519
580
|
}
|
|
520
581
|
} else if (ciStatus.status === 'pending') {
|
|
521
582
|
// CI is still running or queued - wait for completion
|
|
522
|
-
const
|
|
583
|
+
const pendingChecks = [...ciStatus.pendingChecks, ...ciStatus.queuedChecks];
|
|
584
|
+
const pendingDetails = pendingChecks.map(c => {
|
|
585
|
+
const statusPart = c.status ? ` [${c.status}]` : '';
|
|
586
|
+
const urlPart = c.html_url ? ` — ${c.html_url}` : '';
|
|
587
|
+
return `${c.name}${statusPart}${urlPart}`;
|
|
588
|
+
});
|
|
589
|
+
if (verbose) {
|
|
590
|
+
// Issue #1712: One concise line + a per-check entry that includes a plain-English
|
|
591
|
+
// explanation of the status. We do NOT also call getWorkflowRunsForSha here — the
|
|
592
|
+
// detailed CI status already covers the same data via check-runs.
|
|
593
|
+
const commitUrl = `https://github.com/${owner}/${repo}/pull/${prNumber}/commits/${ciStatus.sha}`;
|
|
594
|
+
await log(`[VERBOSE] /merge: ${pendingChecks.length} check-run(s) still running/queued on PR #${prNumber} HEAD ${commitUrl}:`);
|
|
595
|
+
for (const c of pendingChecks) {
|
|
596
|
+
const url = c.html_url ? ` — ${c.html_url}` : '';
|
|
597
|
+
await log(`[VERBOSE] /merge: - ${c.name}: ${explainStatus(c.status, c.conclusion)}${url}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
523
600
|
blockers.push({
|
|
524
601
|
type: 'ci_pending',
|
|
525
602
|
message: 'CI/CD checks are still running or queued',
|
|
526
|
-
details:
|
|
603
|
+
details: pendingDetails,
|
|
527
604
|
});
|
|
528
605
|
} else if (ciStatus.status === 'cancelled') {
|
|
529
606
|
// All non-passed checks are cancelled or stale (no genuine failures)
|
|
@@ -540,10 +617,15 @@ export const getMergeBlockers = async (owner, repo, prNumber, verbose = false, c
|
|
|
540
617
|
} else {
|
|
541
618
|
// These need to be re-triggered, NOT treated as AI-fixable failures
|
|
542
619
|
const cancelledOrStaleChecks = [...ciStatus.cancelledChecks, ...(ciStatus.staleChecks || [])];
|
|
620
|
+
const cancelledDetails = cancelledOrStaleChecks.map(c => {
|
|
621
|
+
const concPart = c.conclusion ? ` [${c.conclusion}]` : '';
|
|
622
|
+
const urlPart = c.html_url ? ` — ${c.html_url}` : '';
|
|
623
|
+
return `${c.name}${concPart}${urlPart}`;
|
|
624
|
+
});
|
|
543
625
|
blockers.push({
|
|
544
626
|
type: 'ci_cancelled',
|
|
545
627
|
message: 'CI/CD checks were cancelled or became stale',
|
|
546
|
-
details:
|
|
628
|
+
details: cancelledDetails,
|
|
547
629
|
sha: ciStatus.sha,
|
|
548
630
|
});
|
|
549
631
|
}
|
|
@@ -903,10 +903,30 @@ No further AI sessions will be started automatically for this run. Please review
|
|
|
903
903
|
const cancelledOnly = blockers.every(b => b.type === 'ci_cancelled' || b.type === 'ci_pending');
|
|
904
904
|
const cancelledBlocker = blockers.find(b => b.type === 'ci_cancelled');
|
|
905
905
|
|
|
906
|
+
// Issue #1712: When `details` contain URLs (which they now always do for ci_pending /
|
|
907
|
+
// ci_cancelled blockers), comma-joining them produces an unreadable single-line wall
|
|
908
|
+
// of text. Render the first detail inline (with the message as the header) and any
|
|
909
|
+
// additional details on their own indented lines. Each detail is already
|
|
910
|
+
// self-explanatory: "<name> [<status>] — <url>".
|
|
911
|
+
const renderBlocker = (icon, header, blocker) => {
|
|
912
|
+
if (!blocker.details || blocker.details.length === 0) {
|
|
913
|
+
return log(formatAligned(icon, header, blocker.message, 2));
|
|
914
|
+
}
|
|
915
|
+
if (blocker.details.length === 1) {
|
|
916
|
+
return log(formatAligned(icon, header, blocker.details[0], 2));
|
|
917
|
+
}
|
|
918
|
+
return (async () => {
|
|
919
|
+
await log(formatAligned(icon, header, blocker.message, 2));
|
|
920
|
+
for (const detail of blocker.details) {
|
|
921
|
+
await log(formatAligned('', '', detail, 4));
|
|
922
|
+
}
|
|
923
|
+
})();
|
|
924
|
+
};
|
|
925
|
+
|
|
906
926
|
if (cancelledOnly && cancelledBlocker) {
|
|
907
|
-
await
|
|
927
|
+
await renderBlocker('🔄', 'Waiting for re-triggered CI:', cancelledBlocker);
|
|
908
928
|
} else if (pendingBlocker) {
|
|
909
|
-
await
|
|
929
|
+
await renderBlocker('⏳', 'Waiting for CI:', pendingBlocker);
|
|
910
930
|
} else {
|
|
911
931
|
await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
|
|
912
932
|
}
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -967,7 +967,9 @@ async function handleSolveCommand(ctx) {
|
|
|
967
967
|
VERBOSE && console.log(`[VERBOSE] Auto-accept invite pre-check failed: ${e.message}`);
|
|
968
968
|
}
|
|
969
969
|
}
|
|
970
|
-
|
|
970
|
+
// Issue #1714: read the parsed argv (default-on per #1694) instead of the raw args list,
|
|
971
|
+
// so the invite hint is suppressed on the default-on path where the literal flag is absent.
|
|
972
|
+
const entityCheck = await validateGitHubEntityExistence({ owner: validation.parsed.owner, repo: validation.parsed.repo, number: validation.parsed.number, type: validation.parsed.type, verbose: VERBOSE, autoAcceptInvite: !!parsedSolveArgs?.autoAcceptInvite });
|
|
971
973
|
if (!entityCheck.valid) {
|
|
972
974
|
await safeReply(ctx, `❌ ${escapeMarkdown(entityCheck.error)}`, { reply_to_message_id: ctx.message.message_id });
|
|
973
975
|
return;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Retry wrapper for `use-m` package loading.
|
|
5
5
|
*
|
|
6
6
|
* Issue #1710: Hosted CI runners occasionally hand back a truncated or
|
|
7
|
-
* partially-installed global package after `npm install -g <pkg>`.
|
|
7
|
+
* partially-installed global package after `npm install -g <pkg>`. Three
|
|
8
8
|
* surface symptoms have been observed:
|
|
9
9
|
*
|
|
10
10
|
* 1. `import` throws a SyntaxError ("Unexpected end of input") wrapped
|
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
* 2. use-m throws `Failed to resolve the path to '<pkg>' from '<dir>'`
|
|
14
14
|
* — the install completed without error but the package tree is
|
|
15
15
|
* missing files that the `main`/`exports` entry depends on.
|
|
16
|
+
* 3. Node throws `Invalid package config <dir>/package.json.` with
|
|
17
|
+
* `code: 'ERR_INVALID_PACKAGE_CONFIG'` — the package.json itself
|
|
18
|
+
* is corrupt/truncated and cannot even be parsed (issue #1712).
|
|
16
19
|
*
|
|
17
|
-
* The recovery is identical for
|
|
20
|
+
* The recovery is identical for all three: delete the broken alias install
|
|
18
21
|
* directory and ask use-m to re-fetch. A clean reinstall almost always
|
|
19
22
|
* succeeds. This helper centralises that retry so every call site picks
|
|
20
23
|
* it up.
|
|
@@ -68,10 +71,18 @@ export const isCorruptInstallError = error => {
|
|
|
68
71
|
if (cause instanceof SyntaxError) return true;
|
|
69
72
|
const causeMessage = typeof cause?.message === 'string' ? cause.message : '';
|
|
70
73
|
if (/Unexpected end of input|Unexpected token/.test(causeMessage)) return true;
|
|
74
|
+
// Mode 3 (issue #1712): package.json itself is corrupt — Node refuses to
|
|
75
|
+
// even parse it and throws ERR_INVALID_PACKAGE_CONFIG before use-m's own
|
|
76
|
+
// resolve/import logic gets a chance to run.
|
|
77
|
+
if (error?.code === 'ERR_INVALID_PACKAGE_CONFIG') return true;
|
|
78
|
+
if (cause?.code === 'ERR_INVALID_PACKAGE_CONFIG') return true;
|
|
71
79
|
// Mode 2 (also seen on hosted CI): npm install completes but the package
|
|
72
80
|
// tree is incomplete, so use-m can't resolve the entry point.
|
|
73
81
|
const message = typeof error?.message === 'string' ? error.message : '';
|
|
74
|
-
|
|
82
|
+
if (/^Failed to resolve the path to /.test(message)) return true;
|
|
83
|
+
// Fallback string match for ERR_INVALID_PACKAGE_CONFIG (in case the error
|
|
84
|
+
// bubbles through use-m without preserving the `code` property).
|
|
85
|
+
return /^Invalid package config /.test(message);
|
|
75
86
|
};
|
|
76
87
|
|
|
77
88
|
export const extractCorruptedFilePath = error => {
|
|
@@ -82,7 +93,12 @@ export const extractCorruptedFilePath = error => {
|
|
|
82
93
|
// is already the alias install directory — return it directly so callers
|
|
83
94
|
// can clean it up (cleanup() handles both files and directories).
|
|
84
95
|
const resolveMatch = message.match(/Failed to resolve the path to '[^']+' from '([^']+)'/);
|
|
85
|
-
|
|
96
|
+
if (resolveMatch) return resolveMatch[1];
|
|
97
|
+
// Mode 3 (issue #1712): "Invalid package config <dir>/package.json." —
|
|
98
|
+
// extract the package.json path so the caller's cleanup() walks up to
|
|
99
|
+
// the alias dir.
|
|
100
|
+
const invalidConfigMatch = message.match(/Invalid package config (\S+?package\.json)/);
|
|
101
|
+
return invalidConfigMatch ? invalidConfigMatch[1] : null;
|
|
86
102
|
};
|
|
87
103
|
|
|
88
104
|
const defaultCleanup = async path => {
|