refactor(release-cleanup): 將清理邏輯由 bash 改寫為 Node.js #5
@@ -1,7 +1,13 @@
|
||||
// 讀取並驗證環境變數,組出後續流程所需的設定物件。
|
||||
// 對應原本 entrypoint.sh 的「參數檢查」區段。
|
||||
|
||||
import { isEmptyOrNull, requireValue, requireInteger } from './validate.js'
|
||||
import {
|
||||
isEmptyOrNull,
|
||||
requireValue,
|
||||
requireInteger,
|
||||
requireUrl,
|
||||
requireRepository,
|
||||
} from './validate.js'
|
||||
import { section, info, warn } from './logger.js'
|
||||
|
Ghost marked this conversation as resolved
|
||||
|
||||
/**
|
||||
@@ -27,9 +33,11 @@ export function loadConfig(env = process.env) {
|
||||
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Leo
**問題**:直接將 env 預設為 process.env,全域相依性可能導致難以追蹤的副作用。
**建議**:建議在複雜系統中,將環境變數讀取抽離為獨立的 Provider 或 ConfigFactory。
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Mage
**問題**:在 loadConfig 函式中,對 GITEA_SERVER_URL 的 replace(//+$/, '') 操作假設了輸入一定是 string。雖然隨後有 requireValue 和 requireUrl 的驗證,但如果 env.GITEA_SERVER_URL 是非字串(例如數字、陣列、物件),這裡的 replace 可能會拋出 TypeError。
**建議**:應在 replace 之前確保其型別為 string,或使用 optional chaining 以及更嚴格的類型防護。例如: `typeof rawServerUrl === 'string' ? rawServerUrl.replace(//+$/, '') : rawServerUrl` 已經做了檢查,但後續的 `requireValue` 檢查順序應確保傳入的是處理後的字串。
|
||||
info(`GITEA_SERVER_URL=${serverUrl}`)
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Assassin
**問題**:將環境變數值直接輸出至日誌,若 `GITEA_SERVER_URL` 等變數內容被注入惡意字元或過長,可能造成 Log Injection 或日誌系統資源耗盡。
**建議**:在輸出前進行 sanitization,移除控制字元並限制長度,類似於 `gitea-client.js` 中的 `sanitizeBody`。
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Maya
**問題**:雖然有 requireUrl 驗證,但在 loadConfig 中,對於 GITEA_SERVER_URL 的解析假設其為完整路徑。若環境變數提供的網址不含 api/v1 或路徑有變化,整合後的 releaseApiUrl 可能無效。
**建議**:建議在 loadConfig 中增加對 serverUrl 的處理,確保結尾沒有多餘的 /,避免拼接 API 路徑時產生類似 //api 的錯誤路徑。
|
||||
requireValue('GITEA_SERVER_URL', serverUrl)
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Assassin
**問題**:僅驗證是否為 URL 格式,未檢查傳入的 `GITEA_SERVER_URL` 是否指向內部網路敏感資源(如 localhost, 169.254.169.254 等),這在容器化環境中可能導致 SSRF(伺服器端請求偽造)。
**建議**:在 `validate.js` 的 `requireUrl` 中加入黑名單機制,禁止解析為內部 IP 位址或 loopback 位址。
|
||||
requireUrl('GITEA_SERVER_URL', serverUrl)
|
||||
|
Ghost marked this conversation as resolved
Outdated
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Maya
**問題**:參數 `GITEA_TOKEN` 若提供,在 `loadConfig` 中只會檢查其是否為空,且為了資安會遮蔽輸出。這很棒,但缺乏對 `GITEA_TOKEN` 格式或長度的邊界測試。
**建議**:在 `app/test/config.test.js` 中增加測試案例,針對 `GITEA_TOKEN` 為極短字串(如空字串、單一字元)或極長字串進行測試,確認系統行為符合預期。
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Maya
**問題**:缺乏對 GITEA_TOKEN 長度或格式的邊界測試,以及對 KEEP_COUNT 格式異常的檢查。
**建議**:在測試檔中增加針對 Token 與 KEEP_COUNT 的邊界測試,並在 loadConfig 內加強格式轉換檢查。
|
||||
|
||||
info(`GITEA_REPOSITORY=${repository}`)
|
||||
requireValue('GITEA_REPOSITORY', repository)
|
||||
|
Ghost marked this conversation as resolved
Outdated
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Assassin
**問題**:GITEA_REPOSITORY 變數直接被拼接進 API URL 中,卻未進行任何消毒。攻擊者可利用 Path Traversal(例如設定為 ../../other-repo)繞過權限,導致 API 呼叫目標被竄改,進而執行未授權的動作。
**建議**:必須對 GITEA_REPOSITORY 進行格式驗證,僅允許合法的 Repository 名稱字元(例如英數字、`-`、`_`、`/`),且必須拒絕包含 `..` 或其他路徑穿越字元的輸入。
|
||||
requireRepository('GITEA_REPOSITORY', repository)
|
||||
|
||||
info(`KEEP_COUNT=${keepCountRaw}`)
|
||||
requireValue('KEEP_COUNT', keepCountRaw)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// 與 Gitea API 溝通的 HTTP 客戶端,封裝認證標頭、分頁讀取與刪除請求。
|
||||
// 對應原本 entrypoint.sh 的 fetch_all_pages 與 curl DELETE 呼叫。
|
||||
|
||||
// 單一 HTTP 請求的逾時(毫秒)。避免 API 緩慢或掛起時容器永久卡死。
|
||||
const REQUEST_TIMEOUT_MS = 30000
|
||||
|
||||
/**
|
||||
* 與 Gitea REST API 溝通的輕量 HTTP 客戶端,負責帶上認證標頭、分頁讀取清單與發出刪除請求。
|
||||
* 取代原 bash 版本以 `curl`/`jq` 進行的 API 操作。
|
||||
@@ -34,10 +37,25 @@ export class GiteaClient {
|
||||
|
||||
while (true) {
|
||||
const url = `${baseUrl}?page=${page}`
|
||||
const res = await fetch(url, { headers: this.headers })
|
||||
const res = await fetch(url, {
|
||||
headers: this.headers,
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`GET ${url} failed: HTTP ${res.status}`)
|
||||
const body = await res.text().catch(() => '')
|
||||
throw new Error(
|
||||
`GET ${url} failed: HTTP ${res.status}${body ? ` - ${body.slice(0, 200)}` : ''}`,
|
||||
|
Ghost marked this conversation as resolved
Outdated
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Assassin
**問題**:將 HTTP 錯誤回應內容不加淨化地直接寫入錯誤訊息。攻擊者可能構造特殊的錯誤回應,透過 log 注入或後續錯誤處理機制進行惡意利用。
**建議**:錯誤訊息應限制內容長度(已做到),建議進一步剝離 HTML 標籤,或僅記錄必要的狀態碼與錯誤類型,避免直接紀錄原始回應內容。
|
||||
)
|
||||
}
|
||||
|
||||
// 確認回應確實是 JSON,避免 API 回傳 HTML 錯誤頁時 res.json() 拋出難以理解的 SyntaxError。
|
||||
const contentType = res.headers.get('content-type') || ''
|
||||
if (!contentType.includes('application/json')) {
|
||||
|
Ghost marked this conversation as resolved
Outdated
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Mage
**問題**:fetchAllPages 的迴圈終止條件僅檢查 `!Array.isArray(items) || items.length === 0`。若 API 因為某種狀況(如認證過期或 API 內部錯誤)回傳了非陣列格式的 JSON 錯誤訊息,會直接視為「最後一頁」而提前終止迴圈,導致回傳不完整的清單且未報錯。
**建議**:應在確認 res.ok 為 true 的前提下,嚴格要求 items 為陣列。若 res.ok 為 true 但 items 不為陣列,應拋出錯誤而非將其視為終止訊號。
|
||||
const body = await res.text().catch(() => '')
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Mage
**問題**:await res.json() 若遇到 API 回傳格式不符的內容(即使 content-type 為 application/json),會直接拋出 SyntaxError,且因為此處未以 try-catch 包裹,會導致程式崩潰並遺失錯誤發生的 URL 上下文。
**建議**:將 await res.json() 包裹在 try-catch 區塊中,解析失敗時捕捉錯誤並拋出包含當前請求 URL 的明確錯誤訊息。
|
||||
throw new Error(
|
||||
`GET ${url} returned non-JSON content-type "${contentType}": ${body.slice(0, 200)}`,
|
||||
)
|
||||
}
|
||||
|
Ghost marked this conversation as resolved
Outdated
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Bard
**問題**:在 `fetchAllPages` 中使用 `all.push(...items)` 展開處理分頁資料,若 API 回傳的項目數量龐大(例如數千筆),極可能觸發「超過呼叫堆疊最大長度」(Maximum call stack size exceeded)導致程式崩潰,這對長期運行的工具而言是一大隱憂。
**建議**:建議改用簡單的迴圈 `for (const item of items) { all.push(item); }` 來逐一加入項目,這能完美規避展開運算子在處理大型陣列時的堆疊風險,讓程式運行得更加穩健優雅。
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Bard
**問題**:fetchAllPages 採展開運算子處理分頁資料,面對龐大項目數量可能導致 Maximum call stack size exceeded 錯誤,並伴隨無窮迴圈風險 (缺少 MAX_PAGES 限制)。
**建議**:改用簡單的 for...of 迴圈逐一 push 項目以規避堆疊風險,並務必加入 MAX_PAGES 常數作為安全斷點,防止 API 異常導致無限迴圈與資源耗盡。
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Bard
**問題**:fetchAllPages 採展開運算子處理分頁資料,面對龐大項目數量可能導致 Maximum call stack size exceeded 錯誤,並伴隨無窮迴圈風險 (缺少 MAX_PAGES 限制)。
**建議**:改用簡單的 for...of 迴圈逐一 push 項目以規避堆疊風險,並務必加入 MAX_PAGES 常數作為安全斷點,防止 API 異常導致無限迴圈與資源耗盡。
|
||||
|
||||
const items = await res.json()
|
||||
@@ -58,7 +76,11 @@ export class GiteaClient {
|
||||
* @returns {Promise<number>} 回應的 HTTP 狀態碼(成功刪除通常為 204)
|
||||
*/
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Assassin
**問題**:攻擊者可透過惡意伺服器回傳極大的 JSON 資料,利用此處未限制大小的 res.json() 直接解析整個回應,導致記憶體耗盡 (DoS)。
**建議**:應對 API 回應設定明確的大小上限,超過時拒絕解析並拋出異常。
|
||||
async deleteResource(url) {
|
||||
const res = await fetch(url, { method: 'DELETE', headers: this.headers })
|
||||
const res = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: this.headers,
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||
})
|
||||
return res.status
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { separator, fail } from './logger.js'
|
||||
*
|
||||
* @returns {Promise<void>} 流程完成時 resolve;設定驗證或讀取 API 失敗時 reject。
|
||||
*/
|
||||
async function main() {
|
||||
export async function main() {
|
||||
|
Ghost marked this conversation as resolved
Outdated
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Maya
**問題**:main 函式作為整個流程的入口,缺乏端對端(E2E)或整合測試,無法確保各模組整合後在失敗情境下(如 process.exit(1) 的觸發)是否能正確運作。
**建議**:針對 main 函式建立整合測試,至少驗證在拋出例外時,logger.fail 是否有被呼叫且程序是否以錯誤代碼結束。
|
||||
const config = loadConfig()
|
||||
const client = new GiteaClient({ token: config.token })
|
||||
|
||||
@@ -24,7 +24,26 @@ async function main() {
|
||||
separator()
|
||||
}
|
||||
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Mage
**問題**:當 `main().catch(...)` 捕捉到錯誤並執行 `process.exit(1)` 時,僅輸出 `error.message`。若錯誤是由 `fetch` 網路層拋出(例如 DNS 解析失敗或 connection refused),Node.js 的 `Error` 物件可能不包含足夠的上下文訊息,導致使用者難以區分是 API 回傳錯誤還是程式碼執行期錯誤。
**建議**:建議在 `catch` 區塊中,若錯誤是 `Error` 物件,輸出 `error.stack` 或至少記錄錯誤類型,並區分不同層級的例外(例如:驗證錯誤 vs. API 請求錯誤)。
|
||||
/**
|
||||
* 將錯誤輸出至 stderr,並盡量保留可供除錯的上下文(錯誤類型與堆疊),
|
||||
* 以便區分設定驗證錯誤與網路/API 請求錯誤。
|
||||
* @param {unknown} error 捕捉到的錯誤
|
||||
*/
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Maya
**問題**:main 函式執行失敗時會觸發 process.exit(1),確保 CI/CD 流程能正確偵測錯誤。目前的測試僅驗證了 Promise 被 reject,但並未驗證程式是否真的正確以非零狀態碼結束。
**建議**:請在 app/test/main.test.js 中,模擬 main 拋出錯誤的情境,並透過 mock process.exit 來驗證當 main 執行失敗時,程式碼確實執行了 process.exit(1)。
|
||||
function reportFatal(error) {
|
||||
if (error instanceof Error) {
|
||||
fail(`${error.name}: ${error.message}`)
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Leo
**問題**:`reportFatal` 直接操作 `process.stderr` 並在 `main().catch` 中手動調用,這與 `logger.js` 中定義的 `fail` 函式職責重疊,這會在未來增加日誌格式變更的維護成本。
**建議**:建議直接呼叫 `fail` 函式,並在 `logger.js` 中根據錯誤類型(是否為 Error 物件)自動處理堆疊追蹤資訊,將「如何輸出錯誤」的邏輯全部收斂至 `logger.js`。
|
||||
if (error.stack) {
|
||||
process.stderr.write(`${error.stack}\n`)
|
||||
}
|
||||
} else {
|
||||
fail(String(error))
|
||||
}
|
||||
}
|
||||
|
||||
// 僅在直接以 `node index.js` 執行時啟動主流程;被測試 import 時不自動執行,方便撰寫整合測試。
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch((error) => {
|
||||
fail(error.message)
|
||||
reportFatal(error)
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Maya
**問題**:專案雖然新增了 `app/test/` 資料夾與多個測試檔案,但目前沒看到任何整合測試,且 `app/index.js` 的 `main()` 邏輯未被驗證過,這意味著最核心的清理流程尚未受到測試保護。
**建議**:請在 `app/test/` 中新增整合測試,模擬 `loadConfig` 回傳假設定後,確認 `cleanupReleases` 與 `cleanupOrphanTags` 有被正確呼叫(例如透過 mock GiteaClient),確保流程串接無誤。
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,9 +12,12 @@ import { isEmptyOrNull } from './validate.js'
|
||||
* @returns {any[]} 需要刪除的成品陣列(較舊者),依新到舊排序
|
||||
*/
|
||||
export function selectReleasesToDelete(releases, keepCount) {
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Leo
**問題**:使用 `new Date(release.created_at)` 進行排序時,若 `created_at` 格式非預期或為空,會導致 `NaN` 並造成排序異常,可能無法正確刪除舊版本。
**建議**:建議在排序邏輯中增加對 `created_at` 的有效性檢查,若無效則給予預設值(例如:`new Date(0)`),確保排序結果可預期。
|
||||
const sorted = [...releases].sort(
|
||||
(a, b) => new Date(b.created_at) - new Date(a.created_at),
|
||||
)
|
||||
// 解析 created_at;格式無效或缺漏時視為最舊(0),避免 NaN 造成排序結果不可預期。
|
||||
const createdTime = (release) => {
|
||||
const time = Date.parse(release?.created_at)
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Rogue
**問題**:selectReleasesToDelete 針對每一筆 release 重複執行 Date.parse,浪費 CPU 週期。
**建議**:應在排序前先執行一次 map 轉換,預先計算時間戳記。
|
||||
return Number.isNaN(time) ? 0 : time
|
||||
}
|
||||
const sorted = [...releases].sort((a, b) => createdTime(b) - createdTime(a))
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Leo
**問題**:在 `selectReleasesToDelete` 函式中,對於 `Date.parse` 無法解析的日期直接視為 `0`,這在資料清理邏輯中是一個隱晦的行為,未來維護者可能不清楚為什麼無效日期會優先被刪除。
**建議**:應明確記錄無效日期的處理方式(例如記錄 warning),或在 `Date.parse` 失敗時,應考慮給予一個明確的邏輯(例如拋出錯誤或放到特定排序位置),以減少不可預期的副作用。
|
||||
return sorted.slice(keepCount)
|
||||
|
Ghost marked this conversation as resolved
Outdated
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Maya
**問題**:cleanupReleases 是執行刪除舊成品的核心函式,但目前缺乏針對 API 呼叫失敗(如 GET 失敗、DELETE 失敗)的測試,無法驗證錯誤處理邏輯是否如預期運作。
**建議**:使用測試框架搭配 mock 伺服器回應,補齊 cleanupReleases 的測試案例,特別是驗證當 deleteResource 回傳非 204 狀態碼時,系統是否正確記錄錯誤並繼續或中斷。
|
||||
}
|
||||
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Rogue
**問題**:為了排序而進行了多次 map 操作,先將陣列 map 為物件陣列,排序後又 map 回原始物件陣列。這在大數據集下會造成多次 O(n) 的陣列分配與垃圾回收壓力,浪費 CPU 與記憶體週期。
**建議**:排序時應嘗試減少陣列中間狀態的產生,例如考慮使用原地排序(若不介意原陣列變更)或簡化 mapping 的邏輯。
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Mage
**問題**:在 `selectReleasesToDelete` 的排序邏輯中,若多個成品具有完全相同的 `created_at` 時間戳,目前的排序行為依賴於 JavaScript 引擎對 `sort()` 的實作(在某些情況下可能不穩定),導致保留與刪除的成品選擇具有不確定性。
**建議**:建議在排序邏輯中加入次要的排序鍵值(如 `id` 或 `tag_name`)作為比較依據(例如:若時間相同,則比較 ID 大小),以確保排序結果在時間相同時仍具有決定性。
|
||||
|
||||
@@ -34,3 +34,35 @@ export function requireInteger(name, value) {
|
||||
throw new Error(`${name} must be a non-negative integer`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 要求值為合法的 http/https URL,藉此避免設定來源指向格式錯誤或非預期協定的伺服器(降低 SSRF 風險)。
|
||||
* @param {string} name 欄位名稱,用於組出錯誤訊息
|
||||
* @param {string} value 待檢查的 URL 字串
|
||||
* @throws {Error} 當 value 無法解析為 URL 時丟出 `${name} must be a valid URL`;
|
||||
* 協定非 http/https 時丟出 `${name} must use http or https protocol`
|
||||
*/
|
||||
export function requireUrl(name, value) {
|
||||
let url
|
||||
try {
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Leo
**問題**:雖然驗證邏輯完整,但 `requireUrl` 和 `requireRepository` 的規則直接寫死在函式內。如果未來有其他的 API 端點需要不同的驗證規則,會產生大量重複程式碼。
**建議**:考慮將驗證規則(Regex 或協定清單)提取為設定物件或共用常數,這能讓維護者一眼看出系統允許的 URL 限制,方便未來擴充。
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Leo
**問題**:驗證規則寫死在函式內,易產生重複程式碼,且缺乏擴充性。
**建議**:將驗證規則提取為設定物件或共用常數,或考慮使用 Zod 等 Schema 套件進行驗證與型別轉換。
|
||||
url = new URL(value)
|
||||
} catch {
|
||||
throw new Error(`${name} must be a valid URL`)
|
||||
}
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||
throw new Error(`${name} must use http or https protocol`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 要求值為合法的 `owner/repo` 形式:僅允許英數字與 `. _ -`、恰好一個 `/`,且不可含路徑穿越片段 `..`。
|
||||
* 用於防止 GITEA_REPOSITORY 被竄改造成 API 目標被導向其他儲存庫。
|
||||
* @param {string} name 欄位名稱,用於組出錯誤訊息
|
||||
* @param {string} value 待檢查的 repository 字串
|
||||
* @throws {Error} 格式不符或含 `..` 時丟出 `${name} must be in the form owner/repo without path traversal`
|
||||
*/
|
||||
export function requireRepository(name, value) {
|
||||
if (String(value).includes('..') || !/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(value)) {
|
||||
throw new Error(`${name} must be in the form owner/repo without path traversal`)
|
||||
}
|
||||
}
|
||||
|
||||
嚴重等級:🟡 警告
審查員:Maya
問題:loadConfig 函式雖有呼叫驗證邏輯,但缺乏針對環境變數異常情境(如必填欄位缺失、KEEP_COUNT 非整數)的單元測試,無法確保配置載入流程的穩定性。
建議:補齊 app/test/config.test.js,測試當 process.env 缺少必要參數或 KEEP_COUNT 為無效數字時,loadConfig 是否會正確拋出錯誤。