@aifabrix/builder 2.40.2 → 2.41.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 (103) hide show
  1. package/README.md +6 -4
  2. package/integration/hubspot/test.js +1 -1
  3. package/lib/api/credential.api.js +40 -0
  4. package/lib/api/dev.api.js +423 -0
  5. package/lib/api/types/credential.types.js +23 -0
  6. package/lib/api/types/dev.types.js +140 -0
  7. package/lib/app/config.js +21 -0
  8. package/lib/app/down.js +2 -1
  9. package/lib/app/index.js +9 -0
  10. package/lib/app/push.js +36 -12
  11. package/lib/app/readme.js +1 -3
  12. package/lib/app/run-env-compose.js +201 -0
  13. package/lib/app/run-helpers.js +121 -118
  14. package/lib/app/run.js +148 -28
  15. package/lib/app/show.js +5 -2
  16. package/lib/build/index.js +11 -3
  17. package/lib/cli/setup-app.js +140 -14
  18. package/lib/cli/setup-dev.js +180 -17
  19. package/lib/cli/setup-environment.js +4 -2
  20. package/lib/cli/setup-external-system.js +71 -21
  21. package/lib/cli/setup-infra.js +29 -2
  22. package/lib/cli/setup-secrets.js +52 -5
  23. package/lib/cli/setup-utility.js +12 -3
  24. package/lib/commands/app-install.js +172 -0
  25. package/lib/commands/app-shell.js +75 -0
  26. package/lib/commands/app-test.js +282 -0
  27. package/lib/commands/app.js +1 -1
  28. package/lib/commands/dev-cli-handlers.js +141 -0
  29. package/lib/commands/dev-down.js +114 -0
  30. package/lib/commands/dev-init.js +309 -0
  31. package/lib/commands/secrets-list.js +118 -0
  32. package/lib/commands/secrets-remove.js +97 -0
  33. package/lib/commands/secrets-set.js +30 -17
  34. package/lib/commands/secrets-validate.js +50 -0
  35. package/lib/commands/up-dataplane.js +2 -2
  36. package/lib/commands/up-miso.js +0 -25
  37. package/lib/commands/upload.js +26 -1
  38. package/lib/core/admin-secrets.js +96 -0
  39. package/lib/core/secrets-ensure.js +378 -0
  40. package/lib/core/secrets-env-write.js +157 -0
  41. package/lib/core/secrets.js +147 -81
  42. package/lib/datasource/field-reference-validator.js +91 -0
  43. package/lib/datasource/validate.js +21 -3
  44. package/lib/deployment/environment-config.js +137 -0
  45. package/lib/deployment/environment.js +21 -98
  46. package/lib/deployment/push.js +32 -2
  47. package/lib/external-system/download.js +7 -0
  48. package/lib/external-system/test-auth.js +7 -3
  49. package/lib/external-system/test.js +5 -1
  50. package/lib/generator/index.js +174 -25
  51. package/lib/generator/wizard.js +8 -0
  52. package/lib/infrastructure/helpers.js +103 -20
  53. package/lib/infrastructure/index.js +88 -10
  54. package/lib/infrastructure/services.js +70 -15
  55. package/lib/schema/application-schema.json +24 -3
  56. package/lib/schema/external-system.schema.json +435 -413
  57. package/lib/utils/api.js +3 -3
  58. package/lib/utils/app-register-auth.js +25 -3
  59. package/lib/utils/cli-utils.js +20 -0
  60. package/lib/utils/compose-generator.js +76 -75
  61. package/lib/utils/compose-handlebars-helpers.js +43 -0
  62. package/lib/utils/compose-vector-helper.js +18 -0
  63. package/lib/utils/config-paths.js +127 -2
  64. package/lib/utils/credential-secrets-env.js +267 -0
  65. package/lib/utils/dev-cert-helper.js +122 -0
  66. package/lib/utils/device-code-helpers.js +224 -0
  67. package/lib/utils/device-code.js +37 -336
  68. package/lib/utils/docker-build.js +40 -8
  69. package/lib/utils/env-copy.js +83 -13
  70. package/lib/utils/env-map.js +35 -5
  71. package/lib/utils/env-template.js +6 -5
  72. package/lib/utils/error-formatters/http-status-errors.js +20 -1
  73. package/lib/utils/help-builder.js +15 -2
  74. package/lib/utils/infra-status.js +30 -1
  75. package/lib/utils/local-secrets.js +7 -52
  76. package/lib/utils/mutagen-install.js +195 -0
  77. package/lib/utils/mutagen.js +146 -0
  78. package/lib/utils/paths.js +43 -33
  79. package/lib/utils/port-resolver.js +28 -16
  80. package/lib/utils/remote-dev-auth.js +38 -0
  81. package/lib/utils/remote-docker-env.js +43 -0
  82. package/lib/utils/remote-secrets-loader.js +60 -0
  83. package/lib/utils/secrets-generator.js +94 -6
  84. package/lib/utils/secrets-helpers.js +33 -25
  85. package/lib/utils/secrets-path.js +2 -2
  86. package/lib/utils/secrets-utils.js +52 -1
  87. package/lib/utils/secrets-validation.js +84 -0
  88. package/lib/utils/ssh-key-helper.js +116 -0
  89. package/lib/utils/token-manager-messages.js +90 -0
  90. package/lib/utils/token-manager.js +5 -4
  91. package/lib/utils/variable-transformer.js +3 -3
  92. package/lib/validation/validator.js +65 -0
  93. package/package.json +2 -2
  94. package/scripts/install-local.js +34 -15
  95. package/templates/README.md +0 -1
  96. package/templates/applications/README.md.hbs +4 -4
  97. package/templates/applications/dataplane/application.yaml +5 -4
  98. package/templates/applications/dataplane/env.template +12 -7
  99. package/templates/applications/keycloak/env.template +2 -0
  100. package/templates/applications/miso-controller/application.yaml +1 -0
  101. package/templates/applications/miso-controller/env.template +11 -9
  102. package/templates/python/docker-compose.hbs +49 -23
  103. package/templates/typescript/docker-compose.hbs +48 -22
@@ -9,10 +9,7 @@
9
9
  * @version 2.0.0
10
10
  */
11
11
 
12
- const fs = require('fs');
13
- const path = require('path');
14
12
  const chalk = require('chalk');
15
- const Ajv = require('ajv');
16
13
  const logger = require('../utils/logger');
17
14
  const config = require('../core/config');
18
15
  const { resolveControllerUrl } = require('../utils/controller-url');
@@ -21,9 +18,12 @@ const { getOrRefreshDeviceToken } = require('../utils/token-manager');
21
18
  const { getPipelineDeployment } = require('../api/pipeline.api');
22
19
  const { deployEnvironment: deployEnvironmentInfra, getDeployment } = require('../api/deployments.api');
23
20
  const { handleDeploymentErrors } = require('../utils/deployment-errors');
24
- const { formatValidationErrors } = require('../utils/error-formatter');
25
21
  const auditLogger = require('../core/audit-logger');
26
- const environmentDeployRequestSchema = require('../schema/environment-deploy-request.schema.json');
22
+ const {
23
+ normalizePreset,
24
+ buildEnvironmentConfigFromPreset,
25
+ loadAndValidateEnvironmentDeployConfig
26
+ } = require('./environment-config');
27
27
 
28
28
  /**
29
29
  * Validates environment deployment prerequisites
@@ -82,102 +82,26 @@ async function getEnvironmentAuth(controllerUrl) {
82
82
  * @returns {Promise<Object>} Deployment result
83
83
  * @throws {Error} If deployment fails
84
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
-
120
85
  /**
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? }
125
- */
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)
139
- };
140
- }
141
-
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
- );
155
- }
156
- const parsed = parseEnvironmentConfigFile(resolvedPath);
157
- return validateEnvironmentDeployParsed(parsed, resolvedPath);
158
- }
159
-
160
- /**
161
- * Builds environment deployment request from options (config file required)
86
+ * Builds environment deployment request from options (config file or --preset)
87
+ * When --config is provided, uses that file; otherwise uses --preset (default s).
162
88
  * @function buildEnvironmentDeploymentRequest
163
89
  * @param {string} validatedEnvKey - Validated environment key
164
- * @param {Object} options - Deployment options (must include options.config)
90
+ * @param {Object} options - Deployment options (options.config optional; options.preset used when no config)
165
91
  * @returns {Object} Deployment request object for API
166
92
  */
167
93
  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
- ));
94
+ if (options.config && typeof options.config === 'string') {
95
+ const deployRequest = loadAndValidateEnvironmentDeployConfig(options.config);
96
+ if (deployRequest.environmentConfig.key && deployRequest.environmentConfig.key !== validatedEnvKey) {
97
+ logger.log(chalk.yellow(
98
+ `⚠ Config key "${deployRequest.environmentConfig.key}" does not match deploy target "${validatedEnvKey}"; using target "${validatedEnvKey}".`
99
+ ));
100
+ }
101
+ return deployRequest;
179
102
  }
180
- return deployRequest;
103
+ const preset = normalizePreset(options.preset);
104
+ return buildEnvironmentConfigFromPreset(validatedEnvKey, preset);
181
105
  }
182
106
 
183
107
  /**
@@ -371,7 +295,8 @@ function displayDeploymentResults(result) {
371
295
  * @param {string} envKey - Environment key (miso, dev, tst, pro)
372
296
  * @param {Object} options - Deployment options
373
297
  * @param {string} options.controller - Controller URL (required)
374
- * @param {string} [options.config] - Environment configuration file (optional)
298
+ * @param {string} [options.config] - Environment configuration file (optional; if omitted, --preset is used)
299
+ * @param {string} [options.preset] - Environment size preset: s, m, l, xl (default: s), used when config is not provided
375
300
  * @param {boolean} [options.skipValidation] - Skip validation checks
376
301
  * @param {boolean} [options.poll] - Poll for deployment status (default: true)
377
302
  * @param {boolean} [options.noPoll] - Do not poll for status
@@ -492,6 +417,4 @@ async function deployEnvironment(envKey, options = {}) {
492
417
  throw error;
493
418
  }
494
419
  }
495
- module.exports = {
496
- deployEnvironment
497
- };
420
+ module.exports = { deployEnvironment };
@@ -16,6 +16,29 @@ const logger = require('../utils/logger');
16
16
 
17
17
  const execAsync = promisify(exec);
18
18
 
19
+ /** Timeout for az account show (fail fast when not logged in) */
20
+ const AZ_ACCOUNT_SHOW_TIMEOUT_MS = 10000;
21
+ /** Timeout for az acr show (avoid hang when Azure CLI prompts for login) */
22
+ const AZ_ACR_SHOW_TIMEOUT_MS = 15000;
23
+ /** Timeout for az acr login */
24
+ const AZ_ACR_LOGIN_TIMEOUT_MS = 120000;
25
+
26
+ /**
27
+ * Check if user is logged in to Azure CLI
28
+ * Fails fast so push does not hang when user is not logged in.
29
+ * @param {Object} [execOptions] - Options for exec (e.g. shell on Windows)
30
+ * @returns {Promise<boolean>} True if logged in
31
+ */
32
+ async function checkAzureLogin(execOptions = {}) {
33
+ const options = process.platform === 'win32' ? { shell: true, ...execOptions } : execOptions;
34
+ try {
35
+ await execAsync('az account show', { ...options, timeout: AZ_ACCOUNT_SHOW_TIMEOUT_MS });
36
+ return true;
37
+ } catch (_error) {
38
+ return false;
39
+ }
40
+ }
41
+
19
42
  /**
20
43
  * Check if Azure CLI is installed
21
44
  * @returns {Promise<boolean>} True if Azure CLI is available
@@ -128,7 +151,7 @@ async function checkACRAuthentication(registry) {
128
151
  const registryName = extractRegistryName(registry);
129
152
  // On Windows, use shell option to ensure proper command resolution
130
153
  const options = process.platform === 'win32' ? { shell: true } : {};
131
- await execAsync(`az acr show --name ${registryName}`, options);
154
+ await execAsync(`az acr show --name ${registryName}`, { ...options, timeout: AZ_ACR_SHOW_TIMEOUT_MS });
132
155
  return true;
133
156
  } catch (error) {
134
157
  return false;
@@ -146,9 +169,15 @@ async function authenticateACR(registry) {
146
169
  logger.log(chalk.blue(`Authenticating with ${registry}...`));
147
170
  // On Windows, use shell option to ensure proper command resolution
148
171
  const options = process.platform === 'win32' ? { shell: true } : {};
149
- await execAsync(`az acr login --name ${registryName}`, options);
172
+ await execAsync(`az acr login --name ${registryName}`, { ...options, timeout: AZ_ACR_LOGIN_TIMEOUT_MS });
150
173
  logger.log(chalk.green(`✓ Authenticated with ${registry}`));
151
174
  } catch (error) {
175
+ const msg = error.message || String(error);
176
+ if (msg.includes('ETIMEDOUT') || msg.includes('timeout')) {
177
+ throw new Error(
178
+ 'Authentication timed out. Make sure you\'re logged in to Azure first: az login'
179
+ );
180
+ }
152
181
  throw new Error(`Failed to authenticate with Azure Container Registry: ${error.message}`);
153
182
  }
154
183
  }
@@ -267,6 +296,7 @@ async function pushImage(imageWithTag, registry = null) {
267
296
 
268
297
  module.exports = {
269
298
  checkAzureCLIInstalled,
299
+ checkAzureLogin,
270
300
  extractRegistryName,
271
301
  validateRegistryURL,
272
302
  checkACRAuthentication,
@@ -390,6 +390,13 @@ async function processDownloadedSystem(systemKey, application, dataSources, temp
390
390
  // Move files from temp to final location
391
391
  await moveFilesToFinalLocation(tempDir, finalPath, systemKey, filePaths);
392
392
 
393
+ try {
394
+ const secretsEnsure = require('../core/secrets-ensure');
395
+ await secretsEnsure.ensureSecretsFromEnvTemplate(path.join(finalPath, 'env.template'), { emptyValuesForCredentials: true });
396
+ } catch (err) {
397
+ if (err.code !== 'ENOENT') logger.warn(`Could not ensure integration placeholder secrets: ${err.message}`);
398
+ }
399
+
393
400
  // Clean up temporary folder
394
401
  await fs.rm(tempDir, { recursive: true, force: true });
395
402
 
@@ -15,17 +15,21 @@ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
15
15
  const { resolveControllerUrl } = require('../utils/controller-url');
16
16
 
17
17
  /**
18
- * Setup authentication and get dataplane URL for integration tests
18
+ * Setup authentication and get dataplane URL for integration tests.
19
+ * Dataplane URL is always discovered from the controller for the given environment.
20
+ *
19
21
  * @async
20
22
  * @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
23
+ * @param {Object} options - Test options; options.environment overrides config env; options.dataplane overrides URL (programmatic only)
22
24
  * @param {Object} _config - Configuration object
23
25
  * @returns {Promise<Object>} Object with authConfig and dataplaneUrl
24
26
  * @throws {Error} If authentication fails
25
27
  */
26
28
  async function setupIntegrationTestAuth(appName, options, _config) {
27
29
  const { resolveEnvironment } = require('../core/config');
28
- const environment = await resolveEnvironment();
30
+ const environment = (options && options.environment && typeof options.environment === 'string' && options.environment.trim())
31
+ ? options.environment.trim()
32
+ : await resolveEnvironment();
29
33
  const controllerUrl = await resolveControllerUrl();
30
34
  const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
31
35
 
@@ -50,7 +50,11 @@ async function loadVariablesYamlFile(variablesPath) {
50
50
  try {
51
51
  const variables = loadConfigFile(variablesPath);
52
52
  if (!variables.externalIntegration) {
53
- throw new Error('externalIntegration block not found in application config');
53
+ throw new Error(
54
+ 'externalIntegration block not found in application config. ' +
55
+ 'test-integration is for external systems only (integration/<app> with externalIntegration in application.yaml). ' +
56
+ 'For builder apps use: aifabrix test <app> or aifabrix test-e2e <app>'
57
+ );
54
58
  }
55
59
  return variables;
56
60
  } catch (error) {
@@ -20,6 +20,8 @@ const { loadVariables, loadEnvTemplate, loadRbac, parseEnvironmentVariables } =
20
20
  const { generateExternalSystemApplicationSchema, splitExternalApplicationSchema } = require('./external');
21
21
  const { generateControllerManifest } = require('./external-controller-manifest');
22
22
  const { resolveVersionForApp } = require('../utils/image-version');
23
+ const { getContainerPort } = require('../utils/port-resolver');
24
+ const { buildEnvVarMap } = require('../utils/env-map');
23
25
 
24
26
  /**
25
27
  * Generates deployment JSON from application configuration files
@@ -59,6 +61,96 @@ function loadDeploymentConfigFiles(appPath, appType, appName) {
59
61
  return { variables, envTemplate, rbac, jsonPath };
60
62
  }
61
63
 
64
+ /** Placeholder replaced with application port from application.yaml */
65
+ const PORT_PLACEHOLDER = '${PORT}';
66
+
67
+ /**
68
+ * Returns the numeric port to use when substituting ${PORT} in the manifest.
69
+ * When application.yaml has port: "${PORT}", uses defaultPort (e.g. 3000).
70
+ *
71
+ * @param {Object} variables - Parsed application config
72
+ * @param {number} [defaultPort=3000] - Default when port is "${PORT}" or invalid
73
+ * @returns {number} Port number for substitution
74
+ */
75
+ function getEffectivePortForSubstitution(variables, defaultPort = 3000) {
76
+ const raw = getContainerPort(variables, defaultPort);
77
+ if (raw === PORT_PLACEHOLDER || (typeof raw === 'string' && raw.trim() === PORT_PLACEHOLDER)) {
78
+ return defaultPort;
79
+ }
80
+ if (typeof raw === 'number' && raw > 0) {
81
+ return raw;
82
+ }
83
+ const num = Number(raw);
84
+ return Number.isFinite(num) && num > 0 ? num : defaultPort;
85
+ }
86
+
87
+ /**
88
+ * Recursively replaces ${PORT} with the given port number in all string values of obj (in-place).
89
+ *
90
+ * @param {Object} obj - Deployment manifest or any nested object
91
+ * @param {number} portNumber - Port to substitute (e.g. from application.yaml)
92
+ */
93
+ function substitutePortInDeployment(obj, portNumber) {
94
+ if (obj === null || obj === undefined) return;
95
+ if (Array.isArray(obj)) {
96
+ for (let i = 0; i < obj.length; i++) {
97
+ if (typeof obj[i] === 'string' && obj[i].includes(PORT_PLACEHOLDER)) {
98
+ obj[i] = obj[i].split(PORT_PLACEHOLDER).join(String(portNumber));
99
+ } else if (typeof obj[i] === 'object' && obj[i] !== null) {
100
+ substitutePortInDeployment(obj[i], portNumber);
101
+ }
102
+ }
103
+ return;
104
+ }
105
+ if (typeof obj === 'object') {
106
+ for (const key of Object.keys(obj)) {
107
+ const value = obj[key];
108
+ if (typeof value === 'string' && value.includes(PORT_PLACEHOLDER)) {
109
+ obj[key] = value.split(PORT_PLACEHOLDER).join(String(portNumber));
110
+ } else if (typeof value === 'object' && value !== null) {
111
+ substitutePortInDeployment(value, portNumber);
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ /** Regex to find ${VAR} placeholders for env substitution */
118
+ const ENV_VAR_PLACEHOLDER_REGEX = /\$\{([^}]+)\}/g;
119
+
120
+ /**
121
+ * Recursively replaces ${VAR} with envVarMap[VAR] in all string values of obj (in-place).
122
+ * Only substitutes when VAR is a key in envVarMap (from env-config.yaml / config).
123
+ *
124
+ * @param {Object} obj - Deployment manifest or any nested object
125
+ * @param {Object} envVarMap - Flat map of variable names to values (e.g. from buildEnvVarMap('docker'))
126
+ */
127
+ function substituteEnvVarsInDeployment(obj, envVarMap) {
128
+ if (!envVarMap || typeof envVarMap !== 'object') return;
129
+ if (obj === null || obj === undefined) return;
130
+ if (Array.isArray(obj)) {
131
+ for (let i = 0; i < obj.length; i++) {
132
+ if (typeof obj[i] === 'string') {
133
+ obj[i] = obj[i].replace(ENV_VAR_PLACEHOLDER_REGEX, (match, varName) =>
134
+ Object.prototype.hasOwnProperty.call(envVarMap, varName) ? String(envVarMap[varName]) : match);
135
+ } else if (typeof obj[i] === 'object' && obj[i] !== null) {
136
+ substituteEnvVarsInDeployment(obj[i], envVarMap);
137
+ }
138
+ }
139
+ return;
140
+ }
141
+ if (typeof obj === 'object') {
142
+ for (const key of Object.keys(obj)) {
143
+ const value = obj[key];
144
+ if (typeof value === 'string') {
145
+ obj[key] = value.replace(ENV_VAR_PLACEHOLDER_REGEX, (match, varName) =>
146
+ Object.prototype.hasOwnProperty.call(envVarMap, varName) ? String(envVarMap[varName]) : match);
147
+ } else if (typeof value === 'object' && value !== null) {
148
+ substituteEnvVarsInDeployment(value, envVarMap);
149
+ }
150
+ }
151
+ }
152
+ }
153
+
62
154
  /**
63
155
  * Builds and validates deployment manifest
64
156
  * @function buildAndValidateDeployment
@@ -66,10 +158,12 @@ function loadDeploymentConfigFiles(appPath, appType, appName) {
66
158
  * @param {Object} variables - Variables configuration
67
159
  * @param {Object} envTemplate - Environment template
68
160
  * @param {Object} rbac - RBAC configuration
161
+ * @param {Object} [options] - Optional options
162
+ * @param {Object} [options.envVarMap] - Env vars from env-config (e.g. REDIS_HOST, DB_HOST) to resolve ${VAR} in manifest
69
163
  * @returns {Object} Deployment manifest
70
164
  * @throws {Error} If validation fails
71
165
  */
72
- function buildAndValidateDeployment(appName, variables, envTemplate, rbac) {
166
+ function buildAndValidateDeployment(appName, variables, envTemplate, rbac, options = null) {
73
167
  // Parse environment variables from template and merge portalInput from application config
74
168
  const configuration = parseEnvironmentVariables(envTemplate, variables);
75
169
 
@@ -83,6 +177,23 @@ function buildAndValidateDeployment(appName, variables, envTemplate, rbac) {
83
177
  throw new Error(`Generated deployment JSON does not match schema:\n${errorMessages}`);
84
178
  }
85
179
 
180
+ // Replace ${PORT} with port from application.yaml so manifest deploys correctly
181
+ const effectivePort = getEffectivePortForSubstitution(variables, 3000);
182
+ substitutePortInDeployment(deployment, effectivePort);
183
+ if (deployment.port !== undefined) {
184
+ deployment.port = typeof deployment.port === 'string' && /^\d+$/.test(deployment.port)
185
+ ? parseInt(deployment.port, 10) : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
186
+ }
187
+
188
+ // Resolve ${REDIS_HOST}, ${DB_HOST}, etc. from env-config.yaml so manifest has no unresolved vars
189
+ const envVarMap = options && options.envVarMap;
190
+ if (envVarMap) {
191
+ substituteEnvVarsInDeployment(deployment, envVarMap);
192
+ }
193
+
194
+ // Ensure no other ${...} placeholders remain in manifest
195
+ _validator.validateNoUnresolvedVariablesInDeployment(deployment);
196
+
86
197
  return deployment;
87
198
  }
88
199
 
@@ -117,46 +228,81 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
117
228
  const configuration = parseEnvironmentVariables(envTemplate, variables);
118
229
  const deployment = builders.buildManifestStructure(appName, variablesWithVersion, configuration, rbac);
119
230
 
231
+ const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
232
+ substitutePortInDeployment(deployment, effectivePort);
233
+ if (deployment.port !== undefined) {
234
+ deployment.port = typeof deployment.port === 'string' && /^\d+$/.test(deployment.port)
235
+ ? parseInt(deployment.port, 10) : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
236
+ }
237
+ const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
238
+ substituteEnvVarsInDeployment(deployment, envVarMap);
239
+ _validator.validateNoUnresolvedVariablesInDeployment(deployment);
120
240
  return { deployment, appPath };
121
241
  }
122
242
 
123
- async function generateDeployJson(appName, options = {}) {
124
- if (!appName || typeof appName !== 'string') {
125
- throw new Error('App name is required and must be a string');
126
- }
127
-
128
- // Detect app type and get correct path (integration first, then builder)
129
- const { isExternal, appPath, appType } = await detectAppType(appName);
130
- logOfflinePathWhenType(appPath);
131
-
132
- // Check if app type is external
133
- if (isExternal) {
134
- const manifest = await generateControllerManifest(appName, options);
135
-
136
- // Determine system key for file naming
137
- const systemKey = manifest.key || appName;
138
- const deployJsonPath = path.join(appPath, `${systemKey}-deploy.json`);
139
-
140
- await fs.promises.writeFile(deployJsonPath, JSON.stringify(manifest, null, 2), { mode: 0o644, encoding: 'utf8' });
141
- return deployJsonPath;
243
+ /**
244
+ * Writes external system deploy JSON (manifest + ${PORT} substitution + validation).
245
+ * @async
246
+ * @param {string} appName - Application name
247
+ * @param {string} appPath - Application path
248
+ * @param {Object} options - Generation options
249
+ * @returns {Promise<string>} Path to written deploy JSON
250
+ */
251
+ async function writeExternalDeployJson(appName, appPath, options) {
252
+ const manifest = await generateControllerManifest(appName, options);
253
+ let effectivePort = 3000;
254
+ try {
255
+ const variablesPath = resolveApplicationConfigPath(appPath);
256
+ const { parsed: variables } = loadVariables(variablesPath);
257
+ effectivePort = getEffectivePortForSubstitution(variables, 3000);
258
+ substitutePortInDeployment(manifest, effectivePort);
259
+ } catch {
260
+ substitutePortInDeployment(manifest, 3000);
142
261
  }
262
+ const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
263
+ substituteEnvVarsInDeployment(manifest, envVarMap);
264
+ _validator.validateNoUnresolvedVariablesInDeployment(manifest);
265
+ const systemKey = manifest.key || appName;
266
+ const deployJsonPath = path.join(appPath, `${systemKey}-deploy.json`);
267
+ await fs.promises.writeFile(deployJsonPath, JSON.stringify(manifest, null, 2), { mode: 0o644, encoding: 'utf8' });
268
+ return deployJsonPath;
269
+ }
143
270
 
144
- // Regular app: generate deployment manifest
271
+ /**
272
+ * Writes regular app deploy JSON (build + validate + write).
273
+ * @async
274
+ * @param {string} appName - Application name
275
+ * @param {string} appPath - Application path
276
+ * @param {string} appType - Application type
277
+ * @returns {Promise<string>} Path to written deploy JSON
278
+ */
279
+ async function writeRegularDeployJson(appName, appPath, appType) {
145
280
  const { variables, envTemplate, rbac, jsonPath } = loadDeploymentConfigFiles(appPath, appType, appName);
146
281
  const resolved = await resolveVersionForApp(appName, variables, { updateBuilder: false });
147
282
  const variablesWithVersion = {
148
283
  ...variables,
149
284
  app: { ...variables.app, version: resolved.version }
150
285
  };
151
- const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac);
152
-
153
- // Write deployment JSON
286
+ const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
287
+ const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
288
+ const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac, { envVarMap });
154
289
  const jsonContent = JSON.stringify(deployment, null, 2);
155
290
  fs.writeFileSync(jsonPath, jsonContent, { mode: 0o644 });
156
-
157
291
  return jsonPath;
158
292
  }
159
293
 
294
+ async function generateDeployJson(appName, options = {}) {
295
+ if (!appName || typeof appName !== 'string') {
296
+ throw new Error('App name is required and must be a string');
297
+ }
298
+ const { isExternal, appPath, appType } = await detectAppType(appName);
299
+ logOfflinePathWhenType(appPath);
300
+ if (isExternal) {
301
+ return writeExternalDeployJson(appName, appPath, options);
302
+ }
303
+ return await writeRegularDeployJson(appName, appPath, appType);
304
+ }
305
+
160
306
  async function generateDeployJsonWithValidation(appName, options = {}) {
161
307
  const jsonPath = await generateDeployJson(appName, options);
162
308
  const jsonContent = fs.readFileSync(jsonPath, 'utf8');
@@ -187,6 +333,9 @@ module.exports = {
187
333
  generateDeployJson,
188
334
  generateDeployJsonWithValidation,
189
335
  buildDeploymentManifestInMemory,
336
+ getEffectivePortForSubstitution,
337
+ substitutePortInDeployment,
338
+ substituteEnvVarsInDeployment,
190
339
  generateExternalSystemApplicationSchema,
191
340
  splitExternalApplicationSchema,
192
341
  parseEnvironmentVariables,
@@ -117,6 +117,14 @@ async function generateConfigFilesForWizard(params) {
117
117
  // Generate env.template with authentication variables
118
118
  await generateEnvTemplate(appPath, systemConfig);
119
119
 
120
+ const envTemplatePath = path.join(appPath, 'env.template');
121
+ try {
122
+ const secretsEnsure = require('../core/secrets-ensure');
123
+ await secretsEnsure.ensureSecretsFromEnvTemplate(envTemplatePath, { emptyValuesForCredentials: true });
124
+ } catch (err) {
125
+ if (err.code !== 'ENOENT') logger.warn(`Could not ensure integration placeholder secrets: ${err.message}`);
126
+ }
127
+
120
128
  // Generate README.md (use AI-generated content if available)
121
129
  await generateReadme(appPath, appName, finalSystemKey, systemConfig, datasourceConfigs, aiGeneratedReadme);
122
130