fix(pr-comments): 議題也讀得了,不再先打 PR 端點
每個 PR 都是議題,議題不一定是 PR——原本的註解把這句話講反了,程式也照著反過來寫:
先打 /pulls/{index},對純議題回 404,於是 /sdlc-sync 在讀到第一則留言之前就斷了。
改成先讀 /issues/{index}(兩種都有),看它有沒有 pull_request 才決定要不要去翻 review。
輸出加上「類型」讓下游知道拿到的是議題還是 PR。
讀取的共用結構沿用 pr-threads(#48 為了讓 pr-watch 數同一件事而抽出來的):readGeneral
改名 readGeneralComments 並導出,純議題只叫它;pr-threads 自己那份 markedByMe 拿掉,
改用 lib 的 mergedByMe。#48 的檔頭擔心「規則寫兩份遲早會各自演化」,現在三處共用一份。
試跑改印 commonRequests:pr-comments 收得下兩種輸入,而試跑階段還沒讀過議題、不知道是
哪一種。與其假設是 PR 而列出五個(對純議題有三個根本不會發),不如只列一定會發的。
pr-watch 的輸入一定是 PR,繼續用 plannedRequests。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+27
-18
@@ -1,9 +1,14 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* 讀 PR 上的三類留言:一般留言、review 總評、行內留言。
|
* 讀 PR 或議題上的留言。
|
||||||
*
|
*
|
||||||
* 三類分散在三個端點,漏掉任何一類就會有 reviewer 的意見沒被處理——而那正是
|
* PR 有三類:一般留言、review 總評、行內留言,分散在三個端點——漏掉任何一類就會有
|
||||||
* `/sdlc-fix` 存在的理由。這一支只讀不寫,分類(必改/建議)由讀到內容的人判斷。
|
* reviewer 的意見沒被處理,而那正是 `/sdlc-fix` 存在的理由。
|
||||||
|
* 純議題只有一般留言,`/sdlc-sync` 要的就是那一份。
|
||||||
|
*
|
||||||
|
* **每個 PR 都是議題,但議題不一定是 PR。** 所以先讀 `/issues/{index}`(兩種都有),
|
||||||
|
* 看它有沒有 `pull_request` 才決定要不要去翻 review;反過來先打 `/pulls/{index}`,
|
||||||
|
* 對純議題會 404,整個流程在讀到第一則留言之前就斷了。
|
||||||
*
|
*
|
||||||
* 讀取本身與「已處理」的判定在 `pr-threads.js`——`pr-watch` 要數同一件事,
|
* 讀取本身與「已處理」的判定在 `pr-threads.js`——`pr-watch` 要數同一件事,
|
||||||
* 規則寫兩份遲早會各自演化。
|
* 規則寫兩份遲早會各自演化。
|
||||||
@@ -12,8 +17,7 @@
|
|||||||
* node scripts/pr-comments.js --repo owner/name --index 45 [--host <網址>] [--dry-run]
|
* node scripts/pr-comments.js --repo owner/name --index 45 [--host <網址>] [--dry-run]
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
expectOk,
|
fetchIssue,
|
||||||
giteaRequest,
|
|
||||||
main,
|
main,
|
||||||
parseFlags,
|
parseFlags,
|
||||||
parseIndex,
|
parseIndex,
|
||||||
@@ -22,9 +26,9 @@ import {
|
|||||||
resolveLogin,
|
resolveLogin,
|
||||||
} from './lib.js';
|
} from './lib.js';
|
||||||
import {
|
import {
|
||||||
COMMENT_REQUEST_NOTE,
|
ISSUE_OR_PULL_NOTE,
|
||||||
fetchPull,
|
commonRequests,
|
||||||
plannedRequests,
|
readGeneralComments,
|
||||||
readPullComments,
|
readPullComments,
|
||||||
unhandledCount,
|
unhandledCount,
|
||||||
} from './pr-threads.js';
|
} from './pr-threads.js';
|
||||||
@@ -43,24 +47,29 @@ main(async () => {
|
|||||||
dryRun: true,
|
dryRun: true,
|
||||||
repo,
|
repo,
|
||||||
index,
|
index,
|
||||||
requests: plannedRequests(repo, index),
|
requests: commonRequests(repo, index),
|
||||||
note: COMMENT_REQUEST_NOTE,
|
note: ISSUE_OR_PULL_NOTE,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = resolveLogin({ host: flags.host });
|
const login = resolveLogin({ host: flags.host });
|
||||||
await preflight(login, repo);
|
const { user } = await preflight(login, repo);
|
||||||
|
const me = user.login;
|
||||||
|
|
||||||
const me = expectOk(await giteaRequest(login, 'GET', '/user'), 'GET /user').login;
|
// 先讀議題:PR 也是議題,反過來不成立。這一步同時決定要不要去翻 review
|
||||||
const pull = await fetchPull(login, repo, index);
|
const issue = await fetchIssue(login, repo, index);
|
||||||
const 留言 = await readPullComments(login, repo, index, me);
|
const isPull = issue.pull_request != null;
|
||||||
|
const 留言 = isPull
|
||||||
|
? await readPullComments(login, repo, index, me)
|
||||||
|
: await readGeneralComments(login, repo, index, me);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
repo,
|
repo,
|
||||||
index: pull.number,
|
index: issue.number,
|
||||||
title: pull.title,
|
類型: isPull ? 'PR' : '議題',
|
||||||
url: pull.html_url,
|
title: issue.title,
|
||||||
state: pull.state,
|
url: issue.html_url,
|
||||||
|
state: issue.state,
|
||||||
留言,
|
留言,
|
||||||
未處理數: unhandledCount(留言),
|
未處理數: unhandledCount(留言),
|
||||||
};
|
};
|
||||||
|
|||||||
+30
-26
@@ -7,7 +7,8 @@
|
|||||||
* 才會被發現。
|
* 才會被發現。
|
||||||
*
|
*
|
||||||
* 「已處理」在三類上的機制不同:
|
* 「已處理」在三類上的機制不同:
|
||||||
* - 一般留言、review 總評 → 自己打的 `+1` reaction
|
* - 一般留言、review 總評 → 自己打的 `+1` reaction(判定在 `lib.js` 的 mergedByMe,
|
||||||
|
* 抽取契約數未整併則數時用的是同一條規則)
|
||||||
* - 行內留言 → 有沒有被 resolve(只有 review comment 有 resolve 端點)
|
* - 行內留言 → 有沒有被 resolve(只有 review comment 有 resolve 端點)
|
||||||
*
|
*
|
||||||
* **總評的 reaction 掛在它的 issue comment id 上,不是 review id。** Gitea 的 review
|
* **總評的 reaction 掛在它的 issue comment id 上,不是 review id。** Gitea 的 review
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
* reaction 要是**自己**打的才算已處理:reviewer 對留言按讚是「我同意」,不是
|
* reaction 要是**自己**打的才算已處理:reviewer 對留言按讚是「我同意」,不是
|
||||||
* 「這則我處理過了」,把它當成已處理會讓那一則被靜靜跳過。
|
* 「這則我處理過了」,把它當成已處理會讓那一則被靜靜跳過。
|
||||||
*/
|
*/
|
||||||
import { ScriptError, expectOk, giteaRequest, pages } from './lib.js';
|
import { ScriptError, expectOk, giteaRequest, listIssueComments, mergedByMe, pages } from './lib.js';
|
||||||
|
|
||||||
/** 還沒送出的 review:reviewer 自己都還看不到,不該被當成意見 */
|
/** 還沒送出的 review:reviewer 自己都還看不到,不該被當成意見 */
|
||||||
const DRAFT = 'PENDING';
|
const DRAFT = 'PENDING';
|
||||||
@@ -33,7 +34,7 @@ const DRAFT = 'PENDING';
|
|||||||
export async function readPullComments(login, repo, index, me) {
|
export async function readPullComments(login, repo, index, me) {
|
||||||
const pullPath = `/repos/${repo}/pulls/${index}`;
|
const pullPath = `/repos/${repo}/pulls/${index}`;
|
||||||
return [
|
return [
|
||||||
...(await readGeneral(login, repo, index, me)),
|
...(await readGeneralComments(login, repo, index, me)),
|
||||||
...(await readReviews(login, repo, index, pullPath, me)),
|
...(await readReviews(login, repo, index, pullPath, me)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -54,6 +55,25 @@ export function plannedRequests(repo, index) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不確定是議題還是 PR 時,一定會發的那兩個請求。
|
||||||
|
*
|
||||||
|
* `pr-comments` 收得下兩種輸入,而它在試跑階段還沒讀過議題、不知道是哪一種。
|
||||||
|
* 與其假設是 PR 而列出五個(對純議題有三個根本不會發),不如只列一定會發的,
|
||||||
|
* 其餘交給 note 說明。`pr-watch` 的輸入一定是 PR,繼續用 plannedRequests。
|
||||||
|
*/
|
||||||
|
export function commonRequests(repo, index) {
|
||||||
|
return [
|
||||||
|
{ method: 'GET', path: `/repos/${repo}/issues/${index}` },
|
||||||
|
{ method: 'GET', path: `/repos/${repo}/issues/${index}/comments` },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `commonRequests` 列不完的那部分:是 PR 的話還要再讀三處。 */
|
||||||
|
export const ISSUE_OR_PULL_NOTE =
|
||||||
|
'每則留言還會各查一次 reaction;是 PR 的話還會再讀 review 清單、每個 review 的行內留言' +
|
||||||
|
'與 timeline。次數取決於留言數,事前無法列舉。';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `plannedRequests` 列不完的那部分。與 readPullComments 同進退——說明的是它發出的請求。
|
* `plannedRequests` 列不完的那部分。與 readPullComments 同進退——說明的是它發出的請求。
|
||||||
*/
|
*/
|
||||||
@@ -83,29 +103,25 @@ export async function fetchPull(login, repo, index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一般留言。PR 在 Gitea 裡也是 issue,所以走 issue 的留言端點。
|
* 一般留言。PR 在 Gitea 裡也是 issue,所以走 issue 的留言端點——
|
||||||
|
* 純議題也只有這一類,`/sdlc-sync` 要的就是它。
|
||||||
* 內容是空的那些多半是狀態變更的系統紀錄(指派、改標題),不是意見。
|
* 內容是空的那些多半是狀態變更的系統紀錄(指派、改標題),不是意見。
|
||||||
*/
|
*/
|
||||||
async function readGeneral(login, repo, index, me) {
|
export async function readGeneralComments(login, repo, index, me) {
|
||||||
const path = `/repos/${repo}/issues/${index}/comments`;
|
|
||||||
const 留言 = [];
|
const 留言 = [];
|
||||||
|
|
||||||
for await (const comments of pages(login, path, {
|
for await (const comment of listIssueComments(login, repo, index)) {
|
||||||
limitCode: 'COMMENT_LIMIT',
|
|
||||||
limitHint: `${path} 的留言太多,讀不完整份清單`,
|
|
||||||
})) {
|
|
||||||
for (const comment of comments) {
|
|
||||||
if ((comment.body ?? '').trim() === '') continue;
|
if ((comment.body ?? '').trim() === '') continue;
|
||||||
留言.push({
|
留言.push({
|
||||||
id: comment.id,
|
id: comment.id,
|
||||||
|
review: null,
|
||||||
類型: '一般',
|
類型: '一般',
|
||||||
作者: comment.user?.login ?? '',
|
作者: comment.user?.login ?? '',
|
||||||
內容: comment.body,
|
內容: comment.body,
|
||||||
已處理: await markedByMe(login, repo, comment.id, me),
|
已處理: await mergedByMe(login, repo, comment.id, me),
|
||||||
可標記: true,
|
可標記: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return 留言;
|
return 留言;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +152,7 @@ async function readReviews(login, repo, index, pullPath, me) {
|
|||||||
作者: review.user?.login ?? '',
|
作者: review.user?.login ?? '',
|
||||||
內容: review.body,
|
內容: review.body,
|
||||||
// 找不到它在 issue comment 表裡的那一份就標不了——那時如實說,不要假裝可以
|
// 找不到它在 issue comment 表裡的那一份就標不了——那時如實說,不要假裝可以
|
||||||
已處理: commentId === undefined ? false : await markedByMe(login, repo, commentId, me),
|
已處理: commentId === undefined ? false : await mergedByMe(login, repo, commentId, me),
|
||||||
可標記: commentId !== undefined,
|
可標記: commentId !== undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -199,15 +215,3 @@ async function reviewCommentIds(login, repo, index) {
|
|||||||
return ids;
|
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);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user