@aifabrix/builder 2.37.0 → 2.37.5

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.
@@ -70,6 +70,23 @@ lib/
70
70
  └── schema/ # JSON schemas
71
71
  ```
72
72
 
73
+ ### Generated Output (integration/ and builder/)
74
+
75
+ Files under **integration/** and **builder/** are **auto-generated** by the CLI. When fixing bugs or changing behavior, validate **where** each file is produced so fixes go into the generator, not only into the generated artifact.
76
+
77
+ - **integration/** – External system / wizard output:
78
+ - Path: `integration/<appName>/` (see `lib/utils/paths.js` → `getIntegrationPath`).
79
+ - Generated by: `lib/generator/wizard.js` (`generateWizardFiles`, `generateConfigFilesForWizard`), `lib/external-system/download.js`, wizard commands in `lib/commands/wizard-core.js` and `lib/commands/wizard.js`.
80
+ - Typical outputs: `variables.yaml`, `env.template`, `README.md`, `*-system.json`, `*-datasource*.json`, `*-deploy.json`, deploy script (`deploy.js`), and optionally `wizard.yaml`, `error.log`.
81
+ - **builder/** – Application (non-external) output:
82
+ - Path: `builder/<appName>/` or custom root via `AIFABRIX_BUILDER_DIR` (see `lib/utils/paths.js` → `getBuilderPath`).
83
+ - Generated by: app create/register flow, `lib/generator/index.js`, `lib/commands/up-common.js`, `lib/core/secrets.js`, and related app/deploy logic.
84
+ - Typical outputs: `variables.yaml`, `env.template`, deploy JSON, `.env` (from secrets), and app-specific config.
85
+
86
+ **Editable vs generated:**
87
+ - Some generated files are **intended to be edited** (e.g. `variables.yaml`, `env.template`, `README.md`, `wizard.yaml`). Improvements to defaults or structure still belong in the generator/templates.
88
+ - **When debugging:** First identify the **source of generation** (which module and function write the file). Fix bugs in that generator or template; avoid treating a one-off edit in integration/ or builder/ as the permanent fix unless it’s a deliberate local override.
89
+
73
90
  ### CLI Command Pattern
74
91
  Commands are defined in `lib/cli.js` using Commander.js:
75
92
  ```javascript
@@ -850,6 +867,7 @@ Define request/response types using JSDoc `@typedef`:
850
867
 
851
868
  ### Must Do (✅)
852
869
  - ✅ Validate all inputs (app names, file paths, URLs)
870
+ - ✅ When fixing bugs in integration/ or builder/ output: identify the generator that produces the file and fix the source (lib/generator, lib/commands, templates), not only the generated artifact
853
871
  - ✅ Use try-catch for all async operations
854
872
  - ✅ Provide meaningful error messages with context
855
873
  - ✅ Use JSDoc for all public functions
@@ -874,6 +892,7 @@ Define request/response types using JSDoc `@typedef`:
874
892
  - ❌ Never use `eval()` or `Function()` constructor
875
893
  - ❌ Never use raw paths (always use path.join)
876
894
  - ❌ Never make direct API calls using `makeApiCall` in new code (use `lib/api/` modules)
895
+ - ❌ Never treat one-off edits in integration/ or builder/ as the permanent fix for a bug—update the generator or template that produces the file
877
896
  - ❌ Never skip type definitions for API request/response types
878
897
  - ❌ Never log authentication tokens or secrets in API calls
879
898
 
@@ -511,7 +511,7 @@ async function checkAppDirectory(appPath) {
511
511
  * @throws {Error} If required files are missing
512
512
  */
513
513
  async function validateRequiredFiles(appPath, entries) {
514
- const requiredFiles = ['variables.yaml', 'env.template', 'README.md', 'deploy.sh', 'deploy.ps1'];
514
+ const requiredFiles = ['variables.yaml', 'env.template', 'README.md', 'deploy.js'];
515
515
  const missingFiles = [];
516
516
  for (const fileName of requiredFiles) {
517
517
  const filePath = path.join(appPath, fileName);
@@ -311,7 +311,7 @@ async function testMcpConnection(dataplaneUrl, authConfig, serverUrl, token) {
311
311
  }
312
312
 
313
313
  /**
314
- * Get deployment documentation for a system
314
+ * Get deployment documentation for a system (from dataplane DB only)
315
315
  * GET /api/v1/wizard/deployment-docs/{systemKey}
316
316
  * @async
317
317
  * @function getDeploymentDocs
@@ -326,6 +326,28 @@ async function getDeploymentDocs(dataplaneUrl, authConfig, systemKey) {
326
326
  return await client.get(`/api/v1/wizard/deployment-docs/${systemKey}`);
327
327
  }
328
328
 
329
+ /**
330
+ * Generate deployment documentation with variables.yaml and deploy JSON for better quality
331
+ * POST /api/v1/wizard/deployment-docs/{systemKey}
332
+ * Sends deployJson and variablesYaml in the request body so the dataplane can align README with the integration folder.
333
+ * @async
334
+ * @function postDeploymentDocs
335
+ * @param {string} dataplaneUrl - Dataplane base URL
336
+ * @param {Object} authConfig - Authentication configuration
337
+ * @param {string} systemKey - System key identifier
338
+ * @param {Object} [body] - Optional request body (WizardDeploymentDocsRequest)
339
+ * @param {Object} [body.deployJson] - Deploy JSON object (e.g. *-deploy.json content)
340
+ * @param {string} [body.variablesYaml] - variables.yaml file content as string
341
+ * @returns {Promise<Object>} Deployment documentation response (content, contentType, systemKey)
342
+ * @throws {Error} If request fails
343
+ */
344
+ async function postDeploymentDocs(dataplaneUrl, authConfig, systemKey, body = null) {
345
+ const client = new ApiClient(dataplaneUrl, authConfig);
346
+ return await client.post(`/api/v1/wizard/deployment-docs/${systemKey}`, {
347
+ body: body || {}
348
+ });
349
+ }
350
+
329
351
  /**
330
352
  * Get known wizard platforms from dataplane.
331
353
  * GET /api/v1/wizard/platforms
@@ -365,5 +387,6 @@ module.exports = {
365
387
  getPreview,
366
388
  testMcpConnection,
367
389
  getDeploymentDocs,
390
+ postDeploymentDocs,
368
391
  getWizardPlatforms
369
392
  };
package/lib/app/deploy.js CHANGED
@@ -15,7 +15,7 @@ const yaml = require('js-yaml');
15
15
  const chalk = require('chalk');
16
16
  const pushUtils = require('../deployment/push');
17
17
  const logger = require('../utils/logger');
18
- const { detectAppType } = require('../utils/paths');
18
+ const { detectAppType, getBuilderPath, getIntegrationPath } = require('../utils/paths');
19
19
  const { checkApplicationExists } = require('../utils/app-existence');
20
20
  const { loadDeploymentConfig } = require('./deploy-config');
21
21
 
@@ -219,15 +219,16 @@ function displayDeploymentResults(result) {
219
219
  }
220
220
 
221
221
  /**
222
- * Check if app is external and handle external deployment
222
+ * Check if app is external and handle external deployment.
223
+ * When options.type === 'external', forces deployment from integration/<app> (no app register needed).
223
224
  * @async
224
225
  * @function handleExternalDeployment
225
226
  * @param {string} appName - Application name
226
- * @param {Object} options - Deployment options
227
+ * @param {Object} options - Deployment options (type: 'external' to force integration/<app>)
227
228
  * @returns {Promise<Object|null>} Deployment result if external, null otherwise
228
229
  */
229
230
  async function handleExternalDeployment(appName, options) {
230
- const { isExternal } = await detectAppType(appName);
231
+ const { isExternal } = await detectAppType(appName, options);
231
232
  if (isExternal) {
232
233
  const externalDeploy = require('../external-system/deploy');
233
234
  await externalDeploy.deployExternalSystem(appName, options);
@@ -317,6 +318,37 @@ async function executeStandardDeployment(appName, options) {
317
318
  }
318
319
  }
319
320
 
321
+ /**
322
+ * Tries external deploy when builder/<app> does not exist but integration/<app> does.
323
+ * @async
324
+ * @param {string} appName - Application name
325
+ * @param {Object} options - Deployment options
326
+ * @returns {Promise<{usedExternalDeploy: boolean, result: Object|null}>}
327
+ */
328
+ async function tryExternalDeployFallback(appName, options) {
329
+ const builderPath = getBuilderPath(appName);
330
+ const integrationPath = getIntegrationPath(appName);
331
+ let builderExists = false;
332
+ let integrationExists = false;
333
+ try {
334
+ await fs.access(builderPath);
335
+ builderExists = true;
336
+ } catch (e) {
337
+ if (e.code !== 'ENOENT') throw e;
338
+ }
339
+ try {
340
+ await fs.access(integrationPath);
341
+ integrationExists = true;
342
+ } catch (e) {
343
+ if (e.code !== 'ENOENT') throw e;
344
+ }
345
+ if (!builderExists && integrationExists) {
346
+ const fallbackResult = await handleExternalDeployment(appName, { ...options, type: 'external' });
347
+ if (fallbackResult) return { usedExternalDeploy: true, result: fallbackResult };
348
+ }
349
+ return { usedExternalDeploy: false, result: null };
350
+ }
351
+
320
352
  /**
321
353
  * Deploys application to Miso Controller
322
354
  * Orchestrates manifest generation, key creation, and deployment
@@ -338,7 +370,7 @@ async function executeStandardDeployment(appName, options) {
338
370
  */
339
371
  async function deployApp(appName, options = {}) {
340
372
  let controllerUrl = null;
341
- let usedExternalDeploy = false;
373
+ let usedExternalDeploy = options.type === 'external';
342
374
 
343
375
  try {
344
376
  if (!appName || typeof appName !== 'string' || appName.trim().length === 0) {
@@ -347,8 +379,12 @@ async function deployApp(appName, options = {}) {
347
379
  validateAppName(appName);
348
380
 
349
381
  const externalResult = await handleExternalDeployment(appName, options);
350
- if (externalResult) {
351
- return externalResult;
382
+ if (externalResult) return externalResult;
383
+
384
+ const fallback = await tryExternalDeployFallback(appName, options);
385
+ if (fallback.result) {
386
+ usedExternalDeploy = fallback.usedExternalDeploy;
387
+ return fallback.result;
352
388
  }
353
389
  usedExternalDeploy = false;
354
390
 
package/lib/app/list.js CHANGED
@@ -164,9 +164,11 @@ function displayApplications(applications, environment, controllerUrl) {
164
164
 
165
165
  logger.log(chalk.bold(`\n📱 ${header}:\n`));
166
166
  applications.forEach((app) => {
167
+ const isExternal = app.configuration?.type === 'external';
168
+ const externalIcon = isExternal ? '🔗 ' : '';
167
169
  const hasPipeline = app.configuration?.pipeline?.isActive ? '✓' : '✗';
168
170
  const urlAndPort = formatUrlAndPort(app);
169
- logger.log(`${hasPipeline} ${chalk.cyan(app.key)} - ${app.displayName} (${app.status || 'unknown'})${urlAndPort}`);
171
+ logger.log(`${externalIcon}${hasPipeline} ${chalk.cyan(app.key)} - ${app.displayName} (${app.status || 'unknown'})${urlAndPort}`);
170
172
  });
171
173
  logger.log(chalk.gray(' To show details for an app: aifabrix app show <appKey>\n'));
172
174
  }
@@ -200,7 +200,7 @@ async function postBuildTasks(appName, buildConfig) {
200
200
  }
201
201
 
202
202
  /**
203
- * Check if app is external type and handle accordingly
203
+ * External apps have no Docker image; deploy JSON is generated by aifabrix json.
204
204
  * @async
205
205
  * @param {string} appName - Application name
206
206
  * @returns {Promise<boolean>} True if external (handled), false if should continue
@@ -208,9 +208,8 @@ async function postBuildTasks(appName, buildConfig) {
208
208
  async function checkExternalAppType(appName) {
209
209
  const variables = await loadVariablesYaml(appName);
210
210
  if (variables.app && variables.app.type === 'external') {
211
- const generator = require('../generator');
212
- const jsonPath = await generator.generateDeployJson(appName);
213
- logger.log(chalk.green(`✓ Generated deployment JSON: ${jsonPath}`));
211
+ logger.log(chalk.blue(`External system: ${appName}`));
212
+ logger.log(chalk.gray('To regenerate deployment JSON, run: aifabrix json ' + appName));
214
213
  return true;
215
214
  }
216
215
  return false;
@@ -197,6 +197,7 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
197
197
 
198
198
  program.command('deploy <app>')
199
199
  .description('Deploy to Azure via Miso Controller')
200
+ .option('--type <type>', 'Application type: external to deploy from integration/<app> (no app register needed)')
200
201
  .option('--client-id <id>', 'Client ID (overrides config)')
201
202
  .option('--client-secret <secret>', 'Client Secret (overrides config)')
202
203
  .option('--poll', 'Poll for deployment status', true)
@@ -66,6 +66,7 @@ function setupExternalSystemCommands(program) {
66
66
  .description('Run integration tests via dataplane pipeline API')
67
67
  .option('-d, --datasource <key>', 'Test specific datasource only')
68
68
  .option('-p, --payload <file>', 'Path to custom test payload file')
69
+ .option('--dataplane <url>', 'Dataplane URL (default: discovered from controller)')
69
70
  .option('-v, --verbose', 'Show detailed test output')
70
71
  .option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
71
72
  .action(async(appName, options) => {
@@ -87,7 +87,7 @@ function setupUtilityCommands(program) {
87
87
  });
88
88
 
89
89
  program.command('json <app>')
90
- .description('Generate deployment JSON (aifabrix-deploy.json for normal apps, application-schema.json for external systems)')
90
+ .description('Generate deployment JSON to disk (<app>-deploy.json). Use before commit so version control has the correct file.')
91
91
  .option('--type <type>', 'Application type (external) - if set, only checks integration folder')
92
92
  .action(async(appName, options) => {
93
93
  try {
@@ -30,6 +30,36 @@ async function ensureTemplateAtPath(appName, targetAppPath) {
30
30
  return true;
31
31
  }
32
32
 
33
+ /**
34
+ * Patches variables.yaml to set build.envOutputPath to null for deploy-only (no local code).
35
+ * Use when running up-miso/up-platform so we do not copy .env to repo paths or show that message.
36
+ * Patches both primary builder path and cwd/builder if different.
37
+ *
38
+ * @param {string} appName - Application name (e.g. miso-controller, dataplane)
39
+ */
40
+ function patchEnvOutputPathForDeployOnly(appName) {
41
+ if (!appName || typeof appName !== 'string') return;
42
+ const pathsToPatch = [pathsUtil.getBuilderPath(appName)];
43
+ const cwdBuilderPath = path.join(process.cwd(), 'builder', appName);
44
+ if (path.resolve(cwdBuilderPath) !== path.resolve(pathsToPatch[0])) {
45
+ pathsToPatch.push(cwdBuilderPath);
46
+ }
47
+ const envOutputPathLine = /^(\s*envOutputPath:)\s*.*$/m;
48
+ const replacement = '$1 null # deploy only, no copy';
49
+ for (const appPath of pathsToPatch) {
50
+ const variablesPath = path.join(appPath, 'variables.yaml');
51
+ if (!fs.existsSync(variablesPath)) continue;
52
+ try {
53
+ let content = fs.readFileSync(variablesPath, 'utf8');
54
+ if (!envOutputPathLine.test(content)) continue;
55
+ content = content.replace(envOutputPathLine, replacement);
56
+ fs.writeFileSync(variablesPath, content, 'utf8');
57
+ } catch (err) {
58
+ logger.warn(chalk.yellow(`Could not patch envOutputPath in ${variablesPath}: ${err.message}`));
59
+ }
60
+ }
61
+ }
62
+
33
63
  /**
34
64
  * Ensures builder app directory exists from template if variables.yaml is missing.
35
65
  * If builder/<appName>/variables.yaml does not exist, copies from templates/applications/<appName>.
@@ -69,4 +99,4 @@ async function ensureAppFromTemplate(appName) {
69
99
  return primaryCopied;
70
100
  }
71
101
 
72
- module.exports = { ensureAppFromTemplate };
102
+ module.exports = { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly };
@@ -19,7 +19,7 @@ const secrets = require('../core/secrets');
19
19
  const infra = require('../infrastructure');
20
20
  const app = require('../app');
21
21
  const { saveLocalSecret } = require('../utils/local-secrets');
22
- const { ensureAppFromTemplate } = require('./up-common');
22
+ const { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly } = require('./up-common');
23
23
 
24
24
  /** Keycloak base port (from templates/applications/keycloak/variables.yaml) */
25
25
  const KEYCLOAK_BASE_PORT = 8082;
@@ -132,6 +132,10 @@ async function handleUpMiso(options = {}) {
132
132
  await ensureAppFromTemplate('keycloak');
133
133
  await ensureAppFromTemplate('miso-controller');
134
134
  await ensureAppFromTemplate('dataplane');
135
+ // Deploy-only: do not copy .env to repo paths; patch variables so envOutputPath is null
136
+ patchEnvOutputPathForDeployOnly('keycloak');
137
+ patchEnvOutputPathForDeployOnly('miso-controller');
138
+ patchEnvOutputPathForDeployOnly('dataplane');
135
139
  const developerId = await config.getDeveloperId();
136
140
  const devIdNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
137
141
  await setMisoSecretsAndResolve(devIdNum);
@@ -18,7 +18,8 @@ const {
18
18
  detectType,
19
19
  generateConfig,
20
20
  validateWizardConfig,
21
- getDeploymentDocs
21
+ getDeploymentDocs,
22
+ postDeploymentDocs
22
23
  } = require('../api/wizard.api');
23
24
  const { generateWizardFiles } = require('../generator/wizard');
24
25
  const {
@@ -353,23 +354,47 @@ async function handleFileSaving(appName, systemConfig, datasourceConfigs, system
353
354
  logger.log(chalk.blue('\n\uD83D\uDCCB Step 7: Save Files'));
354
355
  const spinner = ora('Saving files...').start();
355
356
  try {
356
- let aiGeneratedReadme = null;
357
- if (systemKey && dataplaneUrl && authConfig) {
357
+ const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme: null });
358
+ if (systemKey && dataplaneUrl && authConfig && generatedFiles.appPath) {
358
359
  try {
359
- const docsResponse = await getDeploymentDocs(dataplaneUrl, authConfig, systemKey);
360
- if (docsResponse.success && docsResponse.data?.content) aiGeneratedReadme = docsResponse.data.content;
360
+ const appPath = generatedFiles.appPath;
361
+ const deployKey = appName;
362
+ const variablesPath = path.join(appPath, 'variables.yaml');
363
+ const deployPath = path.join(appPath, `${deployKey}-deploy.json`);
364
+ let variablesYaml = null;
365
+ let deployJson = null;
366
+ try {
367
+ variablesYaml = await fs.readFile(variablesPath, 'utf8');
368
+ } catch {
369
+ // optional
370
+ }
371
+ try {
372
+ const deployContent = await fs.readFile(deployPath, 'utf8');
373
+ deployJson = JSON.parse(deployContent);
374
+ } catch {
375
+ // optional
376
+ }
377
+ const body = (variablesYaml !== null && variablesYaml !== undefined) || (deployJson !== null && deployJson !== undefined) ? { variablesYaml: variablesYaml || null, deployJson: deployJson || null } : null;
378
+ const docsResponse = body
379
+ ? await postDeploymentDocs(dataplaneUrl, authConfig, systemKey, body)
380
+ : await getDeploymentDocs(dataplaneUrl, authConfig, systemKey);
381
+ const content = docsResponse?.data?.content ?? docsResponse?.content;
382
+ if (content && typeof content === 'string') {
383
+ const readmePath = path.join(appPath, 'README.md');
384
+ await fs.writeFile(readmePath, content, 'utf8');
385
+ logger.log(chalk.gray(' Updated README.md from deployment-docs API (variables.yaml + deploy JSON).'));
386
+ }
361
387
  } catch (e) {
362
388
  logger.log(chalk.gray(` Could not fetch AI-generated README: ${e.message}`));
363
389
  }
364
390
  }
365
- const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme });
366
391
  spinner.stop();
367
392
  logger.log(chalk.green('\n\u2713 Wizard completed successfully!'));
368
393
  logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
369
394
  logger.log(chalk.blue('\nNext steps:'));
370
395
  logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
371
396
  logger.log(chalk.gray(' 2. Update env.template with your authentication details'));
372
- logger.log(chalk.gray(` 3. Deploy using: ./deploy.sh or aifabrix deploy ${appName}`));
397
+ logger.log(chalk.gray(` 3. Deploy using: node deploy.js or aifabrix deploy ${appName}`));
373
398
  return generatedFiles;
374
399
  } catch (error) {
375
400
  spinner.stop();
@@ -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
+ };