@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/validator.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Schema Validation
|
|
3
|
+
*
|
|
4
|
+
* This module provides schema validation with developer-friendly error messages.
|
|
5
|
+
* Validates variables.yaml, rbac.yaml, and env.template files.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Schema validation with friendly error messages 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 Ajv = require('ajv');
|
|
16
|
+
const applicationSchema = require('./schema/application-schema.json');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validates variables.yaml file against application schema
|
|
20
|
+
* Provides detailed error messages for configuration issues
|
|
21
|
+
*
|
|
22
|
+
* @async
|
|
23
|
+
* @function validateVariables
|
|
24
|
+
* @param {string} appName - Name of the application
|
|
25
|
+
* @returns {Promise<Object>} Validation result with errors and warnings
|
|
26
|
+
* @throws {Error} If file cannot be read or parsed
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const result = await validateVariables('myapp');
|
|
30
|
+
* // Returns: { valid: true, errors: [], warnings: [] }
|
|
31
|
+
*/
|
|
32
|
+
async function validateVariables(appName) {
|
|
33
|
+
if (!appName || typeof appName !== 'string') {
|
|
34
|
+
throw new Error('App name is required and must be a string');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(variablesPath)) {
|
|
40
|
+
throw new Error(`variables.yaml not found: ${variablesPath}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const content = fs.readFileSync(variablesPath, 'utf8');
|
|
44
|
+
let variables;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
variables = yaml.load(content);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
53
|
+
const validate = ajv.compile(applicationSchema);
|
|
54
|
+
const valid = validate(variables);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
valid,
|
|
58
|
+
errors: valid ? [] : formatValidationErrors(validate.errors),
|
|
59
|
+
warnings: []
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validates rbac.yaml file structure and content
|
|
65
|
+
* Ensures roles and permissions are properly defined
|
|
66
|
+
*
|
|
67
|
+
* @async
|
|
68
|
+
* @function validateRbac
|
|
69
|
+
* @param {string} appName - Name of the application
|
|
70
|
+
* @returns {Promise<Object>} Validation result with errors and warnings
|
|
71
|
+
* @throws {Error} If file cannot be read or parsed
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* const result = await validateRbac('myapp');
|
|
75
|
+
* // Returns: { valid: true, errors: [], warnings: [] }
|
|
76
|
+
*/
|
|
77
|
+
function validateRoles(roles) {
|
|
78
|
+
const errors = [];
|
|
79
|
+
if (!roles || !Array.isArray(roles)) {
|
|
80
|
+
errors.push('rbac.yaml must contain a "roles" array');
|
|
81
|
+
return errors;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const roleNames = new Set();
|
|
85
|
+
roles.forEach((role, index) => {
|
|
86
|
+
if (!role.name || !role.value || !role.description) {
|
|
87
|
+
errors.push(`Role at index ${index} is missing required fields (name, value, description)`);
|
|
88
|
+
} else if (roleNames.has(role.value)) {
|
|
89
|
+
errors.push(`Duplicate role value: ${role.value}`);
|
|
90
|
+
} else {
|
|
91
|
+
roleNames.add(role.value);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return errors;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validatePermissions(permissions) {
|
|
98
|
+
const errors = [];
|
|
99
|
+
if (!permissions || !Array.isArray(permissions)) {
|
|
100
|
+
errors.push('rbac.yaml must contain a "permissions" array');
|
|
101
|
+
return errors;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const permissionNames = new Set();
|
|
105
|
+
permissions.forEach((permission, index) => {
|
|
106
|
+
if (!permission.name || !permission.roles || !permission.description) {
|
|
107
|
+
errors.push(`Permission at index ${index} is missing required fields (name, roles, description)`);
|
|
108
|
+
} else if (permissionNames.has(permission.name)) {
|
|
109
|
+
errors.push(`Duplicate permission name: ${permission.name}`);
|
|
110
|
+
} else {
|
|
111
|
+
permissionNames.add(permission.name);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return errors;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function validateRbac(appName) {
|
|
118
|
+
if (!appName || typeof appName !== 'string') {
|
|
119
|
+
throw new Error('App name is required and must be a string');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const rbacPath = path.join(process.cwd(), 'builder', appName, 'rbac.yaml');
|
|
123
|
+
|
|
124
|
+
if (!fs.existsSync(rbacPath)) {
|
|
125
|
+
return { valid: true, errors: [], warnings: ['rbac.yaml not found - authentication disabled'] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const content = fs.readFileSync(rbacPath, 'utf8');
|
|
129
|
+
let rbac;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
rbac = yaml.load(content);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw new Error(`Invalid YAML syntax in rbac.yaml: ${error.message}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const errors = [
|
|
138
|
+
...validateRoles(rbac.roles),
|
|
139
|
+
...validatePermissions(rbac.permissions)
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
valid: errors.length === 0,
|
|
144
|
+
errors,
|
|
145
|
+
warnings: []
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Validates env.template file for proper kv:// references
|
|
151
|
+
* Checks for syntax errors and missing secret references
|
|
152
|
+
*
|
|
153
|
+
* @async
|
|
154
|
+
* @function validateEnvTemplate
|
|
155
|
+
* @param {string} appName - Name of the application
|
|
156
|
+
* @returns {Promise<Object>} Validation result with errors and warnings
|
|
157
|
+
* @throws {Error} If file cannot be read
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* const result = await validateEnvTemplate('myapp');
|
|
161
|
+
* // Returns: { valid: true, errors: [], warnings: [] }
|
|
162
|
+
*/
|
|
163
|
+
async function validateEnvTemplate(appName) {
|
|
164
|
+
if (!appName || typeof appName !== 'string') {
|
|
165
|
+
throw new Error('App name is required and must be a string');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const templatePath = path.join(process.cwd(), 'builder', appName, 'env.template');
|
|
169
|
+
|
|
170
|
+
if (!fs.existsSync(templatePath)) {
|
|
171
|
+
throw new Error(`env.template not found: ${templatePath}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const content = fs.readFileSync(templatePath, 'utf8');
|
|
175
|
+
const errors = [];
|
|
176
|
+
const warnings = [];
|
|
177
|
+
|
|
178
|
+
// Check for valid environment variable syntax
|
|
179
|
+
const lines = content.split('\n');
|
|
180
|
+
lines.forEach((line, index) => {
|
|
181
|
+
const trimmed = line.trim();
|
|
182
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
183
|
+
if (!trimmed.includes('=')) {
|
|
184
|
+
errors.push(`Line ${index + 1}: Invalid environment variable format (missing =)`);
|
|
185
|
+
} else {
|
|
186
|
+
const [key, value] = trimmed.split('=', 2);
|
|
187
|
+
if (!key || !value) {
|
|
188
|
+
errors.push(`Line ${index + 1}: Invalid environment variable format`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Check for kv:// reference format
|
|
195
|
+
const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
|
|
196
|
+
let match;
|
|
197
|
+
while ((match = kvPattern.exec(content)) !== null) {
|
|
198
|
+
const secretKey = match[1];
|
|
199
|
+
if (!secretKey) {
|
|
200
|
+
errors.push('Invalid kv:// reference format');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
valid: errors.length === 0,
|
|
206
|
+
errors,
|
|
207
|
+
warnings
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Checks the development environment for common issues
|
|
213
|
+
* Validates Docker, ports, secrets, and other requirements
|
|
214
|
+
*
|
|
215
|
+
* @async
|
|
216
|
+
* @function checkEnvironment
|
|
217
|
+
* @returns {Promise<Object>} Environment check result
|
|
218
|
+
* @throws {Error} If critical issues are found
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* const result = await checkEnvironment();
|
|
222
|
+
* // Returns: { docker: 'ok', ports: 'ok', secrets: 'missing', recommendations: [...] }
|
|
223
|
+
*/
|
|
224
|
+
async function checkDocker() {
|
|
225
|
+
try {
|
|
226
|
+
const { exec } = require('child_process');
|
|
227
|
+
const { promisify } = require('util');
|
|
228
|
+
const execAsync = promisify(exec);
|
|
229
|
+
|
|
230
|
+
await execAsync('docker --version');
|
|
231
|
+
await execAsync('docker-compose --version');
|
|
232
|
+
return 'ok';
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return 'error';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function checkPorts() {
|
|
239
|
+
const requiredPorts = [5432, 6379, 5050, 8081];
|
|
240
|
+
const netstat = require('net');
|
|
241
|
+
let portIssues = 0;
|
|
242
|
+
|
|
243
|
+
for (const port of requiredPorts) {
|
|
244
|
+
try {
|
|
245
|
+
await new Promise((resolve, reject) => {
|
|
246
|
+
const server = netstat.createServer();
|
|
247
|
+
server.listen(port, () => {
|
|
248
|
+
server.close(resolve);
|
|
249
|
+
});
|
|
250
|
+
server.on('error', reject);
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
portIssues++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return portIssues === 0 ? 'ok' : 'warning';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function checkSecrets() {
|
|
261
|
+
const os = require('os');
|
|
262
|
+
const secretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
|
|
263
|
+
return fs.existsSync(secretsPath) ? 'ok' : 'missing';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function checkEnvironment() {
|
|
267
|
+
const result = {
|
|
268
|
+
docker: 'unknown',
|
|
269
|
+
ports: 'unknown',
|
|
270
|
+
secrets: 'unknown',
|
|
271
|
+
recommendations: []
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Check Docker
|
|
275
|
+
result.docker = await checkDocker();
|
|
276
|
+
if (result.docker === 'error') {
|
|
277
|
+
result.recommendations.push('Install Docker and Docker Compose');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check ports
|
|
281
|
+
result.ports = await checkPorts();
|
|
282
|
+
if (result.ports === 'warning') {
|
|
283
|
+
result.recommendations.push('Some required ports (5432, 6379, 5050, 8081) are in use');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check secrets
|
|
287
|
+
result.secrets = checkSecrets();
|
|
288
|
+
if (result.secrets === 'missing') {
|
|
289
|
+
result.recommendations.push('Create secrets file: ~/.aifabrix/secrets.yaml');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Formats validation errors into developer-friendly messages
|
|
297
|
+
* Converts technical schema errors into actionable advice
|
|
298
|
+
*
|
|
299
|
+
* @function formatValidationErrors
|
|
300
|
+
* @param {Array} errors - Raw validation errors from Ajv
|
|
301
|
+
* @returns {Array} Formatted error messages
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* const messages = formatValidationErrors(ajvErrors);
|
|
305
|
+
* // Returns: ['Port must be between 1 and 65535', 'Missing required field: displayName']
|
|
306
|
+
*/
|
|
307
|
+
function formatSingleError(error) {
|
|
308
|
+
const path = error.instancePath ? error.instancePath.slice(1) : 'root';
|
|
309
|
+
const field = path ? `Field "${path}"` : 'Configuration';
|
|
310
|
+
|
|
311
|
+
const errorMessages = {
|
|
312
|
+
required: `${field}: Missing required property "${error.params.missingProperty}"`,
|
|
313
|
+
type: `${field}: Expected ${error.params.type}, got ${typeof error.data}`,
|
|
314
|
+
minimum: `${field}: Value must be at least ${error.params.limit}`,
|
|
315
|
+
maximum: `${field}: Value must be at most ${error.params.limit}`,
|
|
316
|
+
minLength: `${field}: Must be at least ${error.params.limit} characters`,
|
|
317
|
+
maxLength: `${field}: Must be at most ${error.params.limit} characters`,
|
|
318
|
+
pattern: `${field}: Invalid format`,
|
|
319
|
+
enum: `${field}: Must be one of: ${error.params.allowedValues?.join(', ') || 'unknown'}`
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
return errorMessages[error.keyword] || `${field}: ${error.message}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function formatValidationErrors(errors) {
|
|
326
|
+
if (!Array.isArray(errors)) {
|
|
327
|
+
return ['Unknown validation error'];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return errors.map(formatSingleError);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Validates all application configuration files
|
|
335
|
+
* Runs complete validation suite for an application
|
|
336
|
+
*
|
|
337
|
+
* @async
|
|
338
|
+
* @function validateApplication
|
|
339
|
+
* @param {string} appName - Name of the application
|
|
340
|
+
* @returns {Promise<Object>} Complete validation result
|
|
341
|
+
* @throws {Error} If validation fails
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* const result = await validateApplication('myapp');
|
|
345
|
+
* // Returns: { valid: true, variables: {...}, rbac: {...}, env: {...} }
|
|
346
|
+
*/
|
|
347
|
+
async function validateApplication(appName) {
|
|
348
|
+
if (!appName || typeof appName !== 'string') {
|
|
349
|
+
throw new Error('App name is required and must be a string');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const variables = await validateVariables(appName);
|
|
353
|
+
const rbac = await validateRbac(appName);
|
|
354
|
+
const env = await validateEnvTemplate(appName);
|
|
355
|
+
|
|
356
|
+
const valid = variables.valid && rbac.valid && env.valid;
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
valid,
|
|
360
|
+
variables,
|
|
361
|
+
rbac,
|
|
362
|
+
env,
|
|
363
|
+
summary: {
|
|
364
|
+
totalErrors: variables.errors.length + rbac.errors.length + env.errors.length,
|
|
365
|
+
totalWarnings: variables.warnings.length + rbac.warnings.length + env.warnings.length
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = {
|
|
371
|
+
validateVariables,
|
|
372
|
+
validateRbac,
|
|
373
|
+
validateEnvTemplate,
|
|
374
|
+
checkEnvironment,
|
|
375
|
+
formatValidationErrors,
|
|
376
|
+
validateApplication
|
|
377
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aifabrix/builder",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "AI Fabrix Local Fabric & Deployment SDK",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aifabrix": "bin/aifabrix.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "jest --coverage",
|
|
11
|
+
"test:watch": "jest --watch",
|
|
12
|
+
"test:ci": "jest --ci --coverage --watchAll=false",
|
|
13
|
+
"lint": "eslint . --ext .js",
|
|
14
|
+
"lint:fix": "eslint . --ext .js --fix",
|
|
15
|
+
"lint:ci": "eslint . --ext .js --format json --output-file eslint-report.json",
|
|
16
|
+
"dev": "node bin/aifabrix.js",
|
|
17
|
+
"build": "npm run lint && npm run test:ci",
|
|
18
|
+
"validate": "npm run build",
|
|
19
|
+
"prepublishOnly": "npm run validate",
|
|
20
|
+
"precommit": "npm run lint:fix && npm run test",
|
|
21
|
+
"posttest": "npm run lint"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"aifabrix",
|
|
25
|
+
"docker",
|
|
26
|
+
"deployment",
|
|
27
|
+
"cli",
|
|
28
|
+
"azure",
|
|
29
|
+
"container"
|
|
30
|
+
],
|
|
31
|
+
"author": "eSystems Nordic Ltd",
|
|
32
|
+
"license": "UNLICENSED",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"commander": "^11.1.0",
|
|
38
|
+
"js-yaml": "^4.1.0",
|
|
39
|
+
"ajv": "^8.12.0",
|
|
40
|
+
"handlebars": "^4.7.8",
|
|
41
|
+
"axios": "^1.6.0",
|
|
42
|
+
"chalk": "^4.1.2",
|
|
43
|
+
"ora": "^5.4.1",
|
|
44
|
+
"inquirer": "^8.2.5"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"jest": "^29.7.0",
|
|
48
|
+
"eslint": "^8.55.0",
|
|
49
|
+
"@types/node": "^20.10.0"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/esystemsdev/aifabrix-builder.git"
|
|
54
|
+
},
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/esystemsdev/aifabrix-builder/issues"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://github.com/esystemsdev/aifabrix-builder#readme"
|
|
59
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# AI Fabrix Builder Templates
|
|
2
|
+
|
|
3
|
+
These are Handlebars (.hbs) template files that generate Docker files for AI Fabrix applications. They should NOT be linted as Dockerfiles since they contain template variables that will be replaced during generation.
|
|
4
|
+
|
|
5
|
+
## Template Files
|
|
6
|
+
|
|
7
|
+
- `python/Dockerfile.hbs` - Python application Dockerfile template
|
|
8
|
+
- `python/docker-compose.hbs` - Python application docker-compose template
|
|
9
|
+
- `typescript/Dockerfile.hbs` - TypeScript/Node.js application Dockerfile template
|
|
10
|
+
- `typescript/docker-compose.hbs` - TypeScript/Node.js application docker-compose template
|
|
11
|
+
- `infra/compose.yaml` - Infrastructure services docker-compose template
|
|
12
|
+
|
|
13
|
+
## Template Variables
|
|
14
|
+
|
|
15
|
+
### Application Configuration
|
|
16
|
+
- `{{app.key}}` - Application key/identifier
|
|
17
|
+
- `{{image.name}}` - Container image name
|
|
18
|
+
- `{{image.tag}}` - Container image tag (defaults to "latest")
|
|
19
|
+
- `{{port}}` - Application port from schema
|
|
20
|
+
- `{{startupCommand}}` - Custom startup command (optional)
|
|
21
|
+
|
|
22
|
+
### Health Check Configuration
|
|
23
|
+
- `{{healthCheck.path}}` - Health check endpoint path (e.g., "/health")
|
|
24
|
+
- `{{healthCheck.interval}}` - Health check interval in seconds
|
|
25
|
+
|
|
26
|
+
### Service Requirements
|
|
27
|
+
- `{{requiresDatabase}}` - Database requirement flag (conditional db-init service)
|
|
28
|
+
- `{{requiresStorage}}` - Storage requirement flag (conditional volume mounting)
|
|
29
|
+
- `{{databases}}` - Array of database configurations
|
|
30
|
+
|
|
31
|
+
### Build Configuration
|
|
32
|
+
- `{{build.localPort}}` - Local development port (different from Docker port)
|
|
33
|
+
- `{{mountVolume}}` - Volume mount path for local development
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
These templates are processed by the AI Fabrix Builder SDK based on the application schema defined in `variables.yaml`. The generated files will be valid Docker files after template processing.
|
|
38
|
+
|
|
39
|
+
## VS Code Configuration
|
|
40
|
+
|
|
41
|
+
The `.vscode/settings.json` file is configured to:
|
|
42
|
+
- Treat `.hbs` files as Handlebars templates (not Dockerfiles)
|
|
43
|
+
- Ignore template files from Docker linting
|
|
44
|
+
- Prevent false linting errors on template variables
|
|
45
|
+
|
|
46
|
+
## Generated Output
|
|
47
|
+
|
|
48
|
+
After processing, these templates generate:
|
|
49
|
+
- Valid Dockerfiles with proper syntax
|
|
50
|
+
- Docker-compose files with conditional services
|
|
51
|
+
- Infrastructure configurations with shared services only
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: CI/CD Pipeline
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [{{mainBranch}}]
|
|
5
|
+
jobs:
|
|
6
|
+
test:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
- name: Setup Node.js
|
|
11
|
+
uses: actions/setup-node@v4
|
|
12
|
+
with:
|
|
13
|
+
node-version: '20'
|
|
14
|
+
- name: Run tests
|
|
15
|
+
run: npm test
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Pull Request Checks
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize, reopened]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
pr-validation:
|
|
9
|
+
name: PR Validation
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
with:
|
|
14
|
+
fetch-depth: 0
|
|
15
|
+
|
|
16
|
+
- name: Setup Node.js
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: '20'
|
|
20
|
+
cache: 'npm'
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: npm ci
|
|
24
|
+
|
|
25
|
+
- name: Check file sizes
|
|
26
|
+
run: |
|
|
27
|
+
find {{sourceDir}} -name "*.{{fileExtension}}" -exec sh -c 'lines=$(wc -l < "$1"); if [ $lines -gt 500 ]; then echo "File $1 exceeds 500 lines ($lines)"; exit 1; fi' _ {} \;
|
|
28
|
+
|
|
29
|
+
- name: Check for TODOs in modified files
|
|
30
|
+
run: |
|
|
31
|
+
git diff origin/${{ github.base_ref }} --name-only | grep "\.{{fileExtension}}$" | xargs grep -n "TODO" || true
|
|
32
|
+
|
|
33
|
+
- name: Validate commit messages
|
|
34
|
+
run: |
|
|
35
|
+
git log origin/${{ github.base_ref }}..HEAD --format=%s | grep -E "^(feat|fix|docs|style|refactor|test|chore|perf)(\(.+\))?: .+" || echo "Warning: Some commits don't follow conventional commits"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
name: Release and Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*.*.*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
validate:
|
|
10
|
+
name: Validate Release
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Setup Node.js
|
|
16
|
+
uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: '20'
|
|
19
|
+
cache: 'npm'
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: npm ci
|
|
23
|
+
|
|
24
|
+
- name: Run all checks
|
|
25
|
+
run: npm run validate
|
|
26
|
+
|
|
27
|
+
- name: Verify version matches tag
|
|
28
|
+
run: |
|
|
29
|
+
TAG_VERSION=${GITHUB_REF#refs/tags/v}
|
|
30
|
+
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
|
31
|
+
if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then
|
|
32
|
+
echo "Tag version ($TAG_VERSION) does not match package.json version ($PACKAGE_VERSION)"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
{{#if publishToNpm}}
|
|
37
|
+
publish-npm:
|
|
38
|
+
name: Publish to NPM
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
needs: validate
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
|
|
44
|
+
- name: Setup Node.js
|
|
45
|
+
uses: actions/setup-node@v4
|
|
46
|
+
with:
|
|
47
|
+
node-version: '20'
|
|
48
|
+
registry-url: 'https://registry.npmjs.org'
|
|
49
|
+
cache: 'npm'
|
|
50
|
+
|
|
51
|
+
- name: Install dependencies
|
|
52
|
+
run: npm ci
|
|
53
|
+
|
|
54
|
+
- name: Build package
|
|
55
|
+
run: npm run build
|
|
56
|
+
|
|
57
|
+
- name: Publish to NPM
|
|
58
|
+
run: npm publish --access public
|
|
59
|
+
env:
|
|
60
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
61
|
+
{{/if}}
|
|
62
|
+
|
|
63
|
+
create-release:
|
|
64
|
+
name: Create GitHub Release
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
needs: {{#if publishToNpm}}publish-npm{{else}}validate{{/if}}
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
|
|
70
|
+
- name: Create Release
|
|
71
|
+
uses: actions/create-release@v1
|
|
72
|
+
env:
|
|
73
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
74
|
+
with:
|
|
75
|
+
tag_name: ${{ github.ref }}
|
|
76
|
+
release_name: Release ${{ github.ref }}
|
|
77
|
+
body: Release {{appName}} ${{ github.ref }}
|
|
78
|
+
draft: false
|
|
79
|
+
prerelease: false
|