refactor(lib): 讀檔與列留言收進 lib,並讓已整併只認自己打的 +1
「讀一個 --xxx-file 或直接失敗」原本在四支腳本各寫一份,錯誤碼還有三種拼法 (BODY_FILE_NOT_FOUND/BODY_FILE_MISSING/CONTENT_FILE_NOT_FOUND)。同一種情況要有同一個 碼,呼叫端才分辨得出是哪一步壞了。留言分頁的那段咒語也是第三份,一併收成 listIssueComments。 countUnmergedComments 原本接受任何人的 +1,而讀留言那邊只認自己的——兩端對「已整併」的 定義不一致。後果是隊友對決策留言按個讚,未處理留言數就掉到 0,analyze 與 feat 再也不提示, 那則決策永遠不會被收進描述。統一成只認自己打的:別人按讚是「我同意」,不是「已經收進去了」。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+61
-14
@@ -663,6 +663,28 @@ function checkTimeTracker(info) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 讀一個由 flag 指定的文字檔。
|
||||||
|
*
|
||||||
|
* 「讀一個 --xxx-file 或直接失敗」原本在四支腳本裡各寫一份,錯誤碼還有三種拼法。
|
||||||
|
* 同一種情況要有同一個碼,呼叫端才分辨得出到底是哪一步壞了。
|
||||||
|
*
|
||||||
|
* @param {string} path 檔案路徑
|
||||||
|
* @param {string} flag 出現在錯誤訊息裡的 flag 名,例如 '--body-file'
|
||||||
|
* @param {{allowEmpty?: boolean}} options 內容可不可以是空的;預設不可以
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function readTextFile(path, flag, { allowEmpty = false } = {}) {
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
throw new ScriptError('FILE_NOT_FOUND', `找不到 ${flag} 指定的檔案 ${path}`);
|
||||||
|
}
|
||||||
|
const content = readFileSync(path, 'utf8');
|
||||||
|
if (!allowEmpty && content.trim() === '') {
|
||||||
|
throw new ScriptError('FILE_EMPTY', `${flag} 指定的檔案 ${path} 是空的`);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 議題讀取:兩支抽取腳本共用 ────────────────────────────────────
|
// ── 議題讀取:兩支抽取腳本共用 ────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -697,32 +719,57 @@ export const UNMERGED_COMMENT_NOTE =
|
|||||||
* 數出尚未被整併回描述的留言則數。
|
* 數出尚未被整併回描述的留言則數。
|
||||||
*
|
*
|
||||||
* 抽取契約只讀 body 不讀留言,這個數字是下游判斷「手上的描述是不是過期了」的唯一依據。
|
* 抽取契約只讀 body 不讀留言,這個數字是下游判斷「手上的描述是不是過期了」的唯一依據。
|
||||||
* 已整併的留言會被打上 `+1` reaction(由 sdlc-sync 負責標記),而 Gitea 的留言物件
|
* 已整併的留言會被打上 `+1` reaction(由 sdlc-sync 負責標記)。留言多時請求數會跟著長,
|
||||||
* 不含 reaction,所以只能逐則再查一次。留言多時請求數會跟著長,但這個數字要準
|
* 但這個數字要準——它決定下游會不會拿著過期的描述做事,所以留言也要逐頁讀完,
|
||||||
* ——它決定下游會不會拿著過期的描述做事,所以留言也要逐頁讀完,讀不完寧可報錯。
|
* 讀不完寧可報錯。
|
||||||
*
|
*
|
||||||
* @param {{base: string, token: string}} login
|
* @param {{base: string, token: string}} login
|
||||||
* @param {string} repo owner/name
|
* @param {string} repo owner/name
|
||||||
* @param {number} index
|
* @param {number} index
|
||||||
|
* @param {string} me 目前登入帳號:只有自己打的 `+1` 才算整併過
|
||||||
* @returns {Promise<number>}
|
* @returns {Promise<number>}
|
||||||
*/
|
*/
|
||||||
export async function countUnmergedComments(login, repo, index) {
|
export async function countUnmergedComments(login, repo, index, me) {
|
||||||
const commentsPath = `/repos/${repo}/issues/${index}/comments`;
|
|
||||||
let unmerged = 0;
|
let unmerged = 0;
|
||||||
|
|
||||||
for await (const comments of pages(login, commentsPath, {
|
for await (const comment of listIssueComments(login, repo, index)) {
|
||||||
limitCode: 'COMMENT_LIMIT',
|
if (!(await mergedByMe(login, repo, comment.id, me))) unmerged += 1;
|
||||||
limitHint: `${commentsPath} 的留言太多,數不完未整併的則數`,
|
|
||||||
})) {
|
|
||||||
for (const comment of comments) {
|
|
||||||
const path = `/repos/${repo}/issues/comments/${comment.id}/reactions`;
|
|
||||||
const reactions = expectOk(await giteaRequest(login, 'GET', path), `GET ${path}`) ?? [];
|
|
||||||
if (!reactions.some((reaction) => reaction.content === '+1')) unmerged += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return unmerged;
|
return unmerged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逐頁走過一顆議題(或 PR)的一般留言。
|
||||||
|
* 三支腳本都要做這件事:數未整併的則數、列出留言內容、核對 --merged 的 id。
|
||||||
|
* @returns {AsyncGenerator<object>} 一則一則交出去
|
||||||
|
*/
|
||||||
|
export async function* listIssueComments(login, repo, index) {
|
||||||
|
const path = `/repos/${repo}/issues/${index}/comments`;
|
||||||
|
|
||||||
|
for await (const comments of pages(login, path, {
|
||||||
|
limitCode: 'COMMENT_LIMIT',
|
||||||
|
limitHint: `${path} 的留言太多,讀不完整份清單`,
|
||||||
|
})) {
|
||||||
|
for (const comment of comments) yield comment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 這一則是不是「我」標記過已整併。
|
||||||
|
*
|
||||||
|
* Gitea 的留言物件不含 reaction,只能逐則再查一次。認的是自己打的 `+1`:
|
||||||
|
* 別人按讚是「我同意」,當成已整併會讓那一則的決策永遠不被收進描述——
|
||||||
|
* 而那正是 /sdlc-sync 要解決的事。
|
||||||
|
*
|
||||||
|
* @param {string} me 目前登入帳號;preflight 的回傳帶得出來
|
||||||
|
*/
|
||||||
|
export async function mergedByMe(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);
|
||||||
|
}
|
||||||
|
|
||||||
// ── 碼錶 ───────────────────────────────────────────────────────────
|
// ── 碼錶 ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
* --body-file <描述檔> --index 13
|
* --body-file <描述檔> --index 13
|
||||||
* [--issue-repo owner/name] [--host <網址>] [--dry-run]
|
* [--issue-repo owner/name] [--host <網址>] [--dry-run]
|
||||||
*/
|
*/
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
|
||||||
import {
|
import {
|
||||||
ScriptError,
|
ScriptError,
|
||||||
expectOk,
|
expectOk,
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
parseIndex,
|
parseIndex,
|
||||||
parseRepo,
|
parseRepo,
|
||||||
preflight,
|
preflight,
|
||||||
|
readTextFile,
|
||||||
resolveLogin,
|
resolveLogin,
|
||||||
} from './lib.js';
|
} from './lib.js';
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ main(async () => {
|
|||||||
const head = flags.head;
|
const head = flags.head;
|
||||||
const base = flags.base;
|
const base = flags.base;
|
||||||
const index = parseIndex(flags.index);
|
const index = parseIndex(flags.index);
|
||||||
const body = readBody(flags['body-file']);
|
const body = readTextFile(flags['body-file'], '--body-file');
|
||||||
|
|
||||||
// 描述先驗完再談寫入:不合格的描述不該等到實跑才發現
|
// 描述先驗完再談寫入:不合格的描述不該等到實跑才發現
|
||||||
checkSections(body);
|
checkSections(body);
|
||||||
@@ -160,12 +160,6 @@ async function findOpenPull(login, repo, head) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function readBody(path) {
|
|
||||||
if (!existsSync(path)) {
|
|
||||||
throw new ScriptError('BODY_FILE_NOT_FOUND', `找不到描述檔 ${path}`);
|
|
||||||
}
|
|
||||||
return readFileSync(path, 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 八個段落一個都不能少,而且順序要與 SECTIONS 一致 */
|
/** 八個段落一個都不能少,而且順序要與 SECTIONS 一致 */
|
||||||
function checkSections(body) {
|
function checkSections(body) {
|
||||||
|
|||||||
Reference in New Issue
Block a user