@aifabrix/builder 2.22.2 → 2.31.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.
Files changed (62) hide show
  1. package/jest.config.coverage.js +37 -0
  2. package/lib/api/pipeline.api.js +10 -9
  3. package/lib/app-deploy.js +36 -14
  4. package/lib/app-list.js +191 -71
  5. package/lib/app-prompts.js +77 -26
  6. package/lib/app-readme.js +123 -5
  7. package/lib/app-rotate-secret.js +101 -57
  8. package/lib/app-run-helpers.js +200 -172
  9. package/lib/app-run.js +137 -68
  10. package/lib/audit-logger.js +8 -7
  11. package/lib/build.js +161 -250
  12. package/lib/cli.js +73 -65
  13. package/lib/commands/login.js +45 -31
  14. package/lib/commands/logout.js +181 -0
  15. package/lib/commands/secure.js +59 -24
  16. package/lib/config.js +79 -45
  17. package/lib/datasource-deploy.js +89 -29
  18. package/lib/deployer.js +164 -129
  19. package/lib/diff.js +63 -21
  20. package/lib/environment-deploy.js +36 -19
  21. package/lib/external-system-deploy.js +134 -66
  22. package/lib/external-system-download.js +244 -171
  23. package/lib/external-system-test.js +199 -164
  24. package/lib/generator-external.js +145 -72
  25. package/lib/generator-helpers.js +49 -17
  26. package/lib/generator-split.js +105 -58
  27. package/lib/infra.js +101 -131
  28. package/lib/schema/application-schema.json +895 -896
  29. package/lib/schema/env-config.yaml +11 -4
  30. package/lib/template-validator.js +13 -4
  31. package/lib/utils/api.js +8 -8
  32. package/lib/utils/app-register-auth.js +36 -18
  33. package/lib/utils/app-run-containers.js +140 -0
  34. package/lib/utils/auth-headers.js +6 -6
  35. package/lib/utils/build-copy.js +60 -2
  36. package/lib/utils/build-helpers.js +94 -0
  37. package/lib/utils/cli-utils.js +177 -76
  38. package/lib/utils/compose-generator.js +12 -2
  39. package/lib/utils/config-tokens.js +151 -9
  40. package/lib/utils/deployment-errors.js +137 -69
  41. package/lib/utils/deployment-validation-helpers.js +103 -0
  42. package/lib/utils/docker-build.js +57 -0
  43. package/lib/utils/dockerfile-utils.js +13 -3
  44. package/lib/utils/env-copy.js +163 -94
  45. package/lib/utils/env-map.js +226 -86
  46. package/lib/utils/error-formatters/network-errors.js +0 -1
  47. package/lib/utils/external-system-display.js +14 -19
  48. package/lib/utils/external-system-env-helpers.js +107 -0
  49. package/lib/utils/external-system-test-helpers.js +144 -0
  50. package/lib/utils/health-check.js +10 -8
  51. package/lib/utils/infra-status.js +123 -0
  52. package/lib/utils/paths.js +228 -49
  53. package/lib/utils/schema-loader.js +125 -57
  54. package/lib/utils/token-manager.js +3 -3
  55. package/lib/utils/yaml-preserve.js +55 -16
  56. package/lib/validate.js +87 -89
  57. package/package.json +4 -4
  58. package/scripts/ci-fix.sh +19 -0
  59. package/scripts/ci-simulate.sh +19 -0
  60. package/templates/applications/miso-controller/test.yaml +1 -0
  61. package/templates/python/Dockerfile.hbs +8 -45
  62. package/templates/typescript/Dockerfile.hbs +8 -42
@@ -42,127 +42,195 @@ function handleDeploymentError(error) {
42
42
  }
43
43
 
44
44
  /**
45
- * Unified error handler for deployment errors
46
- * Handles audit logging, error formatting, and user-friendly messages
47
- * @param {Error} error - Error object
48
- * @param {string} appName - Application name for audit logging
49
- * @param {string} url - Controller URL for audit logging
50
- * @param {boolean} [alreadyLogged=false] - Whether error has already been logged
51
- * @throws {Error} User-friendly error message
45
+ * Extract error message from different error types
46
+ * @param {Error|string|Object} error - Error object, string, or object
47
+ * @returns {string} Error message
52
48
  */
53
- async function handleDeploymentErrors(error, appName, url, alreadyLogged = false) {
54
- // For validation errors (like URL validation), just re-throw them directly
55
- // They already have user-friendly messages
56
- // Handle both Error objects and strings
57
- let errorMessage = '';
49
+ function extractErrorMessage(error) {
58
50
  if (error instanceof Error) {
59
- errorMessage = error.message || '';
60
- } else if (typeof error === 'string') {
61
- errorMessage = error;
62
- } else if (error && typeof error === 'object' && error.message) {
63
- errorMessage = error.message;
51
+ return error.message || '';
52
+ }
53
+ if (typeof error === 'string') {
54
+ return error;
64
55
  }
56
+ if (error && typeof error === 'object' && error.message) {
57
+ return error.message;
58
+ }
59
+ return '';
60
+ }
65
61
 
66
- if (errorMessage && (
62
+ /**
63
+ * Check if error is a validation error that should be re-thrown directly
64
+ * @param {string} errorMessage - Error message
65
+ * @returns {boolean} True if validation error
66
+ */
67
+ function isValidationError(errorMessage) {
68
+ return errorMessage && (
67
69
  errorMessage.includes('Controller URL must use HTTPS') ||
68
70
  errorMessage.includes('Invalid environment key') ||
69
71
  errorMessage.includes('Environment key is required') ||
70
72
  errorMessage.includes('Authentication configuration is required') ||
71
73
  errorMessage.includes('Invalid controller URL format') ||
72
74
  errorMessage.includes('Controller URL is required')
73
- )) {
74
- // If error is a string, convert to Error object
75
- if (typeof error === 'string') {
76
- throw new Error(error);
77
- }
78
- // If it's already an Error object, re-throw it directly
79
- if (error instanceof Error) {
80
- throw error;
81
- }
82
- // Otherwise, create a new Error with the message
83
- throw new Error(errorMessage);
84
- }
75
+ );
76
+ }
85
77
 
86
- // Log to audit log if not already logged
87
- if (!alreadyLogged) {
88
- try {
89
- await auditLogger.logDeploymentFailure(appName, url, error);
90
- } catch (logError) {
91
- // Don't fail if audit logging fails, but log to console
92
- // eslint-disable-next-line no-console
93
- console.error(`[AUDIT LOG ERROR] Failed to log deployment failure: ${logError.message}`);
94
- }
78
+ /**
79
+ * Re-throw validation errors directly
80
+ * @param {Error|string|Object} error - Error object
81
+ * @param {string} errorMessage - Error message
82
+ * @throws {Error} Validation error
83
+ */
84
+ function throwValidationError(error, errorMessage) {
85
+ if (typeof error === 'string') {
86
+ throw new Error(error);
87
+ }
88
+ if (error instanceof Error) {
89
+ throw error;
95
90
  }
91
+ throw new Error(errorMessage);
92
+ }
96
93
 
97
- const safeError = handleDeploymentError(error);
94
+ /**
95
+ * Log deployment failure to audit log
96
+ * @async
97
+ * @param {string} appName - Application name
98
+ * @param {string} url - Controller URL
99
+ * @param {Error|string|Object} error - Error object
100
+ * @returns {Promise<void>}
101
+ */
102
+ async function logDeploymentFailure(appName, url, error) {
103
+ try {
104
+ await auditLogger.logDeploymentFailure(appName, url, error);
105
+ } catch (logError) {
106
+ // Don't fail if audit logging fails, but log to console
107
+ // eslint-disable-next-line no-console
108
+ console.error(`[AUDIT LOG ERROR] Failed to log deployment failure: ${logError.message}`);
109
+ }
110
+ }
98
111
 
99
- // Extract error data from axios response
112
+ /**
113
+ * Extract and normalize error response data
114
+ * @param {Object} safeError - Safe error object
115
+ * @param {Error|Object} error - Original error object
116
+ * @returns {string|Object} Normalized error response
117
+ */
118
+ function extractErrorResponse(safeError, error) {
100
119
  let errorData = safeError.data;
101
120
  if (error.response && error.response.data !== undefined) {
102
121
  errorData = error.response.data;
103
122
  }
104
123
 
105
- // Ensure errorData is not undefined before parsing
106
- // If errorData is undefined, use the error message instead
107
124
  let errorResponse = errorData !== undefined ? errorData : safeError.message;
108
125
 
109
- // Ensure errorResponse is a string or object, not an Error object
110
126
  if (errorResponse instanceof Error) {
111
127
  errorResponse = errorResponse.message || 'Unknown error occurred';
112
128
  }
113
129
 
114
- // Ensure errorResponse is not null or undefined
115
130
  if (errorResponse === null || errorResponse === undefined) {
116
131
  errorResponse = safeError.message || 'Unknown error occurred';
117
132
  }
118
133
 
119
- // Determine if this is a network error
120
- const isNetworkError = safeError.code === 'ECONNREFUSED' ||
134
+ return errorResponse;
135
+ }
136
+
137
+ /**
138
+ * Determine if error is a network error
139
+ * @param {Object} safeError - Safe error object
140
+ * @returns {boolean} True if network error
141
+ */
142
+ function isNetworkError(safeError) {
143
+ return safeError.code === 'ECONNREFUSED' ||
121
144
  safeError.code === 'ENOTFOUND' ||
122
145
  safeError.code === 'ECONNABORTED' ||
123
146
  safeError.timeout;
147
+ }
148
+
149
+ /**
150
+ * Parse error response and ensure valid result
151
+ * @param {string|Object} errorResponse - Error response
152
+ * @param {Object} safeError - Safe error object
153
+ * @returns {Object} Parsed error object
154
+ */
155
+ function parseErrorResponseSafely(errorResponse, safeError) {
156
+ const isNetwork = isNetworkError(safeError);
124
157
 
125
- // Parse error using error handler
126
- let parsedError;
127
158
  try {
128
- parsedError = parseErrorResponse(errorResponse, safeError.status || 0, isNetworkError);
129
- // Ensure parsedError is a valid object
159
+ const parsedError = parseErrorResponse(errorResponse, safeError.status || 0, isNetwork);
130
160
  if (!parsedError || typeof parsedError !== 'object') {
131
161
  throw new Error('parseErrorResponse returned invalid result');
132
162
  }
133
- } catch (parseErr) {
134
- // If parsing fails, use the safe error message
135
- parsedError = {
136
- message: safeError.message || 'Unknown error occurred',
137
- formatted: safeError.message || 'Unknown error occurred',
138
- data: undefined
139
- };
140
- }
141
-
142
- // Ensure parsedError is always a valid object with required properties
143
- if (!parsedError || typeof parsedError !== 'object') {
144
- parsedError = {
163
+ return parsedError;
164
+ } catch {
165
+ return {
145
166
  message: safeError.message || 'Unknown error occurred',
146
167
  formatted: safeError.message || 'Unknown error occurred',
147
168
  data: undefined
148
169
  };
149
170
  }
171
+ }
150
172
 
151
- // Ensure we have a message - handle case where parsedError.message might be undefined
173
+ /**
174
+ * Create formatted error object
175
+ * @param {Object} parsedError - Parsed error object
176
+ * @param {Object} safeError - Safe error object
177
+ * @param {Error|string|Object} originalError - Original error
178
+ * @returns {Error} Formatted error object
179
+ */
180
+ function createFormattedError(parsedError, safeError, originalError) {
152
181
  const finalErrorMessage = (parsedError && parsedError.message) ? parsedError.message : (safeError.message || 'Unknown error occurred');
153
182
 
154
- // Validate finalErrorMessage is a string
155
183
  if (typeof finalErrorMessage !== 'string') {
156
- throw new Error(`Invalid error message type: ${typeof finalErrorMessage}. Error: ${JSON.stringify(error)}`);
184
+ throw new Error(`Invalid error message type: ${typeof finalErrorMessage}. Error: ${JSON.stringify(originalError)}`);
157
185
  }
158
186
 
159
- // Throw clean error message (without emoji) - CLI will format it
160
187
  const formattedError = new Error(finalErrorMessage);
161
188
  formattedError.formatted = parsedError?.formatted || finalErrorMessage;
162
189
  formattedError.status = safeError.status;
163
190
  formattedError.data = parsedError?.data;
164
- formattedError._logged = true; // Mark as logged to prevent double-logging
165
- throw formattedError;
191
+ formattedError._logged = true;
192
+
193
+ return formattedError;
194
+ }
195
+
196
+ /**
197
+ * Unified error handler for deployment errors
198
+ * Handles audit logging, error formatting, and user-friendly messages
199
+ * @param {Error} error - Error object
200
+ * @param {string} appName - Application name for audit logging
201
+ * @param {string} url - Controller URL for audit logging
202
+ * @param {boolean} [alreadyLogged=false] - Whether error has already been logged
203
+ * @throws {Error} User-friendly error message
204
+ */
205
+ async function handleDeploymentErrors(error, appName, url, alreadyLogged = false) {
206
+ // Extract error message
207
+ const errorMessage = extractErrorMessage(error);
208
+
209
+ // For validation errors (like URL validation), just re-throw them directly
210
+ if (isValidationError(errorMessage)) {
211
+ throwValidationError(error, errorMessage);
212
+ }
213
+
214
+ // Log to audit log if not already logged
215
+ if (!alreadyLogged) {
216
+ await logDeploymentFailure(appName, url, error);
217
+ }
218
+
219
+ const safeError = handleDeploymentError(error);
220
+ const errorResponse = extractErrorResponse(safeError, error);
221
+ const parsedError = parseErrorResponseSafely(errorResponse, safeError);
222
+
223
+ // Ensure parsedError is always a valid object
224
+ const validParsedError = (!parsedError || typeof parsedError !== 'object')
225
+ ? {
226
+ message: safeError.message || 'Unknown error occurred',
227
+ formatted: safeError.message || 'Unknown error occurred',
228
+ data: undefined
229
+ }
230
+ : parsedError;
231
+
232
+ // Create and throw formatted error
233
+ throw createFormattedError(validParsedError, safeError, error);
166
234
  }
167
235
 
168
236
  module.exports = {
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Deployment Validation Helper Functions
3
+ *
4
+ * Helper functions for processing deployment validation responses.
5
+ * Separated from deployer.js to maintain file size limits.
6
+ *
7
+ * @fileoverview Deployment validation response processing helpers
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ /**
13
+ * Process successful validation response
14
+ * @param {Object} responseData - Response data
15
+ * @returns {Object} Validation result
16
+ */
17
+ function processSuccessfulValidation(responseData) {
18
+ return {
19
+ success: true,
20
+ validateToken: responseData.validateToken,
21
+ draftDeploymentId: responseData.draftDeploymentId,
22
+ imageServer: responseData.imageServer,
23
+ imageUsername: responseData.imageUsername,
24
+ imagePassword: responseData.imagePassword,
25
+ expiresAt: responseData.expiresAt
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Process validation failure response
31
+ * @param {Object} responseData - Response data
32
+ * @throws {Error} Validation error
33
+ */
34
+ function processValidationFailure(responseData) {
35
+ const errorMessage = responseData.errors && responseData.errors.length > 0
36
+ ? `Validation failed: ${responseData.errors.join(', ')}`
37
+ : 'Validation failed: Invalid configuration';
38
+ const error = new Error(errorMessage);
39
+ error.status = 400;
40
+ error.data = responseData;
41
+ throw error;
42
+ }
43
+
44
+ /**
45
+ * Process validation error response
46
+ * @param {Object} response - API response
47
+ * @throws {Error} Validation error
48
+ */
49
+ function processValidationError(response) {
50
+ const error = new Error(`Validation request failed: ${response.formattedError || response.error || 'Unknown error'}`);
51
+ error.status = response.status || 400;
52
+ error.data = response.data;
53
+ throw error;
54
+ }
55
+
56
+ /**
57
+ * Process unexpected validation response state
58
+ * @param {Object} responseData - Response data
59
+ * @throws {Error} Unexpected state error
60
+ */
61
+ function processUnexpectedValidationState(responseData) {
62
+ const error = new Error('Validation response is in an unexpected state');
63
+ error.status = 400;
64
+ error.data = responseData;
65
+ throw error;
66
+ }
67
+
68
+ /**
69
+ * Handle validation response
70
+ * @param {Object} response - API response
71
+ * @returns {Object|null} Validation result or null if needs retry
72
+ * @throws {Error} If validation fails
73
+ */
74
+ function handleValidationResponse(response) {
75
+ // Handle successful validation (200 OK with valid: true)
76
+ if (response.success && response.data) {
77
+ const responseData = response.data.data || response.data;
78
+ if (responseData.valid === true) {
79
+ return processSuccessfulValidation(responseData);
80
+ }
81
+ // Handle validation failure (valid: false)
82
+ if (responseData.valid === false) {
83
+ processValidationFailure(responseData);
84
+ }
85
+ }
86
+
87
+ // Handle validation errors (non-success responses)
88
+ if (!response.success) {
89
+ processValidationError(response);
90
+ }
91
+
92
+ // If we get here, response.success is true but valid is not true and not false
93
+ processUnexpectedValidationState(response.data);
94
+ }
95
+
96
+ module.exports = {
97
+ processSuccessfulValidation,
98
+ processValidationFailure,
99
+ processValidationError,
100
+ processUnexpectedValidationState,
101
+ handleValidationResponse
102
+ };
103
+
@@ -190,8 +190,65 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
190
190
  });
191
191
  }
192
192
 
193
+ /**
194
+ * Execute Docker build with additional tagging
195
+ * @async
196
+ * @param {string} imageName - Image name to build
197
+ * @param {string} dockerfilePath - Path to Dockerfile
198
+ * @param {string} contextPath - Build context path
199
+ * @param {string} tag - Image tag
200
+ * @param {Object} options - Build options
201
+ * @returns {Promise<void>} Resolves when build completes
202
+ * @throws {Error} If build fails
203
+ */
204
+ async function executeBuild(imageName, dockerfilePath, contextPath, tag, options) {
205
+ await executeDockerBuild(imageName, dockerfilePath, contextPath, tag);
206
+
207
+ // Tag image if additional tag provided
208
+ if (options && options.tag && options.tag !== 'latest') {
209
+ const { promisify } = require('util');
210
+ const { exec } = require('child_process');
211
+ const run = promisify(exec);
212
+ await run(`docker tag ${imageName}:${tag} ${imageName}:latest`);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Execute Docker build and tag with compatibility tag
218
+ * @async
219
+ * @param {string} effectiveImageName - Effective image name
220
+ * @param {string} imageName - Base image name
221
+ * @param {string} dockerfilePath - Dockerfile path
222
+ * @param {string} contextPath - Build context path
223
+ * @param {string} tag - Image tag
224
+ * @param {Object} options - Build options
225
+ * @returns {Promise<void>}
226
+ */
227
+ async function executeDockerBuildWithTag(effectiveImageName, imageName, dockerfilePath, contextPath, tag, options) {
228
+ const logger = require('../utils/logger');
229
+ const chalk = require('chalk');
230
+
231
+ logger.log(chalk.blue(`Using Dockerfile: ${dockerfilePath}`));
232
+ logger.log(chalk.blue(`Using build context: ${contextPath}`));
233
+
234
+ await executeBuild(effectiveImageName, dockerfilePath, contextPath, tag, options);
235
+
236
+ // Back-compat: also tag the built dev image as the base image name
237
+ try {
238
+ const { promisify } = require('util');
239
+ const { exec } = require('child_process');
240
+ const run = promisify(exec);
241
+ await run(`docker tag ${effectiveImageName}:${tag} ${imageName}:${tag}`);
242
+ logger.log(chalk.green(`✓ Tagged image: ${imageName}:${tag}`));
243
+ } catch (err) {
244
+ logger.log(chalk.yellow(`⚠️ Warning: Could not create compatibility tag ${imageName}:${tag} - ${err.message}`));
245
+ }
246
+ }
247
+
193
248
  module.exports = {
194
249
  executeDockerBuild,
250
+ executeBuild,
251
+ executeDockerBuildWithTag,
195
252
  isDockerNotAvailableError
196
253
  };
197
254
 
@@ -22,10 +22,19 @@ const handlebars = require('handlebars');
22
22
  * @throws {Error} If template not found
23
23
  */
24
24
  function loadDockerfileTemplate(language) {
25
- const templatePath = path.join(__dirname, '..', '..', 'templates', language, 'Dockerfile.hbs');
25
+ // Use getProjectRoot to reliably find templates in all environments
26
+ const { getProjectRoot } = require('./paths');
27
+ const projectRoot = getProjectRoot();
28
+ const templatePath = path.join(projectRoot, 'templates', language, 'Dockerfile.hbs');
26
29
 
27
30
  if (!fsSync.existsSync(templatePath)) {
28
- throw new Error(`Template not found for language: ${language}`);
31
+ // Provide helpful error message with actual paths checked
32
+ const errorMessage = `Template not found for language: ${language}\n` +
33
+ ` Expected path: ${templatePath}\n` +
34
+ ` Project root: ${projectRoot}\n` +
35
+ ` Templates directory: ${path.join(projectRoot, 'templates', language)}\n` +
36
+ ` Global PROJECT_ROOT: ${typeof global !== 'undefined' && global.PROJECT_ROOT ? global.PROJECT_ROOT : 'not set'}`;
37
+ throw new Error(errorMessage);
29
38
  }
30
39
 
31
40
  const templateContent = fsSync.readFileSync(templatePath, 'utf8');
@@ -114,7 +123,8 @@ function checkProjectDockerfile(builderPath, appName, buildConfig, contextPath,
114
123
  return customPath;
115
124
  }
116
125
 
117
- const builderCustomPath = path.join(process.cwd(), 'builder', appName, customDockerfile);
126
+ // Use builderPath instead of process.cwd() to handle test scenarios
127
+ const builderCustomPath = path.join(builderPath, customDockerfile);
118
128
  if (fsSync.existsSync(builderCustomPath)) {
119
129
  return builderCustomPath;
120
130
  }