@tamyla/clodo-framework 2.0.16 → 2.0.18

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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [2.0.18](https://github.com/tamylaa/clodo-framework/compare/v2.0.17...v2.0.18) (2025-10-12)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **deployment:** Critical deployment fixes - migrations, worker deployment, status tracking ([23c07e9](https://github.com/tamylaa/clodo-framework/commit/23c07e9a68fa4eb52ccd0df9421eff7d91919cb1)), closes [#2](https://github.com/tamylaa/clodo-framework/issues/2) [#3](https://github.com/tamylaa/clodo-framework/issues/3) [#4](https://github.com/tamylaa/clodo-framework/issues/4)
7
+
8
+ ## [2.0.17](https://github.com/tamylaa/clodo-framework/compare/v2.0.16...v2.0.17) (2025-10-12)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * use ConfigurationValidator as static class, not instance ([12caaeb](https://github.com/tamylaa/clodo-framework/commit/12caaeba17426239f98dacd2fdb36ac45a7a496b))
14
+
1
15
  ## [2.0.16](https://github.com/tamylaa/clodo-framework/compare/v2.0.15...v2.0.16) (2025-10-12)
2
16
 
3
17
 
@@ -628,8 +628,16 @@ export class DatabaseOrchestrator {
628
628
  // Command builders and utility methods
629
629
 
630
630
  buildMigrationCommand(databaseName, environment, isRemote) {
631
- const remoteFlag = isRemote ? '--remote' : '--local';
632
- return `npx wrangler d1 migrations apply ${databaseName} --env ${environment} ${remoteFlag}`;
631
+ let command = `npx wrangler d1 migrations apply ${databaseName}`;
632
+
633
+ // For remote environments, add --env flag
634
+ // For local development, use --local WITHOUT --env (wrangler requirement)
635
+ if (isRemote) {
636
+ command += ` --env ${environment} --remote`;
637
+ } else {
638
+ command += ` --local`;
639
+ }
640
+ return command;
633
641
  }
634
642
  buildBackupCommand(databaseName, environment, backupFile, isRemote) {
635
643
  const remoteFlag = isRemote ? '--remote' : '--local';
@@ -22,6 +22,38 @@ export class DeploymentCoordinator {
22
22
  * @returns {Promise<Object>} Deployment result
23
23
  */
24
24
  async deploySingleDomain(domain, domainState, handlers) {
25
+ const phaseResults = {
26
+ validation: {
27
+ success: false,
28
+ errors: [],
29
+ warnings: []
30
+ },
31
+ initialization: {
32
+ success: false,
33
+ errors: [],
34
+ warnings: []
35
+ },
36
+ database: {
37
+ success: false,
38
+ errors: [],
39
+ warnings: []
40
+ },
41
+ secrets: {
42
+ success: false,
43
+ errors: [],
44
+ warnings: []
45
+ },
46
+ deployment: {
47
+ success: false,
48
+ errors: [],
49
+ warnings: []
50
+ },
51
+ 'post-validation': {
52
+ success: false,
53
+ errors: [],
54
+ warnings: []
55
+ }
56
+ };
25
57
  try {
26
58
  domainState.startTime = new Date();
27
59
  domainState.status = 'deploying';
@@ -29,38 +61,105 @@ export class DeploymentCoordinator {
29
61
  console.log(` Deployment ID: ${domainState.deploymentId}`);
30
62
  console.log(` Environment: ${this.environment}`);
31
63
  let deploymentUrl = null;
64
+ let hasCriticalErrors = false;
32
65
 
33
66
  // Execute deployment phases
34
67
  for (const phase of this.deploymentPhases) {
35
- const phaseResult = await this.executeDeploymentPhase(domain, phase, domainState, handlers);
36
- domainState.phase = `${phase}-complete`;
68
+ try {
69
+ const phaseResult = await this.executeDeploymentPhase(domain, phase, domainState, handlers);
70
+ domainState.phase = `${phase}-complete`;
71
+
72
+ // Mark phase as successful
73
+ phaseResults[phase].success = true;
74
+
75
+ // Capture deployment URL from deployment phase
76
+ if (phase === 'deployment' && phaseResult) {
77
+ if (phaseResult.url) {
78
+ deploymentUrl = phaseResult.url;
79
+ }
80
+ if (phaseResult.deployed === false) {
81
+ phaseResults[phase].warnings.push('Worker deployed in dry-run mode');
82
+ }
83
+ }
37
84
 
38
- // Capture deployment URL from deployment phase
39
- if (phase === 'deployment' && phaseResult && phaseResult.url) {
40
- deploymentUrl = phaseResult.url;
85
+ // Capture database warnings
86
+ if (phase === 'database' && phaseResult) {
87
+ if (phaseResult.error) {
88
+ phaseResults[phase].warnings.push(phaseResult.error);
89
+ }
90
+ }
91
+ } catch (phaseError) {
92
+ phaseResults[phase].success = false;
93
+ phaseResults[phase].errors.push(phaseError.message);
94
+
95
+ // Determine if error is critical (stops deployment)
96
+ const criticalPhases = ['validation', 'initialization', 'deployment'];
97
+ if (criticalPhases.includes(phase)) {
98
+ console.error(` ❌ Critical error in ${phase} phase: ${phaseError.message}`);
99
+ hasCriticalErrors = true;
100
+ throw phaseError; // Stop deployment on critical errors
101
+ } else {
102
+ // Non-critical errors (database migrations, health checks) - log but continue
103
+ console.warn(` ⚠️ ${phase} phase warning: ${phaseError.message}`);
104
+ console.warn(` 💡 Deployment will continue - this can be fixed manually`);
105
+ }
41
106
  }
42
107
  }
43
- domainState.status = 'completed';
108
+
109
+ // Determine overall success based on phase results
110
+ const allPhasesSuccessful = Object.values(phaseResults).every(result => result.success);
111
+ const hasWarnings = Object.values(phaseResults).some(result => result.warnings.length > 0 || result.success === false && result.errors.length === 0);
112
+ domainState.status = hasCriticalErrors ? 'failed' : allPhasesSuccessful ? 'completed' : 'completed-with-warnings';
44
113
  domainState.endTime = new Date();
45
- console.log(` ✅ ${domain} deployed successfully`);
114
+ domainState.phaseResults = phaseResults;
115
+
116
+ // Show appropriate completion message
117
+ if (allPhasesSuccessful) {
118
+ console.log(` ✅ ${domain} deployed successfully`);
119
+ } else if (!hasCriticalErrors) {
120
+ console.log(` ⚠️ ${domain} deployed with warnings`);
121
+ this.displayPhaseWarnings(phaseResults);
122
+ }
46
123
  return {
47
124
  domain,
48
- success: true,
125
+ success: !hasCriticalErrors,
126
+ allPhasesSuccessful,
49
127
  deploymentId: domainState.deploymentId,
50
128
  duration: domainState.endTime - domainState.startTime,
51
129
  phases: this.deploymentPhases.length,
52
130
  url: deploymentUrl || domainState.deploymentUrl,
53
- status: 'deployed'
131
+ status: domainState.status,
132
+ phaseResults
54
133
  };
55
134
  } catch (error) {
56
135
  domainState.status = 'failed';
57
136
  domainState.error = error.message;
58
137
  domainState.endTime = new Date();
138
+ domainState.phaseResults = phaseResults;
59
139
  console.error(` ❌ ${domain} deployment failed: ${error.message}`);
60
140
  throw error;
61
141
  }
62
142
  }
63
143
 
144
+ /**
145
+ * Display warnings for phases that had issues
146
+ * @param {Object} phaseResults - Phase results object
147
+ */
148
+ displayPhaseWarnings(phaseResults) {
149
+ console.log(`\n 📊 Phase Status Summary:`);
150
+ for (const [phase, result] of Object.entries(phaseResults)) {
151
+ if (result.success && result.warnings.length === 0) {
152
+ console.log(` ✅ ${phase}: Success`);
153
+ } else if (result.success && result.warnings.length > 0) {
154
+ console.log(` ⚠️ ${phase}: Success with warnings`);
155
+ result.warnings.forEach(warn => console.log(` • ${warn}`));
156
+ } else {
157
+ console.log(` ❌ ${phase}: Failed`);
158
+ result.errors.forEach(err => console.log(` • ${err}`));
159
+ }
160
+ }
161
+ }
162
+
64
163
  /**
65
164
  * Execute specific deployment phase
66
165
  * @param {string} domain - Domain being deployed
@@ -13,6 +13,10 @@ import { StateManager } from './modules/StateManager.js';
13
13
  import { DatabaseOrchestrator } from '../database/database-orchestrator.js';
14
14
  import { EnhancedSecretManager } from '../utils/deployment/secret-generator.js';
15
15
  import { ConfigurationValidator } from '../security/ConfigurationValidator.js';
16
+ import { exec } from 'child_process';
17
+ import { promisify } from 'util';
18
+ import { join } from 'path';
19
+ const execAsync = promisify(exec);
16
20
 
17
21
  /**
18
22
  * Multi-Domain Deployment Orchestrator
@@ -59,7 +63,9 @@ export class MultiDomainOrchestrator {
59
63
  projectRoot: this.servicePath,
60
64
  dryRun: this.dryRun
61
65
  });
62
- this.configValidator = new ConfigurationValidator();
66
+
67
+ // ConfigurationValidator is a static class - don't instantiate
68
+ // Access via ConfigurationValidator.validate() directly
63
69
 
64
70
  // Legacy compatibility: expose portfolioState for backward compatibility
65
71
  this.portfolioState = this.stateManager.portfolioState;
@@ -214,8 +220,8 @@ export class MultiDomainOrchestrator {
214
220
  const domainState = this.portfolioState.domainStates.get(domain);
215
221
  const config = domainState?.config || {};
216
222
 
217
- // Perform security validation
218
- const validationIssues = this.configValidator.validate(config, this.environment);
223
+ // Perform security validation using static method
224
+ const validationIssues = ConfigurationValidator.validate(config, this.environment);
219
225
  if (validationIssues.length > 0) {
220
226
  console.log(` ⚠️ Found ${validationIssues.length} configuration warnings:`);
221
227
  validationIssues.forEach(issue => {
@@ -357,26 +363,100 @@ export class MultiDomainOrchestrator {
357
363
  }
358
364
 
359
365
  /**
360
- * Deploy domain worker (returns worker URL)
366
+ * Deploy domain worker (executes actual wrangler deploy)
361
367
  */
362
368
  async deployDomainWorker(domain) {
363
- // Placeholder: Add actual worker deployment logic here
364
369
  console.log(` 🚀 Deploying worker for ${domain}`);
370
+ if (this.dryRun) {
371
+ console.log(` 🔍 DRY RUN: Would deploy worker for ${domain}`);
372
+ const subdomain = this.environment === 'production' ? 'api' : `${this.environment}-api`;
373
+ return {
374
+ url: `https://${subdomain}.${domain}`,
375
+ deployed: false,
376
+ dryRun: true
377
+ };
378
+ }
379
+ try {
380
+ // Find wrangler.toml in service path
381
+ const wranglerConfigPath = join(this.servicePath, 'wrangler.toml');
365
382
 
366
- // TODO: Execute actual wrangler deploy command here
367
- // For now, construct the expected URL from domain and environment
368
- const subdomain = this.environment === 'production' ? 'api' : `${this.environment}-api`;
369
- const workerUrl = `https://${subdomain}.${domain}`;
383
+ // Build deploy command with environment
384
+ let deployCommand = `npx wrangler deploy`;
370
385
 
371
- // Store URL in domain state for later retrieval
372
- const domainState = this.portfolioState.domainStates.get(domain);
373
- if (domainState) {
374
- domainState.deploymentUrl = workerUrl;
386
+ // Add environment flag for non-production
387
+ if (this.environment !== 'production') {
388
+ deployCommand += ` --env ${this.environment}`;
389
+ }
390
+ console.log(` � Executing: ${deployCommand}`);
391
+ console.log(` 📁 Working directory: ${this.servicePath}`);
392
+
393
+ // Execute deployment with timeout
394
+ const {
395
+ stdout,
396
+ stderr
397
+ } = await execAsync(deployCommand, {
398
+ cwd: this.servicePath,
399
+ timeout: 120000,
400
+ // 2 minute timeout
401
+ maxBuffer: 1024 * 1024 * 10 // 10MB buffer for large outputs
402
+ });
403
+
404
+ // Log output for debugging
405
+ if (stdout) {
406
+ console.log(` 📄 Deployment output:`);
407
+ stdout.split('\n').filter(line => line.trim()).forEach(line => {
408
+ console.log(` ${line}`);
409
+ });
410
+ }
411
+ if (stderr && !stderr.includes('deprecated')) {
412
+ console.warn(` ⚠️ Deployment warnings: ${stderr}`);
413
+ }
414
+
415
+ // Parse worker URL from wrangler output
416
+ // Wrangler outputs: "Published service-name (version) to https://worker-url"
417
+ const urlMatch = stdout.match(/https:\/\/[^\s]+/);
418
+ const workerUrl = urlMatch ? urlMatch[0] : null;
419
+
420
+ // Also construct custom domain URL
421
+ const subdomain = this.environment === 'production' ? 'api' : `${this.environment}-api`;
422
+ const customUrl = `https://${subdomain}.${domain}`;
423
+
424
+ // Store URLs in domain state
425
+ const domainState = this.portfolioState.domainStates.get(domain);
426
+ if (domainState) {
427
+ domainState.workerUrl = workerUrl;
428
+ domainState.deploymentUrl = customUrl;
429
+ }
430
+ if (workerUrl) {
431
+ console.log(` ✅ Worker deployed successfully`);
432
+ console.log(` 🔗 Worker URL: ${workerUrl}`);
433
+ console.log(` 🔗 Custom URL: ${customUrl}`);
434
+ } else {
435
+ console.log(` ✅ Deployment completed (URL not detected in output)`);
436
+ console.log(` 🔗 Expected URL: ${customUrl}`);
437
+ }
438
+ return {
439
+ url: customUrl,
440
+ workerUrl: workerUrl,
441
+ deployed: true,
442
+ stdout,
443
+ stderr
444
+ };
445
+ } catch (error) {
446
+ console.error(` ❌ Worker deployment failed: ${error.message}`);
447
+
448
+ // Parse error for helpful diagnostics
449
+ if (error.message.includes('wrangler.toml')) {
450
+ console.error(` 💡 Ensure wrangler.toml exists in ${this.servicePath}`);
451
+ }
452
+ if (error.message.includes('No environment found')) {
453
+ console.error(` 💡 Add [env.${this.environment}] section to wrangler.toml`);
454
+ }
455
+ if (error.stderr) {
456
+ console.error(` 📄 Error details: ${error.stderr}`);
457
+ }
458
+ throw new Error(`Worker deployment failed for ${domain}: ${error.message}`);
375
459
  }
376
- return {
377
- url: workerUrl,
378
- deployed: true
379
- };
380
460
  }
381
461
 
382
462
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "2.0.16",
3
+ "version": "2.0.18",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [