feat: 前置驗證納入 git push 認證檢查
AI / Code Review (pull_request) Failing after 2m48s
AI / 計算版本號 (pull_request) Successful in 2s

git push 走 askpass + HTTP 認證,與 Gitea REST API 是兩套機制,API token
有效不代表 push 能用(曾出現 askpass 無法執行、could not read Username 而
push 失敗)。新增 git.js verifyRemoteAccess() 以相同 askpass + remote URL
跑唯讀 git ls-remote,preflight 呼叫並在失敗時 exit 1,提前攔下設定問題。

新增 git.test.js 對 verifyRemoteAccess 的測試(成功、失敗不丟例外、token
不外洩、askpass 清理)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-06-15 13:39:04 +08:00
parent 00f5bc7dae
commit 40ebfe99a8
6 changed files with 79 additions and 7 deletions
+45 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { commitAndPush, cloneRepo, SYNC_PATHS, BOT_COMMIT_MARKER, getHeadCommitMessage, isBotAutoCommit, mergeInstructionText } from './git.js';
import { commitAndPush, cloneRepo, verifyRemoteAccess, SYNC_PATHS, BOT_COMMIT_MARKER, getHeadCommitMessage, isBotAutoCommit, mergeInstructionText } from './git.js';
// --- helpers ---
function makeTmpWorkspace() {
@@ -334,3 +334,47 @@ describe('cloneRepo', () => {
assert.equal(isBotAutoCommit(workspace, spawn), true);
});
});
describe('verifyRemoteAccess', () => {
let workspace;
before(() => { workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'git-lsremote-')); });
after(() => { fs.rmSync(workspace, { recursive: true, force: true }); });
it('runs git ls-remote with the askpass credential env and reports ok on success', () => {
const calls = [];
const spawn = (cmd, args, opts) => {
calls.push({ cmd, args, opts });
return { status: 0, stdout: 'abc123\tHEAD', stderr: '', error: null };
};
const result = verifyRemoteAccess(workspace, spawn);
assert.deepEqual(result, { ok: true });
const lsRemote = calls.find(c => c.args[0] === 'ls-remote');
assert.ok(lsRemote, 'expected git ls-remote to run');
assert.ok(lsRemote.opts?.env?.GIT_ASKPASS, 'expected GIT_ASKPASS env for ls-remote');
});
it('does not leak the token in ls-remote args', () => {
const calls = [];
const spawn = (cmd, args, opts) => {
calls.push({ args });
return { status: 0, stdout: '', stderr: '', error: null };
};
verifyRemoteAccess(workspace, spawn);
for (const { args } of calls) {
assert.ok(!args.join(' ').includes('test-token'), `Token leaked in git args: ${args.join(' ')}`);
}
});
it('reports failure (not throw) when git ls-remote fails', () => {
const spawn = () => ({ status: 128, stdout: '', stderr: 'fatal: could not read Username', error: null });
const result = verifyRemoteAccess(workspace, spawn);
assert.equal(result.ok, false);
assert.match(result.error, /could not read Username/);
});
it('cleans up the askpass script after running', () => {
verifyRemoteAccess(workspace, () => ({ status: 0, stdout: '', stderr: '', error: null }));
const leftover = fs.readdirSync(workspace).filter(f => f.endsWith('.git-askpass.sh'));
assert.equal(leftover.length, 0, 'askpass script was not cleaned up');
});
});