@link-assistant/hive-mind 1.69.17 → 1.70.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
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.70.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 35dc089: Add `--auto-resolve` to the `/merge` Telegram command. After the normal queue finishes, the bot now iterates every PR that was skipped because of merge conflicts and dispatches a `solve <pr-url> --auto-merge` session through `start-screen` — the same path other commands use — so conflict resolution runs with the default `sonnet` model and the PR is merged once the session finishes. Each PR/issue reference in the `/merge` progress and final messages is now rendered as a clickable MarkdownV2 link to the actual pull request or issue. Resolves #1805.
|
|
8
|
+
|
|
9
|
+
## 1.69.18
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 9aa5659: Fix `--auto-fork` mode failing when continuing an existing fork PR whose
|
|
14
|
+
fork name already contained the upstream-owner prefix. `setupRepository`
|
|
15
|
+
in `solve.repository.lib.mjs` was applying the
|
|
16
|
+
`--prefix-fork-name-with-owner-name` option to `forkRepoName` (which is
|
|
17
|
+
the authoritative head repo name from the PR's `headRepository.name`),
|
|
18
|
+
producing a doubled prefix like
|
|
19
|
+
`konard/labtgbot-labtgbot-telegram-claude-agent` and a 404 lookup. The
|
|
20
|
+
prefix option now only controls fork _creation_, not fork _lookup_:
|
|
21
|
+
when `forkRepoName` is present, the expected fork is
|
|
22
|
+
`${forkOwner}/${forkRepoName}` and no alternate-name fallback is
|
|
23
|
+
attempted. Resolves #1803.
|
|
24
|
+
|
|
3
25
|
## 1.69.17
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -818,12 +818,12 @@ Thank you!`;
|
|
|
818
818
|
await log(`\n${formatAligned('🍴', 'Fork mode:', 'DETECTED from PR')}`);
|
|
819
819
|
await log(`${formatAligned('', 'Fork owner:', forkOwner)}`);
|
|
820
820
|
|
|
821
|
-
//
|
|
821
|
+
// Issue #1803: prefix flag controls fork CREATION; for lookup, trust forkRepoName from PR head data.
|
|
822
822
|
const headRepoName = forkRepoName || repo;
|
|
823
823
|
const standardForkName = `${forkOwner}/${headRepoName}`;
|
|
824
824
|
const prefixedForkName = `${forkOwner}/${owner}-${headRepoName}`;
|
|
825
|
-
const expectedForkName = argv.prefixForkNameWithOwnerName ? prefixedForkName : standardForkName;
|
|
826
|
-
const alternateForkName = argv.prefixForkNameWithOwnerName ? standardForkName : prefixedForkName;
|
|
825
|
+
const expectedForkName = forkRepoName ? `${forkOwner}/${forkRepoName}` : argv.prefixForkNameWithOwnerName ? prefixedForkName : standardForkName;
|
|
826
|
+
const alternateForkName = forkRepoName ? null : argv.prefixForkNameWithOwnerName ? standardForkName : prefixedForkName;
|
|
827
827
|
|
|
828
828
|
await log(`${formatAligned('✅', 'Using fork:', expectedForkName)}\n`);
|
|
829
829
|
|
|
@@ -832,9 +832,9 @@ Thank you!`;
|
|
|
832
832
|
let forkCheckResult = await $`gh repo view ${expectedForkName} --json name 2>/dev/null`;
|
|
833
833
|
let actualForkName = expectedForkName;
|
|
834
834
|
|
|
835
|
-
if (forkCheckResult.code !== 0 && !argv.prefixForkNameWithOwnerName) {
|
|
836
|
-
// Only try alternate name if
|
|
837
|
-
//
|
|
835
|
+
if (forkCheckResult.code !== 0 && alternateForkName && !argv.prefixForkNameWithOwnerName) {
|
|
836
|
+
// Only try alternate name if --prefix-fork-name-with-owner-name is off AND we're guessing
|
|
837
|
+
// (forkRepoName authoritative → alternateForkName is null and no fallback is attempted).
|
|
838
838
|
forkCheckResult = await $`gh repo view ${alternateForkName} --json name 2>/dev/null`;
|
|
839
839
|
if (forkCheckResult.code === 0) {
|
|
840
840
|
actualForkName = alternateForkName;
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { parseRepositoryUrl, checkLabelPermissions, ensureReadyLabel } from './github-merge.lib.mjs';
|
|
22
22
|
import { createMergeQueueProcessor, MergeStatus, MERGE_QUEUE_CONFIG } from './telegram-merge-queue.lib.mjs';
|
|
23
|
+
import { executeStartScreen } from './telegram-command-execution.lib.mjs';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Active merge operations map (repoKey -> { processor, chatId, messageId })
|
|
@@ -90,6 +91,77 @@ function parseCommandArgs(text) {
|
|
|
90
91
|
return args;
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Issue #1805: Parse boolean flags out of the tokenised `/merge` args.
|
|
96
|
+
* Supports `--flag`, `--flag=true`, `--flag=false`, `--no-flag` and the
|
|
97
|
+
* trailing positional repository URL. We keep the original positional order
|
|
98
|
+
* so callers can still treat `positionals[0]` as the repo URL.
|
|
99
|
+
*
|
|
100
|
+
* @param {string[]} args - The output of `parseCommandArgs(text)`.
|
|
101
|
+
* @returns {{ positionals: string[], flags: Record<string, boolean> }}
|
|
102
|
+
*/
|
|
103
|
+
export function parseMergeArgs(args) {
|
|
104
|
+
const flags = {};
|
|
105
|
+
const positionals = [];
|
|
106
|
+
for (const arg of args) {
|
|
107
|
+
if (typeof arg !== 'string') continue;
|
|
108
|
+
if (arg.startsWith('--')) {
|
|
109
|
+
const body = arg.slice(2);
|
|
110
|
+
if (!body) continue;
|
|
111
|
+
// --no-foo => foo=false
|
|
112
|
+
if (body.startsWith('no-')) {
|
|
113
|
+
const key = body.slice(3);
|
|
114
|
+
if (key) flags[key] = false;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const eqIdx = body.indexOf('=');
|
|
118
|
+
if (eqIdx === -1) {
|
|
119
|
+
flags[body] = true;
|
|
120
|
+
} else {
|
|
121
|
+
const key = body.slice(0, eqIdx);
|
|
122
|
+
const value = body.slice(eqIdx + 1).toLowerCase();
|
|
123
|
+
flags[key] = !(value === 'false' || value === '0' || value === 'no' || value === 'off');
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
positionals.push(arg);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { positionals, flags };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Issue #1805: Spawner used by the merge queue's auto-resolve pass. For each
|
|
134
|
+
* skipped PR we dispatch a `solve <pr-url> --auto-merge` session through
|
|
135
|
+
* the same `start-screen` runtime the bot uses everywhere else. Keeping this
|
|
136
|
+
* in one place means the per-PR sessions behave exactly like any other
|
|
137
|
+
* `/solve` invocation (same logs, same /watch, same isolation backend).
|
|
138
|
+
*
|
|
139
|
+
* @param {Object} target - Info for the conflicted PR.
|
|
140
|
+
* @param {string} target.url - PR HTML URL passed to `solve`.
|
|
141
|
+
* @param {boolean} verbose - Forwarded to the underlying spawn.
|
|
142
|
+
* @returns {Promise<{ success: boolean, sessionName: string|null, error: string|null, warning: string|null }>}
|
|
143
|
+
*/
|
|
144
|
+
async function spawnAutoResolveSolve(target, verbose) {
|
|
145
|
+
if (!target || !target.url) {
|
|
146
|
+
return { success: false, sessionName: null, error: 'missing PR URL', warning: null };
|
|
147
|
+
}
|
|
148
|
+
const args = [target.url, '--auto-merge'];
|
|
149
|
+
try {
|
|
150
|
+
const result = await executeStartScreen('solve', args, { verbose });
|
|
151
|
+
if (result.warning) {
|
|
152
|
+
return { success: false, sessionName: null, error: null, warning: result.warning };
|
|
153
|
+
}
|
|
154
|
+
if (!result.success) {
|
|
155
|
+
return { success: false, sessionName: null, error: result.error || 'spawn failed', warning: null };
|
|
156
|
+
}
|
|
157
|
+
const match = result.output && (result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -R\s+(\S+)/));
|
|
158
|
+
const sessionName = match ? match[1] : null;
|
|
159
|
+
return { success: true, sessionName, error: null, warning: null };
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return { success: false, sessionName: null, error: error.message || String(error), warning: null };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
93
165
|
/**
|
|
94
166
|
* Format user-friendly error message
|
|
95
167
|
* Hides debug info unless verbose mode is enabled
|
|
@@ -175,13 +247,17 @@ export function registerMergeCommand(bot, options) {
|
|
|
175
247
|
|
|
176
248
|
// Parse arguments
|
|
177
249
|
const args = parseCommandArgs(ctx.message.text);
|
|
250
|
+
// Issue #1805: split positional args from `--auto-resolve` style flags so
|
|
251
|
+
// the repository URL parsing still sees only the URL token.
|
|
252
|
+
const { positionals, flags } = parseMergeArgs(args);
|
|
253
|
+
const autoResolve = flags['auto-resolve'] === true;
|
|
178
254
|
|
|
179
|
-
if (
|
|
180
|
-
return await ctx.reply("Missing repository URL\\.\n\nUsage: `/merge <repository-url
|
|
255
|
+
if (positionals.length === 0) {
|
|
256
|
+
return await ctx.reply("Missing repository URL\\.\n\nUsage: `/merge <repository-url> [--auto-resolve]`\n\nExample: `/merge https://github.com/owner/repo`\n\nThis will merge all PRs with the 'ready' label, one by one, waiting for CI/CD between each merge\\.\n\nWith `--auto-resolve` the bot also dispatches `/solve <pr> --auto-merge` for every PR that was skipped because of merge conflicts\\.", { parse_mode: 'MarkdownV2', reply_to_message_id: ctx.message.message_id });
|
|
181
257
|
}
|
|
182
258
|
|
|
183
259
|
// Parse and validate repository URL
|
|
184
|
-
const repoUrl =
|
|
260
|
+
const repoUrl = positionals[0];
|
|
185
261
|
const parsedUrl = parseRepositoryUrl(repoUrl);
|
|
186
262
|
|
|
187
263
|
if (!parsedUrl.valid) {
|
|
@@ -234,6 +310,11 @@ export function registerMergeCommand(bot, options) {
|
|
|
234
310
|
// Create the merge queue processor
|
|
235
311
|
const processor = await createMergeQueueProcessor(owner, repo, {
|
|
236
312
|
verbose: VERBOSE,
|
|
313
|
+
// Issue #1805: forward the --auto-resolve flag and inject the spawner.
|
|
314
|
+
// The processor only sees the callback, so unit tests can stub it
|
|
315
|
+
// without spawning real screen sessions.
|
|
316
|
+
autoResolve,
|
|
317
|
+
spawnSolveSession: autoResolve ? target => spawnAutoResolveSolve(target, VERBOSE) : null,
|
|
237
318
|
onProgress: async () => {
|
|
238
319
|
// Update message with progress and cancel button
|
|
239
320
|
try {
|
|
@@ -44,8 +44,24 @@ export const MergeItemStatus = {
|
|
|
44
44
|
MERGED: 'merged',
|
|
45
45
|
FAILED: 'failed',
|
|
46
46
|
SKIPPED: 'skipped',
|
|
47
|
+
// Issue #1805: states reached during the post-queue `--auto-resolve` pass.
|
|
48
|
+
// RESOLVING is set while a `/solve <pr> --auto-merge` session is being
|
|
49
|
+
// spawned for a previously-skipped PR; RESOLVE_FAILED records that the
|
|
50
|
+
// spawn (or the resolution itself) didn't succeed.
|
|
51
|
+
RESOLVING: 'resolving',
|
|
52
|
+
RESOLVE_FAILED: 'resolve_failed',
|
|
47
53
|
};
|
|
48
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Marker that identifies SKIPPED items that the auto-resolve pass should
|
|
57
|
+
* pick up. The same string is returned by `checkPRMergeable()` for
|
|
58
|
+
* `mergeStateStatus === 'DIRTY'` (see github-merge.lib.mjs), so matching
|
|
59
|
+
* on it keeps the two modules in sync without sharing extra state.
|
|
60
|
+
*
|
|
61
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1805
|
|
62
|
+
*/
|
|
63
|
+
export const MERGE_CONFLICT_SKIP_REASON = 'PR has merge conflicts';
|
|
64
|
+
|
|
49
65
|
/**
|
|
50
66
|
* Configuration for merge queue operations
|
|
51
67
|
* Values are loaded from config.lib.mjs which supports environment variable overrides.
|
|
@@ -134,6 +150,11 @@ class MergeQueueItem {
|
|
|
134
150
|
return '❌';
|
|
135
151
|
case MergeItemStatus.SKIPPED:
|
|
136
152
|
return '⏭️';
|
|
153
|
+
// Issue #1805: auto-resolve pass states.
|
|
154
|
+
case MergeItemStatus.RESOLVING:
|
|
155
|
+
return '🛠️';
|
|
156
|
+
case MergeItemStatus.RESOLVE_FAILED:
|
|
157
|
+
return '⚠️';
|
|
137
158
|
default:
|
|
138
159
|
return '❓';
|
|
139
160
|
}
|
|
@@ -152,6 +173,12 @@ export class MergeQueueProcessor {
|
|
|
152
173
|
this.onProgress = options.onProgress || null;
|
|
153
174
|
this.onComplete = options.onComplete || null;
|
|
154
175
|
this.onError = options.onError || null;
|
|
176
|
+
// Issue #1805: when true the queue runs a second pass after the normal
|
|
177
|
+
// merge loop, spawning `/solve <pr> --auto-merge` for every PR that was
|
|
178
|
+
// SKIPPED due to merge conflicts. The actual spawner is injected so
|
|
179
|
+
// tests can run without touching the screen runtime.
|
|
180
|
+
this.autoResolve = options.autoResolve === true;
|
|
181
|
+
this.spawnSolveSession = typeof options.spawnSolveSession === 'function' ? options.spawnSolveSession : null;
|
|
155
182
|
|
|
156
183
|
// State
|
|
157
184
|
this.items = [];
|
|
@@ -161,6 +188,9 @@ export class MergeQueueProcessor {
|
|
|
161
188
|
this.startedAt = null;
|
|
162
189
|
this.completedAt = null;
|
|
163
190
|
this.error = null;
|
|
191
|
+
// Issue #1805: track auto-resolve progress so the renderer can surface it.
|
|
192
|
+
this.autoResolveActive = false;
|
|
193
|
+
this.autoResolveCurrent = null;
|
|
164
194
|
|
|
165
195
|
// Statistics
|
|
166
196
|
this.stats = {
|
|
@@ -168,6 +198,11 @@ export class MergeQueueProcessor {
|
|
|
168
198
|
merged: 0,
|
|
169
199
|
failed: 0,
|
|
170
200
|
skipped: 0,
|
|
201
|
+
// Issue #1805: number of skipped conflict PRs the auto-resolve pass
|
|
202
|
+
// successfully handed off to `solve`, and the number that failed to
|
|
203
|
+
// be handed off (e.g. screen runner missing).
|
|
204
|
+
autoResolved: 0,
|
|
205
|
+
autoResolveFailed: 0,
|
|
171
206
|
};
|
|
172
207
|
}
|
|
173
208
|
|
|
@@ -329,6 +364,15 @@ export class MergeQueueProcessor {
|
|
|
329
364
|
}
|
|
330
365
|
}
|
|
331
366
|
|
|
367
|
+
// Issue #1805: After the normal queue settles, optionally hand off
|
|
368
|
+
// every PR that was SKIPPED with a merge-conflict reason to the
|
|
369
|
+
// `/solve <pr> --auto-merge` flow. This lets a single `/merge`
|
|
370
|
+
// invocation both merge the easy PRs and dispatch conflict-resolution
|
|
371
|
+
// sessions for the rest.
|
|
372
|
+
if (this.autoResolve && !this.isCancelled) {
|
|
373
|
+
await this.runAutoResolve();
|
|
374
|
+
}
|
|
375
|
+
|
|
332
376
|
this.completedAt = new Date();
|
|
333
377
|
this.status = this.isCancelled ? MergeStatus.CANCELLED : MergeStatus.COMPLETED;
|
|
334
378
|
|
|
@@ -483,6 +527,100 @@ export class MergeQueueProcessor {
|
|
|
483
527
|
this.log('Cancellation requested');
|
|
484
528
|
}
|
|
485
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Issue #1805: Return queue items that were skipped because of merge
|
|
532
|
+
* conflicts. These are the candidates the auto-resolve pass hands off
|
|
533
|
+
* to `/solve <pr> --auto-merge`.
|
|
534
|
+
*
|
|
535
|
+
* @returns {MergeQueueItem[]}
|
|
536
|
+
*/
|
|
537
|
+
getConflictedItems() {
|
|
538
|
+
return this.items.filter(item => item.status === MergeItemStatus.SKIPPED && item.error === MERGE_CONFLICT_SKIP_REASON);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Issue #1805: Iterate every conflict-skipped item and hand it off to a
|
|
543
|
+
* `/solve <pr-url> --auto-merge` session via the injected
|
|
544
|
+
* `spawnSolveSession` callback. Each spawn is awaited so the bot doesn't
|
|
545
|
+
* fan out unbounded Claude sessions. The PR's status is updated as the
|
|
546
|
+
* spawn succeeds or fails.
|
|
547
|
+
*
|
|
548
|
+
* @returns {Promise<void>}
|
|
549
|
+
*/
|
|
550
|
+
async runAutoResolve() {
|
|
551
|
+
const conflicted = this.getConflictedItems();
|
|
552
|
+
if (conflicted.length === 0) {
|
|
553
|
+
this.log('Auto-resolve: no merge-conflict skips to process');
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (!this.spawnSolveSession) {
|
|
558
|
+
// Guard against misconfiguration — the queue can't resolve without a
|
|
559
|
+
// spawner. Surface this to the user via the same channel as other
|
|
560
|
+
// queue feedback rather than throwing.
|
|
561
|
+
this.log(`Auto-resolve: ${conflicted.length} conflict PR(s) but no spawnSolveSession callback provided`);
|
|
562
|
+
for (const item of conflicted) {
|
|
563
|
+
item.status = MergeItemStatus.RESOLVE_FAILED;
|
|
564
|
+
item.autoResolveError = 'auto-resolve is not configured';
|
|
565
|
+
this.stats.autoResolveFailed++;
|
|
566
|
+
}
|
|
567
|
+
if (this.onProgress) {
|
|
568
|
+
await this.onProgress(this.getProgressUpdate());
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
this.autoResolveActive = true;
|
|
574
|
+
this.log(`Auto-resolve: dispatching ${conflicted.length} conflict PR(s) to /solve --auto-merge`);
|
|
575
|
+
try {
|
|
576
|
+
for (const item of conflicted) {
|
|
577
|
+
if (this.isCancelled) {
|
|
578
|
+
this.log('Auto-resolve: cancelled mid-pass');
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
item.status = MergeItemStatus.RESOLVING;
|
|
583
|
+
this.autoResolveCurrent = item.pr.number;
|
|
584
|
+
if (this.onProgress) {
|
|
585
|
+
await this.onProgress(this.getProgressUpdate());
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
const result = await this.spawnSolveSession({
|
|
590
|
+
url: item.pr.url,
|
|
591
|
+
owner: this.owner,
|
|
592
|
+
repo: this.repo,
|
|
593
|
+
prNumber: item.pr.number,
|
|
594
|
+
title: item.pr.title,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
if (result && result.success) {
|
|
598
|
+
item.autoResolveSession = result.sessionName || result.session || null;
|
|
599
|
+
this.stats.autoResolved++;
|
|
600
|
+
this.log(`Auto-resolve: spawned solve session for PR #${item.pr.number}${item.autoResolveSession ? ` (session ${item.autoResolveSession})` : ''}`);
|
|
601
|
+
} else {
|
|
602
|
+
item.status = MergeItemStatus.RESOLVE_FAILED;
|
|
603
|
+
item.autoResolveError = (result && (result.error || result.warning)) || 'spawn failed';
|
|
604
|
+
this.stats.autoResolveFailed++;
|
|
605
|
+
this.log(`Auto-resolve: failed to spawn solve session for PR #${item.pr.number}: ${item.autoResolveError}`);
|
|
606
|
+
}
|
|
607
|
+
} catch (error) {
|
|
608
|
+
item.status = MergeItemStatus.RESOLVE_FAILED;
|
|
609
|
+
item.autoResolveError = error.message || String(error);
|
|
610
|
+
this.stats.autoResolveFailed++;
|
|
611
|
+
console.error(`[ERROR] /merge-queue: auto-resolve error for PR #${item.pr.number}: ${item.autoResolveError}`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (this.onProgress) {
|
|
615
|
+
await this.onProgress(this.getProgressUpdate());
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} finally {
|
|
619
|
+
this.autoResolveActive = false;
|
|
620
|
+
this.autoResolveCurrent = null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
486
624
|
/**
|
|
487
625
|
* Wait for any active CI runs on the target branch to complete
|
|
488
626
|
* Issue #1307: Prevents merging while post-merge CI from previous merges is still running
|
|
@@ -658,6 +796,13 @@ export class MergeQueueProcessor {
|
|
|
658
796
|
status: this.status,
|
|
659
797
|
current: currentItem ? currentItem.getDescription() : null,
|
|
660
798
|
currentStatus: currentItem ? currentItem.status : null,
|
|
799
|
+
// Issue #1805: surface auto-resolve progress so renderers/tests can
|
|
800
|
+
// show what's happening during the post-queue pass.
|
|
801
|
+
autoResolve: {
|
|
802
|
+
enabled: this.autoResolve,
|
|
803
|
+
active: this.autoResolveActive,
|
|
804
|
+
currentPrNumber: this.autoResolveCurrent,
|
|
805
|
+
},
|
|
661
806
|
progress: {
|
|
662
807
|
processed,
|
|
663
808
|
total: this.stats.total,
|
|
@@ -666,10 +811,17 @@ export class MergeQueueProcessor {
|
|
|
666
811
|
stats: { ...this.stats },
|
|
667
812
|
items: this.items.map(item => ({
|
|
668
813
|
prNumber: item.pr.number,
|
|
814
|
+
// Issue #1805: expose PR/issue URLs so renderers can produce
|
|
815
|
+
// clickable links instead of plain `\#NNN` text.
|
|
816
|
+
prUrl: item.pr.url || null,
|
|
817
|
+
issueNumber: item.issue ? item.issue.number : null,
|
|
818
|
+
issueUrl: item.issue ? item.issue.url || null : null,
|
|
669
819
|
title: item.pr.title,
|
|
670
820
|
status: item.status,
|
|
671
821
|
error: item.error,
|
|
672
822
|
emoji: item.getStatusEmoji(),
|
|
823
|
+
autoResolveSession: item.autoResolveSession || null,
|
|
824
|
+
autoResolveError: item.autoResolveError || null,
|
|
673
825
|
})),
|
|
674
826
|
};
|
|
675
827
|
}
|
|
@@ -684,13 +836,24 @@ export class MergeQueueProcessor {
|
|
|
684
836
|
status: this.status,
|
|
685
837
|
duration: `${Math.floor(duration / 60)}m ${duration % 60}s`,
|
|
686
838
|
stats: { ...this.stats },
|
|
839
|
+
autoResolve: {
|
|
840
|
+
enabled: this.autoResolve,
|
|
841
|
+
resolved: this.stats.autoResolved,
|
|
842
|
+
failed: this.stats.autoResolveFailed,
|
|
843
|
+
},
|
|
687
844
|
items: this.items.map(item => ({
|
|
688
845
|
prNumber: item.pr.number,
|
|
846
|
+
// Issue #1805: expose PR/issue URLs so renderers can produce
|
|
847
|
+
// clickable links instead of plain `\#NNN` text.
|
|
848
|
+
prUrl: item.pr.url || null,
|
|
689
849
|
title: item.pr.title,
|
|
690
850
|
issueNumber: item.issue ? item.issue.number : null,
|
|
851
|
+
issueUrl: item.issue ? item.issue.url || null : null,
|
|
691
852
|
status: item.status,
|
|
692
853
|
error: item.error,
|
|
693
854
|
emoji: item.getStatusEmoji(),
|
|
855
|
+
autoResolveSession: item.autoResolveSession || null,
|
|
856
|
+
autoResolveError: item.autoResolveError || null,
|
|
694
857
|
})),
|
|
695
858
|
};
|
|
696
859
|
}
|
|
@@ -748,11 +911,25 @@ export class MergeQueueProcessor {
|
|
|
748
911
|
message += `⏱️ Waiting for post\\-merge CI \\(PR \\#${this.currentPostMergePR}\\)\\.\\.\\.\n\n`;
|
|
749
912
|
}
|
|
750
913
|
|
|
751
|
-
//
|
|
752
|
-
|
|
914
|
+
// Issue #1805: surface the auto-resolve pass when it is currently
|
|
915
|
+
// active. This appears in place of "current item" because by then the
|
|
916
|
+
// main queue loop has finished.
|
|
917
|
+
if (update.autoResolve && update.autoResolve.active && update.autoResolve.currentPrNumber) {
|
|
918
|
+
const activeItem = update.items.find(it => it.prNumber === update.autoResolve.currentPrNumber);
|
|
919
|
+
const link = activeItem ? this.formatPrLink(activeItem.prNumber, activeItem.title, activeItem.prUrl) : `\\#${update.autoResolve.currentPrNumber}`;
|
|
920
|
+
message += `🛠️ Auto\\-resolving ${link}\n\n`;
|
|
921
|
+
} else if (update.current && !this.waitingForTargetBranchCI && !this.waitingForPostMergeCI) {
|
|
922
|
+
// Current item being processed
|
|
753
923
|
const statusEmoji = update.currentStatus === MergeItemStatus.WAITING_CI ? '⏱️' : '🔄';
|
|
754
|
-
|
|
755
|
-
|
|
924
|
+
const currentItem = this.items[this.currentIndex];
|
|
925
|
+
if (currentItem) {
|
|
926
|
+
const link = this.formatPrLink(currentItem.pr.number, currentItem.pr.title, currentItem.pr.url);
|
|
927
|
+
const issueSuffix = this.formatIssueRef(currentItem.issue ? currentItem.issue.number : null, currentItem.issue ? currentItem.issue.url : null);
|
|
928
|
+
message += `${statusEmoji} ${link}${issueSuffix}\n\n`;
|
|
929
|
+
} else {
|
|
930
|
+
// Fallback: escape the description if we somehow don't have an item handle
|
|
931
|
+
message += `${statusEmoji} ${this.escapeMarkdown(update.current)}\n\n`;
|
|
932
|
+
}
|
|
756
933
|
}
|
|
757
934
|
|
|
758
935
|
// Show errors/failures/skips inline so user gets immediate feedback (Issue #1269, #1294)
|
|
@@ -762,8 +939,9 @@ export class MergeQueueProcessor {
|
|
|
762
939
|
message += `⚠️ *Issues:*\n`;
|
|
763
940
|
for (const item of problemItems.slice(0, 5)) {
|
|
764
941
|
const statusEmoji = item.status === MergeItemStatus.FAILED ? '❌' : '⏭️';
|
|
765
|
-
// Issue #
|
|
766
|
-
|
|
942
|
+
// Issue #1805: emit the PR reference as a clickable link instead of plain text.
|
|
943
|
+
const prRef = this.formatPrLink(item.prNumber, '', item.prUrl);
|
|
944
|
+
message += ` ${statusEmoji} ${prRef}: ${this.escapeMarkdown(item.error.substring(0, 50))}${item.error.length > 50 ? '\\.\\.\\.' : ''}\n`;
|
|
767
945
|
}
|
|
768
946
|
if (problemItems.length > 5) {
|
|
769
947
|
// Issue #1339: escape the ellipsis '...' for MarkdownV2
|
|
@@ -772,11 +950,10 @@ export class MergeQueueProcessor {
|
|
|
772
950
|
message += '\n';
|
|
773
951
|
}
|
|
774
952
|
|
|
775
|
-
// PRs list with emojis
|
|
953
|
+
// PRs list with emojis (Issue #1805: render as clickable MarkdownV2 links)
|
|
776
954
|
message += `*Queue:*\n`;
|
|
777
955
|
for (const item of update.items.slice(0, 10)) {
|
|
778
|
-
|
|
779
|
-
message += `${item.emoji} \\#${item.prNumber}: ${this.escapeMarkdown(item.title.substring(0, 35))}${item.title.length > 35 ? '\\.\\.\\.' : ''}\n`;
|
|
956
|
+
message += `${item.emoji} ${this.formatPrLink(item.prNumber, item.title, item.prUrl)}\n`;
|
|
780
957
|
}
|
|
781
958
|
|
|
782
959
|
if (update.items.length > 10) {
|
|
@@ -830,7 +1007,20 @@ export class MergeQueueProcessor {
|
|
|
830
1007
|
message += `✅ Merged: ${report.stats.merged} `;
|
|
831
1008
|
message += `❌ Failed: ${report.stats.failed} `;
|
|
832
1009
|
message += `⏭️ Skipped: ${report.stats.skipped} `;
|
|
833
|
-
message += `📋 Total: ${report.stats.total}\n
|
|
1010
|
+
message += `📋 Total: ${report.stats.total}\n`;
|
|
1011
|
+
|
|
1012
|
+
// Issue #1805: surface the auto-resolve pass summary when it ran. We
|
|
1013
|
+
// always show the line when the flag was set so users see "0 dispatched"
|
|
1014
|
+
// when there was nothing to do.
|
|
1015
|
+
if (report.autoResolve && report.autoResolve.enabled) {
|
|
1016
|
+
message += `🛠️ Auto\\-resolve dispatched: ${report.autoResolve.resolved}`;
|
|
1017
|
+
if (report.autoResolve.failed > 0) {
|
|
1018
|
+
message += ` ⚠️ Auto\\-resolve failed: ${report.autoResolve.failed}`;
|
|
1019
|
+
}
|
|
1020
|
+
message += '\n';
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
message += '\n';
|
|
834
1024
|
|
|
835
1025
|
// Issue #1341: Show branch CI health failure details if applicable
|
|
836
1026
|
if (this.branchCIFailedRuns && this.branchCIFailedRuns.length > 0) {
|
|
@@ -862,19 +1052,25 @@ export class MergeQueueProcessor {
|
|
|
862
1052
|
message += '\n';
|
|
863
1053
|
}
|
|
864
1054
|
|
|
865
|
-
// Details
|
|
1055
|
+
// Details (Issue #1805: render PR and issue references as clickable
|
|
1056
|
+
// MarkdownV2 links so the user can jump directly to the PR or issue).
|
|
866
1057
|
if (report.items.length > 0) {
|
|
867
1058
|
message += `*Results:*\n`;
|
|
868
1059
|
for (const item of report.items) {
|
|
869
|
-
const
|
|
1060
|
+
const prLink = this.formatPrLink(item.prNumber, item.title, item.prUrl);
|
|
1061
|
+
const issueRef = this.formatIssueRef(item.issueNumber, item.issueUrl);
|
|
870
1062
|
// Issue #1294: Show skip/fail reason so users understand what action is required
|
|
871
1063
|
let reasonText = '';
|
|
872
|
-
|
|
1064
|
+
const isAutoResolveState = item.status === MergeItemStatus.RESOLVING || item.status === MergeItemStatus.RESOLVE_FAILED;
|
|
1065
|
+
if (item.autoResolveError && isAutoResolveState) {
|
|
1066
|
+
const truncated = item.autoResolveError.length > 50 ? item.autoResolveError.substring(0, 47) + '...' : item.autoResolveError;
|
|
1067
|
+
reasonText = ` \\(${this.escapeMarkdown(truncated)}\\)`;
|
|
1068
|
+
} else if (item.error && (item.status === MergeItemStatus.SKIPPED || item.status === MergeItemStatus.FAILED)) {
|
|
873
1069
|
// Truncate long reasons and escape for MarkdownV2
|
|
874
1070
|
const truncatedReason = item.error.length > 50 ? item.error.substring(0, 47) + '...' : item.error;
|
|
875
1071
|
reasonText = `: ${this.escapeMarkdown(truncatedReason)}`;
|
|
876
1072
|
}
|
|
877
|
-
message += `${item.emoji}
|
|
1073
|
+
message += `${item.emoji} ${prLink}${issueRef}${reasonText}\n`;
|
|
878
1074
|
}
|
|
879
1075
|
}
|
|
880
1076
|
|
|
@@ -888,6 +1084,43 @@ export class MergeQueueProcessor {
|
|
|
888
1084
|
return String(text).replace(/[_*[\]()~`>#+\-=|{}.!]/g, '\\$&');
|
|
889
1085
|
}
|
|
890
1086
|
|
|
1087
|
+
/**
|
|
1088
|
+
* Issue #1805: Escape `)` and `\` inside a URL for a MarkdownV2 inline link.
|
|
1089
|
+
* URLs must NOT be passed through `escapeMarkdown()` because that would also
|
|
1090
|
+
* mangle characters that are valid inside URLs (`.`, `-`, `_`, etc.).
|
|
1091
|
+
*/
|
|
1092
|
+
escapeMarkdownLinkUrl(url) {
|
|
1093
|
+
return String(url).replace(/[\\)]/g, '\\$&');
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Issue #1805: Build a clickable MarkdownV2 link for a PR's `\#N: title`
|
|
1098
|
+
* reference. Falls back to plain escaped text when no URL is available so
|
|
1099
|
+
* the message still renders correctly on legacy items.
|
|
1100
|
+
*/
|
|
1101
|
+
formatPrLink(prNumber, title, url, options = {}) {
|
|
1102
|
+
const maxTitle = typeof options.maxTitle === 'number' ? options.maxTitle : 35;
|
|
1103
|
+
const trimmedTitle = title || '';
|
|
1104
|
+
const truncated = trimmedTitle.length > maxTitle ? trimmedTitle.substring(0, maxTitle) : trimmedTitle;
|
|
1105
|
+
const ellipsis = trimmedTitle.length > maxTitle ? '\\.\\.\\.' : '';
|
|
1106
|
+
const titlePart = trimmedTitle ? `: ${this.escapeMarkdown(truncated)}${ellipsis}` : '';
|
|
1107
|
+
const label = `\\#${prNumber}${titlePart}`;
|
|
1108
|
+
if (!url) return label;
|
|
1109
|
+
return `[${label}](${this.escapeMarkdownLinkUrl(url)})`;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Issue #1805: Build the ` (Issue #N)` suffix as a clickable link. The
|
|
1114
|
+
* outer parentheses are literal MarkdownV2 (escaped), so the inner inline
|
|
1115
|
+
* link is not nested inside another entity.
|
|
1116
|
+
*/
|
|
1117
|
+
formatIssueRef(issueNumber, url) {
|
|
1118
|
+
if (!issueNumber) return '';
|
|
1119
|
+
const label = `Issue \\#${issueNumber}`;
|
|
1120
|
+
if (!url) return ` \\(${label}\\)`;
|
|
1121
|
+
return ` \\([${label}](${this.escapeMarkdownLinkUrl(url)})\\)`;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
891
1124
|
/**
|
|
892
1125
|
* Sleep helper
|
|
893
1126
|
*/
|