@aifabrix/builder 2.0.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.
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/bin/aifabrix.js +51 -0
- package/lib/app-deploy.js +209 -0
- package/lib/app-run.js +291 -0
- package/lib/app.js +472 -0
- package/lib/audit-logger.js +162 -0
- package/lib/build.js +313 -0
- package/lib/cli.js +307 -0
- package/lib/deployer.js +256 -0
- package/lib/env-reader.js +250 -0
- package/lib/generator.js +361 -0
- package/lib/github-generator.js +220 -0
- package/lib/infra.js +300 -0
- package/lib/key-generator.js +93 -0
- package/lib/push.js +141 -0
- package/lib/schema/application-schema.json +649 -0
- package/lib/schema/env-config.yaml +15 -0
- package/lib/secrets.js +282 -0
- package/lib/templates.js +301 -0
- package/lib/validator.js +377 -0
- package/package.json +59 -0
- package/templates/README.md +51 -0
- package/templates/github/ci.yaml.hbs +15 -0
- package/templates/github/pr-checks.yaml.hbs +35 -0
- package/templates/github/release.yaml.hbs +79 -0
- package/templates/github/test.hbs +11 -0
- package/templates/github/test.yaml.hbs +11 -0
- package/templates/infra/compose.yaml +93 -0
- package/templates/python/Dockerfile.hbs +49 -0
- package/templates/python/docker-compose.hbs +69 -0
- package/templates/typescript/Dockerfile.hbs +46 -0
- package/templates/typescript/docker-compose.hbs +69 -0
package/lib/generator.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Deployment JSON Generator
|
|
3
|
+
*
|
|
4
|
+
* This module generates deployment JSON manifests for Miso Controller.
|
|
5
|
+
* Combines variables.yaml, env.template, and rbac.yaml into deployment configuration.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Deployment JSON generation for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const yaml = require('js-yaml');
|
|
15
|
+
const _secrets = require('./secrets');
|
|
16
|
+
const _keyGenerator = require('./key-generator');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generates deployment JSON from application configuration files
|
|
20
|
+
* Creates aifabrix-deploy.json for Miso Controller deployment
|
|
21
|
+
*
|
|
22
|
+
* @async
|
|
23
|
+
* @function generateDeployJson
|
|
24
|
+
* @param {string} appName - Name of the application
|
|
25
|
+
* @returns {Promise<string>} Path to generated deployment JSON file
|
|
26
|
+
* @throws {Error} If generation fails or configuration is invalid
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const jsonPath = await generateDeployJson('myapp');
|
|
30
|
+
* // Returns: './builder/myapp/aifabrix-deploy.json'
|
|
31
|
+
*/
|
|
32
|
+
async function generateDeployJson(appName) {
|
|
33
|
+
if (!appName || typeof appName !== 'string') {
|
|
34
|
+
throw new Error('App name is required and must be a string');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const builderPath = path.join(process.cwd(), 'builder', appName);
|
|
38
|
+
const variablesPath = path.join(builderPath, 'variables.yaml');
|
|
39
|
+
const templatePath = path.join(builderPath, 'env.template');
|
|
40
|
+
const rbacPath = path.join(builderPath, 'rbac.yaml');
|
|
41
|
+
const jsonPath = path.join(builderPath, 'aifabrix-deploy.json');
|
|
42
|
+
|
|
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
|
+
}
|
|
73
|
+
|
|
74
|
+
// Generate deployment key
|
|
75
|
+
const deploymentKey = _keyGenerator.generateDeploymentKeyFromContent(variablesContent);
|
|
76
|
+
|
|
77
|
+
// Parse environment variables from template
|
|
78
|
+
const configuration = parseEnvironmentVariables(envTemplate);
|
|
79
|
+
|
|
80
|
+
// 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
|
+
};
|
|
94
|
+
|
|
95
|
+
// Add roles and permissions if RBAC is configured
|
|
96
|
+
if (rbac) {
|
|
97
|
+
deployment.roles = rbac.roles || [];
|
|
98
|
+
deployment.permissions = rbac.permissions || [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Write deployment JSON
|
|
102
|
+
const jsonContent = JSON.stringify(deployment, null, 2);
|
|
103
|
+
fs.writeFileSync(jsonPath, jsonContent, { mode: 0o644 });
|
|
104
|
+
|
|
105
|
+
return jsonPath;
|
|
106
|
+
}
|
|
107
|
+
|
|
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
|
+
function parseEnvironmentVariables(envTemplate) {
|
|
117
|
+
const configuration = [];
|
|
118
|
+
const lines = envTemplate.split('\n');
|
|
119
|
+
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
const trimmed = line.trim();
|
|
122
|
+
|
|
123
|
+
// Skip empty lines and comments
|
|
124
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Parse KEY=VALUE format
|
|
129
|
+
const equalIndex = trimmed.indexOf('=');
|
|
130
|
+
if (equalIndex === -1) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const key = trimmed.substring(0, equalIndex).trim();
|
|
135
|
+
const value = trimmed.substring(equalIndex + 1).trim();
|
|
136
|
+
|
|
137
|
+
if (!key || !value) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Determine location and required status
|
|
142
|
+
let location = 'variable';
|
|
143
|
+
let required = false;
|
|
144
|
+
|
|
145
|
+
if (value.startsWith('kv://')) {
|
|
146
|
+
location = 'keyvault';
|
|
147
|
+
required = true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check if it's a sensitive variable
|
|
151
|
+
const sensitiveKeys = ['password', 'secret', 'key', 'token', 'auth'];
|
|
152
|
+
if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
|
|
153
|
+
required = true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
configuration.push({
|
|
157
|
+
name: key,
|
|
158
|
+
value: value.replace('kv://', ''), // Remove kv:// prefix for KeyVault
|
|
159
|
+
location,
|
|
160
|
+
required
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return configuration;
|
|
165
|
+
}
|
|
166
|
+
|
|
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
|
+
function buildImageReference(variables) {
|
|
176
|
+
const imageName = variables.image?.name || variables.app?.key || 'app';
|
|
177
|
+
const registry = variables.image?.registry;
|
|
178
|
+
const tag = variables.image?.tag || 'latest';
|
|
179
|
+
|
|
180
|
+
if (registry) {
|
|
181
|
+
return `${registry}/${imageName}:${tag}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return `${imageName}:${tag}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
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
|
+
function buildHealthCheck(variables) {
|
|
196
|
+
return {
|
|
197
|
+
path: variables.healthCheck?.path || '/health',
|
|
198
|
+
interval: variables.healthCheck?.interval || 30,
|
|
199
|
+
timeout: variables.healthCheck?.timeout || 10,
|
|
200
|
+
retries: variables.healthCheck?.retries || 3
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
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
|
+
function buildRequirements(variables) {
|
|
213
|
+
const requires = variables.requires || {};
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
database: requires.database || false,
|
|
217
|
+
databases: requires.databases || (requires.database ? [{ name: variables.app?.key || 'app' }] : []),
|
|
218
|
+
redis: requires.redis || false,
|
|
219
|
+
storage: requires.storage || false,
|
|
220
|
+
storageSize: requires.storageSize || '1Gi'
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
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
|
+
function buildAuthentication(rbac) {
|
|
233
|
+
if (!rbac) {
|
|
234
|
+
return {
|
|
235
|
+
enabled: false,
|
|
236
|
+
type: 'none'
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
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
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
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
|
+
async function generateDeployJsonWithValidation(appName) {
|
|
336
|
+
const jsonPath = await generateDeployJson(appName);
|
|
337
|
+
|
|
338
|
+
// Read back the generated file for validation
|
|
339
|
+
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
|
|
340
|
+
const deployment = JSON.parse(jsonContent);
|
|
341
|
+
|
|
342
|
+
const validation = validateDeploymentJson(deployment);
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
success: validation.valid,
|
|
346
|
+
path: jsonPath,
|
|
347
|
+
validation,
|
|
348
|
+
deployment
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
generateDeployJson,
|
|
354
|
+
generateDeployJsonWithValidation,
|
|
355
|
+
parseEnvironmentVariables,
|
|
356
|
+
buildImageReference,
|
|
357
|
+
buildHealthCheck,
|
|
358
|
+
buildRequirements,
|
|
359
|
+
buildAuthentication,
|
|
360
|
+
validateDeploymentJson
|
|
361
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Actions Workflow Generator Module
|
|
3
|
+
*
|
|
4
|
+
* Generates GitHub Actions workflow files from Handlebars templates
|
|
5
|
+
* following ISO 27001 security standards
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs').promises;
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const Handlebars = require('handlebars');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate GitHub Actions workflow files from templates
|
|
14
|
+
* @param {string} appPath - Path to application directory
|
|
15
|
+
* @param {Object} config - Configuration from variables.yaml
|
|
16
|
+
* @param {Object} options - Generation options
|
|
17
|
+
* @returns {Promise<string[]>} Array of generated file paths
|
|
18
|
+
*/
|
|
19
|
+
async function generateGithubWorkflows(appPath, config, options = {}) {
|
|
20
|
+
try {
|
|
21
|
+
// Create .github/workflows directory
|
|
22
|
+
const workflowsDir = path.join(appPath, '.github', 'workflows');
|
|
23
|
+
await fs.mkdir(workflowsDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
// Get template context
|
|
26
|
+
const templateContext = getTemplateContext(config, options);
|
|
27
|
+
|
|
28
|
+
// Load and compile templates
|
|
29
|
+
const templatesDir = path.join(__dirname, '..', 'templates', 'github');
|
|
30
|
+
const templateFiles = await fs.readdir(templatesDir);
|
|
31
|
+
|
|
32
|
+
const generatedFiles = [];
|
|
33
|
+
|
|
34
|
+
for (const templateFile of templateFiles) {
|
|
35
|
+
if (templateFile.endsWith('.hbs')) {
|
|
36
|
+
const templatePath = path.join(templatesDir, templateFile);
|
|
37
|
+
const templateContent = await fs.readFile(templatePath, 'utf8');
|
|
38
|
+
|
|
39
|
+
// Compile template
|
|
40
|
+
const template = Handlebars.compile(templateContent);
|
|
41
|
+
|
|
42
|
+
// Generate content
|
|
43
|
+
const generatedContent = template(templateContext);
|
|
44
|
+
|
|
45
|
+
// Write to workflows directory
|
|
46
|
+
const outputFileName = templateFile.replace('.hbs', '');
|
|
47
|
+
const outputPath = path.join(workflowsDir, outputFileName);
|
|
48
|
+
await fs.writeFile(outputPath, generatedContent);
|
|
49
|
+
|
|
50
|
+
generatedFiles.push(outputPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return generatedFiles;
|
|
55
|
+
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(`Failed to generate GitHub workflows: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get template context from config
|
|
63
|
+
* @param {Object} config - Application configuration
|
|
64
|
+
* @param {Object} options - Additional options
|
|
65
|
+
* @returns {Object} Template context
|
|
66
|
+
*/
|
|
67
|
+
function getTemplateContext(config, options = {}) {
|
|
68
|
+
return {
|
|
69
|
+
appName: config.appName || 'myapp',
|
|
70
|
+
mainBranch: options.mainBranch || 'main',
|
|
71
|
+
language: config.language || 'typescript',
|
|
72
|
+
fileExtension: config.language === 'python' ? 'py' : 'js',
|
|
73
|
+
sourceDir: config.language === 'python' ? 'src' : 'lib',
|
|
74
|
+
buildCommand: options.buildCommand || 'npm run build',
|
|
75
|
+
uploadCoverage: options.uploadCoverage !== false,
|
|
76
|
+
publishToNpm: options.publishToNpm || false,
|
|
77
|
+
port: config.port || 3000,
|
|
78
|
+
database: config.database || false,
|
|
79
|
+
redis: config.redis || false,
|
|
80
|
+
storage: config.storage || false,
|
|
81
|
+
authentication: config.authentication || false
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate a specific workflow file
|
|
87
|
+
* @param {string} appPath - Path to application directory
|
|
88
|
+
* @param {string} templateName - Name of template file (without .hbs)
|
|
89
|
+
* @param {Object} config - Application configuration
|
|
90
|
+
* @param {Object} options - Generation options
|
|
91
|
+
* @returns {Promise<string>} Path to generated file
|
|
92
|
+
*/
|
|
93
|
+
async function generateWorkflowFile(appPath, templateName, config, options = {}) {
|
|
94
|
+
try {
|
|
95
|
+
// Create .github/workflows directory
|
|
96
|
+
const workflowsDir = path.join(appPath, '.github', 'workflows');
|
|
97
|
+
await fs.mkdir(workflowsDir, { recursive: true });
|
|
98
|
+
|
|
99
|
+
// Load template
|
|
100
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'github', `${templateName}.hbs`);
|
|
101
|
+
const templateContent = await fs.readFile(templatePath, 'utf8');
|
|
102
|
+
|
|
103
|
+
// Compile template
|
|
104
|
+
const template = Handlebars.compile(templateContent);
|
|
105
|
+
|
|
106
|
+
// Get template context
|
|
107
|
+
const templateContext = getTemplateContext(config, options);
|
|
108
|
+
|
|
109
|
+
// Generate content
|
|
110
|
+
const generatedContent = template(templateContext);
|
|
111
|
+
|
|
112
|
+
// Write to workflows directory
|
|
113
|
+
const outputPath = path.join(workflowsDir, templateName);
|
|
114
|
+
await fs.writeFile(outputPath, generatedContent);
|
|
115
|
+
|
|
116
|
+
return outputPath;
|
|
117
|
+
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw new Error(`Failed to generate workflow file ${templateName}: ${error.message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Validate GitHub workflow configuration
|
|
125
|
+
* @param {Object} config - Application configuration
|
|
126
|
+
* @param {Object} options - Generation options
|
|
127
|
+
* @returns {Object} Validation result
|
|
128
|
+
*/
|
|
129
|
+
function validateWorkflowConfig(config, options = {}) {
|
|
130
|
+
const errors = [];
|
|
131
|
+
const warnings = [];
|
|
132
|
+
|
|
133
|
+
// Validate required fields
|
|
134
|
+
if (!config.appName) {
|
|
135
|
+
errors.push('Application name is required');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!config.language) {
|
|
139
|
+
errors.push('Language is required');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!['typescript', 'python'].includes(config.language)) {
|
|
143
|
+
errors.push('Language must be either typescript or python');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Validate port
|
|
147
|
+
if (config.port && (config.port < 1 || config.port > 65535)) {
|
|
148
|
+
errors.push('Port must be between 1 and 65535');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Validate branch name
|
|
152
|
+
if (options.mainBranch && !/^[a-zA-Z0-9_-]+$/.test(options.mainBranch)) {
|
|
153
|
+
errors.push('Main branch name contains invalid characters');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Warnings
|
|
157
|
+
if (config.language === 'python' && !config.database) {
|
|
158
|
+
warnings.push('Python applications typically require a database');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (config.authentication && !config.database) {
|
|
162
|
+
warnings.push('Authentication typically requires a database for user storage');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
valid: errors.length === 0,
|
|
167
|
+
errors,
|
|
168
|
+
warnings
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Generate workflow files with validation
|
|
174
|
+
* @param {string} appPath - Path to application directory
|
|
175
|
+
* @param {Object} config - Application configuration
|
|
176
|
+
* @param {Object} options - Generation options
|
|
177
|
+
* @returns {Promise<Object>} Generation result
|
|
178
|
+
*/
|
|
179
|
+
async function generateWorkflowsWithValidation(appPath, config, options = {}) {
|
|
180
|
+
try {
|
|
181
|
+
// Validate configuration
|
|
182
|
+
const validation = validateWorkflowConfig(config, options);
|
|
183
|
+
|
|
184
|
+
if (!validation.valid) {
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
validation,
|
|
188
|
+
files: []
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Generate workflows
|
|
193
|
+
const files = await generateGithubWorkflows(appPath, config, options);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
validation,
|
|
198
|
+
files
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
validation: {
|
|
205
|
+
valid: false,
|
|
206
|
+
errors: [error.message],
|
|
207
|
+
warnings: []
|
|
208
|
+
},
|
|
209
|
+
files: []
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
generateGithubWorkflows,
|
|
216
|
+
getTemplateContext,
|
|
217
|
+
generateWorkflowFile,
|
|
218
|
+
validateWorkflowConfig,
|
|
219
|
+
generateWorkflowsWithValidation
|
|
220
|
+
};
|