@aifabrix/builder 2.0.0 → 2.0.2

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 (58) hide show
  1. package/README.md +6 -2
  2. package/bin/aifabrix.js +9 -3
  3. package/jest.config.integration.js +30 -0
  4. package/lib/app-config.js +157 -0
  5. package/lib/app-deploy.js +233 -82
  6. package/lib/app-dockerfile.js +112 -0
  7. package/lib/app-prompts.js +244 -0
  8. package/lib/app-push.js +172 -0
  9. package/lib/app-run.js +334 -133
  10. package/lib/app.js +208 -274
  11. package/lib/audit-logger.js +2 -0
  12. package/lib/build.js +209 -98
  13. package/lib/cli.js +76 -86
  14. package/lib/commands/app.js +414 -0
  15. package/lib/commands/login.js +304 -0
  16. package/lib/config.js +78 -0
  17. package/lib/deployer.js +225 -81
  18. package/lib/env-reader.js +45 -30
  19. package/lib/generator.js +308 -191
  20. package/lib/github-generator.js +67 -7
  21. package/lib/infra.js +156 -61
  22. package/lib/push.js +105 -10
  23. package/lib/schema/application-schema.json +30 -2
  24. package/lib/schema/infrastructure-schema.json +589 -0
  25. package/lib/secrets.js +229 -24
  26. package/lib/template-validator.js +205 -0
  27. package/lib/templates.js +305 -170
  28. package/lib/utils/api.js +329 -0
  29. package/lib/utils/cli-utils.js +97 -0
  30. package/lib/utils/dockerfile-utils.js +131 -0
  31. package/lib/utils/environment-checker.js +125 -0
  32. package/lib/utils/error-formatter.js +61 -0
  33. package/lib/utils/health-check.js +187 -0
  34. package/lib/utils/logger.js +53 -0
  35. package/lib/utils/template-helpers.js +223 -0
  36. package/lib/utils/variable-transformer.js +271 -0
  37. package/lib/validator.js +27 -112
  38. package/package.json +13 -10
  39. package/templates/README.md +75 -3
  40. package/templates/applications/keycloak/Dockerfile +36 -0
  41. package/templates/applications/keycloak/env.template +32 -0
  42. package/templates/applications/keycloak/rbac.yaml +37 -0
  43. package/templates/applications/keycloak/variables.yaml +56 -0
  44. package/templates/applications/miso-controller/Dockerfile +125 -0
  45. package/templates/applications/miso-controller/env.template +129 -0
  46. package/templates/applications/miso-controller/rbac.yaml +168 -0
  47. package/templates/applications/miso-controller/variables.yaml +56 -0
  48. package/templates/github/release.yaml.hbs +5 -26
  49. package/templates/github/steps/npm.hbs +24 -0
  50. package/templates/infra/compose.yaml +6 -6
  51. package/templates/python/docker-compose.hbs +19 -12
  52. package/templates/python/main.py +80 -0
  53. package/templates/python/requirements.txt +4 -0
  54. package/templates/typescript/Dockerfile.hbs +2 -2
  55. package/templates/typescript/docker-compose.hbs +19 -12
  56. package/templates/typescript/index.ts +116 -0
  57. package/templates/typescript/package.json +26 -0
  58. package/templates/typescript/tsconfig.json +24 -0
package/lib/generator.js CHANGED
@@ -14,6 +14,278 @@ const path = require('path');
14
14
  const yaml = require('js-yaml');
15
15
  const _secrets = require('./secrets');
16
16
  const _keyGenerator = require('./key-generator');
17
+ const _validator = require('./validator');
18
+
19
+ /**
20
+ * Loads variables.yaml file
21
+ * @param {string} variablesPath - Path to variables.yaml
22
+ * @returns {Object} Parsed variables
23
+ * @throws {Error} If file not found or invalid YAML
24
+ */
25
+ function loadVariables(variablesPath) {
26
+ if (!fs.existsSync(variablesPath)) {
27
+ throw new Error(`variables.yaml not found: ${variablesPath}`);
28
+ }
29
+
30
+ const variablesContent = fs.readFileSync(variablesPath, 'utf8');
31
+ try {
32
+ return { content: variablesContent, parsed: yaml.load(variablesContent) };
33
+ } catch (error) {
34
+ throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Loads env.template file
40
+ * @param {string} templatePath - Path to env.template
41
+ * @returns {string} Template content
42
+ * @throws {Error} If file not found
43
+ */
44
+ function loadEnvTemplate(templatePath) {
45
+ if (!fs.existsSync(templatePath)) {
46
+ throw new Error(`env.template not found: ${templatePath}`);
47
+ }
48
+ return fs.readFileSync(templatePath, 'utf8');
49
+ }
50
+
51
+ /**
52
+ * Loads rbac.yaml file if it exists
53
+ * @param {string} rbacPath - Path to rbac.yaml
54
+ * @returns {Object|null} Parsed RBAC configuration or null
55
+ * @throws {Error} If file exists but has invalid YAML
56
+ */
57
+ function loadRbac(rbacPath) {
58
+ if (!fs.existsSync(rbacPath)) {
59
+ return null;
60
+ }
61
+
62
+ const rbacContent = fs.readFileSync(rbacPath, 'utf8');
63
+ try {
64
+ return yaml.load(rbacContent);
65
+ } catch (error) {
66
+ throw new Error(`Invalid YAML syntax in rbac.yaml: ${error.message}`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Filters configuration based on registry mode
72
+ * When registryMode is "external", only DOCKER_REGISTRY_SERVER_* variables are allowed
73
+ * @function filterConfigurationByRegistryMode
74
+ * @param {Array} configuration - Environment configuration
75
+ * @param {string} registryMode - Registry mode ('external' or 'internal')
76
+ * @returns {Array} Filtered configuration
77
+ */
78
+ function filterConfigurationByRegistryMode(configuration, registryMode) {
79
+ if (registryMode !== 'external') {
80
+ return configuration;
81
+ }
82
+
83
+ const allowedDockerRegistryVars = [
84
+ 'DOCKER_REGISTRY_SERVER_URL',
85
+ 'DOCKER_REGISTRY_SERVER_USERNAME',
86
+ 'DOCKER_REGISTRY_SERVER_PASSWORD'
87
+ ];
88
+ return configuration.filter(config => allowedDockerRegistryVars.includes(config.name));
89
+ }
90
+
91
+ /**
92
+ * Builds base deployment structure
93
+ * @function buildBaseDeployment
94
+ * @param {string} appName - Application name
95
+ * @param {Object} variables - Variables configuration
96
+ * @param {Array} filteredConfiguration - Filtered environment configuration
97
+ * @returns {Object} Base deployment structure
98
+ */
99
+ function buildBaseDeployment(appName, variables, filteredConfiguration) {
100
+ const requires = variables.requires || {};
101
+ return {
102
+ key: variables.app?.key || appName,
103
+ displayName: variables.app?.displayName || appName,
104
+ description: variables.app?.description || '',
105
+ type: variables.app?.type || 'webapp',
106
+ image: buildImageReference(variables),
107
+ registryMode: variables.image?.registryMode || 'external',
108
+ port: variables.port || 3000,
109
+ requiresDatabase: requires.database || false,
110
+ requiresRedis: requires.redis || false,
111
+ requiresStorage: requires.storage || false,
112
+ databases: requires.databases || (requires.database ? [{ name: variables.app?.key || 'app' }] : []),
113
+ configuration: filteredConfiguration
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Builds authentication configuration from variables or RBAC
119
+ * @function buildAuthenticationConfig
120
+ * @param {Object} variables - Variables configuration
121
+ * @param {Object|null} rbac - RBAC configuration
122
+ * @returns {Object} Authentication configuration
123
+ */
124
+ function buildAuthenticationConfig(variables, rbac) {
125
+ if (variables.authentication) {
126
+ const auth = {
127
+ type: variables.authentication.type || 'azure',
128
+ enableSSO: variables.authentication.enableSSO !== undefined ? variables.authentication.enableSSO : true,
129
+ requiredRoles: variables.authentication.requiredRoles || []
130
+ };
131
+ if (variables.authentication.endpoints) {
132
+ auth.endpoints = variables.authentication.endpoints;
133
+ }
134
+ return auth;
135
+ }
136
+ return buildAuthentication(rbac);
137
+ }
138
+
139
+ /**
140
+ * Validates and transforms repository configuration
141
+ * @function validateRepositoryConfig
142
+ * @param {Object} repository - Repository configuration
143
+ * @returns {Object|null} Validated repository config or null
144
+ */
145
+ function validateRepositoryConfig(repository) {
146
+ if (!repository || (!repository.enabled && !repository.repositoryUrl)) {
147
+ return null;
148
+ }
149
+
150
+ if (repository.repositoryUrl && repository.repositoryUrl.trim()) {
151
+ return {
152
+ enabled: repository.enabled || false,
153
+ repositoryUrl: repository.repositoryUrl
154
+ };
155
+ }
156
+
157
+ if (repository.enabled) {
158
+ return { enabled: true };
159
+ }
160
+
161
+ return null;
162
+ }
163
+
164
+ /**
165
+ * Validates and transforms build fields
166
+ * @function validateBuildFields
167
+ * @param {Object} build - Build configuration
168
+ * @returns {Object|null} Validated build config or null
169
+ */
170
+ function validateBuildFields(build) {
171
+ if (!build) {
172
+ return null;
173
+ }
174
+
175
+ const buildConfig = {};
176
+ if (build.envOutputPath) {
177
+ buildConfig.envOutputPath = build.envOutputPath;
178
+ }
179
+ if (build.secrets && typeof build.secrets === 'string') {
180
+ buildConfig.secrets = build.secrets;
181
+ }
182
+ if (build.dockerfile && build.dockerfile.trim()) {
183
+ buildConfig.dockerfile = build.dockerfile;
184
+ }
185
+
186
+ return Object.keys(buildConfig).length > 0 ? buildConfig : null;
187
+ }
188
+
189
+ /**
190
+ * Validates and transforms deployment fields
191
+ * @function validateDeploymentFields
192
+ * @param {Object} deployment - Deployment configuration
193
+ * @returns {Object|null} Validated deployment config or null
194
+ */
195
+ function validateDeploymentFields(deployment) {
196
+ if (!deployment) {
197
+ return null;
198
+ }
199
+
200
+ const deploymentConfig = {};
201
+ if (deployment.controllerUrl && deployment.controllerUrl.trim() && deployment.controllerUrl.startsWith('https://')) {
202
+ deploymentConfig.controllerUrl = deployment.controllerUrl;
203
+ }
204
+ if (deployment.clientId && deployment.clientId.trim()) {
205
+ deploymentConfig.clientId = deployment.clientId;
206
+ }
207
+ if (deployment.clientSecret && deployment.clientSecret.trim()) {
208
+ deploymentConfig.clientSecret = deployment.clientSecret;
209
+ }
210
+
211
+ return Object.keys(deploymentConfig).length > 0 ? deploymentConfig : null;
212
+ }
213
+
214
+ /**
215
+ * Adds optional fields to deployment manifest
216
+ * @function buildOptionalFields
217
+ * @param {Object} deployment - Deployment manifest
218
+ * @param {Object} variables - Variables configuration
219
+ * @param {Object|null} rbac - RBAC configuration
220
+ * @returns {Object} Deployment manifest with optional fields
221
+ */
222
+ function buildOptionalFields(deployment, variables, rbac) {
223
+ if (variables.healthCheck) {
224
+ deployment.healthCheck = buildHealthCheck(variables);
225
+ }
226
+
227
+ deployment.authentication = buildAuthenticationConfig(variables, rbac);
228
+
229
+ // Add roles and permissions (from variables.yaml or rbac.yaml)
230
+ // Priority: variables.yaml > rbac.yaml
231
+ if (variables.roles) {
232
+ deployment.roles = variables.roles;
233
+ } else if (rbac && rbac.roles) {
234
+ deployment.roles = rbac.roles;
235
+ }
236
+
237
+ if (variables.permissions) {
238
+ deployment.permissions = variables.permissions;
239
+ } else if (rbac && rbac.permissions) {
240
+ deployment.permissions = rbac.permissions;
241
+ }
242
+
243
+ const repository = validateRepositoryConfig(variables.repository);
244
+ if (repository) {
245
+ deployment.repository = repository;
246
+ }
247
+
248
+ const build = validateBuildFields(variables.build);
249
+ if (build) {
250
+ deployment.build = build;
251
+ }
252
+
253
+ const deploymentConfig = validateDeploymentFields(variables.deployment);
254
+ if (deploymentConfig) {
255
+ deployment.deployment = deploymentConfig;
256
+ }
257
+
258
+ if (variables.startupCommand) {
259
+ deployment.startupCommand = variables.startupCommand;
260
+ }
261
+ if (variables.runtimeVersion) {
262
+ deployment.runtimeVersion = variables.runtimeVersion;
263
+ }
264
+ if (variables.scaling) {
265
+ deployment.scaling = variables.scaling;
266
+ }
267
+ if (variables.frontDoorRouting) {
268
+ deployment.frontDoorRouting = variables.frontDoorRouting;
269
+ }
270
+
271
+ return deployment;
272
+ }
273
+
274
+ /**
275
+ * Builds deployment manifest structure
276
+ * @param {string} appName - Application name
277
+ * @param {Object} variables - Variables configuration
278
+ * @param {string} deploymentKey - Deployment key
279
+ * @param {Array} configuration - Environment configuration
280
+ * @param {Object|null} rbac - RBAC configuration
281
+ * @returns {Object} Deployment manifest
282
+ */
283
+ function buildManifestStructure(appName, variables, deploymentKey, configuration, rbac) {
284
+ const registryMode = variables.image?.registryMode || 'external';
285
+ const filteredConfiguration = filterConfigurationByRegistryMode(configuration, registryMode);
286
+ const deployment = buildBaseDeployment(appName, variables, filteredConfiguration);
287
+ return buildOptionalFields(deployment, variables, rbac);
288
+ }
17
289
 
18
290
  /**
19
291
  * Generates deployment JSON from application configuration files
@@ -40,36 +312,10 @@ async function generateDeployJson(appName) {
40
312
  const rbacPath = path.join(builderPath, 'rbac.yaml');
41
313
  const jsonPath = path.join(builderPath, 'aifabrix-deploy.json');
42
314
 
43
- // Load variables.yaml
44
- if (!fs.existsSync(variablesPath)) {
45
- throw new Error(`variables.yaml not found: ${variablesPath}`);
46
- }
47
-
48
- const variablesContent = fs.readFileSync(variablesPath, 'utf8');
49
- let variables;
50
- try {
51
- variables = yaml.load(variablesContent);
52
- } catch (error) {
53
- throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
54
- }
55
-
56
- // Load env.template
57
- if (!fs.existsSync(templatePath)) {
58
- throw new Error(`env.template not found: ${templatePath}`);
59
- }
60
-
61
- const envTemplate = fs.readFileSync(templatePath, 'utf8');
62
-
63
- // Load rbac.yaml (optional)
64
- let rbac = null;
65
- if (fs.existsSync(rbacPath)) {
66
- const rbacContent = fs.readFileSync(rbacPath, 'utf8');
67
- try {
68
- rbac = yaml.load(rbacContent);
69
- } catch (error) {
70
- throw new Error(`Invalid YAML syntax in rbac.yaml: ${error.message}`);
71
- }
72
- }
315
+ // Load configuration files
316
+ const { content: variablesContent, parsed: variables } = loadVariables(variablesPath);
317
+ const envTemplate = loadEnvTemplate(templatePath);
318
+ const rbac = loadRbac(rbacPath);
73
319
 
74
320
  // Generate deployment key
75
321
  const deploymentKey = _keyGenerator.generateDeploymentKeyFromContent(variablesContent);
@@ -78,24 +324,13 @@ async function generateDeployJson(appName) {
78
324
  const configuration = parseEnvironmentVariables(envTemplate);
79
325
 
80
326
  // Build deployment manifest
81
- const deployment = {
82
- key: variables.app?.key || appName,
83
- displayName: variables.app?.displayName || appName,
84
- description: variables.app?.description || '',
85
- type: variables.app?.type || 'webapp',
86
- image: buildImageReference(variables),
87
- port: variables.port || 3000,
88
- deploymentKey,
89
- configuration,
90
- healthCheck: buildHealthCheck(variables),
91
- requires: buildRequirements(variables),
92
- authentication: buildAuthentication(rbac)
93
- };
327
+ const deployment = buildManifestStructure(appName, variables, deploymentKey, configuration, rbac);
94
328
 
95
- // Add roles and permissions if RBAC is configured
96
- if (rbac) {
97
- deployment.roles = rbac.roles || [];
98
- deployment.permissions = rbac.permissions || [];
329
+ // Validate deployment JSON against schema
330
+ const validation = _validator.validateDeploymentJson(deployment);
331
+ if (!validation.valid) {
332
+ const errorMessages = validation.errors.join('\n');
333
+ throw new Error(`Generated deployment JSON does not match schema:\n${errorMessages}`);
99
334
  }
100
335
 
101
336
  // Write deployment JSON
@@ -105,14 +340,6 @@ async function generateDeployJson(appName) {
105
340
  return jsonPath;
106
341
  }
107
342
 
108
- /**
109
- * Parses environment variables from env.template
110
- * Converts kv:// references to KeyVault configuration
111
- *
112
- * @function parseEnvironmentVariables
113
- * @param {string} envTemplate - Environment template content
114
- * @returns {Array} Array of configuration objects
115
- */
116
343
  function parseEnvironmentVariables(envTemplate) {
117
344
  const configuration = [];
118
345
  const lines = envTemplate.split('\n');
@@ -164,14 +391,6 @@ function parseEnvironmentVariables(envTemplate) {
164
391
  return configuration;
165
392
  }
166
393
 
167
- /**
168
- * Builds image reference from variables configuration
169
- * Handles registry, name, and tag formatting
170
- *
171
- * @function buildImageReference
172
- * @param {Object} variables - Variables configuration
173
- * @returns {string} Complete image reference
174
- */
175
394
  function buildImageReference(variables) {
176
395
  const imageName = variables.image?.name || variables.app?.key || 'app';
177
396
  const registry = variables.image?.registry;
@@ -184,31 +403,29 @@ function buildImageReference(variables) {
184
403
  return `${imageName}:${tag}`;
185
404
  }
186
405
 
187
- /**
188
- * Builds health check configuration from variables
189
- * Provides default health check settings
190
- *
191
- * @function buildHealthCheck
192
- * @param {Object} variables - Variables configuration
193
- * @returns {Object} Health check configuration
194
- */
195
406
  function buildHealthCheck(variables) {
196
- return {
407
+ const healthCheck = {
197
408
  path: variables.healthCheck?.path || '/health',
198
- interval: variables.healthCheck?.interval || 30,
199
- timeout: variables.healthCheck?.timeout || 10,
200
- retries: variables.healthCheck?.retries || 3
409
+ interval: variables.healthCheck?.interval || 30
201
410
  };
411
+
412
+ // Add optional probe fields if present
413
+ if (variables.healthCheck?.probePath) {
414
+ healthCheck.probePath = variables.healthCheck.probePath;
415
+ }
416
+ if (variables.healthCheck?.probeRequestType) {
417
+ healthCheck.probeRequestType = variables.healthCheck.probeRequestType;
418
+ }
419
+ if (variables.healthCheck?.probeProtocol) {
420
+ healthCheck.probeProtocol = variables.healthCheck.probeProtocol;
421
+ }
422
+ if (variables.healthCheck?.probeIntervalInSeconds) {
423
+ healthCheck.probeIntervalInSeconds = variables.healthCheck.probeIntervalInSeconds;
424
+ }
425
+
426
+ return healthCheck;
202
427
  }
203
428
 
204
- /**
205
- * Builds requirements configuration from variables
206
- * Maps database, Redis, and storage requirements
207
- *
208
- * @function buildRequirements
209
- * @param {Object} variables - Variables configuration
210
- * @returns {Object} Requirements configuration
211
- */
212
429
  function buildRequirements(variables) {
213
430
  const requires = variables.requires || {};
214
431
 
@@ -221,126 +438,27 @@ function buildRequirements(variables) {
221
438
  };
222
439
  }
223
440
 
224
- /**
225
- * Builds authentication configuration from RBAC
226
- * Configures authentication settings for the application
227
- *
228
- * @function buildAuthentication
229
- * @param {Object} rbac - RBAC configuration (optional)
230
- * @returns {Object} Authentication configuration
231
- */
232
441
  function buildAuthentication(rbac) {
233
442
  if (!rbac) {
234
443
  return {
235
- enabled: false,
236
- type: 'none'
444
+ type: 'none',
445
+ enableSSO: false,
446
+ requiredRoles: []
237
447
  };
238
448
  }
239
449
 
240
450
  return {
241
- enabled: true,
242
- type: 'keycloak', // Default to Keycloak
243
- sso: true,
244
- requiredRoles: rbac.roles?.map(role => role.value) || [],
245
- permissions: rbac.permissions?.map(perm => perm.name) || []
246
- };
247
- }
248
-
249
- /**
250
- * Validates deployment JSON before writing
251
- * Ensures all required fields are present and valid
252
- *
253
- * @function validateDeploymentJson
254
- * @param {Object} deployment - Deployment configuration object
255
- * @returns {Object} Validation result
256
- */
257
- function validateDeploymentJson(deployment) {
258
- const errors = [];
259
- const warnings = [];
260
-
261
- // Required fields validation
262
- if (!deployment.key) {
263
- errors.push('Missing required field: key');
264
- }
265
-
266
- if (!deployment.displayName) {
267
- errors.push('Missing required field: displayName');
268
- }
269
-
270
- if (!deployment.image) {
271
- errors.push('Missing required field: image');
272
- }
273
-
274
- if (!deployment.port || deployment.port < 1 || deployment.port > 65535) {
275
- errors.push('Invalid port: must be between 1 and 65535');
276
- }
277
-
278
- if (!deployment.deploymentKey) {
279
- errors.push('Missing required field: deploymentKey');
280
- }
281
-
282
- // Validate deployment key format
283
- if (deployment.deploymentKey && !_keyGenerator.validateDeploymentKey(deployment.deploymentKey)) {
284
- errors.push('Invalid deployment key format');
285
- }
286
-
287
- // Configuration validation
288
- if (!deployment.configuration || !Array.isArray(deployment.configuration)) {
289
- errors.push('Missing or invalid configuration array');
290
- }
291
-
292
- // Health check validation
293
- if (deployment.healthCheck) {
294
- if (deployment.healthCheck.path && !deployment.healthCheck.path.startsWith('/')) {
295
- warnings.push('Health check path should start with /');
296
- }
297
-
298
- if (deployment.healthCheck.interval && (deployment.healthCheck.interval < 5 || deployment.healthCheck.interval > 300)) {
299
- warnings.push('Health check interval should be between 5 and 300 seconds');
300
- }
301
- }
302
-
303
- // Authentication validation
304
- if (deployment.authentication?.enabled) {
305
- if (!deployment.roles || deployment.roles.length === 0) {
306
- warnings.push('Authentication enabled but no roles defined');
307
- }
308
-
309
- if (!deployment.permissions || deployment.permissions.length === 0) {
310
- warnings.push('Authentication enabled but no permissions defined');
311
- }
312
- }
313
-
314
- return {
315
- valid: errors.length === 0,
316
- errors,
317
- warnings
451
+ type: 'azure', // Default to azure (enum: azure, local, none)
452
+ enableSSO: true,
453
+ requiredRoles: rbac.roles?.map(role => role.value) || []
318
454
  };
319
455
  }
320
456
 
321
- /**
322
- * Generates deployment JSON with validation
323
- * Validates configuration before writing the file
324
- *
325
- * @async
326
- * @function generateDeployJsonWithValidation
327
- * @param {string} appName - Name of the application
328
- * @returns {Promise<Object>} Generation result with validation info
329
- * @throws {Error} If generation fails
330
- *
331
- * @example
332
- * const result = await generateDeployJsonWithValidation('myapp');
333
- * // Returns: { success: true, path: './builder/myapp/aifabrix-deploy.json', validation: {...} }
334
- */
335
457
  async function generateDeployJsonWithValidation(appName) {
336
458
  const jsonPath = await generateDeployJson(appName);
337
-
338
- // Read back the generated file for validation
339
459
  const jsonContent = fs.readFileSync(jsonPath, 'utf8');
340
460
  const deployment = JSON.parse(jsonContent);
341
-
342
- const validation = validateDeploymentJson(deployment);
343
-
461
+ const validation = _validator.validateDeploymentJson(deployment);
344
462
  return {
345
463
  success: validation.valid,
346
464
  path: jsonPath,
@@ -356,6 +474,5 @@ module.exports = {
356
474
  buildImageReference,
357
475
  buildHealthCheck,
358
476
  buildRequirements,
359
- buildAuthentication,
360
- validateDeploymentJson
477
+ buildAuthentication
361
478
  };
@@ -6,9 +6,42 @@
6
6
  */
7
7
 
8
8
  const fs = require('fs').promises;
9
+ const fsSync = require('fs');
9
10
  const path = require('path');
10
11
  const Handlebars = require('handlebars');
11
12
 
13
+ // Register Handlebars helper for checking array contains
14
+ Handlebars.registerHelper('contains', (array, value) => {
15
+ return array && array.includes(value);
16
+ });
17
+
18
+ /**
19
+ * Load extra workflow step templates from files
20
+ * @param {string[]} stepNames - Array of step names to load
21
+ * @returns {Promise<Object>} Object mapping step names to their compiled content
22
+ */
23
+ async function loadStepTemplates(stepNames = []) {
24
+ const stepsDir = path.join(__dirname, '..', 'templates', 'github', 'steps');
25
+ const stepTemplates = {};
26
+
27
+ for (const stepName of stepNames) {
28
+ const stepPath = path.join(stepsDir, `${stepName}.hbs`);
29
+
30
+ if (fsSync.existsSync(stepPath)) {
31
+ try {
32
+ const stepContent = await fs.readFile(stepPath, 'utf8');
33
+ stepTemplates[stepName] = Handlebars.compile(stepContent);
34
+ } catch (error) {
35
+ throw new Error(`Failed to load step template '${stepName}': ${error.message}`);
36
+ }
37
+ } else {
38
+ throw new Error(`Step template '${stepName}' not found at ${stepPath}`);
39
+ }
40
+ }
41
+
42
+ return stepTemplates;
43
+ }
44
+
12
45
  /**
13
46
  * Generate GitHub Actions workflow files from templates
14
47
  * @param {string} appPath - Path to application directory
@@ -22,8 +55,13 @@ async function generateGithubWorkflows(appPath, config, options = {}) {
22
55
  const workflowsDir = path.join(appPath, '.github', 'workflows');
23
56
  await fs.mkdir(workflowsDir, { recursive: true });
24
57
 
58
+ // Load step templates if githubSteps are provided
59
+ const stepTemplates = options.githubSteps && options.githubSteps.length > 0
60
+ ? await loadStepTemplates(options.githubSteps)
61
+ : {};
62
+
25
63
  // Get template context
26
- const templateContext = getTemplateContext(config, options);
64
+ const templateContext = await getTemplateContext(config, options, stepTemplates);
27
65
 
28
66
  // Load and compile templates
29
67
  const templatesDir = path.join(__dirname, '..', 'templates', 'github');
@@ -62,24 +100,40 @@ async function generateGithubWorkflows(appPath, config, options = {}) {
62
100
  * Get template context from config
63
101
  * @param {Object} config - Application configuration
64
102
  * @param {Object} options - Additional options
65
- * @returns {Object} Template context
103
+ * @param {Object} stepTemplates - Compiled step templates
104
+ * @returns {Promise<Object>} Template context
66
105
  */
67
- function getTemplateContext(config, options = {}) {
68
- return {
106
+ async function getTemplateContext(config, options = {}, stepTemplates = {}) {
107
+ const githubSteps = options.githubSteps || [];
108
+
109
+ // Render step templates with the base context
110
+ const renderedSteps = {};
111
+ const baseContext = {
69
112
  appName: config.appName || 'myapp',
70
113
  mainBranch: options.mainBranch || 'main',
71
114
  language: config.language || 'typescript',
72
115
  fileExtension: config.language === 'python' ? 'py' : 'js',
73
116
  sourceDir: config.language === 'python' ? 'src' : 'lib',
74
117
  buildCommand: options.buildCommand || 'npm run build',
75
- uploadCoverage: options.uploadCoverage !== false,
76
- publishToNpm: options.publishToNpm || false,
77
118
  port: config.port || 3000,
78
119
  database: config.database || false,
79
120
  redis: config.redis || false,
80
121
  storage: config.storage || false,
81
122
  authentication: config.authentication || false
82
123
  };
124
+
125
+ for (const [stepName, template] of Object.entries(stepTemplates)) {
126
+ renderedSteps[stepName] = template(baseContext);
127
+ }
128
+
129
+ return {
130
+ ...baseContext,
131
+ uploadCoverage: options.uploadCoverage !== false,
132
+ githubSteps: githubSteps,
133
+ stepContent: renderedSteps,
134
+ hasSteps: githubSteps.length > 0,
135
+ hasNpmStep: githubSteps.includes('npm')
136
+ };
83
137
  }
84
138
 
85
139
  /**
@@ -103,8 +157,13 @@ async function generateWorkflowFile(appPath, templateName, config, options = {})
103
157
  // Compile template
104
158
  const template = Handlebars.compile(templateContent);
105
159
 
160
+ // Load step templates if githubSteps are provided
161
+ const stepTemplates = options.githubSteps && options.githubSteps.length > 0
162
+ ? await loadStepTemplates(options.githubSteps)
163
+ : {};
164
+
106
165
  // Get template context
107
- const templateContext = getTemplateContext(config, options);
166
+ const templateContext = await getTemplateContext(config, options, stepTemplates);
108
167
 
109
168
  // Generate content
110
169
  const generatedContent = template(templateContext);
@@ -214,6 +273,7 @@ async function generateWorkflowsWithValidation(appPath, config, options = {}) {
214
273
  module.exports = {
215
274
  generateGithubWorkflows,
216
275
  getTemplateContext,
276
+ loadStepTemplates,
217
277
  generateWorkflowFile,
218
278
  validateWorkflowConfig,
219
279
  generateWorkflowsWithValidation