From fff599d0f12acad181f2dc3fe9c34b69e1be6393 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 09:08:05 +0000 Subject: [PATCH] =?UTF-8?q?fix(pr-comments):=20=E8=AD=B0=E9=A1=8C=E4=B9=9F?= =?UTF-8?q?=E8=AE=80=E5=BE=97=E4=BA=86=EF=BC=8C=E4=B8=8D=E5=86=8D=E5=85=88?= =?UTF-8?q?=E6=89=93=20PR=20=E7=AB=AF=E9=BB=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 每個 PR 都是議題,議題不一定是 PR——原本的註解把這句話講反了,程式也照著反過來寫: 先打 /pulls/{index},對純議題回 404,於是 /sdlc-sync 在讀到第一則留言之前就斷了。 改成先讀 /issues/{index}(兩種都有),看它有沒有 pull_request 才決定要不要去翻 review。 輸出加上「類型」讓下游知道拿到的是議題還是 PR。 --- scripts/pr-comments.js | 101 +++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 59 deletions(-) diff --git a/scripts/pr-comments.js b/scripts/pr-comments.js index cae6943..9802fd1 100644 --- a/scripts/pr-comments.js +++ b/scripts/pr-comments.js @@ -1,9 +1,16 @@ #!/usr/bin/env node /** - * 讀 PR 上的三類留言:一般留言、review 總評、行內留言。 + * 讀 PR 或議題上的留言。 * - * 三類分散在三個端點,漏掉任何一類就會有 reviewer 的意見沒被處理——而那正是 - * `/sdlc-fix` 存在的理由。這一支只讀不寫,分類(必改/建議)由讀到內容的人判斷。 + * PR 有三類:一般留言、review 總評、行內留言,分散在三個端點——漏掉任何一類就會有 + * reviewer 的意見沒被處理,而那正是 `/sdlc-fix` 存在的理由。 + * 純議題只有一般留言,`/sdlc-sync` 要的就是那一份。 + * + * **每個 PR 都是議題,但議題不一定是 PR。** 所以先讀 `/issues/{index}`(兩種都有), + * 看它有沒有 `pull_request` 才決定要不要去翻 review;反過來先打 `/pulls/{index}`, + * 對純議題會 404,整個流程在讀到第一則留言之前就斷了。 + * + * 這一支只讀不寫,分類(必改/建議、是不是決策)由讀到內容的人判斷。 * * 「已處理」在三類上的機制不同: * - 一般留言、review 總評 → 自己打的 `+1` reaction @@ -20,10 +27,12 @@ * node scripts/pr-comments.js --repo owner/name --index 45 [--host <網址>] [--dry-run] */ import { - ScriptError, expectOk, + fetchIssue, giteaRequest, + listIssueComments, main, + mergedByMe, pages, parseFlags, parseIndex, @@ -43,6 +52,7 @@ main(async () => { }); const repo = parseRepo(flags.repo); const index = parseIndex(flags.index); + const issuePath = `/repos/${repo}/issues/${index}`; const pullPath = `/repos/${repo}/pulls/${index}`; if (flags['dry-run']) { @@ -51,74 +61,59 @@ main(async () => { repo, index, requests: [ - { method: 'GET', path: '/user' }, - { method: 'GET', path: pullPath }, - { method: 'GET', path: `/repos/${repo}/issues/${index}/comments` }, - { method: 'GET', path: `${pullPath}/reviews` }, - { method: 'GET', path: `/repos/${repo}/issues/${index}/timeline` }, + { method: 'GET', path: issuePath }, + { method: 'GET', path: `${issuePath}/comments` }, ], note: - '每則一般留言還會各查一次 reaction、每個 review 還會各查一次它的行內留言;' + - '次數取決於留言數,事前無法列舉。', + '每則留言還會各查一次 reaction;是 PR 的話還會再讀 review 清單、每個 review 的' + + '行內留言與 timeline。次數取決於留言數,事前無法列舉。', }; } const login = resolveLogin({ host: flags.host }); - await preflight(login, repo); + const { user } = await preflight(login, repo); + const me = user.login; + + // 先讀議題:PR 也是議題,反過來不成立。這一步同時決定要不要去翻 review + const issue = await fetchIssue(login, repo, index); + const isPull = issue.pull_request != null; - const me = expectOk(await giteaRequest(login, 'GET', '/user'), 'GET /user').login; - const pull = await fetchPull(login, repo, index, pullPath); const 留言 = [ ...(await readGeneral(login, repo, index, me)), - ...(await readReviews(login, repo, index, pullPath, me)), + ...(isPull ? await readReviews(login, repo, index, pullPath, me) : []), ]; return { repo, - index: pull.number, - title: pull.title, - url: pull.html_url, - state: pull.state, + index: issue.number, + 類型: isPull ? 'PR' : '議題', + title: issue.title, + url: issue.html_url, + state: issue.state, 留言, 未處理數: 留言.filter((comment) => !comment.已處理).length, }; }); -async function fetchPull(login, repo, index, path) { - const response = await giteaRequest(login, 'GET', path); - if (response.status === 404) { - throw new ScriptError('PULL_NOT_FOUND', `${repo} 沒有編號 ${index} 的 PR`); - } - if (response.status === 403) { - throw new ScriptError('NO_READ_ACCESS', `目前的帳號沒有 ${repo} 的 PR ${index} 的讀取權`); - } - return expectOk(response, `GET ${path}`); -} - /** * 一般留言。PR 在 Gitea 裡也是 issue,所以走 issue 的留言端點。 * 內容是空的那些多半是狀態變更的系統紀錄(指派、改標題),不是意見。 */ async function readGeneral(login, repo, index, me) { - const path = `/repos/${repo}/issues/${index}/comments`; const 留言 = []; - for await (const comments of pages(login, path, { - limitCode: 'COMMENT_LIMIT', - limitHint: `${path} 的留言太多,讀不完整份清單`, - })) { - for (const comment of comments) { - if ((comment.body ?? '').trim() === '') continue; - 留言.push({ - id: comment.id, - 類型: '一般', - 作者: comment.user?.login ?? '', - 內容: comment.body, - 已處理: await markedByMe(login, repo, comment.id, me), - 可標記: true, - }); - } + for await (const comment of listIssueComments(login, repo, index)) { + if ((comment.body ?? '').trim() === '') continue; + 留言.push({ + id: comment.id, + review: null, + 類型: '一般', + 作者: comment.user?.login ?? '', + 內容: comment.body, + 已處理: await mergedByMe(login, repo, comment.id, me), + 可標記: true, + }); } return 留言; } @@ -150,7 +145,7 @@ async function readReviews(login, repo, index, pullPath, me) { 作者: review.user?.login ?? '', 內容: review.body, // 找不到它在 issue comment 表裡的那一份就標不了——那時如實說,不要假裝可以 - 已處理: commentId === undefined ? false : await markedByMe(login, repo, commentId, me), + 已處理: commentId === undefined ? false : await mergedByMe(login, repo, commentId, me), 可標記: commentId !== undefined, }); } @@ -213,15 +208,3 @@ async function reviewCommentIds(login, repo, index) { return ids; } -/** - * 這一則是不是「我」標記過已處理。 - * - * Gitea 的留言物件不含 reaction,只能逐則再查一次。認的是自己打的 `+1`: - * reviewer 對留言按讚是「我同意」,當成已處理會讓那一則被靜靜跳過。 - */ -async function markedByMe(login, repo, id, me) { - const path = `/repos/${repo}/issues/comments/${id}/reactions`; - const reactions = expectOk(await giteaRequest(login, 'GET', path), `GET ${path}`) ?? []; - - return reactions.some((reaction) => reaction.content === '+1' && reaction.user?.login === me); -}