Skip to content

Commit 1a50f9b

Browse files
authored
Merge pull request #5990 from continuedev/jacob/enhancement/bubble-up-config-errors
Bubble up config and block errors to the extension GUI
2 parents 9689333 + 6ca0268 commit 1a50f9b

File tree

4 files changed

+147
-32
lines changed

4 files changed

+147
-32
lines changed

core/config/yaml/loadYaml.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,15 @@ async function loadConfigYaml(options: {
117117
// `Loading config.yaml from ${JSON.stringify(packageIdentifier)} with root path ${rootPath}`,
118118
// );
119119

120-
let config =
121-
overrideConfigYaml ??
120+
const errors: ConfigValidationError[] = [];
121+
122+
let config: AssistantUnrolled | undefined;
123+
124+
if (overrideConfigYaml) {
125+
config = overrideConfigYaml;
126+
} else {
122127
// This is how we allow use of blocks locally
123-
(await unrollAssistant(
128+
const unrollResult = await unrollAssistant(
124129
packageIdentifier,
125130
new RegistryClient({
126131
accessToken: await controlPlaneClient.getAccessToken(),
@@ -139,17 +144,23 @@ async function loadConfigYaml(options: {
139144
),
140145
renderSecrets: true,
141146
injectBlocks: allLocalBlocks,
147+
asConfigResult: true,
142148
},
143-
));
149+
);
150+
config = unrollResult.config;
151+
if (unrollResult.errors) {
152+
errors.push(...unrollResult.errors);
153+
}
154+
}
144155

145-
const errors = isAssistantUnrolledNonNullable(config)
146-
? validateConfigYaml(config)
147-
: [
148-
{
156+
if (config) {
157+
isAssistantUnrolledNonNullable(config)
158+
? errors.push(...validateConfigYaml(config))
159+
: errors.push({
149160
fatal: true,
150161
message: "Assistant includes blocks that don't exist",
151-
},
152-
];
162+
});
163+
}
153164

154165
if (errors?.some((error) => error.fatal)) {
155166
return {

extensions/vscode/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gui/src/components/mainInput/Lump/sections/errors/ErrorSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ export function ErrorSection() {
2424
}`}
2525
>
2626
{error.fatal ? (
27-
<ExclamationCircleIcon className="mr-2 mt-1 h-3.5 w-3.5" />
27+
<ExclamationCircleIcon className="mr-2 mt-1 h-3.5 w-3.5 shrink-0" />
2828
) : (
29-
<ExclamationTriangleIcon className="mr-2 mt-1 h-3.5 w-3.5" />
29+
<ExclamationTriangleIcon className="mr-2 mt-1 h-3.5 w-3.5 shrink-0" />
3030
)}
3131
<p
3232
className="m-0 whitespace-pre-wrap text-wrap"

packages/config-yaml/src/load/unroll.ts

Lines changed: 122 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as YAML from "yaml";
2+
import { ZodError } from "zod";
23
import { PlatformClient, Registry } from "../interfaces/index.js";
34
import { encodeSecretLocation } from "../interfaces/SecretResult.js";
45
import {
@@ -17,6 +18,7 @@ import {
1718
configYamlSchema,
1819
Rule,
1920
} from "../schemas/index.js";
21+
import { ConfigResult, ConfigValidationError } from "../validation.js";
2022
import {
2123
packageIdentifierToShorthandSlug,
2224
useProxyForUnrenderedSecrets,
@@ -30,16 +32,25 @@ export function parseConfigYaml(configYaml: string): ConfigYaml {
3032
if (result.success) {
3133
return result.data;
3234
}
33-
throw new Error(
34-
result.error.errors
35-
.map((e) => `${e.path.join(".")}: ${e.message}`)
36-
.join(""),
37-
);
35+
36+
throw new Error(formatZodError(result.error), {
37+
cause: "result.success was false",
38+
});
3839
} catch (e) {
39-
console.log("Failed to parse rolled assistant:", configYaml);
40-
throw new Error(
41-
`Failed to parse assistant:\n${e instanceof Error ? e.message : e}`,
42-
);
40+
console.error("Failed to parse rolled assistant:", configYaml);
41+
if (
42+
e instanceof Error &&
43+
"cause" in e &&
44+
e.cause === "result.success was false"
45+
) {
46+
throw new Error(`Failed to parse assistant: ${e.message}`);
47+
} else if (e instanceof ZodError) {
48+
throw new Error(`Failed to parse assistant: ${formatZodError(e)}`);
49+
} else {
50+
throw new Error(
51+
`Failed to parse assistant: ${e instanceof Error ? e.message : e}`,
52+
);
53+
}
4354
}
4455
}
4556

@@ -49,9 +60,10 @@ export function parseAssistantUnrolled(configYaml: string): AssistantUnrolled {
4960
const result = assistantUnrolledSchema.parse(parsed);
5061
return result;
5162
} catch (e: any) {
52-
throw new Error(
63+
console.error(
5364
`Failed to parse unrolled assistant: ${e.message}\n\n${configYaml}`,
5465
);
66+
throw new Error(`Failed to parse unrolled assistant: ${formatZodError(e)}`);
5567
}
5668
}
5769

@@ -61,7 +73,7 @@ export function parseBlock(configYaml: string): Block {
6173
const result = blockSchema.parse(parsed);
6274
return result;
6375
} catch (e: any) {
64-
throw new Error(`Failed to parse block: ${e.message}`);
76+
throw new Error(`Failed to parse block: ${formatZodError(e)}`);
6577
}
6678
}
6779

@@ -183,6 +195,7 @@ async function extractRenderedSecretsMap(
183195
export interface BaseUnrollAssistantOptions {
184196
renderSecrets: boolean;
185197
injectBlocks?: PackageIdentifier[];
198+
asConfigResult?: true;
186199
}
187200

188201
export interface DoNotRenderSecretsUnrollAssistantOptions
@@ -204,14 +217,30 @@ export type UnrollAssistantOptions =
204217
| DoNotRenderSecretsUnrollAssistantOptions
205218
| RenderSecretsUnrollAssistantOptions;
206219

220+
// Overload to satisfy existing consumers of unrollAssistant.
221+
export async function unrollAssistant(
222+
id: PackageIdentifier,
223+
registry: Registry,
224+
options: UnrollAssistantOptions & { asConfigResult: true },
225+
): Promise<ConfigResult<AssistantUnrolled>>;
226+
207227
export async function unrollAssistant(
208228
id: PackageIdentifier,
209229
registry: Registry,
210230
options: UnrollAssistantOptions,
211-
): Promise<AssistantUnrolled> {
231+
): Promise<AssistantUnrolled>;
232+
233+
export async function unrollAssistant(
234+
id: PackageIdentifier,
235+
registry: Registry,
236+
options: UnrollAssistantOptions,
237+
): Promise<AssistantUnrolled | ConfigResult<AssistantUnrolled>> {
212238
// Request the content from the registry
213239
const rawContent = await registry.getContent(id);
214-
return unrollAssistantFromContent(id, rawContent, registry, options);
240+
241+
const result = unrollAssistantFromContent(id, rawContent, registry, options);
242+
243+
return result;
215244
}
216245

217246
function renderTemplateData(
@@ -236,7 +265,7 @@ export async function unrollAssistantFromContent(
236265
rawYaml: string,
237266
registry: Registry,
238267
options: UnrollAssistantOptions,
239-
): Promise<AssistantUnrolled> {
268+
): Promise<AssistantUnrolled | ConfigResult<AssistantUnrolled>> {
240269
// Parse string to Zod-validated YAML
241270
let parsedYaml = parseConfigYaml(rawYaml);
242271

@@ -245,10 +274,15 @@ export async function unrollAssistantFromContent(
245274
parsedYaml,
246275
registry,
247276
options.injectBlocks,
277+
options.asConfigResult ?? false,
248278
);
249279

250280
// Back to a string so we can fill in template variables
251-
const rawUnrolledYaml = YAML.stringify(unrolledAssistant);
281+
const rawUnrolledYaml = options.asConfigResult
282+
? YAML.stringify(
283+
(unrolledAssistant as ConfigResult<AssistantUnrolled>).config,
284+
)
285+
: YAML.stringify(unrolledAssistant);
252286

253287
// Convert all of the template variables to FQSNs
254288
// Secrets from the block will have the assistant slug prepended to the FQSN
@@ -277,14 +311,28 @@ export async function unrollAssistantFromContent(
277311
options.orgScopeId,
278312
options.onPremProxyUrl,
279313
);
314+
315+
if (options.asConfigResult) {
316+
return {
317+
config: finalConfig,
318+
errors: (unrolledAssistant as ConfigResult<AssistantUnrolled>).errors,
319+
configLoadInterrupted: (
320+
unrolledAssistant as ConfigResult<AssistantUnrolled>
321+
).configLoadInterrupted,
322+
};
323+
}
324+
280325
return finalConfig;
281326
}
282327

283328
export async function unrollBlocks(
284329
assistant: ConfigYaml,
285330
registry: Registry,
286331
injectBlocks: PackageIdentifier[] | undefined,
287-
): Promise<AssistantUnrolled> {
332+
asConfigError: boolean,
333+
): Promise<AssistantUnrolled | ConfigResult<AssistantUnrolled>> {
334+
const errors: ConfigValidationError[] = [];
335+
288336
const unrolledAssistant: AssistantUnrolled = {
289337
name: assistant.name,
290338
version: assistant.version,
@@ -316,8 +364,23 @@ export async function unrollBlocks(
316364
);
317365
}
318366
} catch (err) {
367+
let msg = "";
368+
if (
369+
typeof unrolledBlock.uses !== "string" &&
370+
"filePath" in unrolledBlock.uses
371+
) {
372+
msg = `${(err as Error).message}.\n> ${unrolledBlock.uses.filePath}`;
373+
} else {
374+
msg = `${(err as Error).message}.\n> ${JSON.stringify(unrolledBlock.uses)}`;
375+
}
376+
377+
errors.push({
378+
fatal: false,
379+
message: msg,
380+
});
381+
319382
console.error(
320-
`Failed to unroll block ${unrolledBlock.uses}: ${(err as Error).message}`,
383+
`Failed to unroll block ${JSON.stringify(unrolledBlock.uses)}: ${(err as Error).message}`,
321384
);
322385
sectionBlocks.push(null);
323386
}
@@ -349,6 +412,11 @@ export async function unrollBlocks(
349412
rules.push(block);
350413
}
351414
} catch (err) {
415+
errors.push({
416+
fatal: false,
417+
message: `${(err as Error).message}:\n${rule.uses}`,
418+
});
419+
352420
console.error(
353421
`Failed to unroll block ${rule.uses}: ${(err as Error).message}`,
354422
);
@@ -381,12 +449,36 @@ export async function unrollBlocks(
381449
);
382450
}
383451
} catch (err) {
452+
let msg = "";
453+
if (injectBlock.uriType === "file") {
454+
msg = `${(err as Error).message}.\n> ${injectBlock.filePath}`;
455+
} else {
456+
msg = `${(err as Error).message}.\n> ${injectBlock.fullSlug}`;
457+
}
458+
errors.push({
459+
fatal: false,
460+
message: msg,
461+
});
462+
384463
console.error(
385-
`Failed to unroll block ${injectBlock}: ${(err as Error).message}`,
464+
`Failed to unroll block ${JSON.stringify(injectBlock)}: ${(err as Error).message}`,
386465
);
387466
}
388467
}
389468

469+
if (asConfigError) {
470+
const configResult: ConfigResult<AssistantUnrolled> = {
471+
config: undefined,
472+
errors: undefined,
473+
configLoadInterrupted: false,
474+
};
475+
configResult.config = unrolledAssistant;
476+
if (errors.length > 0) {
477+
configResult.errors = errors;
478+
}
479+
return configResult;
480+
}
481+
390482
return unrolledAssistant;
391483
}
392484

@@ -439,3 +531,15 @@ export function mergeOverrides<T extends Record<string, any>>(
439531
}
440532
return block;
441533
}
534+
535+
function formatZodError(error: any): string {
536+
if (error.errors && Array.isArray(error.errors)) {
537+
return error.errors
538+
.map((e: any) => {
539+
const path = e.path.length > 0 ? `${e.path.join(".")}: ` : "";
540+
return `${path}${e.message}`;
541+
})
542+
.join(", ");
543+
}
544+
return error.message || "Validation failed";
545+
}

0 commit comments

Comments
 (0)