fix(cli): keep ktx setup alive when a dbt git clone fails (#88)

Wraps the validation clone in defaultValidateDbt so auth or network
failures surface as a clean validation error instead of an unhandled
RepoFetchError that exits the wizard. Verifies pasted tokens with
testGitRepo before saving them as a secret so bad tokens are caught at
paste time. In interactive setup, validation failures now bounce the
user back to source selection (with a "Edit the connection or pick a
different source" hint) instead of killing the process; --source flag
mode still exits with failed as before.
This commit is contained in:
Andrey Avtomonov 2026-05-14 14:39:50 +02:00 committed by GitHub
parent 6c4623f2ff
commit 52dd89481c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 130 additions and 31 deletions

View file

@ -260,6 +260,8 @@ async function chooseGitAuthCredentialRef(input: {
source: KtxSetupSourceType;
connectionId: string;
existingRef?: string;
repoUrl?: string;
testGitRepo?: (args: { repoUrl: string; authToken?: string | null }) => Promise<{ ok: true } | { ok: false; error: string }>;
}): Promise<string | undefined | 'back'> {
const label = input.source === 'dbt' ? 'This' : `This ${sourceLabel(input.source)}`;
while (true) {
@ -280,6 +282,13 @@ async function chooseGitAuthCredentialRef(input: {
const value = await input.prompts.password({ message: 'Git access token' });
if (value === undefined) continue;
if (!value.trim()) continue;
if (input.testGitRepo && input.repoUrl) {
const result = await input.testGitRepo({ repoUrl: input.repoUrl, authToken: value });
if (!result.ok) {
input.prompts.log?.(`Authentication failed: ${result.error}`);
continue;
}
}
const fileName = `${input.connectionId}-auth-token`;
const ref = await writeProjectLocalSecretReference({
projectDir: input.projectDir,
@ -536,12 +545,17 @@ async function defaultValidateDbt(connection: KtxProjectConnectionConfig): Promi
}
if (!sourceDir && repoUrl) {
const cacheDir = await mkdtemp(join(tmpdir(), 'ktx-setup-dbt-'));
await cloneOrPull({
repoUrl,
authToken: repoAuthToken(connection),
cacheDir,
branch: stringField(connection.branch) ?? 'main',
});
try {
await cloneOrPull({
repoUrl,
authToken: repoAuthToken(connection),
cacheDir,
branch: stringField(connection.branch) ?? 'main',
});
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
return { ok: false, message: `Failed to clone ${repoUrl}: ${reason}` };
}
sourceDir = stringField(connection.path) ? join(cacheDir, String(connection.path)) : cacheDir;
}
if (!sourceDir) {
@ -1058,6 +1072,8 @@ async function promptForInteractiveSource(
source,
connectionId: currentState.sourceConnectionId ?? `${source}-main`,
existingRef: currentState.sourceAuthTokenRef,
repoUrl: currentState.sourceGitUrl,
testGitRepo,
});
if (authRef === 'back') return 'back';
if (authRef) {
@ -1857,7 +1873,12 @@ export async function runKtxSetupSourcesStep(
deps,
});
if (choiceResult.status === 'failed') {
return { status: 'failed', projectDir: args.projectDir };
if (args.source) {
return { status: 'failed', projectDir: args.projectDir };
}
prompts.log?.('Edit the connection or pick a different source to continue.');
returnToSourceSelection = true;
break;
}
if (choiceResult.status === 'back') {
if (args.source) {
@ -1923,7 +1944,8 @@ export async function runKtxSetupSourcesStep(
deps,
});
if (choiceResult.status === 'failed') {
return { status: 'failed', projectDir: args.projectDir };
prompts.log?.('Edit the connection or pick a different source to continue.');
continue;
}
if (choiceResult.status === 'back') {
continue;