Skip to content

Commit 26d92b4

Browse files
Wr/compile agents on deploy @W-20129538@ (#1645)
* fix: first implementation working' * chore: more performant approach * test: add UTs * test: cleanups * chore: only auth once, show errors for everything in deploy * chore: reset acess token
1 parent 07e40fd commit 26d92b4

File tree

3 files changed

+357
-7
lines changed

3 files changed

+357
-7
lines changed

src/client/metadataApiDeploy.ts

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
import { join, relative, resolve as pathResolve, sep } from 'node:path';
1717
import { format } from 'node:util';
18+
import { EOL } from 'node:os';
1819
import { isString } from '@salesforce/ts-types';
1920
import JSZip from 'jszip';
2021
import fs from 'graceful-fs';
@@ -23,10 +24,10 @@ import { Messages } from '@salesforce/core/messages';
2324
import { SfError } from '@salesforce/core/sfError';
2425
import { envVars } from '@salesforce/core/envVars';
2526
import { ensureArray } from '@salesforce/kit';
26-
import { RegistryAccess } from '../registry/registryAccess';
27+
import { RegistryAccess } from '../registry';
2728
import { ReplacementEvent } from '../convert/types';
28-
import { MetadataConverter } from '../convert/metadataConverter';
29-
import { ComponentSet } from '../collections/componentSet';
29+
import { MetadataConverter } from '../convert';
30+
import { ComponentSet } from '../collections';
3031
import { MetadataTransfer, MetadataTransferOptions } from './metadataTransfer';
3132
import {
3233
AsyncResult,
@@ -204,7 +205,116 @@ export class MetadataApiDeploy extends MetadataTransfer<
204205
// this is used as the version in the manifest (package.xml).
205206
this.components.sourceApiVersion ??= apiVersion;
206207
}
208+
if (this.options.components) {
209+
// we must ensure AiAuthoringBundles compile before deployment
210+
// Use optimized getter method instead of filtering all components
211+
const aabComponents = this.options.components.getAiAuthoringBundles().toArray();
212+
213+
if (aabComponents.length > 0) {
214+
// we need to use a namedJWT connection for this request
215+
const { accessToken, instanceUrl } = connection.getConnectionOptions();
216+
if (!instanceUrl) {
217+
throw SfError.create({
218+
name: 'ApiAccessError',
219+
message: 'Missing Instance URL for org connection',
220+
});
221+
}
222+
if (!accessToken) {
223+
throw SfError.create({
224+
name: 'ApiAccessError',
225+
message: 'Missing Access Token for org connection',
226+
});
227+
}
228+
const url = `${instanceUrl}/agentforce/bootstrap/nameduser`;
229+
// For the namdeduser endpoint request to work we need to delete the access token
230+
delete connection.accessToken;
231+
const response = await connection.request<{
232+
access_token: string;
233+
}>(
234+
{
235+
method: 'GET',
236+
url,
237+
headers: {
238+
'Content-Type': 'application/json',
239+
Cookie: `sid=${accessToken}`,
240+
},
241+
},
242+
{ retry: { maxRetries: 3 } }
243+
);
244+
connection.accessToken = response.access_token;
245+
const results = await Promise.all(
246+
aabComponents.map(async (aab) => {
247+
// aab.content points to a directory, we need to find the .agent file and read it
248+
if (!aab.content) {
249+
throw new SfError(
250+
messages.getMessage('error_expected_source_files', [aab.fullName, 'aiauthoringbundle']),
251+
'ExpectedSourceFilesError'
252+
);
253+
}
254+
255+
const contentPath = aab.tree.find('content', aab.name, aab.content);
256+
257+
if (!contentPath) {
258+
// if this didn't exist, they'll have deploy issues anyways, but we can check here for type reasons
259+
throw new SfError(`No .agent file found in directory: ${aab.content}`, 'MissingAgentFileError');
260+
}
261+
262+
const agentContent = await fs.promises.readFile(contentPath, 'utf-8');
263+
264+
// to avoid circular dependencies between libraries, just call the compile endpoint here
265+
const result = await connection.request<{
266+
// minimal typings here, more is returned, just using what we need
267+
status: 'failure' | 'success';
268+
errors: Array<{
269+
description: string;
270+
lineStart: number;
271+
colStart: number;
272+
}>;
273+
// name added here for post-processing convenience
274+
name: string;
275+
}>({
276+
method: 'POST',
277+
// this will need to be api.salesforce once changes are in prod
278+
url: 'https://test.api.salesforce.com/einstein/ai-agent/v1.1/authoring/scripts',
279+
headers: {
280+
'x-client-name': 'afdx',
281+
'content-type': 'application/json',
282+
},
283+
body: JSON.stringify({
284+
assets: [
285+
{
286+
type: 'AFScript',
287+
name: 'AFScript',
288+
content: agentContent,
289+
},
290+
],
291+
afScriptVersion: '1.0.1',
292+
}),
293+
});
294+
result.name = aab.name;
295+
return result;
296+
})
297+
);
207298

299+
const errors = results
300+
.filter((result) => result.status === 'failure')
301+
.map((result) =>
302+
result.errors.map((r) => `${result.name}.agent: ${r.description} ${r.lineStart}:${r.colStart}`).join(EOL)
303+
);
304+
305+
if (errors.length > 0) {
306+
throw SfError.create({
307+
message: `${EOL}${errors.join(EOL)}`,
308+
name: 'AgentCompilationError',
309+
});
310+
} else {
311+
// everything successfully compiled
312+
// stop using named user jwt access token
313+
delete connection.accessToken;
314+
await connection.refreshAuth();
315+
}
316+
}
317+
}
208318
// only do event hooks if source, (NOT a metadata format) deploy
209319
if (this.options.components) {
210320
await LifecycleInstance.emit('scopedPreDeploy', {

src/collections/componentSet.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
106106
// used to store components meant for a "constructive" (not destructive) manifest
107107
private manifestComponents = new DecodeableMap<string, DecodeableMap<string, SourceComponent>>();
108108

109+
// optimization: track AiAuthoringBundles separately for faster access during compilation check
110+
private aiAuthoringBundles = new Set<SourceComponent>();
111+
109112
private destructiveChangesType = DestructiveChangesType.POST;
110113

111114
public constructor(components: Iterable<ComponentLike> = [], registry = new RegistryAccess()) {
@@ -527,6 +530,16 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
527530
return new LazyCollection(iter).filter((c) => c instanceof SourceComponent) as LazyCollection<SourceComponent>;
528531
}
529532

533+
/**
534+
* Get all AiAuthoringBundle components in the set.
535+
* This is an optimized method that uses a cached Set of AAB components.
536+
*
537+
* @returns Collection of AiAuthoringBundle source components
538+
*/
539+
public getAiAuthoringBundles(): LazyCollection<SourceComponent> {
540+
return new LazyCollection(this.aiAuthoringBundles);
541+
}
542+
530543
public add(component: ComponentLike, deletionType?: DestructiveChangesType): void {
531544
const key = simpleKey(component);
532545
if (!this.components.has(key)) {
@@ -556,6 +569,11 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
556569
// we're working with SourceComponents now
557570
this.components.get(key)?.set(srcKey, component);
558571

572+
// track AiAuthoringBundles separately for fast access
573+
if (component.type.id === 'aiauthoringbundle') {
574+
this.aiAuthoringBundles.add(component);
575+
}
576+
559577
// Build maps of destructive components and regular components as they are added
560578
// as an optimization when building manifests.
561579
if (deletionType) {

0 commit comments

Comments
 (0)