@aifabrix/builder 2.37.0 → 2.37.9

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.
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Deployment status and polling helpers for deployer.
3
+ *
4
+ * @fileoverview Deployment status checks and polling utilities
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const chalk = require('chalk');
10
+ const logger = require('../utils/logger');
11
+
12
+ /**
13
+ * Checks if deployment status is terminal
14
+ * @param {string} status - Deployment status
15
+ * @returns {boolean} True if status is terminal
16
+ */
17
+ function isTerminalStatus(status) {
18
+ return status === 'completed' || status === 'failed' || status === 'cancelled';
19
+ }
20
+
21
+ /**
22
+ * Convert authConfig to pipeline auth config format
23
+ * @param {Object} authConfig - Authentication configuration
24
+ * @returns {Object} Pipeline auth config
25
+ */
26
+ function convertToPipelineAuthConfig(authConfig) {
27
+ return authConfig.type === 'bearer'
28
+ ? { type: 'bearer', token: authConfig.token }
29
+ : { type: 'client-credentials', clientId: authConfig.clientId, clientSecret: authConfig.clientSecret };
30
+ }
31
+
32
+ /**
33
+ * Handles error response from deployment status check
34
+ * @param {Object} response - API response
35
+ * @param {string} deploymentId - Deployment ID
36
+ * @throws {Error} Appropriate error message
37
+ */
38
+ function handleDeploymentStatusError(response, deploymentId) {
39
+ if (response.status === 404) {
40
+ throw new Error(`Deployment ${deploymentId || response.deploymentId || 'unknown'} not found`);
41
+ }
42
+ throw new Error(`Status check failed: ${response.formattedError || response.error || 'Unknown error'}`);
43
+ }
44
+
45
+ /**
46
+ * Extracts deployment data from response
47
+ * @param {Object} response - API response
48
+ * @returns {Object} Deployment data
49
+ */
50
+ function extractDeploymentData(response) {
51
+ const responseData = response.data;
52
+ return responseData.data || responseData;
53
+ }
54
+
55
+ /**
56
+ * Logs deployment progress
57
+ * @param {Object} deploymentData - Deployment data
58
+ * @param {number} attempt - Current attempt
59
+ * @param {number} maxAttempts - Maximum attempts
60
+ */
61
+ function logDeploymentProgress(deploymentData, attempt, maxAttempts) {
62
+ const status = deploymentData.status;
63
+ const progress = deploymentData.progress || 0;
64
+ logger.log(chalk.blue(` Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`));
65
+ }
66
+
67
+ /**
68
+ * Process deployment status response
69
+ * @param {Object} response - API response
70
+ * @param {number} attempt - Current attempt number
71
+ * @param {number} maxAttempts - Maximum attempts
72
+ * @param {number} interval - Polling interval
73
+ * @param {string} deploymentId - Deployment ID for error messages
74
+ * @returns {Promise<Object|null>} Deployment data if terminal, null if needs to continue polling
75
+ */
76
+ async function processDeploymentStatusResponse(response, attempt, maxAttempts, interval, deploymentId) {
77
+ if (!response.success || !response.data) {
78
+ handleDeploymentStatusError(response, deploymentId);
79
+ }
80
+
81
+ const deploymentData = extractDeploymentData(response);
82
+ if (isTerminalStatus(deploymentData.status)) {
83
+ return deploymentData;
84
+ }
85
+
86
+ logDeploymentProgress(deploymentData, attempt, maxAttempts);
87
+ if (attempt < maxAttempts - 1) {
88
+ await new Promise(resolve => setTimeout(resolve, interval));
89
+ }
90
+
91
+ return null;
92
+ }
93
+
94
+ module.exports = {
95
+ isTerminalStatus,
96
+ convertToPipelineAuthConfig,
97
+ handleDeploymentStatusError,
98
+ extractDeploymentData,
99
+ logDeploymentProgress,
100
+ processDeploymentStatusResponse
101
+ };
@@ -16,6 +16,23 @@ const { validateControllerUrl, validateEnvironmentKey } = require('../utils/depl
16
16
  const { handleDeploymentError, handleDeploymentErrors } = require('../utils/deployment-errors');
17
17
  const { validatePipeline, deployPipeline, getPipelineDeployment } = require('../api/pipeline.api');
18
18
  const { handleValidationResponse } = require('../utils/deployment-validation-helpers');
19
+ const {
20
+ convertToPipelineAuthConfig,
21
+ processDeploymentStatusResponse
22
+ } = require('./deployer-status');
23
+
24
+ /**
25
+ * For external systems, send full manifest (application + inline system + full dataSources).
26
+ * No transform - controller receives complete application, external system, and data sources.
27
+ * @param {Object} manifest - Full manifest (type 'external', system, dataSources as full objects)
28
+ * @returns {Object} Manifest to send to pipeline (external sent as-is)
29
+ */
30
+ function transformExternalManifestForPipeline(manifest) {
31
+ if (!manifest) {
32
+ return manifest;
33
+ }
34
+ return manifest;
35
+ }
19
36
 
20
37
  /**
21
38
  * Build validation data for deployment
@@ -28,27 +45,42 @@ const { handleValidationResponse } = require('../utils/deployment-validation-hel
28
45
  */
29
46
  async function buildValidationData(manifest, validatedEnvKey, authConfig, options) {
30
47
  const tokenManager = require('../utils/token-manager');
31
- const { clientId, clientSecret } = await tokenManager.extractClientCredentials(
32
- authConfig,
33
- manifest.key,
34
- validatedEnvKey,
35
- options
36
- );
48
+ let clientId;
49
+ let clientSecret;
50
+ let pipelineAuthConfig;
51
+
52
+ try {
53
+ const credentials = await tokenManager.extractClientCredentials(
54
+ authConfig,
55
+ manifest.key,
56
+ validatedEnvKey,
57
+ options
58
+ );
59
+ clientId = credentials.clientId;
60
+ clientSecret = credentials.clientSecret;
61
+ pipelineAuthConfig = {
62
+ type: 'client-credentials',
63
+ clientId,
64
+ clientSecret
65
+ };
66
+ } catch (credError) {
67
+ if (authConfig.type === 'bearer' && authConfig.token) {
68
+ pipelineAuthConfig = { type: 'bearer', token: authConfig.token };
69
+ clientId = manifest.key;
70
+ clientSecret = '';
71
+ } else {
72
+ throw credError;
73
+ }
74
+ }
37
75
 
38
76
  const repositoryUrl = options.repositoryUrl || `https://github.com/aifabrix/${manifest.key}`;
39
77
  const validationData = {
40
- clientId: clientId,
41
- clientSecret: clientSecret,
78
+ clientId: clientId || '',
79
+ clientSecret: clientSecret || '',
42
80
  repositoryUrl: repositoryUrl,
43
81
  applicationConfig: manifest
44
82
  };
45
83
 
46
- const pipelineAuthConfig = {
47
- type: 'client-credentials',
48
- clientId: clientId,
49
- clientSecret: clientSecret
50
- };
51
-
52
84
  return { validationData, pipelineAuthConfig };
53
85
  }
54
86
 
@@ -130,7 +162,7 @@ function validateDeploymentCredentials(authConfig) {
130
162
  }
131
163
 
132
164
  /**
133
- * Build deployment data and auth config
165
+ * Build deployment data and auth config (supports bearer-only when no client credentials)
134
166
  * @param {string} validateToken - Validation token
135
167
  * @param {Object} authConfig - Authentication configuration
136
168
  * @param {Object} options - Deployment options
@@ -143,11 +175,13 @@ function buildDeploymentData(validateToken, authConfig, options) {
143
175
  imageTag: imageTag
144
176
  };
145
177
 
146
- const pipelineAuthConfig = {
147
- type: 'client-credentials',
148
- clientId: authConfig.clientId,
149
- clientSecret: authConfig.clientSecret
150
- };
178
+ const pipelineAuthConfig = authConfig.type === 'bearer' && authConfig.token && !authConfig.clientId
179
+ ? { type: 'bearer', token: authConfig.token }
180
+ : {
181
+ type: 'client-credentials',
182
+ clientId: authConfig.clientId,
183
+ clientSecret: authConfig.clientSecret
184
+ };
151
185
 
152
186
  return { deployData, pipelineAuthConfig };
153
187
  }
@@ -212,10 +246,12 @@ async function sendDeploymentRequest(url, envKey, validateToken, authConfig, opt
212
246
  const validatedEnvKey = validateEnvironmentKey(envKey);
213
247
  const maxRetries = options.maxRetries || 3;
214
248
 
215
- // Validate credentials
216
- validateDeploymentCredentials(authConfig);
249
+ const useBearerOnly = authConfig.type === 'bearer' && authConfig.token && !authConfig.clientId;
250
+ if (!useBearerOnly) {
251
+ validateDeploymentCredentials(authConfig);
252
+ }
217
253
 
218
- // Build deployment data
254
+ // Build deployment data (supports bearer-only when no client credentials)
219
255
  const { deployData, pipelineAuthConfig } = buildDeploymentData(validateToken, authConfig, options);
220
256
 
221
257
  // Wrap API call with retry logic
@@ -237,92 +273,6 @@ async function sendDeploymentRequest(url, envKey, validateToken, authConfig, opt
237
273
  throwDeploymentError(lastError, maxRetries);
238
274
  }
239
275
 
240
- /**
241
- * Checks if deployment status is terminal
242
- * @function isTerminalStatus
243
- * @param {string} status - Deployment status
244
- * @returns {boolean} True if status is terminal
245
- */
246
- function isTerminalStatus(status) {
247
- return status === 'completed' || status === 'failed' || status === 'cancelled';
248
- }
249
-
250
- /**
251
- * Convert authConfig to pipeline auth config format
252
- * @param {Object} authConfig - Authentication configuration
253
- * @returns {Object} Pipeline auth config
254
- */
255
- function convertToPipelineAuthConfig(authConfig) {
256
- return authConfig.type === 'bearer'
257
- ? { type: 'bearer', token: authConfig.token }
258
- : { type: 'client-credentials', clientId: authConfig.clientId, clientSecret: authConfig.clientSecret };
259
- }
260
-
261
- /**
262
- * Process deployment status response
263
- * @param {Object} response - API response
264
- * @param {number} attempt - Current attempt number
265
- * @param {number} maxAttempts - Maximum attempts
266
- * @param {number} interval - Polling interval
267
- * @param {string} deploymentId - Deployment ID for error messages
268
- * @returns {Object|null} Deployment data if terminal, null if needs to continue polling
269
- */
270
- /**
271
- * Handles error response from deployment status check
272
- * @function handleDeploymentStatusError
273
- * @param {Object} response - API response
274
- * @param {string} deploymentId - Deployment ID
275
- * @throws {Error} Appropriate error message
276
- */
277
- function handleDeploymentStatusError(response, deploymentId) {
278
- if (response.status === 404) {
279
- throw new Error(`Deployment ${deploymentId || response.deploymentId || 'unknown'} not found`);
280
- }
281
- throw new Error(`Status check failed: ${response.formattedError || response.error || 'Unknown error'}`);
282
- }
283
-
284
- /**
285
- * Extracts deployment data from response
286
- * @function extractDeploymentData
287
- * @param {Object} response - API response
288
- * @returns {Object} Deployment data
289
- */
290
- function extractDeploymentData(response) {
291
- const responseData = response.data;
292
- return responseData.data || responseData;
293
- }
294
-
295
- /**
296
- * Logs deployment progress
297
- * @function logDeploymentProgress
298
- * @param {Object} deploymentData - Deployment data
299
- * @param {number} attempt - Current attempt
300
- * @param {number} maxAttempts - Maximum attempts
301
- */
302
- function logDeploymentProgress(deploymentData, attempt, maxAttempts) {
303
- const status = deploymentData.status;
304
- const progress = deploymentData.progress || 0;
305
- logger.log(chalk.blue(` Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`));
306
- }
307
-
308
- async function processDeploymentStatusResponse(response, attempt, maxAttempts, interval, deploymentId) {
309
- if (!response.success || !response.data) {
310
- handleDeploymentStatusError(response, deploymentId);
311
- }
312
-
313
- const deploymentData = extractDeploymentData(response);
314
- if (isTerminalStatus(deploymentData.status)) {
315
- return deploymentData;
316
- }
317
-
318
- logDeploymentProgress(deploymentData, attempt, maxAttempts);
319
- if (attempt < maxAttempts - 1) {
320
- await new Promise(resolve => setTimeout(resolve, interval));
321
- }
322
-
323
- return null;
324
- }
325
-
326
276
  /**
327
277
  * Polls deployment status from controller
328
278
  * Uses pipeline endpoint for CI/CD monitoring with minimal deployment info
@@ -470,9 +420,11 @@ async function deployToController(manifest, controllerUrl, envKey, authConfig, o
470
420
  // Log deployment attempt for audit
471
421
  await auditLogger.logDeploymentAttempt(manifest.key, url, options);
472
422
 
423
+ const pipelineManifest = transformExternalManifestForPipeline(manifest);
424
+
473
425
  try {
474
426
  // Send deployment request
475
- const result = await sendDeployment(url, validatedEnvKey, manifest, authConfig, options);
427
+ const result = await sendDeployment(url, validatedEnvKey, pipelineManifest, authConfig, options);
476
428
 
477
429
  // Poll for deployment status if enabled
478
430
  return await pollDeployment(result, url, validatedEnvKey, authConfig, options);
@@ -9,16 +9,21 @@
9
9
  * @version 2.0.0
10
10
  */
11
11
 
12
+ const fs = require('fs');
13
+ const path = require('path');
12
14
  const chalk = require('chalk');
15
+ const Ajv = require('ajv');
13
16
  const logger = require('../utils/logger');
14
17
  const config = require('../core/config');
15
18
  const { resolveControllerUrl } = require('../utils/controller-url');
16
19
  const { validateControllerUrl, validateEnvironmentKey } = require('../utils/deployment-validation');
17
20
  const { getOrRefreshDeviceToken } = require('../utils/token-manager');
18
- const { getEnvironmentStatus } = require('../api/environments.api');
19
- const { deployEnvironment: deployEnvironmentInfra } = require('../api/deployments.api');
21
+ const { getPipelineDeployment } = require('../api/pipeline.api');
22
+ const { deployEnvironment: deployEnvironmentInfra, getDeployment } = require('../api/deployments.api');
20
23
  const { handleDeploymentErrors } = require('../utils/deployment-errors');
24
+ const { formatValidationErrors } = require('../utils/error-formatter');
21
25
  const auditLogger = require('../core/audit-logger');
26
+ const environmentDeployRequestSchema = require('../schema/environment-deploy-request.schema.json');
22
27
 
23
28
  /**
24
29
  * Validates environment deployment prerequisites
@@ -77,26 +82,102 @@ async function getEnvironmentAuth(controllerUrl) {
77
82
  * @returns {Promise<Object>} Deployment result
78
83
  * @throws {Error} If deployment fails
79
84
  */
85
+ /** Reads and parses config file; throws if missing, unreadable, or invalid structure. */
86
+ function parseEnvironmentConfigFile(resolvedPath) {
87
+ let raw;
88
+ try {
89
+ raw = fs.readFileSync(resolvedPath, 'utf8');
90
+ } catch (e) {
91
+ throw new Error(`Cannot read config file: ${resolvedPath}. ${e.message}`);
92
+ }
93
+ let parsed;
94
+ try {
95
+ parsed = JSON.parse(raw);
96
+ } catch (e) {
97
+ throw new Error(
98
+ `Invalid JSON in config file: ${resolvedPath}\n${e.message}\n` +
99
+ 'Expected format: { "environmentConfig": { "key", "environment", "preset", "serviceName", "location" }, "dryRun": false }'
100
+ );
101
+ }
102
+ if (parsed === null || typeof parsed !== 'object') {
103
+ throw new Error(
104
+ `Config file must be a JSON object with "environmentConfig". File: ${resolvedPath}\n` +
105
+ 'Example: { "environmentConfig": { "key": "dev", "environment": "dev", "preset": "s", "serviceName": "aifabrix", "location": "swedencentral" }, "dryRun": false }'
106
+ );
107
+ }
108
+ if (parsed.environmentConfig === undefined) {
109
+ throw new Error(
110
+ `Config file must contain "environmentConfig" (object). File: ${resolvedPath}\n` +
111
+ 'Example: { "environmentConfig": { "key": "dev", "environment": "dev", "preset": "s", "serviceName": "aifabrix", "location": "swedencentral" } }'
112
+ );
113
+ }
114
+ if (typeof parsed.environmentConfig !== 'object' || parsed.environmentConfig === null) {
115
+ throw new Error(`"environmentConfig" must be an object. File: ${resolvedPath}`);
116
+ }
117
+ return parsed;
118
+ }
119
+
80
120
  /**
81
- * Builds environment deployment request
82
- * @function buildEnvironmentDeploymentRequest
83
- * @param {string} validatedEnvKey - Validated environment key
84
- * @param {Object} options - Deployment options
85
- * @returns {Object} Deployment request object
121
+ * Validates parsed config against schema and returns deploy request.
122
+ * @param {Object} parsed - Parsed config object
123
+ * @param {string} resolvedPath - Path for error messages
124
+ * @returns {Object} { environmentConfig, dryRun? }
86
125
  */
87
- function buildEnvironmentDeploymentRequest(validatedEnvKey, options) {
88
- const capitalized = validatedEnvKey.charAt(0).toUpperCase() + validatedEnvKey.slice(1);
89
- const request = {
90
- key: validatedEnvKey,
91
- displayName: `${capitalized} Environment`,
92
- description: `${capitalized} environment for application deployments`
126
+ function validateEnvironmentDeployParsed(parsed, resolvedPath) {
127
+ const ajv = new Ajv({ allErrors: true, strict: false });
128
+ const validate = ajv.compile(environmentDeployRequestSchema);
129
+ if (!validate(parsed)) {
130
+ const messages = formatValidationErrors(validate.errors);
131
+ throw new Error(
132
+ `Environment config validation failed (${resolvedPath}):\n • ${messages.join('\n • ')}\n` +
133
+ 'Fix the config file and run the command again. See templates/infra/environment-dev.json for a valid example.'
134
+ );
135
+ }
136
+ return {
137
+ environmentConfig: parsed.environmentConfig,
138
+ dryRun: Boolean(parsed.dryRun)
93
139
  };
140
+ }
94
141
 
95
- if (options.config) {
96
- request.description += ` (config: ${options.config})`;
142
+ /**
143
+ * Loads and validates environment deploy config from a JSON file
144
+ * @param {string} configPath - Absolute or relative path to config JSON
145
+ * @returns {Object} Valid deploy request { environmentConfig, dryRun? }
146
+ * @throws {Error} If file missing, invalid JSON, or validation fails
147
+ */
148
+ function loadAndValidateEnvironmentDeployConfig(configPath) {
149
+ const resolvedPath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
150
+ if (!fs.existsSync(resolvedPath)) {
151
+ throw new Error(
152
+ `Environment config file not found: ${resolvedPath}\n` +
153
+ 'Use --config <file> with a JSON file containing "environmentConfig" (e.g. templates/infra/environment-dev.json).'
154
+ );
97
155
  }
156
+ const parsed = parseEnvironmentConfigFile(resolvedPath);
157
+ return validateEnvironmentDeployParsed(parsed, resolvedPath);
158
+ }
98
159
 
99
- return request;
160
+ /**
161
+ * Builds environment deployment request from options (config file required)
162
+ * @function buildEnvironmentDeploymentRequest
163
+ * @param {string} validatedEnvKey - Validated environment key
164
+ * @param {Object} options - Deployment options (must include options.config)
165
+ * @returns {Object} Deployment request object for API
166
+ */
167
+ function buildEnvironmentDeploymentRequest(validatedEnvKey, options) {
168
+ if (!options.config || typeof options.config !== 'string') {
169
+ throw new Error(
170
+ 'Environment deploy requires a config file with "environmentConfig". Use --config <file>.\n' +
171
+ 'Example: aifabrix environment deploy dev --config templates/infra/environment-dev.json'
172
+ );
173
+ }
174
+ const deployRequest = loadAndValidateEnvironmentDeployConfig(options.config);
175
+ if (deployRequest.environmentConfig.key && deployRequest.environmentConfig.key !== validatedEnvKey) {
176
+ logger.log(chalk.yellow(
177
+ `⚠ Config key "${deployRequest.environmentConfig.key}" does not match deploy target "${validatedEnvKey}"; using target "${validatedEnvKey}".`
178
+ ));
179
+ }
180
+ return deployRequest;
100
181
  }
101
182
 
102
183
  /**
@@ -155,22 +236,47 @@ async function sendEnvironmentDeployment(controllerUrl, envKey, authConfig, opti
155
236
  }
156
237
 
157
238
  /**
158
- * Process environment status response
159
- * @param {Object} response - API response
239
+ * Fetches deployment status by ID (pipeline endpoint first, then environments)
240
+ * Mirrors miso-controller manual test: GET pipeline/deployments/:id then GET environments/deployments/:id
241
+ * @async
242
+ * @param {string} controllerUrl - Controller URL
243
+ * @param {string} envKey - Environment key
244
+ * @param {string} deploymentId - Deployment ID
245
+ * @param {Object} apiAuthConfig - Auth config { type: 'bearer', token }
246
+ * @returns {Promise<Object|null>} Deployment record (status, progress, etc.) or null
247
+ */
248
+ async function getDeploymentStatusById(controllerUrl, envKey, deploymentId, apiAuthConfig) {
249
+ try {
250
+ const pipelineRes = await getPipelineDeployment(controllerUrl, envKey, deploymentId, apiAuthConfig);
251
+ if (pipelineRes?.data?.data) return pipelineRes.data.data;
252
+ if (pipelineRes?.data && typeof pipelineRes.data === 'object' && pipelineRes.data.status) return pipelineRes.data;
253
+ } catch {
254
+ // Fallback to environments endpoint
255
+ }
256
+ try {
257
+ const envRes = await getDeployment(controllerUrl, envKey, deploymentId, apiAuthConfig);
258
+ if (envRes?.data?.data) return envRes.data.data;
259
+ if (envRes?.data && typeof envRes.data === 'object' && envRes.data.status) return envRes.data;
260
+ } catch {
261
+ // Ignore
262
+ }
263
+ return null;
264
+ }
265
+
266
+ /**
267
+ * Process deployment record from GET .../deployments/:deploymentId
268
+ * @param {Object|null} deployment - Deployment record (status, progress, message, error)
160
269
  * @param {string} validatedEnvKey - Validated environment key
161
- * @returns {Object|null} Status result if ready, null if needs to continue polling
270
+ * @returns {Object|null} Status result if completed, null if still in progress
162
271
  * @throws {Error} If deployment failed
163
272
  */
164
- function processEnvironmentStatusResponse(response, validatedEnvKey) {
165
- if (!response.success || !response.data) {
273
+ function processDeploymentStatusResponse(deployment, validatedEnvKey) {
274
+ if (!deployment || typeof deployment !== 'object') {
166
275
  return null;
167
276
  }
168
277
 
169
- const responseData = response.data.data || response.data;
170
- const status = responseData.status || responseData.ready;
171
- const isReady = status === 'ready' || status === 'completed' || responseData.ready === true;
172
-
173
- if (isReady) {
278
+ const status = deployment.status;
279
+ if (status === 'completed') {
174
280
  return {
175
281
  success: true,
176
282
  environment: validatedEnvKey,
@@ -179,9 +285,9 @@ function processEnvironmentStatusResponse(response, validatedEnvKey) {
179
285
  };
180
286
  }
181
287
 
182
- // Check for terminal failure states
183
288
  if (status === 'failed' || status === 'error') {
184
- throw new Error(`Environment deployment failed: ${responseData.message || 'Unknown error'}`);
289
+ const msg = deployment.message || deployment.error || 'Unknown error';
290
+ throw new Error(`Environment deployment failed: ${msg}`);
185
291
  }
186
292
 
187
293
  return null;
@@ -213,8 +319,18 @@ async function pollEnvironmentStatus(deploymentId, controllerUrl, envKey, authCo
213
319
  try {
214
320
  await new Promise(resolve => setTimeout(resolve, pollInterval));
215
321
 
216
- const response = await getEnvironmentStatus(validatedUrl, validatedEnvKey, apiAuthConfig);
217
- const statusResult = processEnvironmentStatusResponse(response, validatedEnvKey);
322
+ const deployment = await getDeploymentStatusById(
323
+ validatedUrl,
324
+ validatedEnvKey,
325
+ deploymentId,
326
+ apiAuthConfig
327
+ );
328
+ const progress = deployment?.progress ?? 0;
329
+ const statusLabel = deployment?.status ?? 'pending';
330
+ if (attempt <= maxAttempts) {
331
+ logger.log(chalk.gray(` Attempt ${attempt}/${maxAttempts}... Status: ${statusLabel} (${progress}%)`));
332
+ }
333
+ const statusResult = processDeploymentStatusResponse(deployment, validatedEnvKey);
218
334
  if (statusResult) {
219
335
  return statusResult;
220
336
  }
@@ -225,10 +341,6 @@ async function pollEnvironmentStatus(deploymentId, controllerUrl, envKey, authCo
225
341
  }
226
342
  // Otherwise, continue polling
227
343
  }
228
-
229
- if (attempt < maxAttempts) {
230
- logger.log(chalk.gray(` Attempt ${attempt}/${maxAttempts}...`));
231
- }
232
344
  }
233
345
 
234
346
  // Timeout
@@ -380,8 +492,6 @@ async function deployEnvironment(envKey, options = {}) {
380
492
  throw error;
381
493
  }
382
494
  }
383
-
384
495
  module.exports = {
385
496
  deployEnvironment
386
497
  };
387
-
@@ -97,7 +97,11 @@ async function deployExternalSystem(appName, options = {}) {
97
97
 
98
98
  return result;
99
99
  } catch (error) {
100
- throw new Error(`Failed to deploy external system: ${error.message}`);
100
+ let message = `Failed to deploy external system: ${error.message}`;
101
+ if (error.message && error.message.includes('Application not found')) {
102
+ message += `\n\n💡 Register the app in the controller first: aifabrix app register ${appName}`;
103
+ }
104
+ throw new Error(message);
101
105
  }
102
106
  }
103
107
 
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * External System Test Authentication Helpers
3
3
  *
4
- * Authentication setup for integration tests
4
+ * Authentication setup for integration tests. Uses the dataplane service URL
5
+ * (discovered from the controller), not the external system app's config—external
6
+ * apps do not store a dataplane URL.
5
7
  *
6
8
  * @fileoverview Authentication helpers for external system testing
7
9
  * @author AI Fabrix Team
@@ -9,19 +11,19 @@
9
11
  */
10
12
 
11
13
  const { getDeploymentAuth } = require('../utils/token-manager');
12
- const { getDataplaneUrl } = require('../datasource/deploy');
14
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
13
15
  const { resolveControllerUrl } = require('../utils/controller-url');
14
16
 
15
17
  /**
16
18
  * Setup authentication and get dataplane URL for integration tests
17
19
  * @async
18
- * @param {string} appName - Application name
19
- * @param {Object} options - Test options
20
- * @param {Object} config - Configuration object
20
+ * @param {string} appName - Application name (used for auth scope; dataplane URL is discovered from controller)
21
+ * @param {Object} options - Test options; options.dataplane overrides discovered URL
22
+ * @param {Object} _config - Configuration object
21
23
  * @returns {Promise<Object>} Object with authConfig and dataplaneUrl
22
24
  * @throws {Error} If authentication fails
23
25
  */
24
- async function setupIntegrationTestAuth(appName, _options, _config) {
26
+ async function setupIntegrationTestAuth(appName, options, _config) {
25
27
  const { resolveEnvironment } = require('../core/config');
26
28
  const environment = await resolveEnvironment();
27
29
  const controllerUrl = await resolveControllerUrl();
@@ -31,7 +33,12 @@ async function setupIntegrationTestAuth(appName, _options, _config) {
31
33
  throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
32
34
  }
33
35
 
34
- const dataplaneUrl = await getDataplaneUrl(controllerUrl, appName, environment, authConfig);
36
+ let dataplaneUrl;
37
+ if (options && options.dataplane && typeof options.dataplane === 'string' && options.dataplane.trim()) {
38
+ dataplaneUrl = options.dataplane.trim();
39
+ } else {
40
+ dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
41
+ }
35
42
 
36
43
  return { authConfig, dataplaneUrl };
37
44
  }