Files
code-review/app/preflight.js
T
Jeffery 9d780788e9
AI / 計算版本號 (pull_request) Successful in 4s
AI / Code Review (pull_request) Failing after 1m42s
test: 補齊 runPreflight 測試並 triage preflight findings
triage 6 筆 review findings:1 筆修正、5 筆移入 exclusions。

修正(Maya, warning):runPreflight 僅測過 env 缺失早退,缺成功路徑與
各失敗點覆蓋。將其驗證步驟改為可注入的 deps 參數(預設沿用原函式,
行為不變),並補上完整成功、comment 略過、各失敗點早停、workspace
傳遞共 8 個測試。

移入 exclusions(誤報,保留原文):
- Rex critical:GITEA_SKIP_TLS_VERIFY 為預設開啟驗證的 opt-in 設定,
  與既有 gitea.js 排除一致,非漏洞
- Leo warning:verifyLLM 內聚清楚,拆分屬主觀重構
- Zara warning:每把 key 30s timeout 為刻意的可靠性下限,僅失敗時累積
- Rex info:axios 錯誤訊息不含認證標頭/內容
- Aria info:預設參數引用 config 常數為刻意且利於測試的 pattern

findings.json 清空(全部已修正或排除)。app/ 測試 112 pass。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:49:30 +08:00

146 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import axios from 'axios';
import https from 'https';
import {
GITEA_TOKEN,
GITEA_COMMENT_TOKEN,
GITEA_SERVER_URL,
GITEA_REPOSITORY,
GITEA_SKIP_TLS_VERIFY,
PR_NUMBER,
getLLMConfig,
} from './config.js';
import { verifyRemoteAccess } from './git.js';
import { step, line, ok, error } from './log.js';
const httpsAgent = GITEA_SKIP_TLS_VERIFY ? new https.Agent({ rejectUnauthorized: false }) : undefined;
const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`;
const giteaHeaders = (token) => ({ Authorization: `token ${token}`, 'Content-Type': 'application/json' });
function giteaErr(e) {
const status = e.response?.status;
return status ? `HTTP ${status} ${e.message}` : e.message;
}
/** 檢查必要環境變數是否齊全;可傳入覆寫值供測試使用 */
export function checkRequiredEnv({ token = GITEA_TOKEN, repo = GITEA_REPOSITORY, pr = PR_NUMBER } = {}) {
const missing = [];
if (!token) missing.push('GITEA_TOKEN');
if (!repo) missing.push('GITEA_REPOSITORY');
if (!pr) missing.push('PR_NUMBER');
return { ok: missing.length === 0, missing };
}
/** 用 GITEA_TOKEN 讀取此 repo,同時驗證 token 有效與有讀取權限 */
export async function verifyGiteaToken(token = GITEA_TOKEN, repo = GITEA_REPOSITORY) {
try {
await axios.get(api(`/repos/${repo}`), { headers: giteaHeaders(token), timeout: 30000, httpsAgent });
return { ok: true };
} catch (e) {
return { ok: false, error: giteaErr(e) };
}
}
/** 若有提供 comment token,用它呼叫 /user 驗證可用;沒提供則略過 */
export async function verifyCommentToken(token = GITEA_COMMENT_TOKEN) {
if (!token) return { ok: true, skipped: true };
try {
await axios.get(api('/user'), { headers: giteaHeaders(token), timeout: 30000, httpsAgent });
return { ok: true };
} catch (e) {
return { ok: false, error: giteaErr(e) };
}
}
/**
* 驗證 LLM 設定可用:
* - 須已選定一個 provider
* - Ollama 檢查 base URL 是否可連線
* - 其餘 provider 以最小請求驗證認證,多把 Key 只要一把成功即可
*/
export async function verifyLLM() {
const { provider, apiKeys, baseURL, model } = getLLMConfig();
if (!provider) return { ok: false, error: '未設定任何 LLM provider 或 API Key' };
if (!baseURL) return { ok: false, provider, error: `${provider} 缺少 base URL` };
const base = baseURL.replace(/\/$/, '');
if (provider === 'ollama') {
try {
await axios.get(`${base}/models`, { timeout: 30000 });
return { ok: true, provider };
} catch (e) {
return { ok: false, provider, error: `Ollama base URL 無法連線: ${e.message}` };
}
}
const headers = { 'Content-Type': 'application/json' };
if (provider === 'claude') headers['anthropic-version'] = '2023-06-01';
const payload = { model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, temperature: 0 };
for (let i = 0; i < apiKeys.length; i++) {
headers['Authorization'] = `Bearer ${apiKeys[i]}`;
try {
await axios.post(`${base}/chat/completions`, payload, { headers, timeout: 30000 });
return { ok: true, provider, keyIndex: i + 1, total: apiKeys.length };
} catch (e) {
line(`[preflight] LLM key[${i + 1}/${apiKeys.length}] 驗證失敗: ${e.message}`);
}
}
return { ok: false, provider, error: `所有 ${apiKeys.length}${provider} API Key 驗證失敗` };
}
/**
* 集中執行所有驗證相關設定的前置檢查;全部通過回傳 true,任一失敗回傳 false。
* 僅做唯讀的認證/連線確認,不發布任何 comment。
*/
export async function runPreflight(workspace = process.env.GITHUB_WORKSPACE || '/workspace', deps = {}) {
const {
checkEnv = checkRequiredEnv,
verifyToken = verifyGiteaToken,
verifyComment = verifyCommentToken,
verifyRemote = verifyRemoteAccess,
verifyLLMFn = verifyLLM,
} = deps;
step('Step1.5', '前置驗證(驗證相關設定)');
const env = checkEnv();
if (!env.ok) {
error(`缺少必要環境變數: ${env.missing.join(', ')}`);
return false;
}
ok('必要環境變數齊全 (GITEA_TOKEN, GITEA_REPOSITORY, PR_NUMBER)');
const gitea = await verifyToken();
if (!gitea.ok) {
error(`GITEA_TOKEN 驗證失敗(無法讀取 repo ${GITEA_REPOSITORY}: ${gitea.error}`);
return false;
}
ok(`GITEA_TOKEN 可讀取 repo ${GITEA_REPOSITORY}`);
const comment = await verifyComment();
if (!comment.ok) {
error(`GITEA_COMMENT_TOKEN 驗證失敗: ${comment.error}`);
return false;
}
if (comment.skipped) line('未提供 GITEA_COMMENT_TOKENcomment 將沿用 GITEA_TOKEN');
else ok('GITEA_COMMENT_TOKEN 可用');
const remote = verifyRemote(workspace);
if (!remote.ok) {
error(`git push 認證/連線驗證失敗(ls-remote: ${remote.error}`);
return false;
}
ok('git remote 認證可用(ls-remote 成功)');
const llm = await verifyLLMFn();
if (!llm.ok) {
error(`LLM 驗證失敗: ${llm.error}`);
return false;
}
if (llm.keyIndex) ok(`LLM provider=${llm.provider} 驗證通過(key ${llm.keyIndex}/${llm.total}`);
else ok(`LLM provider=${llm.provider} 連線正常`);
ok('前置驗證通過');
return true;
}