feat: 前置驗證納入 git push 認證檢查
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:
+45
-1
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user