@link-assistant/hive-mind 2.1.4 → 2.1.5
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/README.hi.md +20 -0
- package/README.md +19 -0
- package/README.ru.md +20 -0
- package/README.zh.md +19 -0
- package/package.json +1 -1
- package/src/github-merge-targets.lib.mjs +205 -0
- package/src/telegram-merge-command.lib.mjs +93 -15
- package/src/telegram-merge-queue.lib.mjs +108 -24
- package/src/telegram-merge-wait.lib.mjs +75 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.1.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 26b07f2: Extend Telegram `/merge` to accept repository, issue, and pull request targets, including reply-based GitHub link extraction, and wait for unfinished PRs to become mergeable before merging.
|
|
8
|
+
|
|
3
9
|
## 2.1.4
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.hi.md
CHANGED
|
@@ -511,6 +511,26 @@ Examples:
|
|
|
511
511
|
/hive https://github.com/microsoft --all-issues --concurrency 3
|
|
512
512
|
```
|
|
513
513
|
|
|
514
|
+
#### `/merge` - तैयार Pull Requests merge करें
|
|
515
|
+
|
|
516
|
+
```
|
|
517
|
+
/merge <repository-url|issue-url|pull-request-url> [--auto-resolve]
|
|
518
|
+
|
|
519
|
+
Examples:
|
|
520
|
+
/merge https://github.com/owner/repo
|
|
521
|
+
/merge https://github.com/owner/repo/issues/123
|
|
522
|
+
/merge https://github.com/owner/repo/pull/456
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Repository targets `ready` label वाले PRs को क्रम से process करते हैं। Issue और
|
|
526
|
+
pull request targets केवल linked या selected PR process करते हैं। आप किसी ऐसे
|
|
527
|
+
message पर `/merge` से reply भी कर सकते हैं जिसमें एक GitHub repository, issue,
|
|
528
|
+
या pull request link हो, जैसे कोई पुराना `/codex ...issues/123` command.
|
|
529
|
+
|
|
530
|
+
अगर target PR अभी finished नहीं है, तो `/merge` merge करने से पहले उसके
|
|
531
|
+
mergeable होने तक wait करता है। Merge-conflict skips अभी भी `--auto-resolve` के
|
|
532
|
+
साथ काम करते हैं।
|
|
533
|
+
|
|
514
534
|
#### `/limits` - उपयोग सीमाएँ दिखाएँ
|
|
515
535
|
|
|
516
536
|
```
|
package/README.md
CHANGED
|
@@ -532,6 +532,25 @@ Examples:
|
|
|
532
532
|
/hive https://github.com/microsoft --all-issues --concurrency 3
|
|
533
533
|
```
|
|
534
534
|
|
|
535
|
+
#### `/merge` - Merge Ready Pull Requests
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
/merge <repository-url|issue-url|pull-request-url> [--auto-resolve]
|
|
539
|
+
|
|
540
|
+
Examples:
|
|
541
|
+
/merge https://github.com/owner/repo
|
|
542
|
+
/merge https://github.com/owner/repo/issues/123
|
|
543
|
+
/merge https://github.com/owner/repo/pull/456
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
Repository targets process PRs with the `ready` label sequentially. Issue and
|
|
547
|
+
pull request targets process only the linked or selected PR. You can also reply
|
|
548
|
+
with `/merge` to a message containing one GitHub repository, issue, or pull
|
|
549
|
+
request link, such as a previous `/codex ...issues/123` command.
|
|
550
|
+
|
|
551
|
+
If a target PR is not finished yet, `/merge` waits for it to become mergeable
|
|
552
|
+
before merging. Merge-conflict skips still work with `--auto-resolve`.
|
|
553
|
+
|
|
535
554
|
#### `/limits` - Show Usage Limits
|
|
536
555
|
|
|
537
556
|
```
|
package/README.ru.md
CHANGED
|
@@ -512,6 +512,26 @@ Examples:
|
|
|
512
512
|
/hive https://github.com/microsoft --all-issues --concurrency 3
|
|
513
513
|
```
|
|
514
514
|
|
|
515
|
+
#### `/merge` — Слияние готовых Pull Request
|
|
516
|
+
|
|
517
|
+
```
|
|
518
|
+
/merge <repository-url|issue-url|pull-request-url> [--auto-resolve]
|
|
519
|
+
|
|
520
|
+
Examples:
|
|
521
|
+
/merge https://github.com/owner/repo
|
|
522
|
+
/merge https://github.com/owner/repo/issues/123
|
|
523
|
+
/merge https://github.com/owner/repo/pull/456
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Цель-репозиторий обрабатывает PR с меткой `ready` последовательно. Цели issue и
|
|
527
|
+
pull request обрабатывают только связанный или выбранный PR. Также можно ответить
|
|
528
|
+
командой `/merge` на сообщение с одной ссылкой на GitHub repository, issue или
|
|
529
|
+
pull request, например на предыдущую команду `/codex ...issues/123`.
|
|
530
|
+
|
|
531
|
+
Если целевой PR ещё не завершён, `/merge` ждёт, пока он станет mergeable, и затем
|
|
532
|
+
выполняет слияние. Пропуск конфликтов слияния по-прежнему работает с
|
|
533
|
+
`--auto-resolve`.
|
|
534
|
+
|
|
515
535
|
#### `/limits` — Показать лимиты использования
|
|
516
536
|
|
|
517
537
|
```
|
package/README.zh.md
CHANGED
|
@@ -508,6 +508,25 @@ Examples:
|
|
|
508
508
|
/hive https://github.com/microsoft --all-issues --concurrency 3
|
|
509
509
|
```
|
|
510
510
|
|
|
511
|
+
#### `/merge` - 合并就绪的 Pull Request
|
|
512
|
+
|
|
513
|
+
```
|
|
514
|
+
/merge <repository-url|issue-url|pull-request-url> [--auto-resolve]
|
|
515
|
+
|
|
516
|
+
Examples:
|
|
517
|
+
/merge https://github.com/owner/repo
|
|
518
|
+
/merge https://github.com/owner/repo/issues/123
|
|
519
|
+
/merge https://github.com/owner/repo/pull/456
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
仓库目标会按顺序处理带有 `ready` 标签的 PR。Issue 和 Pull Request
|
|
523
|
+
目标只处理关联或指定的 PR。你也可以回复一条包含单个 GitHub
|
|
524
|
+
仓库、Issue 或 Pull Request 链接的消息并发送 `/merge`,例如之前的
|
|
525
|
+
`/codex ...issues/123` 命令。
|
|
526
|
+
|
|
527
|
+
如果目标 PR 尚未完成,`/merge` 会等待它变为可合并后再合并。合并冲突跳过仍可配合
|
|
528
|
+
`--auto-resolve` 使用。
|
|
529
|
+
|
|
511
530
|
#### `/limits` - 显示用量限制
|
|
512
531
|
|
|
513
532
|
```
|
package/package.json
CHANGED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GitHub merge target helpers for `/merge`.
|
|
4
|
+
*
|
|
5
|
+
* Repository-wide merge queues still use github-merge.lib.mjs. This module
|
|
6
|
+
* handles the narrower targets introduced in issue #2013: a single PR URL, an
|
|
7
|
+
* issue URL with linked PRs, or a GitHub URL found in a replied message.
|
|
8
|
+
*
|
|
9
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2013
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { promisify } from 'node:util';
|
|
13
|
+
import { exec as execCallback } from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
import { githubLimits } from './config.lib.mjs';
|
|
16
|
+
import { parseGitHubUrl } from './github.lib.mjs';
|
|
17
|
+
import { ghWithRateLimitRetry } from './github-rate-limit.lib.mjs';
|
|
18
|
+
import { getLinkedPRsFromTimeline } from './github-merge-ready-sync.lib.mjs';
|
|
19
|
+
|
|
20
|
+
const execRaw = promisify(execCallback);
|
|
21
|
+
const exec = (cmd, opts = {}) =>
|
|
22
|
+
ghWithRateLimitRetry(() => execRaw(cmd, { maxBuffer: githubLimits.bufferMaxSize, ...opts }), {
|
|
23
|
+
label: `gh exec (${cmd.split(/\s+/).slice(0, 3).join(' ')})`,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const SUPPORTED_REPOSITORY_TYPES = new Set(['repo', 'issues_list', 'pulls_list']);
|
|
27
|
+
const SUPPORTED_TARGET_TYPES = new Set([...SUPPORTED_REPOSITORY_TYPES, 'issue', 'pull']);
|
|
28
|
+
|
|
29
|
+
function buildIssueUrl(owner, repo, issueNumber) {
|
|
30
|
+
return `https://github.com/${owner}/${repo}/issues/${issueNumber}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildPullUrl(owner, repo, prNumber) {
|
|
34
|
+
return `https://github.com/${owner}/${repo}/pull/${prNumber}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse a `/merge` target URL.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} url
|
|
41
|
+
* @returns {{valid: boolean, mode?: 'repository'|'issue'|'pull', owner: string|null, repo: string|null, issueNumber?: number, prNumber?: number, url?: string, error: string|null}}
|
|
42
|
+
*/
|
|
43
|
+
export function parseMergeTargetUrl(url) {
|
|
44
|
+
const parsed = parseGitHubUrl(url);
|
|
45
|
+
|
|
46
|
+
if (!parsed.valid) {
|
|
47
|
+
return { valid: false, owner: null, repo: null, error: parsed.error };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (SUPPORTED_REPOSITORY_TYPES.has(parsed.type)) {
|
|
51
|
+
return {
|
|
52
|
+
valid: true,
|
|
53
|
+
mode: 'repository',
|
|
54
|
+
owner: parsed.owner,
|
|
55
|
+
repo: parsed.repo,
|
|
56
|
+
url: `https://github.com/${parsed.owner}/${parsed.repo}`,
|
|
57
|
+
error: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (parsed.type === 'issue') {
|
|
62
|
+
return {
|
|
63
|
+
valid: true,
|
|
64
|
+
mode: 'issue',
|
|
65
|
+
owner: parsed.owner,
|
|
66
|
+
repo: parsed.repo,
|
|
67
|
+
issueNumber: parsed.number,
|
|
68
|
+
url: buildIssueUrl(parsed.owner, parsed.repo, parsed.number),
|
|
69
|
+
error: null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (parsed.type === 'pull') {
|
|
74
|
+
return {
|
|
75
|
+
valid: true,
|
|
76
|
+
mode: 'pull',
|
|
77
|
+
owner: parsed.owner,
|
|
78
|
+
repo: parsed.repo,
|
|
79
|
+
prNumber: parsed.number,
|
|
80
|
+
url: buildPullUrl(parsed.owner, parsed.repo, parsed.number),
|
|
81
|
+
error: null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (parsed.type === 'user' || parsed.type === 'organization') {
|
|
86
|
+
return {
|
|
87
|
+
valid: false,
|
|
88
|
+
owner: parsed.owner,
|
|
89
|
+
repo: null,
|
|
90
|
+
error: 'URL points to a user/organization. Please provide a repository, issue, or pull request URL.',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
valid: false,
|
|
96
|
+
owner: parsed.owner || null,
|
|
97
|
+
repo: parsed.repo || null,
|
|
98
|
+
error: `URL type '${parsed.type}' is not supported for /merge. Please provide a repository, issue, or pull request URL.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function trimCandidateUrl(candidate) {
|
|
103
|
+
return String(candidate || '')
|
|
104
|
+
.trim()
|
|
105
|
+
.replace(/^[<([{'"]+/, '')
|
|
106
|
+
.replace(/[>)\]}'".,;:!?]+$/, '');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Extract a single `/merge` target URL from arbitrary text, such as a replied
|
|
111
|
+
* `/codex https://github.com/owner/repo/issues/123 --think max` message.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} text
|
|
114
|
+
* @returns {{valid: boolean, url: string|null, target: Object|null, error: string|null}}
|
|
115
|
+
*/
|
|
116
|
+
export function extractMergeTargetUrlFromText(text) {
|
|
117
|
+
if (!text || typeof text !== 'string') {
|
|
118
|
+
return { valid: false, url: null, target: null, error: 'No text to inspect for GitHub links' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const pattern = /(?:https?:\/\/)?github\.com\/[^\s<>)\]]+|[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/(?:issues|pull)\/\d+/g;
|
|
122
|
+
const matches = text.match(pattern) || [];
|
|
123
|
+
const targetsByUrl = new Map();
|
|
124
|
+
|
|
125
|
+
for (const match of matches) {
|
|
126
|
+
const candidate = trimCandidateUrl(match);
|
|
127
|
+
const target = parseMergeTargetUrl(candidate);
|
|
128
|
+
if (target.valid && SUPPORTED_TARGET_TYPES.has(target.mode === 'repository' ? 'repo' : target.mode)) {
|
|
129
|
+
targetsByUrl.set(target.url, target);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const targets = [...targetsByUrl.values()];
|
|
134
|
+
if (targets.length === 0) {
|
|
135
|
+
return { valid: false, url: null, target: null, error: 'No GitHub repository, issue, or pull request link found' };
|
|
136
|
+
}
|
|
137
|
+
if (targets.length > 1) {
|
|
138
|
+
return { valid: false, url: null, target: null, error: `Expected one GitHub merge target, found ${targets.length}` };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const [target] = targets;
|
|
142
|
+
return { valid: true, url: target.url, target, error: null };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Fetch a PR in the shape expected by MergeQueueItem.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} owner
|
|
149
|
+
* @param {string} repo
|
|
150
|
+
* @param {number} prNumber
|
|
151
|
+
* @param {Object|null} issue
|
|
152
|
+
* @param {boolean} verbose
|
|
153
|
+
* @returns {Promise<{pr: Object, issue: Object|null, sortDate: Date}>}
|
|
154
|
+
*/
|
|
155
|
+
export async function fetchMergeTargetPullRequest(owner, repo, prNumber, issue = null, verbose = false) {
|
|
156
|
+
const { stdout } = await exec(`gh pr view ${prNumber} --repo ${owner}/${repo} --json number,title,url,createdAt,headRefName,author,mergeable,mergeStateStatus,isDraft,state`);
|
|
157
|
+
const pr = JSON.parse(stdout.trim());
|
|
158
|
+
if (verbose) {
|
|
159
|
+
console.log(`[VERBOSE] /merge: Loaded target PR #${pr.number} from ${owner}/${repo}`);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
pr,
|
|
163
|
+
issue,
|
|
164
|
+
sortDate: new Date(pr.createdAt),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Resolve a non-repository merge target into queue item data.
|
|
170
|
+
*
|
|
171
|
+
* @param {{mode: string, owner: string, repo: string, issueNumber?: number, prNumber?: number, url?: string}} target
|
|
172
|
+
* @param {boolean} verbose
|
|
173
|
+
* @returns {Promise<Array<{pr: Object, issue: Object|null, sortDate: Date}>|null>}
|
|
174
|
+
*/
|
|
175
|
+
export async function resolveMergeTargetItems(target, verbose = false) {
|
|
176
|
+
if (!target || target.mode === 'repository') {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (target.mode === 'pull') {
|
|
181
|
+
return [await fetchMergeTargetPullRequest(target.owner, target.repo, target.prNumber, null, verbose)];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (target.mode === 'issue') {
|
|
185
|
+
const issue = {
|
|
186
|
+
number: target.issueNumber,
|
|
187
|
+
url: target.url || buildIssueUrl(target.owner, target.repo, target.issueNumber),
|
|
188
|
+
};
|
|
189
|
+
const linkedPRs = await getLinkedPRsFromTimeline(target.owner, target.repo, target.issueNumber, verbose);
|
|
190
|
+
const items = [];
|
|
191
|
+
for (const linkedPR of linkedPRs) {
|
|
192
|
+
items.push(await fetchMergeTargetPullRequest(target.owner, target.repo, linkedPR.number, issue, verbose));
|
|
193
|
+
}
|
|
194
|
+
return items.sort((a, b) => a.sortDate - b.sortDate);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
throw new Error(`Unsupported merge target mode: ${target.mode}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export default {
|
|
201
|
+
parseMergeTargetUrl,
|
|
202
|
+
extractMergeTargetUrlFromText,
|
|
203
|
+
fetchMergeTargetPullRequest,
|
|
204
|
+
resolveMergeTargetItems,
|
|
205
|
+
};
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* @see https://github.com/link-assistant/hive-mind/issues/1143
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { checkLabelPermissions, ensureReadyLabel } from './github-merge.lib.mjs';
|
|
22
|
+
import { extractMergeTargetUrlFromText, parseMergeTargetUrl } from './github-merge-targets.lib.mjs';
|
|
22
23
|
import { createMergeQueueProcessor, MergeStatus, MERGE_QUEUE_CONFIG } from './telegram-merge-queue.lib.mjs';
|
|
23
24
|
import { executeStartScreen } from './telegram-command-execution.lib.mjs';
|
|
24
25
|
|
|
@@ -129,6 +130,81 @@ export function parseMergeArgs(args) {
|
|
|
129
130
|
return { positionals, flags };
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
/**
|
|
134
|
+
* A real user reply can carry the issue/PR URL for `/merge`; forwarded
|
|
135
|
+
* commands still must not be replayed by the bot.
|
|
136
|
+
*
|
|
137
|
+
* @param {Object} ctx - Telegraf context
|
|
138
|
+
* @param {Object} filters - Message filter callbacks
|
|
139
|
+
* @returns {boolean}
|
|
140
|
+
*/
|
|
141
|
+
export function shouldIgnoreMergeCommand(ctx, filters = {}) {
|
|
142
|
+
if (filters.isOldMessage && filters.isOldMessage(ctx)) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
if (typeof filters.isForwarded === 'function') {
|
|
146
|
+
return filters.isForwarded(ctx);
|
|
147
|
+
}
|
|
148
|
+
if (typeof filters.isForwardedOrReply === 'function') {
|
|
149
|
+
return filters.isForwardedOrReply(ctx);
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getMergeReplyText(message) {
|
|
155
|
+
const reply = message?.reply_to_message;
|
|
156
|
+
if (!reply || reply.forum_topic_created) {
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
return reply.text || reply.caption || '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getMergeUsageMessage() {
|
|
163
|
+
return "Missing merge target\\.\n\nUsage: `/merge <repository-url|issue-url|pull-request-url> [--auto-resolve]`\n\nYou can also reply with `/merge` to a message containing one GitHub repository, issue, or pull request link\\.\n\nExamples:\n`/merge https://github.com/owner/repo`\n`/merge https://github.com/owner/repo/issues/123`\n`/merge https://github.com/owner/repo/pull/456`\n\nRepository targets merge all PRs with the 'ready' label\\. Issue and pull request targets wait until the target PR is mergeable, then merge it\\.\n\nWith `--auto-resolve` the bot also dispatches `/solve <pr> --auto-merge` for every PR that was skipped because of merge conflicts\\.";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getTargetFoundText(target, count) {
|
|
167
|
+
if (target.mode === 'repository') {
|
|
168
|
+
return `Found ${count} PRs with 'ready' label\\.`;
|
|
169
|
+
}
|
|
170
|
+
const plural = count === 1 ? '' : 's';
|
|
171
|
+
return `Found ${count} target PR${plural} to merge\\.`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolve the merge target from explicit args, or from a replied message when
|
|
176
|
+
* `/merge` is sent without a URL.
|
|
177
|
+
*
|
|
178
|
+
* @param {string[]} positionals
|
|
179
|
+
* @param {Object} message
|
|
180
|
+
* @returns {{target: Object|null, targetUrl: string|null, fromReply: boolean, error: string|null}}
|
|
181
|
+
*/
|
|
182
|
+
export function resolveMergeCommandTarget(positionals, message) {
|
|
183
|
+
if (positionals.length > 0) {
|
|
184
|
+
const targetUrl = positionals[0];
|
|
185
|
+
const target = parseMergeTargetUrl(targetUrl);
|
|
186
|
+
return {
|
|
187
|
+
target,
|
|
188
|
+
targetUrl,
|
|
189
|
+
fromReply: false,
|
|
190
|
+
error: target.valid ? null : target.error,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const replyText = getMergeReplyText(message);
|
|
195
|
+
if (!replyText) {
|
|
196
|
+
return { target: null, targetUrl: null, fromReply: false, error: null };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const extracted = extractMergeTargetUrlFromText(replyText);
|
|
200
|
+
return {
|
|
201
|
+
target: extracted.target,
|
|
202
|
+
targetUrl: extracted.url,
|
|
203
|
+
fromReply: true,
|
|
204
|
+
error: extracted.valid ? null : extracted.error,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
132
208
|
/**
|
|
133
209
|
* Issue #1805: Spawner used by the merge queue's auto-resolve pass. For each
|
|
134
210
|
* skipped PR we dispatch a `solve <pr-url> --auto-merge` session through
|
|
@@ -209,7 +285,7 @@ function formatUserError(error, verbose) {
|
|
|
209
285
|
* @param {Function} [options.getStoppedChatRejectMessage] - Function to get stopped chat rejection message
|
|
210
286
|
*/
|
|
211
287
|
export function registerMergeCommand(bot, options) {
|
|
212
|
-
const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, addBreadcrumb, isChatStopped, getStoppedChatRejectMessage } = options;
|
|
288
|
+
const { VERBOSE = false, isOldMessage, isForwarded, isForwardedOrReply, isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, addBreadcrumb, isChatStopped, getStoppedChatRejectMessage } = options;
|
|
213
289
|
|
|
214
290
|
bot.command(/^merge$/i, async ctx => {
|
|
215
291
|
VERBOSE && console.log('[VERBOSE] /merge command received');
|
|
@@ -222,7 +298,7 @@ export function registerMergeCommand(bot, options) {
|
|
|
222
298
|
});
|
|
223
299
|
|
|
224
300
|
// Standard checks
|
|
225
|
-
if (
|
|
301
|
+
if (shouldIgnoreMergeCommand(ctx, { isOldMessage, isForwarded, isForwardedOrReply })) return;
|
|
226
302
|
|
|
227
303
|
if (!isGroupChat(ctx)) {
|
|
228
304
|
return await ctx.reply('The /merge command only works in group chats. Please add this bot to a group and make it an admin.', {
|
|
@@ -252,24 +328,25 @@ export function registerMergeCommand(bot, options) {
|
|
|
252
328
|
const { positionals, flags } = parseMergeArgs(args);
|
|
253
329
|
const autoResolve = flags['auto-resolve'] === true;
|
|
254
330
|
|
|
255
|
-
|
|
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 });
|
|
257
|
-
}
|
|
331
|
+
const targetResult = resolveMergeCommandTarget(positionals, ctx.message);
|
|
258
332
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
333
|
+
if (!targetResult.target && !targetResult.error) {
|
|
334
|
+
return await ctx.reply(getMergeUsageMessage(), { parse_mode: 'MarkdownV2', reply_to_message_id: ctx.message.message_id });
|
|
335
|
+
}
|
|
262
336
|
|
|
263
|
-
if (!
|
|
264
|
-
|
|
337
|
+
if (!targetResult.target || !targetResult.target.valid) {
|
|
338
|
+
const invalidPrefix = targetResult.fromReply ? 'Invalid merge target in replied message' : 'Invalid merge target';
|
|
339
|
+
return await ctx.reply(`${invalidPrefix}: ${escapeMarkdownV2(targetResult.error || targetResult.target?.error || 'Unknown target error')}\n\nPlease provide a valid GitHub repository, issue, or pull request URL\\.`, {
|
|
265
340
|
parse_mode: 'MarkdownV2',
|
|
266
341
|
reply_to_message_id: ctx.message.message_id,
|
|
267
342
|
});
|
|
268
343
|
}
|
|
269
344
|
|
|
270
|
-
const
|
|
345
|
+
const target = targetResult.target;
|
|
346
|
+
const targetUrl = targetResult.targetUrl || target.url;
|
|
347
|
+
const { owner, repo } = target;
|
|
271
348
|
const repoKey = getRepoKey(owner, repo);
|
|
272
|
-
VERBOSE && console.log(`[VERBOSE] /merge: Processing
|
|
349
|
+
VERBOSE && console.log(`[VERBOSE] /merge: Processing ${target.mode} target ${targetUrl} in ${owner}/${repo}`);
|
|
273
350
|
|
|
274
351
|
// Check if a merge operation is already running for this repository (per-repository concurrency)
|
|
275
352
|
if (activeMergeOperations.has(repoKey)) {
|
|
@@ -310,6 +387,7 @@ export function registerMergeCommand(bot, options) {
|
|
|
310
387
|
// Create the merge queue processor
|
|
311
388
|
const processor = await createMergeQueueProcessor(owner, repo, {
|
|
312
389
|
verbose: VERBOSE,
|
|
390
|
+
target,
|
|
313
391
|
// Issue #1805: forward the --auto-resolve flag and inject the spawner.
|
|
314
392
|
// The processor only sees the callback, so unit tests can stub it
|
|
315
393
|
// without spawning real screen sessions.
|
|
@@ -384,14 +462,14 @@ export function registerMergeCommand(bot, options) {
|
|
|
384
462
|
|
|
385
463
|
if (initResult.message) {
|
|
386
464
|
// No PRs to merge
|
|
387
|
-
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
|
|
465
|
+
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 repository PRs, or ensure the issue has a linked open PR\n2\\. Run \`/merge ${escapeMarkdownV2(targetUrl)}\` again`, { parse_mode: 'MarkdownV2' });
|
|
388
466
|
return;
|
|
389
467
|
}
|
|
390
468
|
|
|
391
469
|
// Update message with PR list and cancel button, start processing
|
|
392
470
|
const truncatedMsg = initResult.truncated ? `\n\n_Note: Only processing first ${MERGE_QUEUE_CONFIG.MAX_PRS_PER_SESSION} PRs_` : '';
|
|
393
471
|
|
|
394
|
-
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, `*Merge Queue \\- ${escapeMarkdownV2(owner)}/${escapeMarkdownV2(repo)}*${labelMsg}\n\
|
|
472
|
+
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, `*Merge Queue \\- ${escapeMarkdownV2(owner)}/${escapeMarkdownV2(repo)}*${labelMsg}\n\n${getTargetFoundText(target, initResult.count)}${escapeMarkdownV2(truncatedMsg)}\n\nStarting merge process\\.\\.\\.`, {
|
|
395
473
|
parse_mode: 'MarkdownV2',
|
|
396
474
|
reply_markup: {
|
|
397
475
|
inline_keyboard: [[{ text: '🛑 Cancel', callback_data: `merge_cancel_${repoKey}` }]],
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { getAllReadyPRs, checkPRCIStatus, checkPRMergeable, mergePullRequest, waitForCI, ensureReadyLabel, waitForBranchCI, getDefaultBranch, waitForCommitCI, checkBranchCIHealth, getMergeCommitSha, getPRStatus, syncReadyTags, closeLinkedIssueIfNotAutoClosed } from './github-merge.lib.mjs';
|
|
20
|
+
import { resolveMergeTargetItems } from './github-merge-targets.lib.mjs';
|
|
21
|
+
import { waitForPRReady as waitForPRReadyHelper } from './telegram-merge-wait.lib.mjs';
|
|
20
22
|
import { mergeQueue as mergeQueueConfig } from './config.lib.mjs';
|
|
21
23
|
import { getProgressBar } from './limits.lib.mjs';
|
|
22
24
|
|
|
@@ -38,6 +40,7 @@ export const MergeStatus = {
|
|
|
38
40
|
export const MergeItemStatus = {
|
|
39
41
|
PENDING: 'pending',
|
|
40
42
|
CHECKING_CI: 'checking_ci',
|
|
43
|
+
WAITING_READY: 'waiting_ready',
|
|
41
44
|
WAITING_CI: 'waiting_ci',
|
|
42
45
|
READY_TO_MERGE: 'ready_to_merge',
|
|
43
46
|
MERGING: 'merging',
|
|
@@ -144,6 +147,8 @@ class MergeQueueItem {
|
|
|
144
147
|
return '⏳';
|
|
145
148
|
case MergeItemStatus.CHECKING_CI:
|
|
146
149
|
return '🔍';
|
|
150
|
+
case MergeItemStatus.WAITING_READY:
|
|
151
|
+
return '⏱️';
|
|
147
152
|
case MergeItemStatus.WAITING_CI:
|
|
148
153
|
return '⏱️';
|
|
149
154
|
case MergeItemStatus.READY_TO_MERGE:
|
|
@@ -185,6 +190,8 @@ export class MergeQueueProcessor {
|
|
|
185
190
|
// tests can run without touching the screen runtime.
|
|
186
191
|
this.autoResolve = options.autoResolve === true;
|
|
187
192
|
this.spawnSolveSession = typeof options.spawnSolveSession === 'function' ? options.spawnSolveSession : null;
|
|
193
|
+
this.target = options.target || { mode: 'repository', owner: this.owner, repo: this.repo, url: `https://github.com/${this.owner}/${this.repo}` };
|
|
194
|
+
this.waitForUnfinished = options.waitForUnfinished !== false;
|
|
188
195
|
|
|
189
196
|
// State
|
|
190
197
|
this.items = [];
|
|
@@ -208,6 +215,17 @@ export class MergeQueueProcessor {
|
|
|
208
215
|
this.getPRStatus = typeof options.getPRStatus === 'function' ? options.getPRStatus : getPRStatus;
|
|
209
216
|
// Same idea for `getMergeCommitSha` so tests don't need to stub gh.
|
|
210
217
|
this.getMergeCommitSha = typeof options.getMergeCommitSha === 'function' ? options.getMergeCommitSha : getMergeCommitSha;
|
|
218
|
+
this.checkPRMergeable = typeof options.checkPRMergeable === 'function' ? options.checkPRMergeable : checkPRMergeable;
|
|
219
|
+
this.checkPRCIStatus = typeof options.checkPRCIStatus === 'function' ? options.checkPRCIStatus : checkPRCIStatus;
|
|
220
|
+
this.waitForCI = typeof options.waitForCI === 'function' ? options.waitForCI : waitForCI;
|
|
221
|
+
this.mergePullRequest = typeof options.mergePullRequest === 'function' ? options.mergePullRequest : mergePullRequest;
|
|
222
|
+
this.closeLinkedIssueIfNotAutoClosed = typeof options.closeLinkedIssueIfNotAutoClosed === 'function' ? options.closeLinkedIssueIfNotAutoClosed : closeLinkedIssueIfNotAutoClosed;
|
|
223
|
+
this.resolveMergeTargetItems = typeof options.resolveMergeTargetItems === 'function' ? options.resolveMergeTargetItems : resolveMergeTargetItems;
|
|
224
|
+
this.ensureReadyLabel = typeof options.ensureReadyLabel === 'function' ? options.ensureReadyLabel : ensureReadyLabel;
|
|
225
|
+
this.syncReadyTags = typeof options.syncReadyTags === 'function' ? options.syncReadyTags : syncReadyTags;
|
|
226
|
+
this.getAllReadyPRs = typeof options.getAllReadyPRs === 'function' ? options.getAllReadyPRs : getAllReadyPRs;
|
|
227
|
+
this.targetItemsTimeoutMs = options.targetItemsTimeoutMs ?? MERGE_QUEUE_CONFIG.CI_TIMEOUT_MS;
|
|
228
|
+
this.targetItemsPollIntervalMs = options.targetItemsPollIntervalMs ?? MERGE_QUEUE_CONFIG.CI_POLL_INTERVAL_MS;
|
|
211
229
|
|
|
212
230
|
// Statistics
|
|
213
231
|
this.stats = {
|
|
@@ -241,7 +259,7 @@ export class MergeQueueProcessor {
|
|
|
241
259
|
this.log(`Initializing merge queue for ${this.owner}/${this.repo}`);
|
|
242
260
|
|
|
243
261
|
// Ensure ready label exists
|
|
244
|
-
const labelResult = await ensureReadyLabel(this.owner, this.repo, this.verbose);
|
|
262
|
+
const labelResult = await this.ensureReadyLabel(this.owner, this.repo, this.verbose);
|
|
245
263
|
if (!labelResult.success) {
|
|
246
264
|
return { success: false, error: labelResult.error };
|
|
247
265
|
}
|
|
@@ -249,21 +267,27 @@ export class MergeQueueProcessor {
|
|
|
249
267
|
this.log("Created 'ready' label in repository");
|
|
250
268
|
}
|
|
251
269
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
let readyPRs;
|
|
271
|
+
if (this.target?.mode && this.target.mode !== 'repository') {
|
|
272
|
+
readyPRs = await this.resolveMergeTargetItemsWithWait();
|
|
273
|
+
} else {
|
|
274
|
+
// Issue #1367: Sync 'ready' tags between linked PRs and issues before collecting the queue
|
|
275
|
+
// This ensures the final list reflects all ready work regardless of where the tag was applied
|
|
276
|
+
const syncResult = await this.syncReadyTags(this.owner, this.repo, this.verbose);
|
|
277
|
+
if (syncResult.synced > 0) {
|
|
278
|
+
this.log(`Synced 'ready' tag: ${syncResult.synced} item(s) updated`);
|
|
279
|
+
}
|
|
280
|
+
if (syncResult.errors > 0) {
|
|
281
|
+
this.log(`Tag sync had ${syncResult.errors} error(s) (non-fatal, proceeding)`);
|
|
282
|
+
}
|
|
261
283
|
|
|
262
|
-
|
|
263
|
-
|
|
284
|
+
// Fetch all ready PRs
|
|
285
|
+
readyPRs = await this.getAllReadyPRs(this.owner, this.repo, this.verbose);
|
|
286
|
+
}
|
|
264
287
|
|
|
265
288
|
if (readyPRs.length === 0) {
|
|
266
|
-
|
|
289
|
+
const message = this.target?.mode === 'issue' ? `No open PRs linked to issue #${this.target.issueNumber} found` : this.target?.mode === 'pull' ? `Pull request #${this.target.prNumber} was not found` : "No PRs with 'ready' label found";
|
|
290
|
+
return { success: true, error: null, message };
|
|
267
291
|
}
|
|
268
292
|
|
|
269
293
|
// Limit to max PRs per session
|
|
@@ -279,6 +303,7 @@ export class MergeQueueProcessor {
|
|
|
279
303
|
success: true,
|
|
280
304
|
error: null,
|
|
281
305
|
count: this.items.length,
|
|
306
|
+
targetMode: this.target?.mode || 'repository',
|
|
282
307
|
truncated: readyPRs.length > MERGE_QUEUE_CONFIG.MAX_PRS_PER_SESSION,
|
|
283
308
|
};
|
|
284
309
|
} catch (error) {
|
|
@@ -419,6 +444,46 @@ export class MergeQueueProcessor {
|
|
|
419
444
|
}
|
|
420
445
|
}
|
|
421
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Resolve targeted PR data, waiting for issue-linked PRs that may be created
|
|
449
|
+
* shortly after a replied `/codex <issue>` message.
|
|
450
|
+
*
|
|
451
|
+
* @returns {Promise<Array<{pr: Object, issue: Object|null, sortDate: Date}>>}
|
|
452
|
+
*/
|
|
453
|
+
async resolveMergeTargetItemsWithWait() {
|
|
454
|
+
const maxPollInterval = Math.max(1, this.targetItemsPollIntervalMs || 1);
|
|
455
|
+
const maxAttempts = Math.max(1, Math.ceil((this.targetItemsTimeoutMs || 0) / maxPollInterval));
|
|
456
|
+
|
|
457
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
458
|
+
if (this.isCancelled) {
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const items = (await this.resolveMergeTargetItems(this.target, this.verbose)) || [];
|
|
463
|
+
if (items.length > 0 || this.target?.mode !== 'issue') {
|
|
464
|
+
return items;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (attempt === maxAttempts) {
|
|
468
|
+
return [];
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
this.log(`Waiting for a linked PR to appear for issue #${this.target.issueNumber}`);
|
|
472
|
+
await this.sleep(this.targetItemsPollIntervalMs);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async waitForPRReady(item, initialCheck) {
|
|
479
|
+
return waitForPRReadyHelper(this, item, initialCheck, {
|
|
480
|
+
MergeItemStatus,
|
|
481
|
+
conflictSkipReason: MERGE_CONFLICT_SKIP_REASON,
|
|
482
|
+
timeoutMs: MERGE_QUEUE_CONFIG.CI_TIMEOUT_MS,
|
|
483
|
+
pollIntervalMs: MERGE_QUEUE_CONFIG.CI_POLL_INTERVAL_MS,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
422
487
|
/**
|
|
423
488
|
* Process a single merge queue item
|
|
424
489
|
* @param {MergeQueueItem} item
|
|
@@ -430,7 +495,7 @@ export class MergeQueueProcessor {
|
|
|
430
495
|
try {
|
|
431
496
|
// Step 1: Check if PR is mergeable
|
|
432
497
|
item.status = MergeItemStatus.CHECKING_CI;
|
|
433
|
-
const mergeableCheck = await checkPRMergeable(this.owner, this.repo, item.pr.number, this.verbose);
|
|
498
|
+
const mergeableCheck = await this.checkPRMergeable(this.owner, this.repo, item.pr.number, this.verbose);
|
|
434
499
|
|
|
435
500
|
if (mergeableCheck.terminal) {
|
|
436
501
|
item.status = MergeItemStatus.FAILED;
|
|
@@ -441,15 +506,34 @@ export class MergeQueueProcessor {
|
|
|
441
506
|
}
|
|
442
507
|
|
|
443
508
|
if (!mergeableCheck.mergeable) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
509
|
+
if (!this.waitForUnfinished || mergeableCheck.reason === MERGE_CONFLICT_SKIP_REASON) {
|
|
510
|
+
item.status = MergeItemStatus.SKIPPED;
|
|
511
|
+
item.error = mergeableCheck.reason;
|
|
512
|
+
this.stats.skipped++;
|
|
513
|
+
this.log(`Skipped PR #${item.pr.number}: ${mergeableCheck.reason}`);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const waitForReadyResult = await this.waitForPRReady(item, mergeableCheck);
|
|
518
|
+
if (!waitForReadyResult.success) {
|
|
519
|
+
if (waitForReadyResult.status === 'cancelled' || waitForReadyResult.status === 'conflict') {
|
|
520
|
+
item.status = MergeItemStatus.SKIPPED;
|
|
521
|
+
item.error = waitForReadyResult.error;
|
|
522
|
+
this.stats.skipped++;
|
|
523
|
+
this.log(`Skipped PR #${item.pr.number}: ${waitForReadyResult.error}`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
item.status = MergeItemStatus.FAILED;
|
|
528
|
+
item.error = waitForReadyResult.error;
|
|
529
|
+
this.stats.failed++;
|
|
530
|
+
this.log(`Failed PR #${item.pr.number}: ${waitForReadyResult.error}`);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
449
533
|
}
|
|
450
534
|
|
|
451
535
|
// Step 2: Check CI status
|
|
452
|
-
const ciStatus = await checkPRCIStatus(this.owner, this.repo, item.pr.number, this.verbose);
|
|
536
|
+
const ciStatus = await this.checkPRCIStatus(this.owner, this.repo, item.pr.number, this.verbose);
|
|
453
537
|
item.ciStatus = ciStatus;
|
|
454
538
|
|
|
455
539
|
if (ciStatus.status === 'failure') {
|
|
@@ -472,7 +556,7 @@ export class MergeQueueProcessor {
|
|
|
472
556
|
if (ciStatus.status === 'pending') {
|
|
473
557
|
item.status = MergeItemStatus.WAITING_CI;
|
|
474
558
|
|
|
475
|
-
const waitResult = await waitForCI(
|
|
559
|
+
const waitResult = await this.waitForCI(
|
|
476
560
|
this.owner,
|
|
477
561
|
this.repo,
|
|
478
562
|
item.pr.number,
|
|
@@ -512,7 +596,7 @@ export class MergeQueueProcessor {
|
|
|
512
596
|
// Step 4: Merge the PR
|
|
513
597
|
// Issue #1269: Pass the configured merge method to prevent "not running interactively" error
|
|
514
598
|
item.status = MergeItemStatus.MERGING;
|
|
515
|
-
const mergeResult = await mergePullRequest(this.owner, this.repo, item.pr.number, { mergeMethod: MERGE_QUEUE_CONFIG.MERGE_METHOD }, this.verbose);
|
|
599
|
+
const mergeResult = await this.mergePullRequest(this.owner, this.repo, item.pr.number, { mergeMethod: MERGE_QUEUE_CONFIG.MERGE_METHOD }, this.verbose);
|
|
516
600
|
|
|
517
601
|
if (!mergeResult.success) {
|
|
518
602
|
item.status = MergeItemStatus.FAILED;
|
|
@@ -531,7 +615,7 @@ export class MergeQueueProcessor {
|
|
|
531
615
|
// Issue #1895: GitHub does not auto-close linked issues for PRs merged into
|
|
532
616
|
// a non-default branch. Close the linked issue explicitly in that case.
|
|
533
617
|
try {
|
|
534
|
-
const closeResult = await closeLinkedIssueIfNotAutoClosed(this.owner, this.repo, item.pr.number, this.verbose);
|
|
618
|
+
const closeResult = await this.closeLinkedIssueIfNotAutoClosed(this.owner, this.repo, item.pr.number, this.verbose);
|
|
535
619
|
if (closeResult.closed) {
|
|
536
620
|
this.log(`Closed linked issue #${closeResult.issueNumber} for PR #${item.pr.number} (merged into non-default branch)`);
|
|
537
621
|
}
|
|
@@ -542,7 +626,7 @@ export class MergeQueueProcessor {
|
|
|
542
626
|
// Issue #1341: Get the merge commit SHA for post-merge CI tracking
|
|
543
627
|
// Need a small delay to allow GitHub to update the PR state
|
|
544
628
|
await this.sleep(5000);
|
|
545
|
-
const mergeCommitResult = await getMergeCommitSha(this.owner, this.repo, item.pr.number, this.verbose);
|
|
629
|
+
const mergeCommitResult = await this.getMergeCommitSha(this.owner, this.repo, item.pr.number, this.verbose);
|
|
546
630
|
if (mergeCommitResult.sha) {
|
|
547
631
|
item.mergeCommitSha = mergeCommitResult.sha;
|
|
548
632
|
this.log(`PR #${item.pr.number} merge commit: ${mergeCommitResult.sha.substring(0, 7)}`);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Wait helpers for Telegram merge queue.
|
|
4
|
+
*
|
|
5
|
+
* Split from telegram-merge-queue.lib.mjs to keep that file under the
|
|
6
|
+
* repository line limit.
|
|
7
|
+
*
|
|
8
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2013
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wait for an unfinished PR to become mergeable before CI/merge processing.
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} processor - MergeQueueProcessor-like object
|
|
15
|
+
* @param {Object} item - Merge queue item
|
|
16
|
+
* @param {{mergeable: boolean, terminal?: boolean, reason?: string}} initialCheck
|
|
17
|
+
* @param {Object} options
|
|
18
|
+
* @param {Object} options.MergeItemStatus
|
|
19
|
+
* @param {string} options.conflictSkipReason
|
|
20
|
+
* @param {number} options.timeoutMs
|
|
21
|
+
* @param {number} options.pollIntervalMs
|
|
22
|
+
* @returns {Promise<{success: boolean, status: string, error: string|null}>}
|
|
23
|
+
*/
|
|
24
|
+
export async function waitForPRReady(processor, item, initialCheck, options) {
|
|
25
|
+
const { MergeItemStatus, conflictSkipReason, timeoutMs, pollIntervalMs } = options;
|
|
26
|
+
const startedAt = Date.now();
|
|
27
|
+
let latestCheck = initialCheck;
|
|
28
|
+
|
|
29
|
+
while (true) {
|
|
30
|
+
if (processor.isCancelled) {
|
|
31
|
+
return { success: false, status: 'cancelled', error: 'Cancelled' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (latestCheck?.terminal) {
|
|
35
|
+
return {
|
|
36
|
+
success: false,
|
|
37
|
+
status: 'terminal',
|
|
38
|
+
error: latestCheck.reason || 'GitHub repository, pull request, issue, or branch is no longer accessible',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (latestCheck?.mergeable) {
|
|
43
|
+
item.status = MergeItemStatus.CHECKING_CI;
|
|
44
|
+
item.error = null;
|
|
45
|
+
return { success: true, status: 'ready', error: null };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (latestCheck?.reason === conflictSkipReason) {
|
|
49
|
+
return { success: false, status: 'conflict', error: conflictSkipReason };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
53
|
+
const reason = latestCheck?.reason || 'PR did not become mergeable';
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
status: 'timeout',
|
|
57
|
+
error: `Timed out waiting for PR #${item.pr.number} to become mergeable: ${reason}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
item.status = MergeItemStatus.WAITING_READY;
|
|
62
|
+
item.error = latestCheck?.reason || 'Waiting for PR to become mergeable';
|
|
63
|
+
processor.log(`Waiting for PR #${item.pr.number} to become mergeable: ${item.error}`);
|
|
64
|
+
if (processor.onProgress) {
|
|
65
|
+
await processor.onProgress(processor.getProgressUpdate());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await processor.sleep(pollIntervalMs);
|
|
69
|
+
latestCheck = await processor.checkPRMergeable(processor.owner, processor.repo, item.pr.number, processor.verbose);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default {
|
|
74
|
+
waitForPRReady,
|
|
75
|
+
};
|