fix(watch): normalize SSH repo identity paths

This commit is contained in:
weiyf 2026-08-26 16:53:09 +08:00
parent 3b590acc79
commit e1d8c65f8c
7 changed files with 60 additions and 7 deletions

View file

@ -5,6 +5,7 @@ import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { AnalyzeOptions, AnalyzeResult } from '../run-analyze.js';
import type { WorkerMessage } from '../../server/analyze-worker.js';
import { autoHeapCapMb } from '../ingestion/utils/effective-ram.js';
const _require = createRequire(import.meta.url);
const TERMINATION_GRACE_MS = 10_000;
@ -58,9 +59,14 @@ export function createAutoSyncAnalysisRunner(
reject(new Error(`Auto-sync analyze worker is missing: ${workerPath}`));
return;
}
const workerHeapMb = Math.min(8192, autoHeapCapMb());
const execArgv = isDev
? ['--import', pathToFileURL(_require.resolve('tsx/esm')).href, '--max-old-space-size=8192']
: ['--max-old-space-size=8192'];
? [
'--import',
pathToFileURL(_require.resolve('tsx/esm')).href,
`--max-old-space-size=${workerHeapMb}`,
]
: [`--max-old-space-size=${workerHeapMb}`];
const child = deps.forkWorker(workerPath, execArgv);
child.stdout?.resume();
child.stderr?.resume();

View file

@ -246,7 +246,11 @@ export function parseAutoSyncConfig(content: string, configPath: string): AutoSy
}
export function validateAutoSyncRemoteUrl(remoteUrl: string): void {
const match = /^git@([^:\s/]+):([^\s]+)$/.exec(remoteUrl.trim());
const trimmed = remoteUrl.trim();
if (trimmed.includes('?') || trimmed.includes('#')) {
throw new Error('must not include query strings or fragments');
}
const match = /^git@([^:\s/]+):([^\s]+)$/.exec(trimmed);
if (!match) {
throw new Error(
'must use an SSH URL on github.com, gitlab.com, or gitee.com',

View file

@ -117,6 +117,10 @@ export function validateGitUrl(url: string): void {
throw new Error('Only https:// and http:// git URLs are allowed');
}
if (parsed.search || parsed.hash) {
throw new Error('Git URLs must not include query strings or fragments');
}
const host = parsed.hostname.toLowerCase();
// Block known dangerous hostnames (cloud metadata services)

View file

@ -1,5 +1,11 @@
import { EventEmitter } from 'node:events';
import { describe, expect, it, vi } from 'vitest';
const { autoHeapCapMbMock } = vi.hoisted(() => ({ autoHeapCapMbMock: vi.fn(() => 512) }));
vi.mock('../../src/core/ingestion/utils/effective-ram.js', () => ({
autoHeapCapMb: autoHeapCapMbMock,
}));
import { createAutoSyncAnalysisRunner } from '../../src/core/auto-sync/analysis-worker-launch.js';
describe('auto-sync analysis worker', () => {
@ -10,9 +16,14 @@ describe('auto-sync analysis worker', () => {
stdout: { resume: vi.fn() },
stderr: { resume: vi.fn() },
});
const run = createAutoSyncAnalysisRunner({ forkWorker: vi.fn(() => child as any) });
const forkWorker = vi.fn(() => child as any);
const run = createAutoSyncAnalysisRunner({ forkWorker });
const result = run('/tmp/repo', { branch: 'main' }, 50);
expect(forkWorker).toHaveBeenCalledWith(
expect.any(String),
expect.arrayContaining(['--max-old-space-size=512']),
);
child.emit('message', { type: 'progress', phase: 'parsing', progress: 20 });
child.emit('message', { type: 'complete', result: { stats: { files: 3 } } });
child.emit('exit', 0, null);

View file

@ -452,6 +452,12 @@ describe('auto-sync', () => {
expect(() => validateAutoSyncRemoteUrl('git@example.com:owner/repo.git')).toThrow(
'host must be',
);
expect(() => validateAutoSyncRemoteUrl('git@github.com:owner/repo.git?ref=main')).toThrow(
'must not include query strings or fragments',
);
expect(() => validateAutoSyncRemoteUrl('git@github.com:owner/repo.git#main')).toThrow(
'must not include query strings or fragments',
);
});
it('parses repo git timeout durations', () => {

View file

@ -84,11 +84,24 @@ describe('file lock', () => {
await nextRelease();
});
it('fails closed for a legacy or invalid lock without owner metadata', async () => {
it('fails closed for legacy or invalid lock contents without owner metadata', async () => {
const lockPath = await tempLockPath();
await fs.mkdir(lockPath, { recursive: true });
const invalidContents = ['legacy lock', '{not json', JSON.stringify({ pid: 123 })];
await fs.mkdir(path.dirname(lockPath), { recursive: true });
await expect(acquireFileLock(lockPath)).rejects.toBeInstanceOf(FileLockBusyError);
for (const content of invalidContents) {
await fs.writeFile(lockPath, content, 'utf-8');
await expect(
acquireFileLock(lockPath, { pid: 456, processStartTime: 'next-start' }),
).rejects.toBeInstanceOf(FileLockBusyError);
await expect(fs.readFile(lockPath, 'utf-8')).resolves.toBe(content);
await fs.rm(lockPath);
}
await fs.mkdir(lockPath, { recursive: true });
await expect(
acquireFileLock(lockPath, { pid: 456, processStartTime: 'next-start' }),
).rejects.toBeInstanceOf(FileLockBusyError);
await expect(fs.access(lockPath)).resolves.toBeUndefined();
});

View file

@ -197,6 +197,15 @@ describe('git-clone', () => {
expect(() => validateGitUrl('http://gitlab.com/user/repo.git')).not.toThrow();
});
it('rejects query strings and fragments instead of reinterpreting clone remotes', () => {
expect(() => validateGitUrl('https://github.com/user/repo.git?ref=main')).toThrow(
'must not include query strings or fragments',
);
expect(() => validateGitUrl('https://github.com/user/repo.git#main')).toThrow(
'must not include query strings or fragments',
);
});
it('blocks SSH protocol', () => {
expect(() => validateGitUrl('ssh://git@github.com/user/repo.git')).toThrow(
'Only https:// and http://',