@link-assistant/hive-mind 1.7.2 → 1.9.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 +30 -0
- package/package.json +3 -2
- package/src/claude.lib.mjs +89 -2
- package/src/claude.prompts.lib.mjs +8 -2
- package/src/config.lib.mjs +94 -1
- package/src/github-merge.lib.mjs +560 -0
- package/src/github.lib.mjs +1 -0
- package/src/hive.config.lib.mjs +12 -2
- package/src/hive.mjs +2 -0
- package/src/option-suggestions.lib.mjs +3 -0
- package/src/solve.config.lib.mjs +17 -2
- package/src/telegram-bot.mjs +20 -10
- package/src/telegram-merge-command.lib.mjs +366 -0
- package/src/telegram-merge-queue.lib.mjs +560 -0
package/src/telegram-bot.mjs
CHANGED
|
@@ -762,12 +762,15 @@ bot.command('help', async ctx => {
|
|
|
762
762
|
message += '*/limits* - Show usage limits\n';
|
|
763
763
|
message += '*/version* - Show bot and runtime versions\n';
|
|
764
764
|
message += '*/accept\\_invites* - Accept all pending GitHub invitations\n';
|
|
765
|
+
message += '*/merge* - Merge queue (experimental)\n';
|
|
766
|
+
message += 'Usage: `/merge <github-repo-url>`\n';
|
|
767
|
+
message += "Merges all PRs with 'ready' label sequentially.\n";
|
|
765
768
|
message += '*/help* - Show this help message\n\n';
|
|
766
|
-
message += '⚠️ *Note:* /solve, /hive, /limits, /version
|
|
769
|
+
message += '⚠️ *Note:* /solve, /hive, /limits, /version, /accept\\_invites and /merge commands only work in group chats.\n\n';
|
|
767
770
|
message += '🔧 *Common Options:*\n';
|
|
768
771
|
message += '• `--model <model>` or `-m` - Specify AI model (sonnet, opus, haiku, haiku-3-5, haiku-3)\n';
|
|
769
772
|
message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
|
|
770
|
-
message += '• `--think <level>` - Thinking level (low/medium/high/max)\n';
|
|
773
|
+
message += '• `--think <level>` - Thinking level (off/low/medium/high/max) | `--thinking-budget <num>` - Token budget (0-63999)\n';
|
|
771
774
|
message += '• `--verbose` or `-v` - Verbose output | `--attach-logs` - Attach logs to PR\n';
|
|
772
775
|
message += '\n💡 *Tip:* Many more options available. See full documentation for complete list.\n';
|
|
773
776
|
|
|
@@ -901,6 +904,17 @@ registerAcceptInvitesCommand(bot, {
|
|
|
901
904
|
addBreadcrumb,
|
|
902
905
|
});
|
|
903
906
|
|
|
907
|
+
// Register /merge command from separate module (experimental, see issue #1143)
|
|
908
|
+
const { registerMergeCommand } = await import('./telegram-merge-command.lib.mjs');
|
|
909
|
+
registerMergeCommand(bot, {
|
|
910
|
+
VERBOSE,
|
|
911
|
+
isOldMessage,
|
|
912
|
+
isForwardedOrReply,
|
|
913
|
+
isGroupChat,
|
|
914
|
+
isChatAuthorized,
|
|
915
|
+
addBreadcrumb,
|
|
916
|
+
});
|
|
917
|
+
|
|
904
918
|
bot.command(/^solve$/i, async ctx => {
|
|
905
919
|
if (VERBOSE) {
|
|
906
920
|
console.log('[VERBOSE] /solve command received');
|
|
@@ -1460,14 +1474,12 @@ bot.telegram
|
|
|
1460
1474
|
process.exit(1);
|
|
1461
1475
|
});
|
|
1462
1476
|
|
|
1463
|
-
// Helper to stop solve queue gracefully on shutdown
|
|
1464
|
-
// See: https://github.com/link-assistant/hive-mind/issues/1083
|
|
1477
|
+
// Helper to stop solve queue gracefully on shutdown (see issue #1083)
|
|
1465
1478
|
const stopSolveQueue = () => {
|
|
1466
1479
|
try {
|
|
1467
1480
|
getSolveQueue({ verbose: VERBOSE }).stop();
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
if (VERBOSE) console.log('[VERBOSE] Could not stop solve queue:', err.message);
|
|
1481
|
+
} catch {
|
|
1482
|
+
/* ignore errors during shutdown */
|
|
1471
1483
|
}
|
|
1472
1484
|
};
|
|
1473
1485
|
|
|
@@ -1481,10 +1493,8 @@ process.once('SIGINT', () => {
|
|
|
1481
1493
|
|
|
1482
1494
|
process.once('SIGTERM', () => {
|
|
1483
1495
|
isShuttingDown = true;
|
|
1484
|
-
console.log('\n🛑 Received SIGTERM, stopping bot...');
|
|
1496
|
+
console.log('\n🛑 Received SIGTERM, stopping bot... (Check system logs: journalctl -u <service> or dmesg)');
|
|
1485
1497
|
if (VERBOSE) console.log(`[VERBOSE] Signal: SIGTERM, PID: ${process.pid}, PPID: ${process.ppid}`);
|
|
1486
|
-
console.log('ℹ️ SIGTERM is typically sent by: system shutdown, process manager, kill command, or container orchestration');
|
|
1487
|
-
console.log('💡 Check system logs for details: journalctl -u <service> or dmesg');
|
|
1488
1498
|
stopSolveQueue();
|
|
1489
1499
|
bot.stop('SIGTERM');
|
|
1490
1500
|
});
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram /merge command implementation
|
|
3
|
+
*
|
|
4
|
+
* This module provides the /merge command functionality for the Telegram bot,
|
|
5
|
+
* allowing users to process a repository's merge queue - merging all PRs
|
|
6
|
+
* with the 'ready' label sequentially.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Accepts repository URL
|
|
10
|
+
* - Checks and creates 'ready' label if needed
|
|
11
|
+
* - Fetches all PRs/issues with 'ready' label
|
|
12
|
+
* - Merges PRs sequentially (oldest first)
|
|
13
|
+
* - Monitors CI/CD between merges (every 5 minutes)
|
|
14
|
+
* - Provides progress updates via single updated Telegram message
|
|
15
|
+
* - Cancel via inline button
|
|
16
|
+
* - Per-repository concurrency control (not per-chat)
|
|
17
|
+
*
|
|
18
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1143
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { parseRepositoryUrl, checkLabelPermissions, ensureReadyLabel } from './github-merge.lib.mjs';
|
|
22
|
+
import { createMergeQueueProcessor, MergeStatus, MERGE_QUEUE_CONFIG } from './telegram-merge-queue.lib.mjs';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Active merge operations map (repoKey -> { processor, chatId, messageId })
|
|
26
|
+
* Uses repository key (owner/repo) for per-repository concurrency control
|
|
27
|
+
*/
|
|
28
|
+
const activeMergeOperations = new Map();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate repository key for the operations map
|
|
32
|
+
* @param {string} owner - Repository owner
|
|
33
|
+
* @param {string} repo - Repository name
|
|
34
|
+
* @returns {string} Repository key
|
|
35
|
+
*/
|
|
36
|
+
function getRepoKey(owner, repo) {
|
|
37
|
+
return `${owner}/${repo}`.toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Escapes special characters in text for Telegram MarkdownV2 formatting
|
|
42
|
+
* @param {string} text - The text to escape
|
|
43
|
+
* @returns {string} The escaped text
|
|
44
|
+
*/
|
|
45
|
+
function escapeMarkdownV2(text) {
|
|
46
|
+
return String(text).replace(/[_*[\]()~`>#+\-=|{}.!]/g, '\\$&');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse command arguments for /merge
|
|
51
|
+
* @param {string} text - Message text
|
|
52
|
+
* @returns {string[]} Array of arguments
|
|
53
|
+
*/
|
|
54
|
+
function parseCommandArgs(text) {
|
|
55
|
+
const firstLine = text.split('\n')[0].trim();
|
|
56
|
+
const argsText = firstLine.replace(/^\/\w+\s*/, '');
|
|
57
|
+
|
|
58
|
+
if (!argsText.trim()) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const args = [];
|
|
63
|
+
let currentArg = '';
|
|
64
|
+
let inQuotes = false;
|
|
65
|
+
let quoteChar = null;
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < argsText.length; i++) {
|
|
68
|
+
const char = argsText[i];
|
|
69
|
+
|
|
70
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
71
|
+
inQuotes = true;
|
|
72
|
+
quoteChar = char;
|
|
73
|
+
} else if (char === quoteChar && inQuotes) {
|
|
74
|
+
inQuotes = false;
|
|
75
|
+
quoteChar = null;
|
|
76
|
+
} else if (char === ' ' && !inQuotes) {
|
|
77
|
+
if (currentArg) {
|
|
78
|
+
args.push(currentArg);
|
|
79
|
+
currentArg = '';
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
currentArg += char;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (currentArg) {
|
|
87
|
+
args.push(currentArg);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return args;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Format user-friendly error message
|
|
95
|
+
* Hides debug info unless verbose mode is enabled
|
|
96
|
+
* @param {Error} error - The error object
|
|
97
|
+
* @param {boolean} verbose - Whether verbose logging is enabled
|
|
98
|
+
* @returns {string} User-friendly error message
|
|
99
|
+
*/
|
|
100
|
+
function formatUserError(error, verbose) {
|
|
101
|
+
// Map common errors to user-friendly messages
|
|
102
|
+
const errorMessage = error.message || String(error);
|
|
103
|
+
|
|
104
|
+
if (errorMessage.includes('rate limit')) {
|
|
105
|
+
return 'GitHub API rate limit exceeded. Please try again later.';
|
|
106
|
+
}
|
|
107
|
+
if (errorMessage.includes('permission') || errorMessage.includes('403')) {
|
|
108
|
+
return 'Insufficient permissions to access this repository. Please check access rights.';
|
|
109
|
+
}
|
|
110
|
+
if (errorMessage.includes('not found') || errorMessage.includes('404')) {
|
|
111
|
+
return 'Repository not found. Please check the URL and try again.';
|
|
112
|
+
}
|
|
113
|
+
if (errorMessage.includes('network') || errorMessage.includes('ECONNREFUSED')) {
|
|
114
|
+
return 'Network error. Please check your connection and try again.';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// For unknown errors, show generic message (detailed logs are in verbose mode)
|
|
118
|
+
if (verbose) {
|
|
119
|
+
return `Error: ${errorMessage}`;
|
|
120
|
+
}
|
|
121
|
+
return 'An error occurred. Please try again or contact support.';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Registers the /merge command handler with the bot
|
|
126
|
+
* @param {Object} bot - The Telegraf bot instance
|
|
127
|
+
* @param {Object} options - Options object
|
|
128
|
+
* @param {boolean} options.VERBOSE - Whether to enable verbose logging
|
|
129
|
+
* @param {Function} options.isOldMessage - Function to check if message is old
|
|
130
|
+
* @param {Function} options.isForwardedOrReply - Function to check if message is forwarded/reply
|
|
131
|
+
* @param {Function} options.isGroupChat - Function to check if chat is a group
|
|
132
|
+
* @param {Function} options.isChatAuthorized - Function to check if chat is authorized
|
|
133
|
+
* @param {Function} options.addBreadcrumb - Function to add breadcrumbs for monitoring
|
|
134
|
+
*/
|
|
135
|
+
export function registerMergeCommand(bot, options) {
|
|
136
|
+
const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, addBreadcrumb } = options;
|
|
137
|
+
|
|
138
|
+
bot.command(/^merge$/i, async ctx => {
|
|
139
|
+
VERBOSE && console.log('[VERBOSE] /merge command received');
|
|
140
|
+
|
|
141
|
+
await addBreadcrumb({
|
|
142
|
+
category: 'telegram.command',
|
|
143
|
+
message: '/merge command received',
|
|
144
|
+
level: 'info',
|
|
145
|
+
data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Standard checks
|
|
149
|
+
if (isOldMessage(ctx) || isForwardedOrReply(ctx)) return;
|
|
150
|
+
|
|
151
|
+
if (!isGroupChat(ctx)) {
|
|
152
|
+
return await ctx.reply('The /merge command only works in group chats. Please add this bot to a group and make it an admin.', {
|
|
153
|
+
reply_to_message_id: ctx.message.message_id,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const chatId = ctx.chat.id;
|
|
158
|
+
if (!isChatAuthorized(chatId)) {
|
|
159
|
+
return await ctx.reply(`This chat (ID: ${chatId}) is not authorized to use this bot. Please contact the bot administrator.`, {
|
|
160
|
+
reply_to_message_id: ctx.message.message_id,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Parse arguments
|
|
165
|
+
const args = parseCommandArgs(ctx.message.text);
|
|
166
|
+
|
|
167
|
+
if (args.length === 0) {
|
|
168
|
+
return await ctx.reply("Missing repository URL\\.\n\nUsage: `/merge <repository-url>`\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\\.", { parse_mode: 'MarkdownV2', reply_to_message_id: ctx.message.message_id });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Parse and validate repository URL
|
|
172
|
+
const repoUrl = args[0];
|
|
173
|
+
const parsedUrl = parseRepositoryUrl(repoUrl);
|
|
174
|
+
|
|
175
|
+
if (!parsedUrl.valid) {
|
|
176
|
+
return await ctx.reply(`Invalid repository URL: ${escapeMarkdownV2(parsedUrl.error)}\n\nPlease provide a valid GitHub repository URL\\.`, {
|
|
177
|
+
parse_mode: 'MarkdownV2',
|
|
178
|
+
reply_to_message_id: ctx.message.message_id,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { owner, repo } = parsedUrl;
|
|
183
|
+
const repoKey = getRepoKey(owner, repo);
|
|
184
|
+
VERBOSE && console.log(`[VERBOSE] /merge: Processing repository ${owner}/${repo}`);
|
|
185
|
+
|
|
186
|
+
// Check if a merge operation is already running for this repository (per-repository concurrency)
|
|
187
|
+
if (activeMergeOperations.has(repoKey)) {
|
|
188
|
+
const existingOp = activeMergeOperations.get(repoKey);
|
|
189
|
+
if (existingOp.processor.status === MergeStatus.RUNNING) {
|
|
190
|
+
return await ctx.reply(`A merge operation is already running for ${escapeMarkdownV2(owner)}/${escapeMarkdownV2(repo)}\\.\n\nPlease wait for it to complete or cancel it\\.`, {
|
|
191
|
+
parse_mode: 'MarkdownV2',
|
|
192
|
+
reply_to_message_id: ctx.message.message_id,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Send initial status message (reply to the /merge command)
|
|
198
|
+
const statusMessage = await ctx.reply(`Initializing merge queue for ${escapeMarkdownV2(owner)}/${escapeMarkdownV2(repo)}\\.\\.\\.\n\nThis may take a moment\\.`, {
|
|
199
|
+
parse_mode: 'MarkdownV2',
|
|
200
|
+
reply_to_message_id: ctx.message.message_id,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
// Check permissions
|
|
205
|
+
const permCheck = await checkLabelPermissions(owner, repo, VERBOSE);
|
|
206
|
+
if (!permCheck.canManageLabels) {
|
|
207
|
+
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, `No permission to manage repository ${escapeMarkdownV2(owner)}/${escapeMarkdownV2(repo)}\\.\n\nPlease ensure you have write access to this repository\\.`, { parse_mode: 'MarkdownV2' });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Ensure ready label exists
|
|
212
|
+
const labelResult = await ensureReadyLabel(owner, repo, VERBOSE);
|
|
213
|
+
if (!labelResult.success) {
|
|
214
|
+
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, `Failed to setup 'ready' label: ${escapeMarkdownV2(labelResult.error)}`, {
|
|
215
|
+
parse_mode: 'MarkdownV2',
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const labelMsg = labelResult.created ? "\nCreated 'ready' label in repository\\." : '';
|
|
221
|
+
|
|
222
|
+
// Create the merge queue processor
|
|
223
|
+
const processor = await createMergeQueueProcessor(owner, repo, {
|
|
224
|
+
verbose: VERBOSE,
|
|
225
|
+
onProgress: async () => {
|
|
226
|
+
// Update message with progress and cancel button
|
|
227
|
+
try {
|
|
228
|
+
const message = processor.formatProgressMessage();
|
|
229
|
+
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, message, {
|
|
230
|
+
parse_mode: 'MarkdownV2',
|
|
231
|
+
reply_markup: {
|
|
232
|
+
inline_keyboard: [[{ text: '🛑 Cancel', callback_data: `merge_cancel_${repoKey}` }]],
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
} catch (err) {
|
|
236
|
+
// Ignore message edit errors (e.g., message not modified)
|
|
237
|
+
if (!err.message?.includes('message is not modified')) {
|
|
238
|
+
VERBOSE && console.log(`[VERBOSE] /merge: Error updating message: ${err.message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
onComplete: async () => {
|
|
243
|
+
try {
|
|
244
|
+
const message = processor.formatFinalMessage();
|
|
245
|
+
// Remove cancel button on completion
|
|
246
|
+
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, message, {
|
|
247
|
+
parse_mode: 'MarkdownV2',
|
|
248
|
+
});
|
|
249
|
+
} catch (err) {
|
|
250
|
+
VERBOSE && console.log(`[VERBOSE] /merge: Error sending final message: ${err.message}`);
|
|
251
|
+
}
|
|
252
|
+
activeMergeOperations.delete(repoKey);
|
|
253
|
+
},
|
|
254
|
+
onError: async error => {
|
|
255
|
+
VERBOSE && console.error(`[VERBOSE] /merge error for ${repoKey}:`, error);
|
|
256
|
+
try {
|
|
257
|
+
const userMessage = formatUserError(error, VERBOSE);
|
|
258
|
+
const finalReport = processor.formatFinalMessage();
|
|
259
|
+
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, `❌ *Merge queue failed*\n\n${escapeMarkdownV2(userMessage)}\n\n${finalReport}`, {
|
|
260
|
+
parse_mode: 'MarkdownV2',
|
|
261
|
+
});
|
|
262
|
+
} catch (err) {
|
|
263
|
+
VERBOSE && console.log(`[VERBOSE] /merge: Error sending error message: ${err.message}`);
|
|
264
|
+
}
|
|
265
|
+
activeMergeOperations.delete(repoKey);
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Initialize the processor
|
|
270
|
+
const initResult = await processor.initialize();
|
|
271
|
+
|
|
272
|
+
if (!initResult.success) {
|
|
273
|
+
const userMessage = formatUserError(new Error(initResult.error), VERBOSE);
|
|
274
|
+
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, `Failed to initialize merge queue: ${escapeMarkdownV2(userMessage)}`, {
|
|
275
|
+
parse_mode: 'MarkdownV2',
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (initResult.message) {
|
|
281
|
+
// No PRs to merge
|
|
282
|
+
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, `*Merge Queue \\- ${escapeMarkdownV2(owner)}/${escapeMarkdownV2(repo)}*${labelMsg}\n\n${escapeMarkdownV2(initResult.message)}\n\nTo use the merge queue:\n1\\. Add the \`ready\` label to PRs you want to merge\n2\\. Run \`/merge ${escapeMarkdownV2(repoUrl)}\` again`, { parse_mode: 'MarkdownV2' });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Update message with PR list and cancel button, start processing
|
|
287
|
+
const truncatedMsg = initResult.truncated ? `\n\n_Note: Only processing first ${MERGE_QUEUE_CONFIG.MAX_PRS_PER_SESSION} PRs_` : '';
|
|
288
|
+
|
|
289
|
+
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, `*Merge Queue \\- ${escapeMarkdownV2(owner)}/${escapeMarkdownV2(repo)}*${labelMsg}\n\nFound ${initResult.count} PRs with 'ready' label\\.${escapeMarkdownV2(truncatedMsg)}\n\nStarting merge process\\.\\.\\.`, {
|
|
290
|
+
parse_mode: 'MarkdownV2',
|
|
291
|
+
reply_markup: {
|
|
292
|
+
inline_keyboard: [[{ text: '🛑 Cancel', callback_data: `merge_cancel_${repoKey}` }]],
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Store processor for this repository
|
|
297
|
+
activeMergeOperations.set(repoKey, {
|
|
298
|
+
processor,
|
|
299
|
+
chatId,
|
|
300
|
+
messageId: statusMessage.message_id,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Run the merge queue (this runs asynchronously)
|
|
304
|
+
processor.run().catch(error => {
|
|
305
|
+
VERBOSE && console.error(`[VERBOSE] /merge: Unhandled error in run(): ${error.message}`);
|
|
306
|
+
activeMergeOperations.delete(repoKey);
|
|
307
|
+
});
|
|
308
|
+
} catch (error) {
|
|
309
|
+
VERBOSE && console.error('[VERBOSE] /merge error:', error);
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const userMessage = formatUserError(error, VERBOSE);
|
|
313
|
+
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, `Error processing merge queue: ${escapeMarkdownV2(userMessage)}\n\nPlease check the repository URL and try again\\.`, { parse_mode: 'MarkdownV2' });
|
|
314
|
+
} catch (editError) {
|
|
315
|
+
VERBOSE && console.error('[VERBOSE] /merge: Failed to edit error message:', editError);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Handle cancel button callback
|
|
321
|
+
bot.action(/^merge_cancel_(.+)$/, async ctx => {
|
|
322
|
+
const repoKey = ctx.match[1];
|
|
323
|
+
VERBOSE && console.log(`[VERBOSE] /merge cancel callback received for ${repoKey}`);
|
|
324
|
+
|
|
325
|
+
const operation = activeMergeOperations.get(repoKey);
|
|
326
|
+
if (!operation || operation.processor.status !== MergeStatus.RUNNING) {
|
|
327
|
+
await ctx.answerCbQuery('No active merge operation found.');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Cancel the operation
|
|
332
|
+
operation.processor.cancel();
|
|
333
|
+
await ctx.answerCbQuery('Merge operation cancellation requested. The current PR will finish processing.');
|
|
334
|
+
|
|
335
|
+
VERBOSE && console.log(`[VERBOSE] /merge: Cancelled operation for ${repoKey}`);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get active merge operation for a repository
|
|
341
|
+
* @param {string} owner - Repository owner
|
|
342
|
+
* @param {string} repo - Repository name
|
|
343
|
+
* @returns {Object|null} Operation object or null
|
|
344
|
+
*/
|
|
345
|
+
export function getActiveMergeOperation(owner, repo) {
|
|
346
|
+
const repoKey = getRepoKey(owner, repo);
|
|
347
|
+
return activeMergeOperations.get(repoKey) || null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Clear all active merge operations (useful for testing)
|
|
352
|
+
*/
|
|
353
|
+
export function clearAllMergeOperations() {
|
|
354
|
+
for (const [, operation] of activeMergeOperations) {
|
|
355
|
+
if (operation.processor.status === MergeStatus.RUNNING) {
|
|
356
|
+
operation.processor.cancel();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
activeMergeOperations.clear();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export default {
|
|
363
|
+
registerMergeCommand,
|
|
364
|
+
getActiveMergeOperation,
|
|
365
|
+
clearAllMergeOperations,
|
|
366
|
+
};
|