@tamyla/clodo-framework 2.0.17 → 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,10 @@
|
|
|
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
|
+
|
|
1
8
|
## [2.0.17](https://github.com/tamylaa/clodo-framework/compare/v2.0.16...v2.0.17) (2025-10-12)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -628,8 +628,16 @@ export class DatabaseOrchestrator {
|
|
|
628
628
|
// Command builders and utility methods
|
|
629
629
|
|
|
630
630
|
buildMigrationCommand(databaseName, environment, isRemote) {
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
@@ -359,26 +363,100 @@ export class MultiDomainOrchestrator {
|
|
|
359
363
|
}
|
|
360
364
|
|
|
361
365
|
/**
|
|
362
|
-
* Deploy domain worker (
|
|
366
|
+
* Deploy domain worker (executes actual wrangler deploy)
|
|
363
367
|
*/
|
|
364
368
|
async deployDomainWorker(domain) {
|
|
365
|
-
// Placeholder: Add actual worker deployment logic here
|
|
366
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');
|
|
367
382
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const subdomain = this.environment === 'production' ? 'api' : `${this.environment}-api`;
|
|
371
|
-
const workerUrl = `https://${subdomain}.${domain}`;
|
|
383
|
+
// Build deploy command with environment
|
|
384
|
+
let deployCommand = `npx wrangler deploy`;
|
|
372
385
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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}`);
|
|
377
459
|
}
|
|
378
|
-
return {
|
|
379
|
-
url: workerUrl,
|
|
380
|
-
deployed: true
|
|
381
|
-
};
|
|
382
460
|
}
|
|
383
461
|
|
|
384
462
|
/**
|