170 lines
4.8 KiB
TypeScript
170 lines
4.8 KiB
TypeScript
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { execFileSync } from 'node:child_process';
|
|
|
|
export interface LocalRepo {
|
|
path: string;
|
|
cleanup: () => void;
|
|
}
|
|
|
|
export interface TestRepo {
|
|
path: string;
|
|
repoName: string;
|
|
branch: string;
|
|
cleanup: () => void;
|
|
}
|
|
|
|
/**
|
|
* Create a local git repository in a temporary directory.
|
|
* Use this for tests that don't need a remote (GitHub) repository.
|
|
*/
|
|
export function createLocalRepo(): LocalRepo {
|
|
const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-'));
|
|
execFileSync('git', ['init'], { cwd: repoPath, stdio: 'pipe' });
|
|
execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: repoPath, stdio: 'pipe' });
|
|
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: repoPath, stdio: 'pipe' });
|
|
writeFileSync(join(repoPath, 'README.md'), '# test\n');
|
|
execFileSync('git', ['add', '.'], { cwd: repoPath, stdio: 'pipe' });
|
|
execFileSync('git', ['commit', '-m', 'init'], { cwd: repoPath, stdio: 'pipe' });
|
|
return {
|
|
path: repoPath,
|
|
cleanup: () => {
|
|
rmSync(repoPath, { recursive: true, force: true });
|
|
},
|
|
};
|
|
}
|
|
|
|
export interface CreateTestRepoOptions {
|
|
/** Skip creating a test branch (stay on default branch). Use for pipeline tests. */
|
|
skipBranch?: boolean;
|
|
}
|
|
|
|
function getGitHubUser(): string {
|
|
const user = execFileSync('gh', ['api', 'user', '--jq', '.login'], {
|
|
encoding: 'utf-8',
|
|
}).trim();
|
|
|
|
if (!user) {
|
|
throw new Error(
|
|
'Failed to get GitHub user. Make sure `gh` CLI is authenticated.',
|
|
);
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* Clone the takt-testing repository and create a test branch.
|
|
*
|
|
* Cleanup order (important):
|
|
* 1. Delete remote branch (requires local directory to exist)
|
|
* 2. Close any PRs created during the test
|
|
* 3. Delete local directory
|
|
*/
|
|
export function createTestRepo(options?: CreateTestRepoOptions): TestRepo {
|
|
const user = getGitHubUser();
|
|
const repoName = `${user}/takt-testing`;
|
|
|
|
// Verify repository exists
|
|
try {
|
|
execFileSync('gh', ['repo', 'view', repoName], {
|
|
encoding: 'utf-8',
|
|
stdio: 'pipe',
|
|
});
|
|
} catch {
|
|
throw new Error(
|
|
`Repository "${repoName}" not found. Please create it first:\n` +
|
|
` gh repo create takt-testing --private --add-readme`,
|
|
);
|
|
}
|
|
|
|
// Clone to temporary directory
|
|
const repoPath = mkdtempSync(join(tmpdir(), 'takt-e2e-repo-'));
|
|
execFileSync('gh', ['repo', 'clone', repoName, repoPath], {
|
|
stdio: 'pipe',
|
|
});
|
|
|
|
// Create test branch (unless skipped for pipeline tests)
|
|
const testBranch = options?.skipBranch
|
|
? undefined
|
|
: `e2e-test-${Date.now()}`;
|
|
if (testBranch) {
|
|
execFileSync('git', ['checkout', '-b', testBranch], {
|
|
cwd: repoPath,
|
|
stdio: 'pipe',
|
|
});
|
|
}
|
|
|
|
const currentBranch = testBranch
|
|
?? execFileSync('git', ['branch', '--show-current'], {
|
|
cwd: repoPath,
|
|
encoding: 'utf-8',
|
|
}).trim();
|
|
|
|
return {
|
|
path: repoPath,
|
|
repoName,
|
|
branch: currentBranch,
|
|
cleanup: () => {
|
|
if (testBranch) {
|
|
// 1. Delete remote branch (best-effort)
|
|
try {
|
|
execFileSync(
|
|
'git',
|
|
['push', 'origin', '--delete', testBranch],
|
|
{ cwd: repoPath, stdio: 'pipe' },
|
|
);
|
|
} catch {
|
|
// Branch may not have been pushed; ignore
|
|
}
|
|
|
|
// 2. Close any PRs from this branch (best-effort)
|
|
try {
|
|
const prList = execFileSync(
|
|
'gh',
|
|
['pr', 'list', '--head', testBranch, '--repo', repoName, '--json', 'number', '--jq', '.[].number'],
|
|
{ encoding: 'utf-8', stdio: 'pipe' },
|
|
).trim();
|
|
|
|
for (const prNumber of prList.split('\n').filter(Boolean)) {
|
|
execFileSync(
|
|
'gh',
|
|
['pr', 'close', prNumber, '--repo', repoName, '--delete-branch'],
|
|
{ stdio: 'pipe' },
|
|
);
|
|
}
|
|
} catch {
|
|
// No PRs or already closed; ignore
|
|
}
|
|
} else {
|
|
// Pipeline mode: clean up takt-created PRs (best-effort)
|
|
try {
|
|
const prNumbers = execFileSync(
|
|
'gh',
|
|
['pr', 'list', '--state', 'open', '--repo', repoName, '--json', 'number', '--jq', '.[].number'],
|
|
{ encoding: 'utf-8', stdio: 'pipe' },
|
|
).trim();
|
|
|
|
for (const prNumber of prNumbers.split('\n').filter(Boolean)) {
|
|
execFileSync(
|
|
'gh',
|
|
['pr', 'close', prNumber, '--repo', repoName, '--delete-branch'],
|
|
{ stdio: 'pipe' },
|
|
);
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
// Delete local directory last
|
|
try {
|
|
rmSync(repoPath, { recursive: true, force: true });
|
|
} catch {
|
|
// Best-effort cleanup
|
|
}
|
|
},
|
|
};
|
|
}
|