suthep 0.1.0-beta.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.
Files changed (65) hide show
  1. package/.editorconfig +17 -0
  2. package/.prettierignore +6 -0
  3. package/.prettierrc +7 -0
  4. package/.vscode/settings.json +19 -0
  5. package/LICENSE +21 -0
  6. package/README.md +217 -0
  7. package/dist/commands/deploy.js +318 -0
  8. package/dist/commands/deploy.js.map +1 -0
  9. package/dist/commands/init.js +188 -0
  10. package/dist/commands/init.js.map +1 -0
  11. package/dist/commands/setup.js +90 -0
  12. package/dist/commands/setup.js.map +1 -0
  13. package/dist/index.js +19 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/utils/certbot.js +64 -0
  16. package/dist/utils/certbot.js.map +1 -0
  17. package/dist/utils/config-loader.js +95 -0
  18. package/dist/utils/config-loader.js.map +1 -0
  19. package/dist/utils/deployment.js +76 -0
  20. package/dist/utils/deployment.js.map +1 -0
  21. package/dist/utils/docker.js +393 -0
  22. package/dist/utils/docker.js.map +1 -0
  23. package/dist/utils/nginx.js +303 -0
  24. package/dist/utils/nginx.js.map +1 -0
  25. package/docs/README.md +95 -0
  26. package/docs/TRANSLATIONS.md +211 -0
  27. package/docs/en/README.md +76 -0
  28. package/docs/en/api-reference.md +545 -0
  29. package/docs/en/architecture.md +369 -0
  30. package/docs/en/commands.md +273 -0
  31. package/docs/en/configuration.md +347 -0
  32. package/docs/en/developer-guide.md +588 -0
  33. package/docs/en/docker-ports-config.md +333 -0
  34. package/docs/en/examples.md +537 -0
  35. package/docs/en/getting-started.md +202 -0
  36. package/docs/en/port-binding.md +268 -0
  37. package/docs/en/troubleshooting.md +441 -0
  38. package/docs/th/README.md +64 -0
  39. package/docs/th/commands.md +202 -0
  40. package/docs/th/configuration.md +325 -0
  41. package/docs/th/getting-started.md +203 -0
  42. package/example/README.md +85 -0
  43. package/example/docker-compose.yml +76 -0
  44. package/example/docker-ports-example.yml +81 -0
  45. package/example/muacle.yml +47 -0
  46. package/example/port-binding-example.yml +45 -0
  47. package/example/suthep.yml +46 -0
  48. package/example/suthep=1.yml +46 -0
  49. package/package.json +45 -0
  50. package/src/commands/deploy.ts +405 -0
  51. package/src/commands/init.ts +214 -0
  52. package/src/commands/setup.ts +112 -0
  53. package/src/index.ts +42 -0
  54. package/src/types/config.ts +52 -0
  55. package/src/utils/certbot.ts +144 -0
  56. package/src/utils/config-loader.ts +121 -0
  57. package/src/utils/deployment.ts +157 -0
  58. package/src/utils/docker.ts +755 -0
  59. package/src/utils/nginx.ts +326 -0
  60. package/suthep-0.1.1.tgz +0 -0
  61. package/suthep.example.yml +98 -0
  62. package/test +0 -0
  63. package/todo.md +6 -0
  64. package/tsconfig.json +26 -0
  65. package/vite.config.ts +46 -0
@@ -0,0 +1,112 @@
1
+ import chalk from 'chalk';
2
+ import { execa } from 'execa';
3
+
4
+ interface SetupOptions {
5
+ nginxOnly?: boolean;
6
+ certbotOnly?: boolean;
7
+ }
8
+
9
+ export async function setupCommand(options: SetupOptions): Promise<void> {
10
+ console.log(chalk.blue.bold('\nšŸ”§ Setting up prerequisites\n'));
11
+
12
+ const setupNginx = !options.certbotOnly;
13
+ const setupCertbot = !options.nginxOnly;
14
+
15
+ try {
16
+ // Setup Nginx
17
+ if (setupNginx) {
18
+ console.log(chalk.cyan('šŸ“¦ Installing Nginx...'));
19
+
20
+ // Check if Nginx is already installed
21
+ try {
22
+ await execa('nginx', ['-v']);
23
+ console.log(chalk.green('āœ… Nginx is already installed'));
24
+ } catch {
25
+ // Install Nginx based on OS
26
+ const platform = process.platform;
27
+
28
+ if (platform === 'linux') {
29
+ // Detect Linux distribution
30
+ try {
31
+ await execa('apt-get', ['--version']);
32
+ console.log(chalk.dim('Using apt-get...'));
33
+ await execa('sudo', ['apt-get', 'update'], { stdio: 'inherit' });
34
+ await execa('sudo', ['apt-get', 'install', '-y', 'nginx'], { stdio: 'inherit' });
35
+ } catch {
36
+ try {
37
+ await execa('yum', ['--version']);
38
+ console.log(chalk.dim('Using yum...'));
39
+ await execa('sudo', ['yum', 'install', '-y', 'nginx'], { stdio: 'inherit' });
40
+ } catch {
41
+ throw new Error('Unsupported Linux distribution. Please install Nginx manually.');
42
+ }
43
+ }
44
+ } else if (platform === 'darwin') {
45
+ console.log(chalk.dim('Using Homebrew...'));
46
+ await execa('brew', ['install', 'nginx'], { stdio: 'inherit' });
47
+ } else {
48
+ throw new Error(`Unsupported platform: ${platform}. Please install Nginx manually.`);
49
+ }
50
+
51
+ console.log(chalk.green('āœ… Nginx installed successfully'));
52
+ }
53
+
54
+ // Start Nginx service
55
+ console.log(chalk.cyan('šŸš€ Starting Nginx service...'));
56
+ try {
57
+ await execa('sudo', ['systemctl', 'start', 'nginx']);
58
+ await execa('sudo', ['systemctl', 'enable', 'nginx']);
59
+ console.log(chalk.green('āœ… Nginx service started'));
60
+ } catch (error) {
61
+ console.log(chalk.yellow('āš ļø Could not start Nginx via systemctl (might not be available)'));
62
+ }
63
+ }
64
+
65
+ // Setup Certbot
66
+ if (setupCertbot) {
67
+ console.log(chalk.cyan('\nšŸ” Installing Certbot...'));
68
+
69
+ // Check if Certbot is already installed
70
+ try {
71
+ await execa('certbot', ['--version']);
72
+ console.log(chalk.green('āœ… Certbot is already installed'));
73
+ } catch {
74
+ const platform = process.platform;
75
+
76
+ if (platform === 'linux') {
77
+ // Install Certbot based on package manager
78
+ try {
79
+ await execa('apt-get', ['--version']);
80
+ console.log(chalk.dim('Using apt-get...'));
81
+ await execa('sudo', ['apt-get', 'update'], { stdio: 'inherit' });
82
+ await execa('sudo', ['apt-get', 'install', '-y', 'certbot', 'python3-certbot-nginx'], { stdio: 'inherit' });
83
+ } catch {
84
+ try {
85
+ await execa('yum', ['--version']);
86
+ console.log(chalk.dim('Using yum...'));
87
+ await execa('sudo', ['yum', 'install', '-y', 'certbot', 'python3-certbot-nginx'], { stdio: 'inherit' });
88
+ } catch {
89
+ throw new Error('Unsupported Linux distribution. Please install Certbot manually.');
90
+ }
91
+ }
92
+ } else if (platform === 'darwin') {
93
+ console.log(chalk.dim('Using Homebrew...'));
94
+ await execa('brew', ['install', 'certbot'], { stdio: 'inherit' });
95
+ } else {
96
+ throw new Error(`Unsupported platform: ${platform}. Please install Certbot manually.`);
97
+ }
98
+
99
+ console.log(chalk.green('āœ… Certbot installed successfully'));
100
+ }
101
+ }
102
+
103
+ console.log(chalk.green.bold('\n✨ Setup completed successfully!\n'));
104
+ console.log(chalk.dim('Next steps:'));
105
+ console.log(chalk.dim(' 1. Create a configuration file: suthep init'));
106
+ console.log(chalk.dim(' 2. Deploy your services: suthep deploy\n'));
107
+
108
+ } catch (error) {
109
+ console.error(chalk.red('\nāŒ Setup failed:'), error instanceof Error ? error.message : error);
110
+ process.exit(1);
111
+ }
112
+ }
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { Command } from 'commander'
2
+ import { readFileSync } from 'fs'
3
+ import { dirname, join } from 'path'
4
+ import { fileURLToPath } from 'url'
5
+ import { deployCommand } from './commands/deploy'
6
+ import { initCommand } from './commands/init'
7
+ import { setupCommand } from './commands/setup'
8
+
9
+ const __filename = fileURLToPath(import.meta.url)
10
+ const __dirname = dirname(__filename)
11
+ const packageJsonPath = join(__dirname, '..', 'package.json')
12
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
13
+
14
+ const program = new Command()
15
+
16
+ program
17
+ .name('suthep')
18
+ .description('CLI tool for deploying projects with automatic Nginx reverse proxy and HTTPS setup')
19
+ .version(packageJson.version)
20
+
21
+ program
22
+ .command('init')
23
+ .description('Initialize a new deployment configuration file')
24
+ .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')
25
+ .action(initCommand)
26
+
27
+ program
28
+ .command('setup')
29
+ .description('Setup Nginx and Certbot on the system')
30
+ .option('--nginx-only', 'Only setup Nginx')
31
+ .option('--certbot-only', 'Only setup Certbot')
32
+ .action(setupCommand)
33
+
34
+ program
35
+ .command('deploy')
36
+ .description('Deploy a project using the configuration file')
37
+ .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')
38
+ .option('--no-https', 'Skip HTTPS setup')
39
+ .option('--no-nginx', 'Skip Nginx configuration')
40
+ .action(deployCommand)
41
+
42
+ program.parse()
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Configuration type definitions for the Suthep deployment tool
3
+ */
4
+
5
+ export interface ProjectConfig {
6
+ name: string
7
+ version: string
8
+ }
9
+
10
+ export interface HealthCheckConfig {
11
+ path: string
12
+ interval: number
13
+ }
14
+
15
+ export interface DockerConfig {
16
+ image?: string
17
+ container: string
18
+ port: number
19
+ }
20
+
21
+ export interface ServiceConfig {
22
+ name: string
23
+ port: number
24
+ domains: string[]
25
+ path?: string // Path prefix for this service (e.g., '/api', '/'). Defaults to '/'
26
+ docker?: DockerConfig
27
+ healthCheck?: HealthCheckConfig
28
+ environment?: Record<string, string>
29
+ }
30
+
31
+ export interface NginxConfig {
32
+ configPath: string
33
+ reloadCommand: string
34
+ }
35
+
36
+ export interface CertbotConfig {
37
+ email: string
38
+ staging: boolean
39
+ }
40
+
41
+ export interface DeploymentConfig {
42
+ strategy: 'rolling' | 'blue-green'
43
+ healthCheckTimeout: number
44
+ }
45
+
46
+ export interface DeployConfig {
47
+ project: ProjectConfig
48
+ services: ServiceConfig[]
49
+ nginx: NginxConfig
50
+ certbot: CertbotConfig
51
+ deployment: DeploymentConfig
52
+ }
@@ -0,0 +1,144 @@
1
+ import { execa } from 'execa'
2
+
3
+ /**
4
+ * Request an SSL certificate from Let's Encrypt using Certbot
5
+ */
6
+ export async function requestCertificate(
7
+ domain: string,
8
+ email: string,
9
+ staging: boolean = false
10
+ ): Promise<void> {
11
+ // Check if certificate already exists before requesting
12
+ const exists = await certificateExists(domain)
13
+ if (exists) {
14
+ throw new Error(
15
+ `Certificate for ${domain} already exists. Use certificateExists() to check before calling this function.`
16
+ )
17
+ }
18
+
19
+ const args = [
20
+ 'certonly',
21
+ '--nginx',
22
+ '-d',
23
+ domain,
24
+ '--non-interactive',
25
+ '--agree-tos',
26
+ '--email',
27
+ email,
28
+ ]
29
+
30
+ if (staging) {
31
+ args.push('--staging')
32
+ }
33
+
34
+ try {
35
+ await execa('sudo', ['certbot', ...args])
36
+ } catch (error: any) {
37
+ const errorMessage = error?.stderr || error?.message || String(error) || 'Unknown error'
38
+ const errorLower = errorMessage.toLowerCase()
39
+
40
+ // Check if error is due to certificate already existing
41
+ if (
42
+ errorLower.includes('certificate already exists') ||
43
+ errorLower.includes('already have a certificate') ||
44
+ errorLower.includes('duplicate certificate')
45
+ ) {
46
+ throw new Error(`Certificate for ${domain} already exists. Skipping certificate creation.`)
47
+ }
48
+
49
+ throw new Error(`Failed to obtain SSL certificate for ${domain}: ${errorMessage}`)
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Renew all SSL certificates
55
+ */
56
+ export async function renewCertificates(): Promise<void> {
57
+ try {
58
+ await execa('sudo', ['certbot', 'renew', '--quiet'])
59
+ } catch (error) {
60
+ throw new Error(
61
+ `Failed to renew SSL certificates: ${error instanceof Error ? error.message : error}`
62
+ )
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Check if a certificate exists for a domain
68
+ */
69
+ export async function certificateExists(domain: string): Promise<boolean> {
70
+ try {
71
+ // First, check if certificate files exist using test command (most reliable)
72
+ try {
73
+ await execa('sudo', ['test', '-f', `/etc/letsencrypt/live/${domain}/fullchain.pem`])
74
+ await execa('sudo', ['test', '-f', `/etc/letsencrypt/live/${domain}/privkey.pem`])
75
+ // Both files exist
76
+ return true
77
+ } catch {
78
+ // Files don't exist, continue to certbot check
79
+ }
80
+
81
+ // Fallback: Check using certbot certificates command
82
+ try {
83
+ const { stdout } = await execa('sudo', ['certbot', 'certificates'])
84
+
85
+ // Check if the domain appears in the certificates list
86
+ const lines = stdout.split('\n')
87
+ for (let i = 0; i < lines.length; i++) {
88
+ const line = lines[i]
89
+ // Check if this line contains "Domains:" and includes our domain
90
+ if (line.includes('Domains:') && line.includes(domain)) {
91
+ return true
92
+ }
93
+ // Also check for the domain in certificate paths
94
+ if (
95
+ line.includes(domain) &&
96
+ (line.includes('/live/') || line.includes('Certificate Name:'))
97
+ ) {
98
+ return true
99
+ }
100
+ }
101
+ } catch {
102
+ // If certbot command fails, assume no certificate exists
103
+ }
104
+
105
+ return false
106
+ } catch (error) {
107
+ // If all checks fail, assume no certificate exists
108
+ return false
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Check certificate expiration for a domain
114
+ */
115
+ export async function checkCertificateExpiration(domain: string): Promise<Date | null> {
116
+ try {
117
+ const { stdout } = await execa('sudo', ['certbot', 'certificates', '-d', domain])
118
+
119
+ // Parse expiration date from output
120
+ const expiryMatch = stdout.match(/Expiry Date: ([^\n]+)/)
121
+ if (expiryMatch) {
122
+ return new Date(expiryMatch[1])
123
+ }
124
+
125
+ return null
126
+ } catch (error) {
127
+ return null
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Revoke a certificate for a domain
133
+ */
134
+ export async function revokeCertificate(domain: string): Promise<void> {
135
+ try {
136
+ await execa('sudo', ['certbot', 'revoke', '-d', domain, '--non-interactive'])
137
+ } catch (error) {
138
+ throw new Error(
139
+ `Failed to revoke certificate for ${domain}: ${
140
+ error instanceof Error ? error.message : error
141
+ }`
142
+ )
143
+ }
144
+ }
@@ -0,0 +1,121 @@
1
+ import fs from 'fs-extra'
2
+ import yaml from 'js-yaml'
3
+ import type { DeployConfig } from '../types/config'
4
+
5
+ /**
6
+ * Load and parse a YAML configuration file
7
+ */
8
+ export async function loadConfig(filePath: string): Promise<DeployConfig> {
9
+ try {
10
+ const fileContent = await fs.readFile(filePath, 'utf8')
11
+ const config = yaml.load(fileContent) as DeployConfig
12
+
13
+ validateConfig(config)
14
+
15
+ return config
16
+ } catch (error) {
17
+ if (error instanceof Error) {
18
+ throw new Error(`Failed to load configuration from ${filePath}: ${error.message}`)
19
+ }
20
+ throw error
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Validate the configuration object
26
+ */
27
+ function validateConfig(config: any): asserts config is DeployConfig {
28
+ if (!config.project || !config.project.name) {
29
+ throw new Error('Configuration must include project.name')
30
+ }
31
+
32
+ if (!config.services || !Array.isArray(config.services) || config.services.length === 0) {
33
+ throw new Error('Configuration must include at least one service')
34
+ }
35
+
36
+ // Track ports and container names to detect conflicts
37
+ const usedPorts = new Map<number, string[]>()
38
+ const usedContainers = new Map<string, string>()
39
+
40
+ for (const service of config.services) {
41
+ if (!service.name) {
42
+ throw new Error('Each service must have a name')
43
+ }
44
+ if (!service.port) {
45
+ throw new Error(`Service ${service.name} must have a port`)
46
+ }
47
+ if (!service.domains || !Array.isArray(service.domains) || service.domains.length === 0) {
48
+ throw new Error(`Service ${service.name} must have at least one domain`)
49
+ }
50
+
51
+ // Check for port conflicts
52
+ if (usedPorts.has(service.port)) {
53
+ const conflictingServices = usedPorts.get(service.port)!
54
+ throw new Error(
55
+ `Port conflict: Service "${service.name}" uses port ${
56
+ service.port
57
+ } which is already used by: ${conflictingServices.join(
58
+ ', '
59
+ )}. Each service must use a unique port.`
60
+ )
61
+ }
62
+ usedPorts.set(service.port, [service.name])
63
+
64
+ // Check for Docker container name conflicts
65
+ if (service.docker) {
66
+ const containerName = service.docker.container
67
+ if (usedContainers.has(containerName)) {
68
+ const conflictingService = usedContainers.get(containerName)!
69
+ throw new Error(
70
+ `Docker container name conflict: Service "${service.name}" uses container name "${containerName}" which is already used by service "${conflictingService}". Each Docker container must have a unique name.`
71
+ )
72
+ }
73
+ usedContainers.set(containerName, service.name)
74
+ }
75
+ }
76
+
77
+ // Check for duplicate service names
78
+ const serviceNames = new Set<string>()
79
+ for (const service of config.services) {
80
+ if (serviceNames.has(service.name)) {
81
+ throw new Error(
82
+ `Duplicate service name: "${service.name}" is used multiple times. Each service must have a unique name.`
83
+ )
84
+ }
85
+ serviceNames.add(service.name)
86
+ }
87
+
88
+ if (!config.nginx) {
89
+ config.nginx = {
90
+ configPath: '/etc/nginx/sites-available',
91
+ reloadCommand: 'sudo nginx -t && sudo systemctl reload nginx',
92
+ }
93
+ }
94
+
95
+ if (!config.certbot) {
96
+ config.certbot = {
97
+ email: '',
98
+ staging: false,
99
+ }
100
+ }
101
+
102
+ if (!config.deployment) {
103
+ config.deployment = {
104
+ strategy: 'rolling',
105
+ healthCheckTimeout: 30000,
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Save configuration to a YAML file
112
+ */
113
+ export async function saveConfig(filePath: string, config: DeployConfig): Promise<void> {
114
+ const yamlContent = yaml.dump(config, {
115
+ indent: 2,
116
+ lineWidth: 120,
117
+ noRefs: true,
118
+ })
119
+
120
+ await fs.writeFile(filePath, yamlContent, 'utf8')
121
+ }
@@ -0,0 +1,157 @@
1
+ import type { DeploymentConfig, ServiceConfig } from '../types/config'
2
+ import type { ZeroDowntimeContainerInfo } from './docker'
3
+
4
+ /**
5
+ * Perform a health check on a service endpoint
6
+ */
7
+ export async function performHealthCheck(url: string, timeout: number = 30000): Promise<boolean> {
8
+ const startTime = Date.now()
9
+ const interval = 2000 // Check every 2 seconds
10
+
11
+ while (Date.now() - startTime < timeout) {
12
+ try {
13
+ const response = await fetch(url, {
14
+ method: 'GET',
15
+ signal: AbortSignal.timeout(5000), // 5 second timeout per request
16
+ })
17
+
18
+ if (response.ok) {
19
+ return true
20
+ }
21
+ } catch (error) {
22
+ // Endpoint not ready yet, continue waiting
23
+ }
24
+
25
+ // Wait before next check
26
+ await new Promise((resolve) => setTimeout(resolve, interval))
27
+ }
28
+
29
+ return false
30
+ }
31
+
32
+ /**
33
+ * Deploy a service with zero-downtime strategy
34
+ */
35
+ export async function deployService(
36
+ service: ServiceConfig,
37
+ deploymentConfig: DeploymentConfig,
38
+ tempInfo: ZeroDowntimeContainerInfo | null = null
39
+ ): Promise<void> {
40
+ if (deploymentConfig.strategy === 'rolling') {
41
+ await rollingDeploy(service, deploymentConfig, tempInfo)
42
+ } else if (deploymentConfig.strategy === 'blue-green') {
43
+ await blueGreenDeploy(service, deploymentConfig, tempInfo)
44
+ } else {
45
+ throw new Error(`Unknown deployment strategy: ${deploymentConfig.strategy}`)
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Rolling deployment strategy
51
+ * For single instance, uses zero-downtime approach similar to blue-green
52
+ */
53
+ async function rollingDeploy(
54
+ service: ServiceConfig,
55
+ deploymentConfig: DeploymentConfig,
56
+ tempInfo: ZeroDowntimeContainerInfo | null
57
+ ): Promise<void> {
58
+ // For rolling deployment with single instance:
59
+ // Similar to blue-green - use temporary container and port
60
+
61
+ if (!tempInfo || !tempInfo.oldContainerExists) {
62
+ // No existing container, just check health on the new container
63
+ if (service.healthCheck) {
64
+ const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`
65
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)
66
+
67
+ if (!isHealthy) {
68
+ throw new Error(`Service ${service.name} failed health check during rolling deployment`)
69
+ }
70
+ }
71
+ } else {
72
+ // Check health on temporary port
73
+ if (service.healthCheck) {
74
+ const healthUrl = `http://localhost:${tempInfo.tempPort}${service.healthCheck.path}`
75
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)
76
+
77
+ if (!isHealthy) {
78
+ throw new Error(
79
+ `Service ${service.name} failed health check on temporary container during rolling deployment`
80
+ )
81
+ }
82
+ }
83
+ }
84
+
85
+ // Add a small delay to ensure service is fully ready
86
+ await new Promise((resolve) => setTimeout(resolve, 2000))
87
+ }
88
+
89
+ /**
90
+ * Blue-green deployment strategy for single instance
91
+ * Uses temporary container and port for zero-downtime deployment
92
+ */
93
+ async function blueGreenDeploy(
94
+ service: ServiceConfig,
95
+ deploymentConfig: DeploymentConfig,
96
+ tempInfo: ZeroDowntimeContainerInfo | null
97
+ ): Promise<void> {
98
+ // For blue-green deployment with single instance:
99
+ // 1. New container is already started on temporary port (handled in deploy command)
100
+ // 2. Run health checks on new container
101
+ // 3. Switch nginx to new port (handled in deploy command)
102
+ // 4. Stop old container and promote new one (handled in deploy command)
103
+
104
+ if (!tempInfo || !tempInfo.oldContainerExists) {
105
+ // No existing container, just check health on the new container
106
+ if (service.healthCheck) {
107
+ const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`
108
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)
109
+
110
+ if (!isHealthy) {
111
+ throw new Error(`Service ${service.name} failed health check during blue-green deployment`)
112
+ }
113
+ }
114
+ } else {
115
+ // Check health on temporary port
116
+ if (service.healthCheck) {
117
+ const healthUrl = `http://localhost:${tempInfo.tempPort}${service.healthCheck.path}`
118
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)
119
+
120
+ if (!isHealthy) {
121
+ throw new Error(
122
+ `Service ${service.name} failed health check on temporary container during blue-green deployment`
123
+ )
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Wait for a service to become healthy
131
+ */
132
+ export async function waitForService(
133
+ service: ServiceConfig,
134
+ timeout: number = 60000
135
+ ): Promise<boolean> {
136
+ if (!service.healthCheck) {
137
+ // No health check configured, assume service is ready after a short delay
138
+ await new Promise((resolve) => setTimeout(resolve, 5000))
139
+ return true
140
+ }
141
+
142
+ const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`
143
+ return await performHealthCheck(healthUrl, timeout)
144
+ }
145
+
146
+ /**
147
+ * Gracefully shutdown a service
148
+ */
149
+ export async function gracefulShutdown(
150
+ _service: ServiceConfig,
151
+ timeout: number = 30000
152
+ ): Promise<void> {
153
+ // Send shutdown signal and wait for graceful termination
154
+ // This is a placeholder - actual implementation would depend on how services are managed
155
+
156
+ await new Promise((resolve) => setTimeout(resolve, Math.min(timeout, 5000)))
157
+ }