@uipath/packager-tool-workflowcompiler-browser 0.0.29 → 1.196.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@ import {
3
3
  type IFileSystem,
4
4
  type IProjectPackOptions,
5
5
  type IProjectToolFactory,
6
+ type IProjectValidateOptions,
6
7
  type IToolLogger,
7
8
  type ProjectOperationContext,
8
9
  ToolResult,
@@ -18,12 +19,27 @@ import {
18
19
 
19
20
  type ResolveCallback = (result: ToolResult) => void;
20
21
 
21
- interface ProjectEntry {
22
+ export enum BundleOperation {
23
+ Pack = "Pack",
24
+ Validate = "Validate",
25
+ }
26
+
27
+ interface PackEntry {
28
+ operation: BundleOperation.Pack;
22
29
  options: IProjectPackOptions;
23
30
  resolve: ResolveCallback;
24
31
  }
25
32
 
33
+ interface ValidateEntry {
34
+ operation: BundleOperation.Validate;
35
+ options: IProjectValidateOptions;
36
+ resolve: ResolveCallback;
37
+ }
38
+
39
+ type ProjectEntry = PackEntry | ValidateEntry;
40
+
26
41
  interface BundleSession {
42
+ operation: BundleOperation;
27
43
  expectedCount: number;
28
44
  projects: Map<string, ProjectEntry>;
29
45
  timeout?: ReturnType<typeof setTimeout>;
@@ -34,16 +50,40 @@ export interface IProjectBundleExecutor {
34
50
  options: IProjectPackOptions,
35
51
  context: ProjectOperationContext,
36
52
  ): Promise<ToolResult>;
53
+
54
+ validateAsync(
55
+ options: IProjectValidateOptions,
56
+ context: ProjectOperationContext,
57
+ ): Promise<ToolResult>;
37
58
  }
38
59
 
39
60
  /**
40
- * Coordinates packing of multiple projects within a single operation context.
41
- * Waits until all compatible projects have submitted their pack options,
42
- * then executes a single bundled operation and resolves all pending promises.
61
+ * Coordinates pack or validate of multiple projects within a single operation context.
62
+ * Waits until all compatible projects have submitted their options,
63
+ * then executes a single bundled remote-agent operation and resolves all pending promises.
64
+ */
65
+ /**
66
+ * Shape of the `POST /odata/Attachments` response. Orchestrator returns
67
+ * PascalCase (`Id`, `BlobFileAccess.Uri`); the camelCase fallbacks mirror the
68
+ * defensive reads in `@uipath/orchestrator-sdk`'s `uploadJobAttachment`.
43
69
  */
70
+ interface AttachmentResponse {
71
+ Id?: string;
72
+ id?: string;
73
+ BlobFileAccess?: { Uri: string };
74
+ blobFileAccess?: { Uri: string };
75
+ }
76
+
44
77
  export class ProjectBundleExecutor implements IProjectBundleExecutor {
45
78
  static readonly SESSION_TIMEOUT_MS = 1_800_000;
46
79
 
80
+ /**
81
+ * Maximum length (in characters) of the inline `inputArguments` payload
82
+ * accepted by the serverless jobs endpoint. Beyond this the arguments must
83
+ * be uploaded as a file attachment and referenced via `inputFile`.
84
+ */
85
+ static readonly MAX_INLINE_INPUT_ARGUMENTS_LENGTH = 10_000;
86
+
47
87
  private readonly sessions = new Map<string, BundleSession>();
48
88
  private readonly pendingSessionCreations = new Map<
49
89
  string,
@@ -59,25 +99,59 @@ export class ProjectBundleExecutor implements IProjectBundleExecutor {
59
99
  options: IProjectPackOptions,
60
100
  context: ProjectOperationContext,
61
101
  ): Promise<ToolResult> {
62
- const projectPath = options.projectPath;
102
+ return this.enqueueAsync(context, {
103
+ operation: BundleOperation.Pack,
104
+ options,
105
+ resolve: () => {},
106
+ });
107
+ }
108
+
109
+ async validateAsync(
110
+ options: IProjectValidateOptions,
111
+ context: ProjectOperationContext,
112
+ ): Promise<ToolResult> {
113
+ return this.enqueueAsync(context, {
114
+ operation: BundleOperation.Validate,
115
+ options,
116
+ resolve: () => {},
117
+ });
118
+ }
119
+
120
+ private async enqueueAsync(
121
+ context: ProjectOperationContext,
122
+ entry: ProjectEntry,
123
+ ): Promise<ToolResult> {
124
+ const projectPath = entry.options.projectPath;
63
125
  context.logger?.info(
64
- `[BundleExecutor] packAsync called for project: ${projectPath}`,
126
+ `[BundleExecutor] ${entry.operation}Async called for project: ${projectPath}`,
65
127
  );
66
- const session = await this.getSessionAsync(context);
128
+ const session = await this.getSessionAsync(context, entry.operation);
67
129
 
68
- let resolve!: (result: ToolResult) => void;
69
- const promise = new Promise<ToolResult>((r) => {
70
- resolve = r;
130
+ if (session.operation !== entry.operation) {
131
+ return ToolResult.error(
132
+ "BundleOperationMismatch",
133
+ translate.t(
134
+ "toolWorkflowCompilerBrowser.errors.bundleOperationMismatch",
135
+ {
136
+ sessionOperation: session.operation,
137
+ entryOperation: entry.operation,
138
+ },
139
+ ),
140
+ );
141
+ }
142
+
143
+ const promise = new Promise<ToolResult>((resolve) => {
144
+ entry.resolve = resolve;
71
145
  });
72
- session.projects.set(projectPath, { options, resolve });
146
+ session.projects.set(projectPath, entry);
73
147
  const allReceived = session.projects.size === session.expectedCount;
74
148
  context.logger?.info(
75
- `[BundleExecutor] session: ${session.projects.size}/${session.expectedCount} projects received. allReceived=${allReceived}`,
149
+ `[BundleExecutor] session (${session.operation}): ${session.projects.size}/${session.expectedCount} projects received. allReceived=${allReceived}`,
76
150
  );
77
151
 
78
152
  if (allReceived) {
79
153
  clearTimeout(session.timeout);
80
- await this.packOnAgentAsync(session, context);
154
+ await this.runAgentAsync(session, context);
81
155
  this.sessions.delete(context.id);
82
156
  }
83
157
 
@@ -86,6 +160,7 @@ export class ProjectBundleExecutor implements IProjectBundleExecutor {
86
160
 
87
161
  private getSessionAsync(
88
162
  context: ProjectOperationContext,
163
+ operation: BundleOperation,
89
164
  ): Promise<BundleSession> {
90
165
  const existing = this.sessions.get(context.id);
91
166
  if (existing) {
@@ -94,7 +169,7 @@ export class ProjectBundleExecutor implements IProjectBundleExecutor {
94
169
 
95
170
  let pending = this.pendingSessionCreations.get(context.id);
96
171
  if (!pending) {
97
- pending = this.createSessionAsync(context);
172
+ pending = this.createSessionAsync(context, operation);
98
173
  this.pendingSessionCreations.set(context.id, pending);
99
174
  }
100
175
  return pending;
@@ -102,9 +177,11 @@ export class ProjectBundleExecutor implements IProjectBundleExecutor {
102
177
 
103
178
  private async createSessionAsync(
104
179
  context: ProjectOperationContext,
180
+ operation: BundleOperation,
105
181
  ): Promise<BundleSession> {
106
182
  const expectedCount = this.countCompatibleProjects(context);
107
183
  const session: BundleSession = {
184
+ operation,
108
185
  expectedCount,
109
186
  projects: new Map(),
110
187
  };
@@ -135,15 +212,16 @@ export class ProjectBundleExecutor implements IProjectBundleExecutor {
135
212
  return session;
136
213
  }
137
214
 
138
- private async packOnAgentAsync(
215
+ private async runAgentAsync(
139
216
  session: BundleSession,
140
217
  context: ProjectOperationContext,
141
218
  ): Promise<void> {
142
219
  const projectPaths = [...session.projects.keys()];
143
220
  context.logger?.info(
144
221
  translate.t(
145
- "toolWorkflowCompilerBrowser.info.startingRemotePackAgent",
222
+ "toolWorkflowCompilerBrowser.info.startingRemoteAgent",
146
223
  {
224
+ operation: session.operation,
147
225
  count: session.expectedCount,
148
226
  paths: projectPaths.join(", "),
149
227
  },
@@ -205,16 +283,16 @@ export class ProjectBundleExecutor implements IProjectBundleExecutor {
205
283
  context.logger?.info(
206
284
  `[BundleExecutor] Got result: errorCode=${result.errorCode}, status=${result.status}, message=${result.message}, bucketId=${result.bucketId}`,
207
285
  );
208
- await this.handlePackResultAsync(result, session, context);
286
+ await this.handleAgentResultAsync(result, session, context);
209
287
  } catch (error) {
210
288
  const message =
211
289
  error instanceof Error ? error.message : String(error);
212
290
  context.logger?.error(
213
- `[BundleExecutor] packOnAgentAsync caught error: ${message}`,
291
+ `[BundleExecutor] runAgentAsync caught error: ${message}`,
214
292
  );
215
293
  this.resolveAll(
216
294
  session,
217
- ToolResult.error("PackAgentError", message),
295
+ ToolResult.error(`${session.operation}AgentError`, message),
218
296
  );
219
297
  } finally {
220
298
  await hubConnection.disconnectAsync();
@@ -251,7 +329,7 @@ export class ProjectBundleExecutor implements IProjectBundleExecutor {
251
329
  }
252
330
  }
253
331
 
254
- private async handlePackResultAsync(
332
+ private async handleAgentResultAsync(
255
333
  result: PackAgentResult,
256
334
  session: BundleSession,
257
335
  context: ProjectOperationContext,
@@ -263,11 +341,12 @@ export class ProjectBundleExecutor implements IProjectBundleExecutor {
263
341
  this.resolveAll(
264
342
  session,
265
343
  ToolResult.error(
266
- "PackAgentError",
344
+ `${session.operation}AgentError`,
267
345
  result.message ??
268
346
  translate.t(
269
- "toolWorkflowCompilerBrowser.errors.packAgentFailed",
347
+ "toolWorkflowCompilerBrowser.errors.agentFailed",
270
348
  {
349
+ operation: session.operation,
271
350
  status: result.status,
272
351
  errorCode: result.errorCode,
273
352
  },
@@ -277,6 +356,19 @@ export class ProjectBundleExecutor implements IProjectBundleExecutor {
277
356
  return;
278
357
  }
279
358
 
359
+ if (session.operation === BundleOperation.Pack) {
360
+ await this.handlePackBucketAsync(result, session, context);
361
+ return;
362
+ }
363
+
364
+ this.resolveAll(session, ToolResult.success());
365
+ }
366
+
367
+ private async handlePackBucketAsync(
368
+ result: PackAgentResult,
369
+ session: BundleSession,
370
+ context: ProjectOperationContext,
371
+ ): Promise<void> {
280
372
  if (result.bucketId == null) {
281
373
  this.resolveAll(
282
374
  session,
@@ -290,13 +382,20 @@ export class ProjectBundleExecutor implements IProjectBundleExecutor {
290
382
  return;
291
383
  }
292
384
 
385
+ const packEntries = new Map<string, { options: IProjectPackOptions }>();
386
+ for (const [projectPath, entry] of session.projects) {
387
+ if (entry.operation === BundleOperation.Pack) {
388
+ packEntries.set(projectPath, { options: entry.options });
389
+ }
390
+ }
391
+
293
392
  const resultProcessor = new PackResultProcessor(
294
393
  this.fileSystem,
295
394
  context.logger,
296
395
  );
297
396
  const perProjectResults = await resultProcessor.processAsync(
298
397
  result.bucketId,
299
- session.projects,
398
+ packEntries,
300
399
  context,
301
400
  );
302
401
  for (const [projectPath, entry] of session.projects) {
@@ -321,56 +420,107 @@ export class ProjectBundleExecutor implements IProjectBundleExecutor {
321
420
  ): Promise<void> {
322
421
  const relativeBase = toRelativeUrl(connection.cloudUrl!);
323
422
  const url = `${relativeBase}/orchestrator_/api/serverless/jobs`;
423
+ //const url = `${relativeBase}/orchestrator_/api/RobotDebug/BeginSession`;
424
+
324
425
  context.logger?.info(`[BundleExecutor] BeginSession URL: ${url}`);
325
426
 
326
- // Use the first entry for shared fields (package info, options)
327
427
  const firstEntry = session.projects.values().next().value!;
328
- const pkg = firstEntry.options.package;
329
428
  const opts = firstEntry.options;
330
429
 
331
- // Build RpaProjects from all projects in the session
332
430
  const rpaProjects = [...session.projects.entries()].map(
333
431
  ([projectPath, entry]) => {
334
- const project = context.solution.Projects.find(
432
+ const project = context.projects.find(
335
433
  (p) => p.ProjectPath === projectPath,
336
434
  );
337
435
  return {
338
436
  RelativePath: project?.ProjectRelativePath ?? "",
339
437
  DesignId: project?.Id ?? "",
340
- PackageId: entry.options.package.id,
438
+ ProjectStorageId:
439
+ entry.operation === BundleOperation.Pack
440
+ ? (entry.options.projectStorageId ?? "")
441
+ : "",
442
+ PackageId:
443
+ entry.operation === BundleOperation.Pack
444
+ ? entry.options.package.id
445
+ : "",
341
446
  };
342
447
  },
343
448
  );
344
449
 
345
- const executorInfo = JSON.stringify({
346
- Command: "Pack",
450
+ const executorInfo: Record<string, unknown> = {
451
+ Command: session.operation,
347
452
  SupportedProtocolVersions: ["2.0"],
348
- ProjectId: "",
349
453
  DownloadUrl: context.downloadUrl ?? "",
350
454
  SessionId: sessionId,
351
455
  ProfileKey: "StudioWeb",
352
456
  LicenseType: "Development",
353
457
  LogLevel: opts.logLevel ?? "Warning",
354
- Name: pkg.id,
355
- Configuration: context.configuration,
356
458
  Culture: "en-US",
357
- Version: pkg.version,
358
- Author: pkg.author ?? "",
359
- Description: pkg.description ?? "",
459
+ DownloadTimeoutInSeconds: 300,
360
460
  SkipAnalyze: String(opts.skipAnalyze),
361
461
  SkipValidate: String(opts.skipValidate),
362
462
  TargetFramework: context.targetFramework,
363
463
  RpaProjects: rpaProjects,
364
464
  WorkflowCompilerVersion: context.workflowCompilerVersion ?? "",
365
- });
465
+ };
466
+
467
+ if (firstEntry.operation === BundleOperation.Pack) {
468
+ executorInfo.Configuration = context.configuration;
469
+ const pkg = firstEntry.options.package;
470
+ executorInfo.Package = {
471
+ Name: pkg.id,
472
+ Version: pkg.version,
473
+ Author: pkg.author ?? "",
474
+ Description: pkg.description ?? "",
475
+ ReleaseNotes: pkg.releaseNotes ?? "",
476
+ Tags: pkg.tags ?? "",
477
+ IconUrl: pkg.iconUrl ?? "",
478
+ ProjectUrl: pkg.projectUrl ?? "",
479
+ RepositoryType: pkg.repositoryType ?? "",
480
+ RepositoryUrl: pkg.repositoryUrl ?? "",
481
+ RepositoryBranch: pkg.repositoryBranch ?? "",
482
+ RepositoryCommit: pkg.repositoryCommit ?? "",
483
+ };
484
+ // Flat fields retained for backward compatibility with agents that
485
+ // predate the nested Package structure.
486
+ executorInfo.Name = pkg.id;
487
+ executorInfo.Version = pkg.version;
488
+ executorInfo.Author = pkg.author ?? "";
489
+ executorInfo.Description = pkg.description ?? "";
490
+ executorInfo.ReleaseNotes = pkg.releaseNotes ?? "";
491
+ }
492
+
493
+ const inputArguments = JSON.stringify(executorInfo);
366
494
 
367
- const body = {
495
+ const body: Record<string, unknown> = {
368
496
  source: "StudioWeb",
369
497
  targetFramework: context.targetFramework,
370
498
  jobType: "PublishStudioProject",
371
- inputArguments: executorInfo,
372
499
  };
373
500
 
501
+ // The serverless jobs endpoint caps inline inputArguments at
502
+ // MAX_INLINE_INPUT_ARGUMENTS_LENGTH characters. For larger payloads we
503
+ // upload the arguments as a file attachment and reference it via
504
+ // inputFile instead (the two are mutually exclusive on the server).
505
+ if (
506
+ inputArguments.length >
507
+ ProjectBundleExecutor.MAX_INLINE_INPUT_ARGUMENTS_LENGTH
508
+ ) {
509
+ context.logger?.info(
510
+ translate.t(
511
+ "toolWorkflowCompilerBrowser.info.offloadingInputArguments",
512
+ { length: inputArguments.length },
513
+ ),
514
+ );
515
+ body.inputFile = await this.uploadInputArgumentsFileAsync(
516
+ inputArguments,
517
+ connection,
518
+ context.logger,
519
+ );
520
+ } else {
521
+ body.inputArguments = inputArguments;
522
+ }
523
+
374
524
  const headers: Record<string, string> = {
375
525
  "Content-Type": "application/json",
376
526
  Authorization: `Bearer ${connection.accessToken}`,
@@ -398,6 +548,86 @@ export class ProjectBundleExecutor implements IProjectBundleExecutor {
398
548
  }
399
549
  }
400
550
 
551
+ /**
552
+ * Uploads the input arguments JSON as a file attachment and returns the
553
+ * attachment id, to be passed as `inputFile` on the serverless job request.
554
+ * Mirrors the two-step flow used by the Orchestrator UI: create the
555
+ * attachment (which returns a signed blob write URI) then PUT the contents
556
+ * to that URI using the storage-provided headers.
557
+ */
558
+ private async uploadInputArgumentsFileAsync(
559
+ inputArguments: string,
560
+ connection: ConnectionInfo,
561
+ logger?: IToolLogger,
562
+ ): Promise<string> {
563
+ const baseUrl = toRelativeUrl(connection.cloudUrl!);
564
+ const attachmentsUrl = `${baseUrl}/orchestrator_/odata/Attachments`;
565
+
566
+ const headers: Record<string, string> = {
567
+ "Content-Type": "application/json",
568
+ Authorization: `Bearer ${connection.accessToken}`,
569
+ "X-UIPATH-OrganizationUnitId": connection.folderId ?? "",
570
+ };
571
+ if (connection.tenantId) {
572
+ headers["x-uipath-tenantid"] = connection.tenantId;
573
+ }
574
+
575
+ const createResponse = await fetch(attachmentsUrl, {
576
+ method: "POST",
577
+ headers,
578
+ body: JSON.stringify({ name: "input.json" }),
579
+ });
580
+ if (!createResponse.ok) {
581
+ const errorText = await createResponse.text();
582
+ throw new Error(
583
+ translate.t(
584
+ "toolWorkflowCompilerBrowser.errors.createAttachmentFailed",
585
+ { status: createResponse.status, errorText },
586
+ ),
587
+ );
588
+ }
589
+
590
+ const attachment = (await createResponse.json()) as AttachmentResponse;
591
+ const attachmentId = attachment.Id ?? attachment.id ?? "";
592
+ const writeUri =
593
+ attachment.BlobFileAccess?.Uri ?? attachment.blobFileAccess?.Uri;
594
+ if (!attachmentId || !writeUri) {
595
+ throw new Error(
596
+ translate.t(
597
+ "toolWorkflowCompilerBrowser.errors.attachmentResponseMalformed",
598
+ ),
599
+ );
600
+ }
601
+
602
+ // PUT the raw bytes to the SAS blob URI. The Azure blob endpoint
603
+ // requires the BlockBlob type header; matches the proven contract in
604
+ // `@uipath/orchestrator-sdk`'s uploadJobAttachment.
605
+ const uploadResponse = await fetch(writeUri, {
606
+ method: "PUT",
607
+ headers: {
608
+ "Content-Type": "application/octet-stream",
609
+ "x-ms-blob-type": "BlockBlob",
610
+ },
611
+ body: inputArguments,
612
+ });
613
+ if (!uploadResponse.ok) {
614
+ throw new Error(
615
+ translate.t(
616
+ "toolWorkflowCompilerBrowser.errors.uploadInputArgumentsFailed",
617
+ {
618
+ status: uploadResponse.status,
619
+ statusText: uploadResponse.statusText,
620
+ },
621
+ ),
622
+ );
623
+ }
624
+
625
+ logger?.info(
626
+ `[BundleExecutor] Uploaded input arguments as attachment ${attachmentId}`,
627
+ );
628
+ return attachmentId;
629
+ }
630
+
401
631
  private resolveAll(session: BundleSession, result: ToolResult): void {
402
632
  for (const [, entry] of session.projects) {
403
633
  entry.resolve(result);
@@ -405,7 +635,7 @@ export class ProjectBundleExecutor implements IProjectBundleExecutor {
405
635
  }
406
636
 
407
637
  private countCompatibleProjects(context: ProjectOperationContext): number {
408
- return context.solution.Projects.filter(
638
+ return context.projects.filter(
409
639
  (p) =>
410
640
  p.ProjectPath != null &&
411
641
  this.toolFactory.supportedTypes.includes(p.Type),
@@ -7,14 +7,16 @@ import {
7
7
  type IToolLogger,
8
8
  type ProjectOperationContext,
9
9
  ProjectTool,
10
+ ToolErrorCodes,
10
11
  ToolResult,
11
12
  } from "@uipath/solutionpackager-tool-core";
12
13
  import type { IProjectBundleExecutor } from "./project-bundle-executor.js";
13
14
 
14
15
  /**
15
16
  * Browser workflow compiler tool implementation.
16
- * DUMMY: Logs messages indicating where robot/remote agent integration will be added.
17
- * Future: Will send pack commands to robot which starts remote agent to pack projects.
17
+ * Delegates pack and validate to a bundle executor that coordinates a
18
+ * single remote-agent invocation across all compatible projects in the
19
+ * solution via SignalR.
18
20
  */
19
21
  export class WorkflowCompilerTool extends ProjectTool {
20
22
  constructor(
@@ -37,13 +39,17 @@ export class WorkflowCompilerTool extends ProjectTool {
37
39
  }
38
40
 
39
41
  async validateAsync(
40
- _options: IProjectValidateOptions,
42
+ options: IProjectValidateOptions,
41
43
  _cancellationToken?: AbortSignal,
42
44
  ): Promise<ToolResult> {
43
- this.logger.warn(
44
- "Validate operation is not supported in browser environment",
45
- );
46
- return ToolResult.success();
45
+ if (!this.context) {
46
+ return ToolResult.error(
47
+ ToolErrorCodes.InternalError,
48
+ "Validate operation requires a project operation context",
49
+ );
50
+ }
51
+
52
+ return this.bundleExecutor.validateAsync(options, this.context);
47
53
  }
48
54
 
49
55
  async buildAsync(
@@ -61,10 +67,10 @@ export class WorkflowCompilerTool extends ProjectTool {
61
67
  _cancellationToken?: AbortSignal,
62
68
  ): Promise<ToolResult> {
63
69
  if (!this.context) {
64
- this.logger.warn(
70
+ return ToolResult.error(
71
+ ToolErrorCodes.InternalError,
65
72
  "Pack operation requires a project operation context",
66
73
  );
67
- return ToolResult.success();
68
74
  }
69
75
 
70
76
  return this.bundleExecutor.packAsync(options, this.context);