@tamyla/clodo-framework 3.2.0 ā 3.2.1
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 +7 -0
- package/dist/cli/commands/deploy.js +58 -20
- package/dist/config/.env.staging.example +25 -0
- package/dist/deployment/index.js +2 -2
- package/dist/deployment/orchestration/BaseDeploymentOrchestrator.js +1 -1
- package/dist/deployment/orchestration/UnifiedDeploymentOrchestrator.js +2 -4
- package/dist/lib/deployment/orchestration/BaseDeploymentOrchestrator.js +1 -1
- package/dist/lib/shared/cloudflare/ops.js +103 -17
- package/dist/lib/shared/deployment/credential-collector.js +134 -1
- package/dist/lib/shared/deployment/index.js +7 -3
- package/dist/lib/shared/deployment/rollback-manager.js +1 -1
- package/dist/lib/shared/deployment/utilities/d1-error-recovery.js +1 -1
- package/dist/lib/shared/deployment/workflows/index.js +13 -0
- package/dist/lib/shared/deployment/workflows/interactive-confirmation.js +1 -1
- package/dist/lib/shared/deployment/workflows/interactive-deployment-coordinator.js +229 -0
- package/dist/lib/shared/deployment/workflows/interactive-domain-info-gatherer.js +1 -1
- package/dist/lib/shared/deployment/workflows/interactive-secret-workflow.js +3 -3
- package/dist/lib/shared/deployment/workflows/interactive-testing-workflow.js +2 -2
- package/dist/lib/shared/deployment/workflows/interactive-validation.js +2 -2
- package/dist/lib/shared/monitoring/health-checker.js +11 -5
- package/dist/lib/shared/validation/ValidationRegistry.js +1 -1
- package/dist/utils/deployment/index.js +2 -1
- package/dist/utils/health-checker.js +53 -76
- package/dist/utils/index.js +3 -0
- package/package.json +2 -2
- package/templates/generic/src/worker/index.js +1 -1
- package/templates/static-site/src/worker/index.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [3.2.1](https://github.com/tamylaa/clodo-framework/compare/v3.2.0...v3.2.1) (2025-12-05)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* enhance deploy command with prerequisite checking and organize project structure ([660c5ab](https://github.com/tamylaa/clodo-framework/commit/660c5ab82546ca1a80b42c1d4cbc6d3972cc6203))
|
|
7
|
+
|
|
1
8
|
# [3.2.0](https://github.com/tamylaa/clodo-framework/compare/v3.1.27...v3.2.0) (2025-12-03)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -2,10 +2,11 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { Clodo } from '@tamyla/clodo-framework';
|
|
3
3
|
import { StandardOptions } from '../../lib/shared/utils/cli-options.js';
|
|
4
4
|
import { ConfigLoader } from '../../lib/shared/utils/config-loader.js';
|
|
5
|
+
import { InteractiveDeploymentCoordinator } from '../../lib/shared/deployment/workflows/interactive-deployment-coordinator.js';
|
|
5
6
|
export function registerDeployCommand(program) {
|
|
6
|
-
const command = program.command('deploy').description('Deploy a Clodo service with
|
|
7
|
+
const command = program.command('deploy').description('Deploy a Clodo service with interactive configuration and validation')
|
|
7
8
|
// Cloudflare-specific options
|
|
8
|
-
.option('--token <token>', 'Cloudflare API token').option('--account-id <id>', 'Cloudflare account ID').option('--zone-id <id>', 'Cloudflare zone ID').option('--domain <domain>', 'Specific domain to deploy to
|
|
9
|
+
.option('--token <token>', 'Cloudflare API token (or set CLOUDFLARE_API_TOKEN env var)').option('--account-id <id>', 'Cloudflare account ID (or set CLOUDFLARE_ACCOUNT_ID env var)').option('--zone-id <id>', 'Cloudflare zone ID (or set CLOUDFLARE_ZONE_ID env var)').option('--domain <domain>', 'Specific domain to deploy to').option('--environment <env>', 'Target environment (development, staging, production)', 'production').option('--development', 'Deploy to development environment (shorthand for --environment development)').option('--staging', 'Deploy to staging environment (shorthand for --environment staging)').option('--production', 'Deploy to production environment (shorthand for --environment production)').option('--dry-run', 'Simulate deployment without making changes').option('-y, --yes', 'Skip confirmation prompts (for CI/CD)').option('--service-path <path>', 'Path to service directory', '.').option('--check-prereqs', 'Check deployment prerequisites before starting').option('--check-auth', 'Check Wrangler authentication status').option('--check-network', 'Check network connectivity to Cloudflare');
|
|
9
10
|
|
|
10
11
|
// Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
|
|
11
12
|
StandardOptions.define(command).action(async options => {
|
|
@@ -38,26 +39,63 @@ export function registerDeployCommand(program) {
|
|
|
38
39
|
// Merge config file defaults with CLI options (CLI takes precedence)
|
|
39
40
|
const mergedOptions = configLoader.merge(configFileData, options);
|
|
40
41
|
|
|
41
|
-
//
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
// Determine if interactive mode should be enabled
|
|
43
|
+
const interactive = !mergedOptions.nonInteractive && !mergedOptions.yes;
|
|
44
|
+
if (interactive) {
|
|
45
|
+
console.log(chalk.cyan('\nš Interactive Clodo Service Deployment'));
|
|
46
|
+
console.log(chalk.gray('ā'.repeat(60)));
|
|
47
|
+
console.log(chalk.white('Welcome to the interactive deployment wizard!\n'));
|
|
48
|
+
|
|
49
|
+
// Use the interactive deployment coordinator
|
|
50
|
+
const coordinator = new InteractiveDeploymentCoordinator({
|
|
51
|
+
servicePath: mergedOptions.servicePath || '.',
|
|
52
|
+
environment: mergedOptions.environment || 'production',
|
|
53
|
+
domain: mergedOptions.domain,
|
|
54
|
+
dryRun: mergedOptions.dryRun || false,
|
|
55
|
+
credentials: {
|
|
56
|
+
token: mergedOptions.token,
|
|
57
|
+
accountId: mergedOptions.accountId,
|
|
58
|
+
zoneId: mergedOptions.zoneId
|
|
59
|
+
},
|
|
60
|
+
checkPrereqs: mergedOptions.checkPrereqs,
|
|
61
|
+
checkAuth: mergedOptions.checkAuth,
|
|
62
|
+
checkNetwork: mergedOptions.checkNetwork,
|
|
63
|
+
verbose: mergedOptions.verbose,
|
|
64
|
+
quiet: mergedOptions.quiet,
|
|
65
|
+
json: mergedOptions.json
|
|
66
|
+
});
|
|
67
|
+
const result = await coordinator.runInteractiveDeployment();
|
|
68
|
+
if (result.success) {
|
|
69
|
+
output.success(result.message);
|
|
70
|
+
if (result.deployedDomains && result.deployedDomains.length > 0) {
|
|
71
|
+
output.info(`Deployed to domains: ${result.deployedDomains.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
output.error('Interactive deployment failed');
|
|
75
|
+
process.exit(1);
|
|
57
76
|
}
|
|
58
77
|
} else {
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
// Use simple API for deployment (non-interactive/CI mode)
|
|
79
|
+
const result = await Clodo.deploy({
|
|
80
|
+
servicePath: mergedOptions.servicePath || '.',
|
|
81
|
+
environment: mergedOptions.environment || 'production',
|
|
82
|
+
domain: mergedOptions.domain,
|
|
83
|
+
dryRun: mergedOptions.dryRun || false,
|
|
84
|
+
credentials: {
|
|
85
|
+
token: mergedOptions.token,
|
|
86
|
+
accountId: mergedOptions.accountId,
|
|
87
|
+
zoneId: mergedOptions.zoneId
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
if (result.success) {
|
|
91
|
+
output.success(result.message);
|
|
92
|
+
if (result.deployedDomains && result.deployedDomains.length > 0) {
|
|
93
|
+
output.info(`Deployed to domains: ${result.deployedDomains.join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
output.error('Deployment failed');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
61
99
|
}
|
|
62
100
|
} catch (error) {
|
|
63
101
|
const output = new (await import('../../lib/shared/utils/output-formatter.js')).OutputFormatter(options || {});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Staging Environment Configuration
|
|
2
|
+
# Copy this file to .env.staging and fill in your actual Cloudflare credentials
|
|
3
|
+
# DO NOT commit this file to version control
|
|
4
|
+
|
|
5
|
+
# Cloudflare API Credentials for Staging Environment
|
|
6
|
+
CLOUDFLARE_API_TOKEN=your_staging_cloudflare_api_token_here
|
|
7
|
+
CLOUDFLARE_ACCOUNT_ID=your_staging_cloudflare_account_id_here
|
|
8
|
+
CLOUDFLARE_ZONE_ID=your_staging_cloudflare_zone_id_here
|
|
9
|
+
|
|
10
|
+
# Staging Domain Configuration
|
|
11
|
+
STAGING_DOMAIN=staging.example.com
|
|
12
|
+
STAGING_ZONE_ID=${CLOUDFLARE_ZONE_ID}
|
|
13
|
+
|
|
14
|
+
# Database Configuration (if different from development)
|
|
15
|
+
STAGING_DATABASE_ID=your_staging_database_id_here
|
|
16
|
+
STAGING_DATABASE_NAME=api2-com-staging-db
|
|
17
|
+
|
|
18
|
+
# Monitoring and Alerting
|
|
19
|
+
STAGING_METRICS_ENDPOINT=https://metrics-staging.example.com
|
|
20
|
+
STAGING_ALERT_EMAIL=staging-alerts@example.com
|
|
21
|
+
|
|
22
|
+
# Security Settings
|
|
23
|
+
STAGING_WAF_ENABLED=true
|
|
24
|
+
STAGING_RATE_LIMIT=500
|
|
25
|
+
STAGING_CORS_ORIGINS=https://staging.example.com,https://app-staging.example.com
|
package/dist/deployment/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Deployment Module
|
|
2
2
|
// Core deployment components for the Clodo Framework
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// NOTE: WranglerDeployer has lib/ dependencies not available in npm distribution
|
|
5
|
+
// export { WranglerDeployer } from './wrangler-deployer.js';
|
|
5
6
|
|
|
6
7
|
// Orchestration Framework - Public API for downstream consumers
|
|
7
8
|
export { BaseDeploymentOrchestrator } from './orchestration/BaseDeploymentOrchestrator.js';
|
|
8
9
|
export { SingleServiceOrchestrator } from './orchestration/SingleServiceOrchestrator.js';
|
|
9
10
|
export { PortfolioOrchestrator } from './orchestration/PortfolioOrchestrator.js';
|
|
10
|
-
export { EnterpriseOrchestrator } from './orchestration/EnterpriseOrchestrator.js';
|
|
11
11
|
export { UnifiedDeploymentOrchestrator } from './orchestration/UnifiedDeploymentOrchestrator.js';
|
|
@@ -36,8 +36,7 @@
|
|
|
36
36
|
import { BaseDeploymentOrchestrator } from './BaseDeploymentOrchestrator.js';
|
|
37
37
|
import { SingleServiceOrchestrator } from './SingleServiceOrchestrator.js';
|
|
38
38
|
import { PortfolioOrchestrator } from './PortfolioOrchestrator.js';
|
|
39
|
-
import {
|
|
40
|
-
import { ErrorHandler } from '../../shared/utils/ErrorHandler.js';
|
|
39
|
+
import { ErrorHandler } from '../../../lib/shared/utils/ErrorHandler.js';
|
|
41
40
|
|
|
42
41
|
/**
|
|
43
42
|
* Capability descriptors - Defines what each capability provides
|
|
@@ -204,8 +203,7 @@ export class UnifiedDeploymentOrchestrator extends BaseDeploymentOrchestrator {
|
|
|
204
203
|
// System adapters for backward compatibility
|
|
205
204
|
this.systemAdapters = {
|
|
206
205
|
single: SingleServiceOrchestrator,
|
|
207
|
-
portfolio: PortfolioOrchestrator
|
|
208
|
-
enterprise: EnterpriseOrchestrator
|
|
206
|
+
portfolio: PortfolioOrchestrator
|
|
209
207
|
};
|
|
210
208
|
|
|
211
209
|
// Deployment context
|
|
@@ -242,7 +242,7 @@ export async function listDatabases(options = {}) {
|
|
|
242
242
|
if (apiToken && accountId) {
|
|
243
243
|
const {
|
|
244
244
|
CloudflareAPI
|
|
245
|
-
} = await import('
|
|
245
|
+
} = await import('@tamyla/clodo-framework/utils/cloudflare');
|
|
246
246
|
const cf = new CloudflareAPI(apiToken);
|
|
247
247
|
return await cf.listD1Databases(accountId);
|
|
248
248
|
}
|
|
@@ -267,7 +267,7 @@ export async function databaseExists(databaseName, options = {}) {
|
|
|
267
267
|
if (apiToken && accountId) {
|
|
268
268
|
const {
|
|
269
269
|
CloudflareAPI
|
|
270
|
-
} = await import('
|
|
270
|
+
} = await import('@tamyla/clodo-framework/utils/cloudflare');
|
|
271
271
|
const cf = new CloudflareAPI(apiToken);
|
|
272
272
|
return await cf.d1DatabaseExists(accountId, databaseName);
|
|
273
273
|
}
|
|
@@ -290,7 +290,7 @@ export async function createDatabase(name, options = {}) {
|
|
|
290
290
|
if (apiToken && accountId) {
|
|
291
291
|
const {
|
|
292
292
|
CloudflareAPI
|
|
293
|
-
} = await import('
|
|
293
|
+
} = await import('@tamyla/clodo-framework/utils/cloudflare');
|
|
294
294
|
const cf = new CloudflareAPI(apiToken);
|
|
295
295
|
const result = await cf.createD1Database(accountId, name);
|
|
296
296
|
return result.uuid; // Return UUID to match CLI behavior
|
|
@@ -405,7 +405,7 @@ export async function getDatabaseId(databaseName, options = {}) {
|
|
|
405
405
|
if (apiToken && accountId) {
|
|
406
406
|
const {
|
|
407
407
|
CloudflareAPI
|
|
408
|
-
} = await import('
|
|
408
|
+
} = await import('@tamyla/clodo-framework/utils/cloudflare');
|
|
409
409
|
const cf = new CloudflareAPI(apiToken);
|
|
410
410
|
const db = await cf.getD1Database(accountId, databaseName);
|
|
411
411
|
return db?.uuid || null;
|
|
@@ -430,33 +430,119 @@ export async function getDatabaseId(databaseName, options = {}) {
|
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
// Validate prerequisites
|
|
433
|
-
export async function validatePrerequisites() {
|
|
433
|
+
export async function validatePrerequisites(options = {}) {
|
|
434
|
+
const {
|
|
435
|
+
checkAuth = false,
|
|
436
|
+
checkNetwork = false,
|
|
437
|
+
verbose = false
|
|
438
|
+
} = options;
|
|
434
439
|
const checks = [{
|
|
435
440
|
name: 'Node.js',
|
|
436
|
-
command: 'node --version'
|
|
441
|
+
command: 'node --version',
|
|
442
|
+
required: true,
|
|
443
|
+
description: 'Required for running the framework and build processes',
|
|
444
|
+
minVersion: '18.0.0'
|
|
437
445
|
}, {
|
|
438
446
|
name: 'NPM',
|
|
439
|
-
command: 'npm --version'
|
|
447
|
+
command: 'npm --version',
|
|
448
|
+
required: true,
|
|
449
|
+
description: 'Required for package management and scripts',
|
|
450
|
+
minVersion: '8.0.0'
|
|
440
451
|
}, {
|
|
441
452
|
name: 'Wrangler',
|
|
442
|
-
command: 'npx wrangler --version'
|
|
453
|
+
command: 'npx wrangler --version',
|
|
454
|
+
required: true,
|
|
455
|
+
description: 'Cloudflare CLI for Workers and D1 database management',
|
|
456
|
+
minVersion: '3.0.0'
|
|
443
457
|
}];
|
|
458
|
+
|
|
459
|
+
// Add optional checks
|
|
460
|
+
if (checkAuth) {
|
|
461
|
+
checks.push({
|
|
462
|
+
name: 'Wrangler Auth',
|
|
463
|
+
command: 'npx wrangler whoami',
|
|
464
|
+
required: false,
|
|
465
|
+
description: 'Cloudflare authentication (optional - can authenticate during deployment)',
|
|
466
|
+
async: true
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
if (checkNetwork) {
|
|
470
|
+
checks.push({
|
|
471
|
+
name: 'Network',
|
|
472
|
+
command: null,
|
|
473
|
+
required: false,
|
|
474
|
+
description: 'Internet connectivity to Cloudflare API',
|
|
475
|
+
async: true,
|
|
476
|
+
checkFunction: async () => {
|
|
477
|
+
try {
|
|
478
|
+
const response = await fetch('https://api.cloudflare.com/client/v4/user/tokens/verify', {
|
|
479
|
+
method: 'GET',
|
|
480
|
+
headers: {
|
|
481
|
+
'Authorization': 'Bearer test'
|
|
482
|
+
},
|
|
483
|
+
signal: AbortSignal.timeout(5000)
|
|
484
|
+
});
|
|
485
|
+
return {
|
|
486
|
+
status: 'ok',
|
|
487
|
+
version: 'Connected'
|
|
488
|
+
};
|
|
489
|
+
} catch (error) {
|
|
490
|
+
if (error.name === 'AbortError') {
|
|
491
|
+
return {
|
|
492
|
+
status: 'failed',
|
|
493
|
+
error: 'Connection timeout'
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
status: 'ok',
|
|
498
|
+
version: 'Connected (API responded)'
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
444
504
|
const results = [];
|
|
445
505
|
for (const check of checks) {
|
|
446
506
|
try {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
507
|
+
let result;
|
|
508
|
+
if (check.async && check.checkFunction) {
|
|
509
|
+
result = await check.checkFunction();
|
|
510
|
+
} else if (check.command) {
|
|
511
|
+
const {
|
|
512
|
+
stdout: version
|
|
513
|
+
} = await execAsync(check.command, {
|
|
514
|
+
encoding: 'utf8',
|
|
515
|
+
timeout: 10000
|
|
516
|
+
});
|
|
517
|
+
result = {
|
|
518
|
+
status: 'ok',
|
|
519
|
+
version: version.trim()
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// Version checking for critical components
|
|
523
|
+
if (check.minVersion && check.name === 'Node.js') {
|
|
524
|
+
const versionNum = version.trim().replace('v', '');
|
|
525
|
+
if (versionNum < check.minVersion) {
|
|
526
|
+
result = {
|
|
527
|
+
status: 'warning',
|
|
528
|
+
version: version.trim(),
|
|
529
|
+
warning: `Node.js ${check.minVersion}+ recommended, found ${versionNum}`
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
result = {
|
|
535
|
+
status: 'skipped',
|
|
536
|
+
version: 'N/A'
|
|
537
|
+
};
|
|
538
|
+
}
|
|
452
539
|
results.push({
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
version: version.trim()
|
|
540
|
+
...check,
|
|
541
|
+
...result
|
|
456
542
|
});
|
|
457
543
|
} catch (error) {
|
|
458
544
|
results.push({
|
|
459
|
-
|
|
545
|
+
...check,
|
|
460
546
|
status: 'failed',
|
|
461
547
|
error: error.message
|
|
462
548
|
});
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import { askUser, askPassword, askChoice, closePrompts } from '../utils/interactive-prompts.js';
|
|
13
13
|
import { ApiTokenManager } from '../security/api-token-manager.js';
|
|
14
|
-
import { CloudflareAPI } from '
|
|
14
|
+
import { CloudflareAPI } from '@tamyla/clodo-framework/utils/cloudflare';
|
|
15
|
+
import { validatePrerequisites } from '../cloudflare/ops.js';
|
|
15
16
|
export class DeploymentCredentialCollector {
|
|
16
17
|
constructor(options = {}) {
|
|
17
18
|
this.servicePath = options.servicePath || '.';
|
|
@@ -29,6 +30,10 @@ export class DeploymentCredentialCollector {
|
|
|
29
30
|
* 4. Return complete credential set
|
|
30
31
|
*/
|
|
31
32
|
async collectCredentials(options = {}) {
|
|
33
|
+
// First, check prerequisites
|
|
34
|
+
if (!this.quiet) {
|
|
35
|
+
await this.displayPrerequisites(options);
|
|
36
|
+
}
|
|
32
37
|
const startCredentials = {
|
|
33
38
|
token: options.token || process.env.CLOUDFLARE_API_TOKEN || null,
|
|
34
39
|
accountId: options.accountId || process.env.CLOUDFLARE_ACCOUNT_ID || null,
|
|
@@ -36,6 +41,11 @@ export class DeploymentCredentialCollector {
|
|
|
36
41
|
zoneName: null // Will be populated when fetching zone
|
|
37
42
|
};
|
|
38
43
|
|
|
44
|
+
// Show credential status if not quiet
|
|
45
|
+
if (!this.quiet) {
|
|
46
|
+
this.displayCredentialStatus(startCredentials, options);
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
// All credentials provided - quick path
|
|
40
50
|
if (startCredentials.token && startCredentials.accountId && startCredentials.zoneId) {
|
|
41
51
|
if (!this.quiet) {
|
|
@@ -247,5 +257,128 @@ export class DeploymentCredentialCollector {
|
|
|
247
257
|
cleanup() {
|
|
248
258
|
closePrompts();
|
|
249
259
|
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Display prerequisite check results
|
|
263
|
+
* @param {Object} options - Options for prerequisite checking
|
|
264
|
+
*/
|
|
265
|
+
async displayPrerequisites(options = {}) {
|
|
266
|
+
console.log(chalk.cyan('\nš§ Deployment Prerequisites\n'));
|
|
267
|
+
try {
|
|
268
|
+
const results = await validatePrerequisites({
|
|
269
|
+
checkAuth: options.checkAuth || false,
|
|
270
|
+
checkNetwork: options.checkNetwork || false,
|
|
271
|
+
verbose: !this.quiet
|
|
272
|
+
});
|
|
273
|
+
let allOk = true;
|
|
274
|
+
let hasWarnings = false;
|
|
275
|
+
results.forEach(result => {
|
|
276
|
+
const icon = result.status === 'ok' ? 'ā
' : result.status === 'warning' ? 'ā ļø' : result.status === 'failed' ? 'ā' : 'āļø';
|
|
277
|
+
const color = result.status === 'ok' ? chalk.green : result.status === 'warning' ? chalk.yellow : result.status === 'failed' ? chalk.red : chalk.gray;
|
|
278
|
+
console.log(`${icon} ${color(result.name)}: ${result.version || 'N/A'}`);
|
|
279
|
+
if (result.description) {
|
|
280
|
+
console.log(` ${chalk.gray(result.description)}`);
|
|
281
|
+
}
|
|
282
|
+
if (result.warning) {
|
|
283
|
+
console.log(` ${chalk.yellow('ā ļø ' + result.warning)}`);
|
|
284
|
+
hasWarnings = true;
|
|
285
|
+
}
|
|
286
|
+
if (result.error) {
|
|
287
|
+
console.log(` ${chalk.red('ā ' + result.error)}`);
|
|
288
|
+
allOk = false;
|
|
289
|
+
}
|
|
290
|
+
if (result.status === 'failed' && result.required) {
|
|
291
|
+
console.log(` ${chalk.red('š” Required for deployment - please install/update ' + result.name.toLowerCase())}`);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
console.log('');
|
|
295
|
+
|
|
296
|
+
// Summary
|
|
297
|
+
if (allOk && !hasWarnings) {
|
|
298
|
+
console.log(chalk.green('š All prerequisites met - ready for deployment!\n'));
|
|
299
|
+
} else if (allOk && hasWarnings) {
|
|
300
|
+
console.log(chalk.yellow('ā ļø Prerequisites OK but with warnings - deployment may proceed\n'));
|
|
301
|
+
} else {
|
|
302
|
+
console.log(chalk.red('ā Some required prerequisites are missing - please resolve before deploying\n'));
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.log(chalk.yellow(`ā ļø Could not check prerequisites: ${error.message}\n`));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Display credential status showing what's available vs missing
|
|
311
|
+
* @param {Object} credentials - Current credential state
|
|
312
|
+
* @param {Object} options - CLI options
|
|
313
|
+
*/
|
|
314
|
+
displayCredentialStatus(credentials, options) {
|
|
315
|
+
console.log(chalk.cyan('\nš Credential Status\n'));
|
|
316
|
+
const status = {
|
|
317
|
+
token: {
|
|
318
|
+
available: !!credentials.token,
|
|
319
|
+
source: this.getCredentialSource('token', credentials.token, options.token),
|
|
320
|
+
required: true
|
|
321
|
+
},
|
|
322
|
+
accountId: {
|
|
323
|
+
available: !!credentials.accountId,
|
|
324
|
+
source: this.getCredentialSource('accountId', credentials.accountId, options.accountId),
|
|
325
|
+
required: false
|
|
326
|
+
},
|
|
327
|
+
zoneId: {
|
|
328
|
+
available: !!credentials.zoneId,
|
|
329
|
+
source: this.getCredentialSource('zoneId', credentials.zoneId, options.zoneId),
|
|
330
|
+
required: false
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Display each credential
|
|
335
|
+
Object.entries(status).forEach(([key, info]) => {
|
|
336
|
+
const icon = info.available ? 'ā
' : 'ā';
|
|
337
|
+
const color = info.available ? chalk.green : chalk.red;
|
|
338
|
+
const label = key === 'token' ? 'API Token' : key === 'accountId' ? 'Account ID' : 'Zone ID';
|
|
339
|
+
const envVar = key === 'token' ? 'CLOUDFLARE_API_TOKEN' : key === 'accountId' ? 'CLOUDFLARE_ACCOUNT_ID' : 'CLOUDFLARE_ZONE_ID';
|
|
340
|
+
console.log(`${icon} ${color(label)}: ${info.available ? info.source : 'Missing'}`);
|
|
341
|
+
if (!info.available) {
|
|
342
|
+
console.log(` ${chalk.gray(`Set via --${key.replace('Id', '-id')} flag or ${envVar} environment variable`)}`);
|
|
343
|
+
console.log(` ${chalk.gray('To set environment variable:')}`);
|
|
344
|
+
console.log(` ${chalk.gray(` Windows PowerShell: $env:${envVar}="your-value-here"`)}`);
|
|
345
|
+
console.log(` ${chalk.gray(` Windows CMD: set ${envVar}=your-value-here`)}`);
|
|
346
|
+
console.log(` ${chalk.gray(` Linux/macOS: export ${envVar}="your-value-here"`)}`);
|
|
347
|
+
console.log(` ${chalk.gray(` Or add to .env file: ${envVar}=your-value-here`)}`);
|
|
348
|
+
console.log('');
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
console.log('');
|
|
352
|
+
|
|
353
|
+
// Summary
|
|
354
|
+
const availableCount = Object.values(status).filter(s => s.available).length;
|
|
355
|
+
const totalCount = Object.keys(status).length;
|
|
356
|
+
if (availableCount === totalCount) {
|
|
357
|
+
console.log(chalk.green('š All credentials available - ready for deployment!\n'));
|
|
358
|
+
} else if (availableCount >= 1) {
|
|
359
|
+
console.log(chalk.yellow(`ā ļø ${availableCount}/${totalCount} credentials available. Missing credentials will be fetched interactively.\n`));
|
|
360
|
+
} else {
|
|
361
|
+
console.log(chalk.red('ā No credentials found. Interactive collection required.\n'));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get the source of a credential (flag, env var, or none)
|
|
367
|
+
* @param {string} key - Credential key
|
|
368
|
+
* @param {*} value - Credential value
|
|
369
|
+
* @param {*} flagValue - CLI flag value
|
|
370
|
+
* @returns {string} Source description
|
|
371
|
+
*/
|
|
372
|
+
getCredentialSource(key, value, flagValue) {
|
|
373
|
+
if (!value) return 'Not set';
|
|
374
|
+
|
|
375
|
+
// Check if it came from CLI flag
|
|
376
|
+
if (flagValue) return 'CLI flag';
|
|
377
|
+
|
|
378
|
+
// Check if it came from environment variable
|
|
379
|
+
const envVar = key === 'token' ? 'CLOUDFLARE_API_TOKEN' : key === 'accountId' ? 'CLOUDFLARE_ACCOUNT_ID' : 'CLOUDFLARE_ZONE_ID';
|
|
380
|
+
if (process.env[envVar]) return 'Environment variable';
|
|
381
|
+
return 'Unknown source';
|
|
382
|
+
}
|
|
250
383
|
}
|
|
251
384
|
export default DeploymentCredentialCollector;
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export { DeploymentValidator } from './validator.js';
|
|
7
|
-
export { MultiDomainOrchestrator } from '
|
|
8
|
-
export { CrossDomainCoordinator } from '
|
|
7
|
+
export { MultiDomainOrchestrator } from '@tamyla/clodo-framework/orchestration';
|
|
8
|
+
export { CrossDomainCoordinator } from '@tamyla/clodo-framework/orchestration';
|
|
9
9
|
export { DeploymentAuditor } from './auditor.js';
|
|
10
|
-
export { RollbackManager } from './rollback-manager.js';
|
|
10
|
+
export { RollbackManager } from './rollback-manager.js';
|
|
11
|
+
export { DeploymentCredentialCollector } from './credential-collector.js';
|
|
12
|
+
|
|
13
|
+
// Export workflow modules
|
|
14
|
+
export * from './workflows/index.js';
|
|
@@ -42,7 +42,7 @@ export class D1ErrorRecoveryManager {
|
|
|
42
42
|
// Import WranglerDeployer for D1 error handling
|
|
43
43
|
const {
|
|
44
44
|
WranglerDeployer
|
|
45
|
-
} = await import('
|
|
45
|
+
} = await import('@tamyla/clodo-framework/deployment');
|
|
46
46
|
deployer = new WranglerDeployer({
|
|
47
47
|
cwd: config.cwd || process.cwd(),
|
|
48
48
|
environment: config.environment
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Workflows Module
|
|
3
|
+
* Exports all interactive deployment workflow coordinators and helpers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { InteractiveDeploymentCoordinator } from './interactive-deployment-coordinator.js';
|
|
7
|
+
export { InteractiveConfirmationWorkflow } from './interactive-confirmation.js';
|
|
8
|
+
export { InteractiveDatabaseWorkflow } from './interactive-database-workflow.js';
|
|
9
|
+
export { InteractiveDomainInfoGatherer } from './interactive-domain-info-gatherer.js';
|
|
10
|
+
export { InteractiveSecretWorkflow } from './interactive-secret-workflow.js';
|
|
11
|
+
export { InteractiveTestingWorkflow } from './interactive-testing-workflow.js';
|
|
12
|
+
export { InteractiveValidationWorkflow } from './interactive-validation.js';
|
|
13
|
+
export { DeploymentSummaryWorkflow } from './deployment-summary.js';
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Deployment Coordinator
|
|
3
|
+
*
|
|
4
|
+
* Restores the interactive deployment functionality that was lost during enterprise code separation.
|
|
5
|
+
* Orchestrates the interactive workflows that were extracted from enterprise-deployment/master-deploy.js.
|
|
6
|
+
*
|
|
7
|
+
* This coordinator brings together:
|
|
8
|
+
* - InteractiveDomainInfoGatherer
|
|
9
|
+
* - InteractiveDatabaseWorkflow
|
|
10
|
+
* - InteractiveSecretWorkflow
|
|
11
|
+
* - InteractiveValidation
|
|
12
|
+
* - InteractiveConfirmation
|
|
13
|
+
* - InteractiveTestingWorkflow
|
|
14
|
+
*
|
|
15
|
+
* @module interactive-deployment-coordinator
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { InteractiveDomainInfoGatherer } from './interactive-domain-info-gatherer.js';
|
|
19
|
+
import { InteractiveDatabaseWorkflow } from './interactive-database-workflow.js';
|
|
20
|
+
import { InteractiveSecretWorkflow } from './interactive-secret-workflow.js';
|
|
21
|
+
import { InteractiveValidation } from './interactive-validation.js';
|
|
22
|
+
import { InteractiveConfirmation } from './interactive-confirmation.js';
|
|
23
|
+
import { InteractiveTestingWorkflow } from './interactive-testing-workflow.js';
|
|
24
|
+
import { Clodo } from '@tamyla/clodo-framework';
|
|
25
|
+
import { OutputFormatter } from '../../utils/output-formatter.js';
|
|
26
|
+
import { DeploymentCredentialCollector } from '../credential-collector.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Interactive Deployment Coordinator
|
|
30
|
+
* Orchestrates the complete interactive deployment workflow
|
|
31
|
+
*/
|
|
32
|
+
export class InteractiveDeploymentCoordinator {
|
|
33
|
+
/**
|
|
34
|
+
* @param {Object} options - Coordinator options
|
|
35
|
+
* @param {string} options.servicePath - Path to service directory
|
|
36
|
+
* @param {string} options.environment - Target environment
|
|
37
|
+
* @param {string} options.domain - Specific domain (optional)
|
|
38
|
+
* @param {boolean} options.dryRun - Dry run mode
|
|
39
|
+
* @param {Object} options.credentials - Cloudflare credentials
|
|
40
|
+
* @param {boolean} options.checkPrereqs - Check prerequisites
|
|
41
|
+
* @param {boolean} options.checkAuth - Check authentication
|
|
42
|
+
* @param {boolean} options.checkNetwork - Check network connectivity
|
|
43
|
+
* @param {boolean} options.verbose - Verbose output
|
|
44
|
+
* @param {boolean} options.quiet - Quiet output
|
|
45
|
+
* @param {boolean} options.json - JSON output
|
|
46
|
+
*/
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
this.options = options;
|
|
49
|
+
this.output = new OutputFormatter(options);
|
|
50
|
+
this.deploymentState = {
|
|
51
|
+
config: {},
|
|
52
|
+
validation: {},
|
|
53
|
+
resources: {},
|
|
54
|
+
testing: {}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Initialize credential collector
|
|
58
|
+
this.credentialCollector = new DeploymentCredentialCollector({
|
|
59
|
+
quiet: options.quiet,
|
|
60
|
+
servicePath: options.servicePath
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Initialize interactive workflow components
|
|
64
|
+
this.workflows = {
|
|
65
|
+
domainGatherer: new InteractiveDomainInfoGatherer({
|
|
66
|
+
interactive: true,
|
|
67
|
+
configCache: null // TODO: Add config cache if available
|
|
68
|
+
}),
|
|
69
|
+
databaseWorkflow: new InteractiveDatabaseWorkflow({
|
|
70
|
+
interactive: true
|
|
71
|
+
}),
|
|
72
|
+
secretWorkflow: new InteractiveSecretWorkflow({
|
|
73
|
+
interactive: true
|
|
74
|
+
}),
|
|
75
|
+
validation: new InteractiveValidation({
|
|
76
|
+
interactive: true
|
|
77
|
+
}),
|
|
78
|
+
confirmation: InteractiveConfirmation,
|
|
79
|
+
testingWorkflow: new InteractiveTestingWorkflow({
|
|
80
|
+
interactive: true
|
|
81
|
+
})
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Run the complete interactive deployment workflow
|
|
87
|
+
* @returns {Promise<Object>} Deployment result
|
|
88
|
+
*/
|
|
89
|
+
async runInteractiveDeployment() {
|
|
90
|
+
try {
|
|
91
|
+
console.log('\nš Starting Interactive Deployment Workflow...\n');
|
|
92
|
+
|
|
93
|
+
// Phase 1: Gather domain and environment information
|
|
94
|
+
await this.gatherDeploymentInfo();
|
|
95
|
+
|
|
96
|
+
// Phase 2: Configure database resources
|
|
97
|
+
await this.configureDatabase();
|
|
98
|
+
|
|
99
|
+
// Phase 3: Configure secrets and credentials
|
|
100
|
+
await this.configureSecrets();
|
|
101
|
+
|
|
102
|
+
// Phase 4: Validate configuration
|
|
103
|
+
await this.validateConfiguration();
|
|
104
|
+
|
|
105
|
+
// Phase 5: Show final confirmation
|
|
106
|
+
await this.showConfirmation();
|
|
107
|
+
|
|
108
|
+
// Phase 6: Execute deployment
|
|
109
|
+
const result = await this.executeDeployment();
|
|
110
|
+
|
|
111
|
+
// Phase 7: Run post-deployment testing
|
|
112
|
+
await this.runPostDeploymentTests();
|
|
113
|
+
return result;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
this.output.error(`Interactive deployment failed: ${error.message}`);
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Phase 1: Gather domain and environment information
|
|
122
|
+
*/
|
|
123
|
+
async gatherDeploymentInfo() {
|
|
124
|
+
console.log('š Phase 1: Gathering Deployment Information');
|
|
125
|
+
console.log('ā'.repeat(50));
|
|
126
|
+
|
|
127
|
+
// Start with basic config from options
|
|
128
|
+
this.deploymentState.config = {
|
|
129
|
+
servicePath: this.options.servicePath || '.',
|
|
130
|
+
environment: this.options.environment || 'production',
|
|
131
|
+
domain: this.options.domain,
|
|
132
|
+
dryRun: this.options.dryRun || false
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Collect credentials with prerequisite checking
|
|
136
|
+
const credentialOptions = {
|
|
137
|
+
token: this.options.credentials?.token,
|
|
138
|
+
accountId: this.options.credentials?.accountId,
|
|
139
|
+
zoneId: this.options.credentials?.zoneId,
|
|
140
|
+
checkAuth: this.options.checkAuth,
|
|
141
|
+
checkNetwork: this.options.checkNetwork
|
|
142
|
+
};
|
|
143
|
+
this.deploymentState.config.credentials = await this.credentialCollector.collectCredentials(credentialOptions);
|
|
144
|
+
|
|
145
|
+
// Use interactive domain gatherer for missing information
|
|
146
|
+
if (!this.deploymentState.config.domain) {
|
|
147
|
+
this.deploymentState.config = await this.workflows.domainGatherer.gatherSingleDomainInfo(this.deploymentState.config);
|
|
148
|
+
}
|
|
149
|
+
console.log('ā
Domain information gathered\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Phase 2: Configure database resources
|
|
154
|
+
*/
|
|
155
|
+
async configureDatabase() {
|
|
156
|
+
console.log('šļø Phase 2: Configuring Database Resources');
|
|
157
|
+
console.log('ā'.repeat(50));
|
|
158
|
+
this.deploymentState.resources.database = await this.workflows.databaseWorkflow.handleDatabaseSetup(this.deploymentState.config.domain, this.deploymentState.config.environment, {
|
|
159
|
+
interactive: true
|
|
160
|
+
});
|
|
161
|
+
console.log('ā
Database configuration complete\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Phase 3: Configure secrets and credentials
|
|
166
|
+
*/
|
|
167
|
+
async configureSecrets() {
|
|
168
|
+
console.log('š Phase 3: Configuring Secrets & Credentials');
|
|
169
|
+
console.log('ā'.repeat(50));
|
|
170
|
+
const workerName = `${this.deploymentState.config.domain}-data-service`;
|
|
171
|
+
this.deploymentState.resources.secrets = await this.workflows.secretWorkflow.handleSecretManagement(this.deploymentState.config.domain, this.deploymentState.config.environment, workerName, {
|
|
172
|
+
interactive: true
|
|
173
|
+
});
|
|
174
|
+
console.log('ā
Secrets configuration complete\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Phase 4: Validate configuration
|
|
179
|
+
*/
|
|
180
|
+
async validateConfiguration() {
|
|
181
|
+
console.log('ā
Phase 4: Validating Configuration');
|
|
182
|
+
console.log('ā'.repeat(50));
|
|
183
|
+
this.deploymentState.validation = await this.workflows.validation.executePreDeploymentChecks(this.deploymentState.config);
|
|
184
|
+
console.log('ā
Configuration validation complete\n');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Phase 5: Show final confirmation
|
|
189
|
+
*/
|
|
190
|
+
async showConfirmation() {
|
|
191
|
+
console.log('šÆ Phase 5: Final Confirmation');
|
|
192
|
+
console.log('ā'.repeat(50));
|
|
193
|
+
await this.workflows.confirmation.showFinalConfirmation(this.deploymentState.config, this.deploymentState, {
|
|
194
|
+
defaultAnswer: 'n'
|
|
195
|
+
});
|
|
196
|
+
console.log('ā
Deployment confirmed\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Phase 6: Execute deployment
|
|
201
|
+
*/
|
|
202
|
+
async executeDeployment() {
|
|
203
|
+
console.log('š Phase 6: Executing Deployment');
|
|
204
|
+
console.log('ā'.repeat(50));
|
|
205
|
+
const result = await Clodo.deploy({
|
|
206
|
+
servicePath: this.deploymentState.config.servicePath,
|
|
207
|
+
environment: this.deploymentState.config.environment,
|
|
208
|
+
domain: this.deploymentState.config.domain,
|
|
209
|
+
dryRun: this.deploymentState.config.dryRun,
|
|
210
|
+
credentials: this.deploymentState.config.credentials
|
|
211
|
+
});
|
|
212
|
+
if (result.success) {
|
|
213
|
+
console.log('ā
Deployment executed successfully\n');
|
|
214
|
+
} else {
|
|
215
|
+
throw new Error('Deployment execution failed');
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Phase 7: Run post-deployment testing
|
|
222
|
+
*/
|
|
223
|
+
async runPostDeploymentTests() {
|
|
224
|
+
console.log('š§Ŗ Phase 7: Running Post-Deployment Tests');
|
|
225
|
+
console.log('ā'.repeat(50));
|
|
226
|
+
this.deploymentState.testing = await this.workflows.testingWorkflow.executePostDeploymentTesting(this.deploymentState.config);
|
|
227
|
+
console.log('ā
Post-deployment testing complete\n');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module interactive-domain-info-gatherer
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { askUser, askYesNo, askChoice } from '
|
|
10
|
+
import { askUser, askYesNo, askChoice } from '../../utils/interactive-prompts.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Interactive Domain Info Gatherer
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* @module interactive-secret-workflow
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { askYesNo } from '
|
|
11
|
-
import { generateSecrets, saveSecrets, distributeSecrets } from '
|
|
12
|
-
import { deploySecret } from '
|
|
10
|
+
import { askYesNo } from '../../utils/interactive-prompts.js';
|
|
11
|
+
import { generateSecrets, saveSecrets, distributeSecrets } from '../../security/secret-generator.js';
|
|
12
|
+
import { deploySecret } from '../../cloudflare/ops.js';
|
|
13
13
|
import { join } from 'path';
|
|
14
14
|
import { existsSync, readFileSync } from 'fs';
|
|
15
15
|
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* @module interactive-testing-workflow
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { askYesNo } from '
|
|
11
|
-
import { checkHealth } from '
|
|
10
|
+
import { askYesNo } from '../../utils/interactive-prompts.js';
|
|
11
|
+
import { checkHealth } from '../../monitoring/health-checker.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Interactive Testing Workflow
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* @module interactive-validation
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { askYesNo } from '
|
|
11
|
-
import { validatePrerequisites, checkAuth, authenticate, workerExists } from '
|
|
10
|
+
import { askYesNo } from '../../utils/interactive-prompts.js';
|
|
11
|
+
import { validatePrerequisites, checkAuth, authenticate, workerExists } from '../../cloudflare/ops.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Interactive Validation Workflow
|
|
@@ -12,10 +12,16 @@ import http from 'http';
|
|
|
12
12
|
const execAsync = promisify(exec);
|
|
13
13
|
|
|
14
14
|
// Load framework configuration
|
|
15
|
-
const {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
// const { frameworkConfig } = await import('@tamyla/clodo-framework/utils');
|
|
16
|
+
// const timing = frameworkConfig.getTiming();
|
|
17
|
+
|
|
18
|
+
// Use default timing values for now
|
|
19
|
+
const timing = {
|
|
20
|
+
healthCheckTimeout: 10000,
|
|
21
|
+
deploymentTimeout: 300000,
|
|
22
|
+
deploymentInterval: 5000,
|
|
23
|
+
endpointValidationTimeout: 5000
|
|
24
|
+
};
|
|
19
25
|
function makeHttpRequest(url, method = 'GET', timeout = 5000) {
|
|
20
26
|
return new Promise((resolve, reject) => {
|
|
21
27
|
const protocol = url.startsWith('https:') ? https : http;
|
|
@@ -546,7 +552,7 @@ export async function verifyWorkerDeployment(workerName, credentials, options =
|
|
|
546
552
|
}
|
|
547
553
|
const {
|
|
548
554
|
CloudflareAPI
|
|
549
|
-
} = await import('
|
|
555
|
+
} = await import('@tamyla/clodo-framework/utils/cloudflare');
|
|
550
556
|
const cfApi = new CloudflareAPI(credentials.token);
|
|
551
557
|
|
|
552
558
|
// List all workers to find ours
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
/**
|
|
9
9
|
* Import validators from utils (source of truth)
|
|
10
10
|
*/
|
|
11
|
-
import { validateServiceName, validateDomainName, validateCloudflareToken, validateCloudflareId, validateServiceType, validateEnvironment } from '
|
|
11
|
+
import { validateServiceName, validateDomainName, validateCloudflareToken, validateCloudflareId, validateServiceType, validateEnvironment } from '@tamyla/clodo-framework/utils';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Validation Registry - Single source of truth for all validators
|
|
@@ -4,4 +4,5 @@
|
|
|
4
4
|
export { ConfigurationCacheManager } from './config-cache.js';
|
|
5
5
|
export { EnhancedSecretManager } from './secret-generator.js';
|
|
6
6
|
export { UnifiedConfigManager, unifiedConfigManager } from '../config/unified-config-manager.js';
|
|
7
|
-
export { askUser, askYesNo, askChoice, closePrompts } from '../interactive-prompts.js';
|
|
7
|
+
export { askUser, askYesNo, askChoice, closePrompts } from '../interactive-prompts.js';
|
|
8
|
+
export { DeploymentCredentialCollector } from '../../../lib/shared/deployment/credential-collector.js';
|
|
@@ -1,43 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Health Checker Module
|
|
3
3
|
* Endpoint health checking and validation utilities
|
|
4
|
+
* Worker-compatible version using fetch API
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import https from 'https';
|
|
7
|
-
import http from 'http';
|
|
8
|
-
|
|
9
7
|
/**
|
|
10
|
-
* Make HTTP request with timeout
|
|
8
|
+
* Make HTTP request with timeout using fetch
|
|
11
9
|
* @param {string} url - URL to request
|
|
12
10
|
* @param {string} method - HTTP method
|
|
13
11
|
* @param {number} timeout - Request timeout in ms
|
|
14
12
|
* @returns {Promise<Object>} Response data and status
|
|
15
13
|
*/
|
|
16
|
-
function makeHttpRequest(url, method = 'GET', timeout = 5000) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
async function makeHttpRequest(url, method = 'GET', timeout = 5000) {
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(url, {
|
|
20
19
|
method,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
res.on('end', () => {
|
|
26
|
-
resolve({
|
|
27
|
-
data,
|
|
28
|
-
statusCode: res.statusCode
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
req.on('error', err => {
|
|
33
|
-
reject(err);
|
|
20
|
+
signal: controller.signal,
|
|
21
|
+
headers: {
|
|
22
|
+
'User-Agent': 'Clodo-Framework-Health-Check/1.0'
|
|
23
|
+
}
|
|
34
24
|
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
clearTimeout(timeoutId);
|
|
26
|
+
const data = await response.text();
|
|
27
|
+
return {
|
|
28
|
+
data,
|
|
29
|
+
statusCode: response.status,
|
|
30
|
+
headers: Object.fromEntries(response.headers.entries())
|
|
31
|
+
};
|
|
32
|
+
} catch (error) {
|
|
33
|
+
clearTimeout(timeoutId);
|
|
34
|
+
if (error.name === 'AbortError') {
|
|
35
|
+
throw new Error('Request timed out');
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
/**
|
|
@@ -47,55 +45,34 @@ function makeHttpRequest(url, method = 'GET', timeout = 5000) {
|
|
|
47
45
|
* @returns {Promise<Object>} Health check result
|
|
48
46
|
*/
|
|
49
47
|
export async function checkHealth(url, timeout = 10000) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
req.startTime = Date.now();
|
|
81
|
-
req.on('error', err => {
|
|
82
|
-
reject({
|
|
83
|
-
url: healthUrl,
|
|
84
|
-
error: err.message,
|
|
85
|
-
healthy: false,
|
|
86
|
-
responseTime: Date.now() - req.startTime
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
req.on('timeout', () => {
|
|
90
|
-
req.destroy();
|
|
91
|
-
reject({
|
|
92
|
-
url: healthUrl,
|
|
93
|
-
error: 'Health check timed out',
|
|
94
|
-
healthy: false,
|
|
95
|
-
responseTime: timeout
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
});
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
const healthUrl = `${url.replace(/\/$/, '')}/health`;
|
|
50
|
+
try {
|
|
51
|
+
const result = await makeHttpRequest(healthUrl, 'GET', timeout);
|
|
52
|
+
const responseTime = Date.now() - startTime;
|
|
53
|
+
let data = null;
|
|
54
|
+
let parseError = null;
|
|
55
|
+
try {
|
|
56
|
+
data = result.data ? JSON.parse(result.data) : null;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
parseError = error.message;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
url: healthUrl,
|
|
62
|
+
status: result.statusCode,
|
|
63
|
+
healthy: result.statusCode >= 200 && result.statusCode < 300,
|
|
64
|
+
responseTime,
|
|
65
|
+
data,
|
|
66
|
+
parseError
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return {
|
|
70
|
+
url: healthUrl,
|
|
71
|
+
error: error.message,
|
|
72
|
+
healthy: false,
|
|
73
|
+
responseTime: Date.now() - startTime
|
|
74
|
+
};
|
|
75
|
+
}
|
|
99
76
|
}
|
|
100
77
|
|
|
101
78
|
/**
|
package/dist/utils/index.js
CHANGED
|
@@ -32,5 +32,8 @@ export const createLogger = (prefix = 'ClodoFramework') => {
|
|
|
32
32
|
// Health checking utilities
|
|
33
33
|
export * from './health-checker.js';
|
|
34
34
|
|
|
35
|
+
// Framework configuration
|
|
36
|
+
export * from './framework-config.js';
|
|
37
|
+
|
|
35
38
|
// Deployment utilities
|
|
36
39
|
export * from './deployment/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamyla/clodo-framework",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"./config/customers": "./dist/config/customers.js",
|
|
23
23
|
"./utils/config": "./dist/utils/config/unified-config-manager.js",
|
|
24
24
|
"./worker": "./dist/worker/index.js",
|
|
25
|
-
"./utils": "./dist/utils/index.js",
|
|
25
|
+
"./utils/cloudflare": "./dist/utils/cloudflare/index.js",
|
|
26
26
|
"./utils/deployment": "./dist/utils/deployment/index.js",
|
|
27
27
|
"./orchestration": "./dist/orchestration/index.js",
|
|
28
28
|
"./deployment": "./dist/deployment/index.js",
|