feat: AI Pull Request action — 以 opencode 自動產生並建立 PR #1
+7
-3
@@ -2,7 +2,7 @@ import { loadInputs, logInputs } from './lib/inputs.js';
|
||||
import { Git } from './lib/git.js';
|
||||
import { GiteaClient } from './lib/gitea.js';
|
||||
import { OpenCode } from './lib/opencode.js';
|
||||
import { log } from './lib/util.js';
|
||||
import { log, maskSecrets } from './lib/util.js';
|
||||
|
||||
async function main() {
|
||||
|
Ghost marked this conversation as resolved
|
||||
const inputs = loadInputs();
|
||||
@@ -143,7 +143,9 @@ function buildResolveBranchName(target, source) {
|
||||
const runId = process.env.GITHUB_RUN_NUMBER || process.env.GITHUB_RUN_ID || '';
|
||||
const safe = (s) => s.replace(/[^a-zA-Z0-9._/-]/g, '-');
|
||||
const suffix = runId ? `-${runId}` : '';
|
||||
return `resolve-conflict/${safe(target)}-into-${safe(source)}${suffix}`;
|
||||
// 截斷主體長度,避免 target/source 過長使分支名稱超出 Git 限制
|
||||
const stem = `${safe(target)}-into-${safe(source)}`.slice(0, 180);
|
||||
return `resolve-conflict/${stem}${suffix}`;
|
||||
}
|
||||
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Maya
**問題**:對於 `fallbackSummary` 函數,當 opencode 產生摘要失敗時會觸發,但目前缺乏測試案例驗證在各種輸入下,fallback 的結果是否符合預期格式。
**建議**:補上單元測試,驗證 `fallbackSummary` 在不同輸入下(如為空、多行訊息、stat 為空)產生的標題與描述格式是否正確。
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Maya
**問題**:缺乏測試案例驗證 fallbackSummary 的結果是否符合預期格式。
**建議**:補上單元測試,驗證 fallbackSummary 在不同輸入下的產出格式。
|
||||
/** 解衝突 PR 的描述。 */
|
||||
@@ -180,6 +182,8 @@ function reportPull(pull, created) {
|
||||
}
|
||||
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Assassin
**問題**:錯誤處理中的 `maskSecrets` 基於字串取代,可能無法處理所有 Token 變體導致敏感資訊洩漏。
**建議**:確保 `maskSecrets` 處理所有可能的變體,並在生產環境中禁止輸出原始錯誤物件。
|
||||
main().catch((err) => {
|
||||
log.error(err?.stack || err?.message || String(err));
|
||||
const detail = err?.stack || err?.message || String(err);
|
||||
// 錯誤訊息/stack 可能夾帶 token,輸出到 CI 日誌前先遮蔽
|
||||
log.error(maskSecrets(detail, [process.env.GITEA_TOKEN]));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
+31
-26
@@ -1,6 +1,6 @@
|
||||
import { writeFileSync, mkdtempSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { writeFileSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir, homedir } from 'node:os';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { run, log, maskSecrets } from './util.js';
|
||||
|
||||
/**
|
||||
@@ -74,33 +74,38 @@ export class OpenCode {
|
||||
const configPath = this._writeConfig();
|
||||
const prompt = buildPrompt({ ...ctx, language: this.language });
|
||||
|
||||
log.info(`呼叫 opencode(${this.provider}/${this.model})分析 diff...`);
|
||||
const result = run(
|
||||
'opencode',
|
||||
['run', '--model', `${this.provider}/${this.model}`, prompt],
|
||||
{
|
||||
cwd: tmpdir(),
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCODE_CONFIG: configPath,
|
||||
// 確保 opencode 有可寫的 HOME / 設定目錄
|
||||
HOME: process.env.HOME || '/root',
|
||||
try {
|
||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Assassin
**問題**:呼叫外部指令時傳遞整個 `process.env`,導致敏感環境變數暴露。
**建議**:應明確篩選並只傳遞必要環境變數。
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Assassin
**問題**:傳遞整個 `process.env` 導致敏感環境變數暴露。
**建議**:明確篩選並只傳遞必要環境變數。
|
||||
log.info(`呼叫 opencode(${this.provider}/${this.model})分析 diff...`);
|
||||
const result = run(
|
||||
'opencode',
|
||||
['run', '--model', `${this.provider}/${this.model}`, prompt],
|
||||
{
|
||||
cwd: tmpdir(),
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCODE_CONFIG: configPath,
|
||||
// 確保 opencode 有可寫的 HOME / 設定目錄
|
||||
HOME: process.env.HOME || homedir(),
|
||||
},
|
||||
timeout: 5 * 60 * 1000,
|
||||
},
|
||||
timeout: 5 * 60 * 1000,
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
if (result.status !== 0) {
|
||||
log.warn(`opencode 執行失敗 (${result.status}):${maskSecrets(result.stderr).slice(0, 500)}`);
|
||||
return null;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
log.warn(`opencode 執行失敗 (${result.status}):${maskSecrets(result.stderr).slice(0, 500)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = extractResult(result.stdout);
|
||||
if (!parsed) {
|
||||
log.warn('無法從 opencode 輸出解析出標題/描述');
|
||||
return null;
|
||||
const parsed = extractResult(result.stdout);
|
||||
if (!parsed) {
|
||||
log.warn('無法從 opencode 輸出解析出標題/描述');
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} finally {
|
||||
// 清理 _writeConfig 建立的暫存設定目錄,避免堆積
|
||||
|
Ghost marked this conversation as resolved
Outdated
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Mage
**問題**:在 `summarize` 方法中將 `HOME` 環境變數硬編碼為 `/root`,若 Dockerfile 變更使用者,將導致無法寫入設定檔。
**建議**:建議動態獲取當前環境的使用者家目錄(如使用 `os.homedir()`),增加相容性。
|
||||
rmSync(dirname(configPath), { recursive: true, force: true });
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user
嚴重等級:🟡 警告
審查員:Leo
問題:函式
main()承擔過多責任,違反單一職責原則。建議:將職責拆解,抽離衝突處理邏輯為
ConflictManager,並封裝 Gitea API 互動。