diff --git a/app/git.js b/app/git.js index 9a315aa..8fc9423 100644 --- a/app/git.js +++ b/app/git.js @@ -1,7 +1,7 @@ import { spawnSync } from 'child_process'; import fs from 'fs'; import path from 'path'; -import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; +import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, GITEA_COMMENT_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; import { line, ok, warn } from './log.js'; const REVIEW_FILE_PATHS = [FINDINGS_PATH, '.gitea/ai-review/exclusions.json']; @@ -50,14 +50,18 @@ function makeRunner(spawn) { * @template T * @param {string} workspace 寫入暫存 askpass 腳本的目錄。 * @param {(credEnv: NodeJS.ProcessEnv) => T} fn 帶入憑證環境變數執行的回呼。 + * @param {string} [token=GITEA_TOKEN] 供 git HTTP 認證使用的 token。預設為自動的 + * `GITEA_TOKEN`(適用於唯讀的 clone/ls-remote);需要讓 push 出來的 commit + * 重新觸發 workflow 時,呼叫端應改傳真人 PAT(`GITEA_COMMENT_TOKEN`),因為 + * Gitea 不會為「自動 token」推送的 commit 發出事件。 * @returns {T} 即 `fn` 的回傳值(Promise 會被包成 `.finally(cleanup)` 後回傳)。 * @throws 透傳 `fn` 丟出的任何例外(同步路徑會先清理暫存腳本再 re-throw)。 - * @remarks askpass 腳本以權限 0o700 寫出;token 取自 config 的 `GITEA_TOKEN`。 + * @remarks askpass 腳本以權限 0o700 寫出;token 由參數帶入並透過 `GIT_TOKEN` 提供給腳本。 */ -function withAskpass(workspace, fn) { +function withAskpass(workspace, fn, token = GITEA_TOKEN) { const askpassScript = path.join(workspace, '.git-askpass.sh'); fs.writeFileSync(askpassScript, '#!/bin/sh\necho "$GIT_TOKEN"\n', { mode: 0o700 }); - const credEnv = { ...process.env, GIT_ASKPASS: askpassScript, GIT_USERNAME: 'x-token', GIT_TOKEN: GITEA_TOKEN }; + const credEnv = { ...process.env, GIT_ASKPASS: askpassScript, GIT_USERNAME: 'x-token', GIT_TOKEN: token }; const cleanup = () => { try { fs.unlinkSync(askpassScript); } catch {} }; let result; try { @@ -203,9 +207,14 @@ export function cloneRepo(workspace, _spawnSync = spawnSync) { * @returns {Promise} 無回傳值;所有失敗皆以 log 記錄後吞掉。 * @remarks `git reset --hard origin/` 會丟棄本地未對齊變更,請確認 * review 檔是在 reset 之後才複製進來(流程已如此安排)。 + * @remarks push 優先使用真人 PAT `GITEA_COMMENT_TOKEN`(無則退回 `GITEA_TOKEN`), + * 目的是讓 bot commit 能重新觸發 PR 的 workflow:以自動 token 推送的 commit + * 不會發出 `synchronize` 事件,新 head commit 便拿不到檢查而卡住。改用 PAT + * 推送後會正常重觸發,重跑時由 main.js Step3 偵測 `[ai-review-bot]` 標記後跳過。 */ export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync, _sourceRoot = null, reviewOutcome = 'success') { const run = makeRunner(_spawnSync); + const pushToken = GITEA_COMMENT_TOKEN || GITEA_TOKEN; try { await withAskpass(workspace, async credEnv => { @@ -242,7 +251,7 @@ export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync, } catch (pushErr) { warn(`Step8 commit 成功但 push 失敗: commit=${commitHash} push=${PR_HEAD_BRANCH} review_outcome=${reviewOutcome} error=${pushErr.message}`); } - }); + }, pushToken); } catch (e) { warn(`Runner failed: commit/push 失敗: ${e.message}`); } diff --git a/app/test/git.test.js b/app/test/git.test.js index b011f0a..a4b4965 100644 --- a/app/test/git.test.js +++ b/app/test/git.test.js @@ -4,6 +4,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import { commitAndPush, cloneRepo, verifyRemoteAccess, BOT_COMMIT_MARKER, getHeadCommitMessage, isBotAutoCommit } from '../git.js'; +import { GITEA_TOKEN, GITEA_COMMENT_TOKEN } from '../config.js'; // --- helpers --- function makeTmpWorkspace() { @@ -74,6 +75,17 @@ describe('commitAndPush', () => { assert.ok(commitCall.args.some(arg => arg.includes('[failure]')), 'expected commit message to include failure outcome'); }); + it('pushes with the comment-token (PAT) so the bot commit can re-trigger the workflow', async () => { + const spawn = makeSpawn(); + await commitAndPush(workspace, path.join(workspace, 'repo'), spawn, sourceRoot); + + // push 必須帶「優先 PAT」的 token,Gitea 才會為 bot commit 重新發出 synchronize 事件。 + const expectedToken = GITEA_COMMENT_TOKEN || GITEA_TOKEN; + const pushCall = spawn.calls.find(c => c.args[0] === 'push'); + assert.ok(pushCall, 'expected git push to run'); + assert.equal(pushCall.opts?.env?.GIT_TOKEN, expectedToken, 'push must use GITEA_COMMENT_TOKEN (fallback GITEA_TOKEN)'); + }); + it('uses GIT_ASKPASS env for network operations (fetch, push, clone)', async () => { const spawn = makeSpawn(); await commitAndPush(workspace, path.join(workspace, 'repo'), spawn, sourceRoot);