Files
tea-sdlc/scripts/comments-merge.js
T
jiantw83andClaude Opus 5 16f93f6e8b feat(comments-merge): 把留言裡的決策整併回議題描述並標記
判斷「哪幾則有決策、該併進哪一段」是讀得懂內容的人的事;這一支只負責把結果安全地寫回去。

先寫描述再標記,描述寫失敗就不標記:反過來的話,那幾則已經被標成處理過,再也不會被提出來。
--merged 只收真的併進去的那幾則,略過的保持未標記。標記之前先核對那幾則確實在這顆議題上——
打錯 id 的 reaction 會落在別顆議題的留言上,而那幾乎不會有人發現。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 09:19:09 +00:00

142 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 把留言裡的決策整併回議題描述,並標記那幾則留言。
*
* 判斷「哪幾則留言有決策、該併進哪一段、併成什麼樣子」是讀得懂內容的人的事;
* 這一支只負責把結果安全地寫回去。
*
* 兩件事錯了都很安靜,所以都做得很窄:
*
* - **局部更新。** 只換指定那一段,標題與其餘段落一字不動。整份重寫會把別人在其他
* 段落的編輯一起蓋掉,而議題的編輯紀錄沒有人會去比對。
* - **標記只給真的整併進去的那幾則。** 略過的要保持未標記,下次才會再被提出來;
* 描述沒寫成功就不標記——標了就等於這則再也不會被看到。
*
* 用法:
* node scripts/comments-merge.js --repo owner/name --index 7
* --section <段落名> --content-file <檔案> --merged 101,102
* [--host <網址>] [--dry-run]
*/
import {
ScriptError,
expectOk,
fetchIssue,
giteaRequest,
listIssueComments,
main,
parseFlags,
parseIndex,
parseRepo,
preflight,
readTextFile,
resolveLogin,
} from './lib.js';
import { replaceSection } from './issue-body.js';
main(async () => {
const flags = parseFlags(process.argv.slice(2), {
required: ['repo', 'index', 'section', 'content-file', 'merged'],
optional: ['host'],
booleans: ['dry-run'],
});
const repo = parseRepo(flags.repo);
const index = parseIndex(flags.index);
const section = flags.section;
const content = readTextFile(flags['content-file'], '--content-file');
const merged = parseMerged(flags.merged);
const dryRun = flags['dry-run'] === true;
const login = resolveLogin({ host: flags.host });
if (!dryRun) await preflight(login, repo);
const issue = await fetchIssue(login, repo, index);
const issuePath = `/repos/${repo}/issues/${index}`;
const result = replaceSection(issue.body ?? '', section, content);
if (result.status === 'not-found') {
throw new ScriptError(
'SECTION_NOT_FOUND',
`議題 #${index} 上沒有「${section}」這個段落;請確認段落名與議題上的 \`## 標題\` 完全一致。` +
'本工具不會把內容補到議題末尾——段落名打錯時那樣做比什麼都不做更難收拾',
);
}
if (result.status === 'ambiguous') {
throw new ScriptError(
'SECTION_AMBIGUOUS',
`議題 #${index} 上有 ${result.count} 個「${section}」段落,分不出要換哪一個;` +
'請先到議題上把重複的標題改成看得出差別的名稱',
);
}
const body = result.body;
// 標記之前先確認這幾則留言真的在這顆議題上:標錯地方的 reaction 很難發現
await checkComments(login, repo, index, merged);
// 描述沒變就不送:空的 PATCH 會把議題的 updated_at 推新,看起來像有人動過
const 描述已更新 = body !== issue.body;
const requests = [
...(描述已更新 ? [{ method: 'PATCH', path: issuePath, body: { body } }] : []),
...merged.map((id) => ({
method: 'POST',
path: `/repos/${repo}/issues/comments/${id}/reactions`,
body: { content: '+1' },
})),
];
if (dryRun) {
return { dryRun: true, repo, index, section, 描述已更新, 已標記: merged, requests };
}
// 順序是先寫描述再標記:標記是「這則已經收進去了」的結論,
// 反過來的話,描述寫失敗時那幾則已經被標成處理過,再也不會被提出來
for (const { method, path, body: payload } of requests) {
expectOk(await giteaRequest(login, method, path, { body: payload }), `${method} ${path}`);
}
return {
repo,
index,
url: issue.html_url,
section,
描述已更新,
已標記: merged,
};
});
/** 逗號分隔的留言 id。整併卻不標記的話,下次會重複處理同一則,所以這個 flag 是必填。 */
function parseMerged(value) {
const ids = value
.split(',')
.map((item) => item.trim())
.filter((item) => item !== '')
.map((item) => parseIndex(item, '--merged'));
if (ids.length === 0) {
throw new ScriptError('MISSING_FLAG', '--merged 至少要有一則留言 id');
}
return [...new Set(ids)];
}
/**
* 確認這幾則留言都在這顆議題上。
* 打錯 id 的 reaction 會落在別顆議題的留言上,而那幾乎不會有人發現。
*/
async function checkComments(login, repo, index, merged) {
const seen = new Set();
for await (const comment of listIssueComments(login, repo, index)) {
seen.add(comment.id);
}
const missing = merged.filter((id) => !seen.has(id));
if (missing.length > 0) {
throw new ScriptError(
'COMMENT_NOT_FOUND',
`議題 #${index} 上沒有這幾則留言:${missing.join('、')};` +
'請確認 id 來自這顆議題(必要時重跑 issue-extract 或 wp-extract)',
);
}
}