From 5638593c4c2260c0f269d6a9117fdbca81252277 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 19:32:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor(lib):=20=E5=81=9C=E9=8C=B6=E8=88=87?= =?UTF-8?q?=E8=AD=B0=E9=A1=8C=E5=B7=A5=E6=99=82=E6=B8=85=E5=96=AE=E4=B8=8B?= =?UTF-8?q?=E6=B2=89=E5=88=B0=20lib?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 停錶原本只長在 pr-create 裡,而規劃與分析接下來也要停自己的錶(議題 #57)。 那段容錯邏輯——「錶沒在跑」的狀態碼隨站台版本而異,認的是狀態碼在 409/500 這一組**且**訊息說的是碼錶——各寫一份遲早會在某一邊漏掉一種狀態碼,而它漏掉的 症狀正是「事情做完了卻回報失敗」。 順手把簽章改成與同伴一致的 (login, repo, index):fetchIssue、listIssueTimes、 stopwatchOnIssue 都這樣收,只有它收一條手組的路徑字串。 新增 listIssueTimes 供補登判斷「這顆議題上已經有工時了嗎」。它在議題讀得到卻 404 時報 TIME_TRACKER_OFF,與四層前置檢查第四層說同一句話——試跑不跑前置檢查, 那句話得由它自己說,否則試跑看到的是一句看不懂的 404。 議題 #57 Co-Authored-By: Claude Opus 5 (1M context) --- scripts/lib.js | 61 ++++++++++++++++++++++++++++++++++++++++++++ scripts/pr-create.js | 28 ++------------------ 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/scripts/lib.js b/scripts/lib.js index 4cd363f..8bd2ebf 100644 --- a/scripts/lib.js +++ b/scripts/lib.js @@ -1007,6 +1007,67 @@ export function stopwatchOnIssue(watches, repo, index) { ); } +/** + * 「這顆議題上沒有碼錶在跑」的回法不只一種:看過 500,也看過 409。 + * 狀態碼隨站台版本而異,所以認的是「狀態碼在這一組裡 **且** 訊息說的是碼錶」—— + * 只看訊息會把真的伺服器錯誤一起吞掉,只看狀態碼會把別的衝突也當成沒錶。 + */ +const NO_STOPWATCH_STATUS = [409, 500]; + +/** + * 停錶。停在指定議題上,只停那一顆——端點本身就是議題範圍的, + * 停錶不會波及別顆議題上的錶,那正是領取鎖那條規則要守住的事。 + * + * 錶沒在跑不算失敗:停錶多半排在別的事情做完之後(開完 PR、回報完), + * 把「本來就沒在跑」報成失敗,只會讓人以為前面那件事沒做成而重跑一次。 + * + * @param {{base: string, token: string}} login + * @param {string} repo owner/name + * @param {number} index + * @returns {Promise} 這次真的停了一支錶才是 true + */ +export async function stopStopwatch(login, repo, index) { + const path = `/repos/${repo}/issues/${index}/stopwatch/stop`; + const response = await giteaRequest(login, 'POST', path, { body: {} }); + if (response.status >= 200 && response.status < 300) return true; + + if ( + NO_STOPWATCH_STATUS.includes(response.status) && + /stopwatch/i.test(response.body?.message ?? '') + ) { + return false; + } + expectOk(response, `POST ${path}`); + return false; +} + +/** + * 這顆議題上已經記到的工時。 + * + * 清單的範圍隨權限而異:不是 repo admin 時 Gitea 只回自己的那幾筆,是 admin 時連別人的 + * 一起回。呼叫端(補登)問的是「這顆議題上已經有工時了嗎」,**兩種範圍都答得了那一問**: + * 剛建好的議題上一筆都不該有,有了就代表這個流程跑過一次。範圍寬一點只會讓它偏向不補, + * 而那是安全的方向——工時記重複比記不到更難在報表上被發現。 + * @param {{base: string, token: string}} login + * @param {string} repo owner/name + * @param {number} index + * @returns {Promise} + */ +export async function listIssueTimes(login, repo, index) { + const path = `/repos/${repo}/issues/${index}/times`; + const response = await giteaRequest(login, 'GET', path); + + // 議題本身讀得到卻在這裡 404,代表的是 repo 的時間追蹤關著。訊息與前置檢查第四層 + // 對齊:同一件事在試跑與實跑上要說同一句話,否則試跑看到的是一句看不懂的 404 + if (response.status === 404) { + throw new ScriptError( + 'TIME_TRACKER_OFF', + 'repo 尚未開啟時間追蹤,工時記不進去;請到 Settings → Advanced Settings → Enable Time Tracker 開啟', + ); + } + return expectOk(response, `GET ${path}`) ?? []; +} + // ── 標籤 ─────────────────────────────────────────────────────────── /** diff --git a/scripts/pr-create.js b/scripts/pr-create.js index 4167b17..1b86fd5 100644 --- a/scripts/pr-create.js +++ b/scripts/pr-create.js @@ -36,6 +36,7 @@ import { preflight, readTextFile, resolveLogin, + stopStopwatch, } from './lib.js'; /** 描述的固定段落,順序即 reviewer 閱讀的順序 */ @@ -126,7 +127,7 @@ main(async () => { ); // 錶只在 PR 確實存在之後才停。既有的 PR 也要停——那一步可能正是上次中斷的地方。 - const stopped = await stopStopwatch(login, stopPath); + const stopped = await stopStopwatch(login, issueRepo, index); return { repo, @@ -211,28 +212,3 @@ function checkTestResult(body) { ); } } - -/** - * 「這顆議題上沒有碼錶在跑」的回法不只一種:看過 500,也看過 409。 - * 狀態碼隨站台版本而異,所以認的是「狀態碼在這一組裡 **且** 訊息說的是碼錶」—— - * 只看訊息會把真的伺服器錯誤一起吞掉,只看狀態碼會把別的衝突也當成沒錶。 - */ -const NO_STOPWATCH_STATUS = [409, 500]; - -/** - * 停錶。錶沒在跑時不算失敗——PR 已經開出去了,把整件事報成失敗只會讓人以為 - * PR 沒開成而重跑一次。 - */ -async function stopStopwatch(login, path) { - const response = await giteaRequest(login, 'POST', path, { body: {} }); - if (response.status >= 200 && response.status < 300) return true; - - if ( - NO_STOPWATCH_STATUS.includes(response.status) && - /stopwatch/i.test(response.body?.message ?? '') - ) { - return false; - } - expectOk(response, `POST ${path}`); - return false; -}