Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Sign in
Toggle navigation
C
ctas-box
Project overview
Project overview
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Packages
Packages
Container Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Fachri
ctas-box
Commits
0fb4762a
Commit
0fb4762a
authored
Dec 31, 2025
by
Rais Aryaguna
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add: zero downtime deployment script and utility functions
parent
ed0527de
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
933 additions
and
0 deletions
+933
-0
script/zero-dowtime/deploy.js
script/zero-dowtime/deploy.js
+448
-0
script/zero-dowtime/utils.js
script/zero-dowtime/utils.js
+485
-0
No files found.
script/zero-dowtime/deploy.js
0 → 100644
View file @
0fb4762a
#!/usr/bin/env node
/* eslint-disable no-shadow */
/* eslint-disable no-loop-func */
/* eslint-disable no-await-in-loop */
/**
* Universal Zero Downtime Deployment Script
* Supports: development, staging, production
* All configurations loaded from environment variables
*/
import
{
execSync
}
from
'
child_process
'
;
import
fs
from
'
fs
'
;
import
path
from
'
path
'
;
import
{
fileURLToPath
}
from
'
url
'
;
import
http
from
'
http
'
;
import
{
config
}
from
'
dotenv
'
;
// Load environment-specific .env file
function
loadEnvConfig
(
environment
)
{
const
envFiles
=
[
`.env.
${
environment
}
.local`
,
`.env.
${
environment
}
`
,
'
.env.local
'
,
'
.env
'
];
// eslint-disable-next-line no-restricted-syntax
for
(
const
envFile
of
envFiles
)
{
const
envPath
=
path
.
join
(
process
.
cwd
(),
envFile
);
if
(
fs
.
existsSync
(
envPath
))
{
logMessage
(
`📄 Loading config from:
${
envFile
}
`
);
config
({
path
:
envPath
});
break
;
}
}
}
// Environment Configuration (loaded from .env files)
const
getConfig
=
(
environment
)
=>
({
// App Configuration
APP_NAME
:
process
.
env
.
APP_NAME
||
`app-
${
environment
}
`
,
APP_PORT
:
parseInt
(
process
.
env
.
APP_PORT
||
'
9020
'
,
10
),
// PM2 Configuration
PM2_INSTANCES
:
parseInt
(
process
.
env
.
PM2_INSTANCES
||
'
2
'
,
10
),
PM2_EXEC_MODE
:
process
.
env
.
PM2_EXEC_MODE
||
'
cluster
'
,
PM2_MAX_MEMORY
:
process
.
env
.
PM2_MAX_MEMORY
||
'
500M
'
,
PM2_WAIT_READY
:
process
.
env
.
PM2_WAIT_READY
===
'
true
'
,
PM2_LISTEN_TIMEOUT
:
parseInt
(
process
.
env
.
PM2_LISTEN_TIMEOUT
||
'
10000
'
,
10
),
PM2_KILL_TIMEOUT
:
parseInt
(
process
.
env
.
PM2_KILL_TIMEOUT
||
'
5000
'
,
10
),
// Build Configuration
BUILD_COMMAND
:
process
.
env
.
BUILD_COMMAND
||
'
yarn build
'
,
INSTALL_COMMAND
:
process
.
env
.
INSTALL_COMMAND
||
'
yarn install --frozen-lockfile
'
,
TMP_BUILD_DIR
:
process
.
env
.
TMP_BUILD_DIR
||
'
dist_building
'
,
FINAL_DIST_DIR
:
process
.
env
.
FINAL_DIST_DIR
||
'
dist
'
,
BACKUP_DIST_DIR
:
process
.
env
.
BACKUP_DIST_DIR
||
'
dist_backup
'
,
// Health Check Configuration
HEALTH_CHECK_ENABLED
:
process
.
env
.
HEALTH_CHECK_ENABLED
!==
'
false
'
,
HEALTH_CHECK_PATH
:
process
.
env
.
HEALTH_CHECK_PATH
||
'
/
'
,
HEALTH_CHECK_RETRIES
:
parseInt
(
process
.
env
.
HEALTH_CHECK_RETRIES
||
'
5
'
,
10
),
HEALTH_CHECK_INTERVAL
:
parseInt
(
process
.
env
.
HEALTH_CHECK_INTERVAL
||
'
2000
'
,
10
),
HEALTH_CHECK_TIMEOUT
:
parseInt
(
process
.
env
.
HEALTH_CHECK_TIMEOUT
||
'
5000
'
,
10
),
// Deployment Configuration
AUTO_ROLLBACK
:
process
.
env
.
AUTO_ROLLBACK
!==
'
false
'
,
CLEANUP_BACKUP
:
process
.
env
.
CLEANUP_BACKUP
!==
'
false
'
,
PM2_SAVE
:
process
.
env
.
PM2_SAVE
!==
'
false
'
,
// Logging Configuration
LOG_DIR
:
process
.
env
.
LOG_DIR
||
'
logs
'
,
LOG_FILE_NAME
:
process
.
env
.
LOG_FILE_NAME
||
'
deployment.log
'
,
ENABLE_CONSOLE_LOG
:
process
.
env
.
ENABLE_CONSOLE_LOG
!==
'
false
'
,
// Environment
ENVIRONMENT
:
environment
,
NODE_ENV
:
process
.
env
.
NODE_ENV
||
environment
,
});
let
CONFIG
;
// Helper functions
function
logMessage
(
message
,
config
=
CONFIG
)
{
const
timestamp
=
new
Date
().
toISOString
();
const
logMessage
=
`[
${
timestamp
}
] [
${
config
?.
ENVIRONMENT
||
'
INIT
'
}]
$
{
message
}
`;
if (!config || config.ENABLE_CONSOLE_LOG) {
console.log(logMessage);
}
if (config) {
const logDir = path.join(process.cwd(), config.LOG_DIR);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logFile = path.join(logDir, config.LOG_FILE_NAME);
fs.appendFileSync(logFile, `
$
{
logMessage
}
\
n
`);
}
}
function executeCommand(command, throwOnError = true) {
logMessage(`
⚙️
Executing
:
$
{
command
}
`
);
try
{
const
output
=
execSync
(
command
,
{
encoding
:
'
utf8
'
,
stdio
:
CONFIG
.
ENABLE_CONSOLE_LOG
?
'
inherit
'
:
'
pipe
'
,
});
return
output
?
output
.
trim
()
:
''
;
}
catch
(
error
)
{
logMessage
(
`❌ Error executing command:
${
error
.
message
}
`
);
if
(
throwOnError
)
throw
error
;
return
null
;
}
}
function
checkAppStatus
(
appName
)
{
try
{
const
status
=
executeCommand
(
`pm2 describe
${
appName
}
`
,
false
);
return
!!
status
;
}
catch
(
error
)
{
return
false
;
}
}
// Health check function
async
function
checkHealth
(
retries
=
CONFIG
.
HEALTH_CHECK_RETRIES
)
{
if
(
!
CONFIG
.
HEALTH_CHECK_ENABLED
)
{
logMessage
(
'
⚠️ Health check disabled, skipping...
'
);
return
true
;
}
logMessage
(
`🏥 Starting health check (
${
retries
}
retries)...`
);
// eslint-disable-next-line no-plusplus
for
(
let
i
=
0
;
i
<
retries
;
i
++
)
{
try
{
await
new
Promise
((
resolve
,
reject
)
=>
{
const
url
=
`http://localhost:
${
CONFIG
.
APP_PORT
}${
CONFIG
.
HEALTH_CHECK_PATH
}
`
;
const
req
=
http
.
get
(
url
,
(
res
)
=>
{
if
(
res
.
statusCode
===
200
)
{
logMessage
(
`✅ Health check passed (attempt
${
i
+
1
}
/
${
retries
}
)`
);
resolve
(
true
);
}
else
{
reject
(
new
Error
(
`Status code:
${
res
.
statusCode
}
`
));
}
});
req
.
on
(
'
error
'
,
reject
);
req
.
setTimeout
(
CONFIG
.
HEALTH_CHECK_TIMEOUT
,
()
=>
{
req
.
destroy
();
reject
(
new
Error
(
'
Timeout
'
));
});
});
return
true
;
}
catch
(
error
)
{
logMessage
(
`⚠️ Health check failed (attempt
${
i
+
1
}
/
${
retries
}
):
${
error
.
message
}
`
);
if
(
i
<
retries
-
1
)
{
logMessage
(
`⏳ Waiting
${
CONFIG
.
HEALTH_CHECK_INTERVAL
}
ms before retry...`
);
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
CONFIG
.
HEALTH_CHECK_INTERVAL
));
}
}
}
return
false
;
}
// Atomic folder switch with backup
function
atomicFolderSwitch
()
{
logMessage
(
'
🔄 Performing atomic folder switch...
'
);
const
tmpPath
=
path
.
join
(
process
.
cwd
(),
CONFIG
.
TMP_BUILD_DIR
);
const
finalPath
=
path
.
join
(
process
.
cwd
(),
CONFIG
.
FINAL_DIST_DIR
);
const
backupPath
=
path
.
join
(
process
.
cwd
(),
CONFIG
.
BACKUP_DIST_DIR
);
// Create backup of current dist
if
(
fs
.
existsSync
(
finalPath
))
{
if
(
fs
.
existsSync
(
backupPath
))
{
fs
.
rmSync
(
backupPath
,
{
recursive
:
true
,
force
:
true
});
}
fs
.
renameSync
(
finalPath
,
backupPath
);
logMessage
(
`📦 Created backup:
${
CONFIG
.
FINAL_DIST_DIR
}
→
${
CONFIG
.
BACKUP_DIST_DIR
}
`
);
}
// Switch to new build (atomic operation)
if
(
!
fs
.
existsSync
(
tmpPath
))
{
throw
new
Error
(
`Build directory
${
CONFIG
.
TMP_BUILD_DIR
}
not found!`
);
}
fs
.
renameSync
(
tmpPath
,
finalPath
);
logMessage
(
`✅ Switched to new build:
${
CONFIG
.
TMP_BUILD_DIR
}
→
${
CONFIG
.
FINAL_DIST_DIR
}
`
);
}
// Rollback function
function
rollback
()
{
if
(
!
CONFIG
.
AUTO_ROLLBACK
)
{
logMessage
(
'
⚠️ Auto rollback disabled, skipping...
'
);
return
;
}
logMessage
(
'
🔙 ROLLBACK: Restoring previous version...
'
);
const
finalPath
=
path
.
join
(
process
.
cwd
(),
CONFIG
.
FINAL_DIST_DIR
);
const
backupPath
=
path
.
join
(
process
.
cwd
(),
CONFIG
.
BACKUP_DIST_DIR
);
if
(
fs
.
existsSync
(
backupPath
))
{
if
(
fs
.
existsSync
(
finalPath
))
{
fs
.
rmSync
(
finalPath
,
{
recursive
:
true
,
force
:
true
});
}
fs
.
renameSync
(
backupPath
,
finalPath
);
logMessage
(
'
✅ Rollback completed. Reloading PM2...
'
);
try
{
executeCommand
(
`pm2 reload
${
CONFIG
.
APP_NAME
}
`
);
logMessage
(
'
✅ PM2 reloaded with previous version
'
);
}
catch
(
error
)
{
logMessage
(
'
❌ Failed to reload PM2 after rollback
'
);
}
}
else
{
logMessage
(
'
❌ No backup found for rollback!
'
);
}
}
// Generate PM2 ecosystem config
function
generateEcosystemConfig
()
{
const
configPath
=
path
.
join
(
process
.
cwd
(),
`ecosystem.
${
CONFIG
.
ENVIRONMENT
}
.config.cjs`
);
const
ecosystemConfig
=
`module.exports = {
apps: [
{
name: '
${
CONFIG
.
APP_NAME
}
',
script: 'npx',
args: 'serve
${
CONFIG
.
FINAL_DIST_DIR
}
-s -l
${
CONFIG
.
APP_PORT
}
',
// Instance Configuration
instances:
${
CONFIG
.
PM2_INSTANCES
}
,
exec_mode: '
${
CONFIG
.
PM2_EXEC_MODE
}
',
// Zero downtime reload settings
wait_ready:
${
CONFIG
.
PM2_WAIT_READY
}
,
listen_timeout:
${
CONFIG
.
PM2_LISTEN_TIMEOUT
}
,
kill_timeout:
${
CONFIG
.
PM2_KILL_TIMEOUT
}
,
// Auto restart settings
autorestart: true,
max_restarts: 10,
min_uptime: '10s',
// Resource limits
max_memory_restart: '
${
CONFIG
.
PM2_MAX_MEMORY
}
',
// Environment
env: {
NODE_ENV: '
${
CONFIG
.
NODE_ENV
}
',
PORT:
${
CONFIG
.
APP_PORT
}
,
ENVIRONMENT: '
${
CONFIG
.
ENVIRONMENT
}
',
},
// Logging
error_file: './
${
CONFIG
.
LOG_DIR
}
/pm2-error.log',
out_file: './
${
CONFIG
.
LOG_DIR
}
/pm2-out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
},
],
};`
;
fs
.
writeFileSync
(
configPath
,
ecosystemConfig
);
logMessage
(
`📝 Generated PM2 config:
${
configPath
}
`
);
return
configPath
;
}
// Main deployment function
// eslint-disable-next-line consistent-return
async
function
deployApp
(
environment
=
'
production
'
)
{
const
startTime
=
new
Date
();
// Load environment-specific configuration
loadEnvConfig
(
environment
);
CONFIG
=
getConfig
(
environment
);
logMessage
(
'
═
'
.
repeat
(
60
));
logMessage
(
`🚀 STARTING ZERO DOWNTIME DEPLOYMENT`
);
logMessage
(
`📍 Environment:
${
CONFIG
.
ENVIRONMENT
}
`
);
logMessage
(
`📦 App Name:
${
CONFIG
.
APP_NAME
}
`
);
logMessage
(
`🔌 Port:
${
CONFIG
.
APP_PORT
}
`
);
logMessage
(
`⚙️ Instances:
${
CONFIG
.
PM2_INSTANCES
}
(
${
CONFIG
.
PM2_EXEC_MODE
}
mode)`
);
logMessage
(
'
═
'
.
repeat
(
60
));
try
{
// Step 1: Build the application
logMessage
(
''
);
logMessage
(
'
📦 STEP 1: Building application...
'
);
logMessage
(
'
─
'
.
repeat
(
60
));
const
tmpPath
=
path
.
join
(
process
.
cwd
(),
CONFIG
.
TMP_BUILD_DIR
);
if
(
fs
.
existsSync
(
tmpPath
))
{
logMessage
(
`🧹 Cleaning up temp folder:
${
CONFIG
.
TMP_BUILD_DIR
}
`
);
fs
.
rmSync
(
tmpPath
,
{
recursive
:
true
,
force
:
true
});
}
logMessage
(
`📥 Running:
${
CONFIG
.
INSTALL_COMMAND
}
`
);
executeCommand
(
CONFIG
.
INSTALL_COMMAND
);
const
buildCmd
=
`
${
CONFIG
.
BUILD_COMMAND
}
-- --mode
${
CONFIG
.
ENVIRONMENT
}
--outDir
${
CONFIG
.
TMP_BUILD_DIR
}
`
;
logMessage
(
`🔨 Running:
${
buildCmd
}
`
);
executeCommand
(
buildCmd
);
logMessage
(
'
✅ Build completed successfully
'
);
// Step 2: Generate PM2 config
logMessage
(
''
);
logMessage
(
'
📝 STEP 2: Generating PM2 configuration...
'
);
logMessage
(
'
─
'
.
repeat
(
60
));
const
configFile
=
generateEcosystemConfig
();
// Step 3: Check if app is running
logMessage
(
''
);
logMessage
(
'
🔍 STEP 3: Checking application status...
'
);
logMessage
(
'
─
'
.
repeat
(
60
));
const
isRunning
=
checkAppStatus
(
CONFIG
.
APP_NAME
);
logMessage
(
`App status:
${
isRunning
?
'
🟢 Running
'
:
'
🔴 Not running
'
}
`
);
if
(
isRunning
)
{
// Step 4: Zero-downtime deployment
logMessage
(
''
);
logMessage
(
'
🔄 STEP 4: Performing zero-downtime deployment...
'
);
logMessage
(
'
─
'
.
repeat
(
60
));
// 4a: Atomic folder switch
atomicFolderSwitch
();
// 4b: Reload PM2
logMessage
(
'
🔄 Reloading PM2...
'
);
executeCommand
(
`pm2 reload
${
configFile
}
--env
${
CONFIG
.
ENVIRONMENT
}
`
);
// 4c: Wait for PM2 to stabilize
const
waitTime
=
3000
;
logMessage
(
`⏳ Waiting
${
waitTime
}
ms for PM2 to stabilize...`
);
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
waitTime
));
// 4d: Health check
logMessage
(
''
);
logMessage
(
'
🏥 STEP 5: Running health check...
'
);
logMessage
(
'
─
'
.
repeat
(
60
));
const
isHealthy
=
await
checkHealth
();
if
(
!
isHealthy
)
{
logMessage
(
'
❌ Health check failed! Initiating rollback...
'
);
rollback
();
throw
new
Error
(
'
Deployment failed: Health check unsuccessful
'
);
}
logMessage
(
'
✅ Health check passed!
'
);
// 4e: Cleanup backup after successful deployment
if
(
CONFIG
.
CLEANUP_BACKUP
)
{
const
backupPath
=
path
.
join
(
process
.
cwd
(),
CONFIG
.
BACKUP_DIST_DIR
);
if
(
fs
.
existsSync
(
backupPath
))
{
fs
.
rmSync
(
backupPath
,
{
recursive
:
true
,
force
:
true
});
logMessage
(
'
🧹 Cleaned up backup folder
'
);
}
}
}
else
{
// Step 4: First time deployment
logMessage
(
''
);
logMessage
(
'
🚀 STEP 4: Starting application for the first time...
'
);
logMessage
(
'
─
'
.
repeat
(
60
));
atomicFolderSwitch
();
executeCommand
(
`pm2 start
${
configFile
}
--env
${
CONFIG
.
ENVIRONMENT
}
`
);
// Wait and health check
logMessage
(
'
⏳ Waiting for application to start...
'
);
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
5000
));
const
isHealthy
=
await
checkHealth
();
if
(
!
isHealthy
)
{
logMessage
(
'
❌ Health check failed on first start!
'
);
executeCommand
(
`pm2 delete
${
CONFIG
.
APP_NAME
}
`
,
false
);
throw
new
Error
(
'
Deployment failed: Health check unsuccessful
'
);
}
logMessage
(
'
✅ Application started successfully!
'
);
}
// Save PM2 process list
if
(
CONFIG
.
PM2_SAVE
)
{
logMessage
(
'
💾 Saving PM2 process list...
'
);
executeCommand
(
'
pm2 save
'
);
}
const
endTime
=
new
Date
();
const
deploymentTime
=
((
endTime
-
startTime
)
/
1000
).
toFixed
(
2
);
logMessage
(
''
);
logMessage
(
'
═
'
.
repeat
(
60
));
logMessage
(
`✅ DEPLOYMENT COMPLETED SUCCESSFULLY!`
);
logMessage
(
`⏱️ Total time:
${
deploymentTime
}
seconds`
);
logMessage
(
`🌍 Environment:
${
CONFIG
.
ENVIRONMENT
}
`
);
logMessage
(
`🔌 App running on: http://localhost:
${
CONFIG
.
APP_PORT
}
`
);
logMessage
(
'
═
'
.
repeat
(
60
));
logMessage
(
''
);
logMessage
(
'
📊 Quick commands:
'
);
logMessage
(
` pm2 status
${
CONFIG
.
APP_NAME
}
`
);
logMessage
(
` pm2 logs
${
CONFIG
.
APP_NAME
}
`
);
logMessage
(
` pm2 monit`
);
logMessage
(
''
);
return
{
success
:
true
,
message
:
`Deployment to
${
CONFIG
.
ENVIRONMENT
}
completed successfully`
,
deploymentTime
:
parseFloat
(
deploymentTime
),
config
:
CONFIG
,
};
}
catch
(
error
)
{
logMessage
(
''
);
logMessage
(
'
═
'
.
repeat
(
60
));
logMessage
(
`❌ DEPLOYMENT FAILED!`
);
logMessage
(
`Error:
${
error
.
message
}
`
);
logMessage
(
'
═
'
.
repeat
(
60
));
logMessage
(
'
Stack trace:
'
);
logMessage
(
error
.
stack
);
process
.
exit
(
1
);
}
}
// Execute deployment if script is run directly
if
(
process
.
argv
[
1
]
===
fileURLToPath
(
import
.
meta
.
url
))
{
const
environment
=
process
.
argv
[
2
]
||
'
production
'
;
// Validate environment
const
validEnvironments
=
[
'
development
'
,
'
staging
'
,
'
production
'
];
if
(
!
validEnvironments
.
includes
(
environment
))
{
console
.
error
(
`❌ Invalid environment:
${
environment
}
`
);
console
.
error
(
`✅ Valid environments:
${
validEnvironments
.
join
(
'
,
'
)}
`
);
process
.
exit
(
1
);
}
deployApp
(
environment
);
}
// eslint-disable-next-line import/prefer-default-export
export
{
deployApp
};
script/zero-dowtime/utils.js
0 → 100644
View file @
0fb4762a
#!/usr/bin/env node
/* eslint-disable no-case-declarations */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-plusplus */
/* eslint-disable no-shadow */
/* eslint-disable no-restricted-syntax */
/**
* Utility Scripts for Deployment Management
* Provides standalone functions for rollback, health check, and other operations
*/
import
{
execSync
}
from
'
child_process
'
;
import
fs
from
'
fs
'
;
import
path
,
{
dirname
}
from
'
path
'
;
import
{
fileURLToPath
}
from
'
url
'
;
import
http
from
'
http
'
;
import
{
config
}
from
'
dotenv
'
;
const
__filename
=
fileURLToPath
(
import
.
meta
.
url
);
const
__dirname
=
dirname
(
__filename
);
// Colors for console output
const
colors
=
{
reset
:
'
\
x1b[0m
'
,
red
:
'
\
x1b[31m
'
,
green
:
'
\
x1b[32m
'
,
yellow
:
'
\
x1b[33m
'
,
blue
:
'
\
x1b[34m
'
,
cyan
:
'
\
x1b[36m
'
,
};
// Load environment configuration
function
loadEnvConfig
(
environment
)
{
const
envFiles
=
[
`.env.
${
environment
}
.local`
,
`.env.
${
environment
}
`
,
'
.env.local
'
,
'
.env
'
];
for
(
const
envFile
of
envFiles
)
{
const
envPath
=
path
.
join
(
process
.
cwd
(),
envFile
);
if
(
fs
.
existsSync
(
envPath
))
{
console
.
log
(
`
${
colors
.
blue
}
📄 Loading config from:
${
envFile
}${
colors
.
reset
}
`
);
config
({
path
:
envPath
});
return
true
;
}
}
return
false
;
}
// Get configuration from environment
const
getConfig
=
(
environment
)
=>
({
APP_NAME
:
process
.
env
.
APP_NAME
||
`app-
${
environment
}
`
,
APP_PORT
:
parseInt
(
process
.
env
.
APP_PORT
||
'
9020
'
,
10
),
FINAL_DIST_DIR
:
process
.
env
.
FINAL_DIST_DIR
||
'
dist
'
,
BACKUP_DIST_DIR
:
process
.
env
.
BACKUP_DIST_DIR
||
'
dist_backup
'
,
HEALTH_CHECK_PATH
:
process
.
env
.
HEALTH_CHECK_PATH
||
'
/
'
,
HEALTH_CHECK_RETRIES
:
parseInt
(
process
.
env
.
HEALTH_CHECK_RETRIES
||
'
5
'
,
10
),
HEALTH_CHECK_INTERVAL
:
parseInt
(
process
.
env
.
HEALTH_CHECK_INTERVAL
||
'
2000
'
,
10
),
HEALTH_CHECK_TIMEOUT
:
parseInt
(
process
.
env
.
HEALTH_CHECK_TIMEOUT
||
'
5000
'
,
10
),
LOG_DIR
:
process
.
env
.
LOG_DIR
||
'
logs
'
,
ENVIRONMENT
:
environment
,
});
// Execute shell command
function
executeCommand
(
command
,
silent
=
false
)
{
if
(
!
silent
)
{
console
.
log
(
`
${
colors
.
cyan
}
⚙️ Executing:
${
command
}${
colors
.
reset
}
`
);
}
try
{
const
output
=
execSync
(
command
,
{
encoding
:
'
utf8
'
,
stdio
:
silent
?
'
pipe
'
:
'
inherit
'
});
return
output
?
output
.
trim
()
:
''
;
}
catch
(
error
)
{
if
(
!
silent
)
{
console
.
error
(
`
${
colors
.
red
}
❌ Error:
${
error
.
message
}${
colors
.
reset
}
`
);
}
return
null
;
}
}
// Health check function
async
function
performHealthCheck
(
config
,
verbose
=
true
)
{
if
(
verbose
)
{
console
.
log
(
''
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
`
${
colors
.
blue
}
🏥 HEALTH CHECK
${
colors
.
reset
}
`
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
`Environment:
${
config
.
ENVIRONMENT
}
`
);
console
.
log
(
`URL: http://localhost:
${
config
.
APP_PORT
}${
config
.
HEALTH_CHECK_PATH
}
`
);
console
.
log
(
`Retries:
${
config
.
HEALTH_CHECK_RETRIES
}
`
);
console
.
log
(
`Interval:
${
config
.
HEALTH_CHECK_INTERVAL
}
ms`
);
console
.
log
(
'
─
'
.
repeat
(
60
));
}
for
(
let
i
=
0
;
i
<
config
.
HEALTH_CHECK_RETRIES
;
i
++
)
{
try
{
const
startTime
=
Date
.
now
();
await
new
Promise
((
resolve
,
reject
)
=>
{
const
url
=
`http://localhost:
${
config
.
APP_PORT
}${
config
.
HEALTH_CHECK_PATH
}
`
;
const
req
=
http
.
get
(
url
,
(
res
)
=>
{
const
responseTime
=
Date
.
now
()
-
startTime
;
if
(
res
.
statusCode
===
200
)
{
if
(
verbose
)
{
console
.
log
(
`
${
colors
.
green
}
✅ Health check passed (attempt
${
i
+
1
}
/
${
config
.
HEALTH_CHECK_RETRIES
}
)
${
colors
.
reset
}
`
);
console
.
log
(
` Status:
${
res
.
statusCode
}
| Response time:
${
responseTime
}
ms`
);
}
resolve
(
true
);
}
else
{
reject
(
new
Error
(
`Status code:
${
res
.
statusCode
}
`
));
}
});
req
.
on
(
'
error
'
,
(
error
)
=>
{
reject
(
error
);
});
req
.
setTimeout
(
config
.
HEALTH_CHECK_TIMEOUT
,
()
=>
{
req
.
destroy
();
reject
(
new
Error
(
'
Timeout
'
));
});
});
if
(
verbose
)
{
console
.
log
(
'
─
'
.
repeat
(
60
));
console
.
log
(
`
${
colors
.
green
}
✅ HEALTH CHECK SUCCESSFUL
${
colors
.
reset
}
`
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
''
);
}
return
true
;
}
catch
(
error
)
{
if
(
verbose
)
{
console
.
log
(
`
${
colors
.
yellow
}
⚠️ Health check failed (attempt
${
i
+
1
}
/
${
config
.
HEALTH_CHECK_RETRIES
}
)
${
colors
.
reset
}
`
);
console
.
log
(
` Error:
${
error
.
message
}
`
);
}
if
(
i
<
config
.
HEALTH_CHECK_RETRIES
-
1
)
{
if
(
verbose
)
{
console
.
log
(
` Waiting
${
config
.
HEALTH_CHECK_INTERVAL
}
ms before retry...`
);
}
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
config
.
HEALTH_CHECK_INTERVAL
));
}
}
}
if
(
verbose
)
{
console
.
log
(
'
─
'
.
repeat
(
60
));
console
.
log
(
`
${
colors
.
red
}
❌ HEALTH CHECK FAILED
${
colors
.
reset
}
`
);
console
.
log
(
`All
${
config
.
HEALTH_CHECK_RETRIES
}
attempts failed`
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
''
);
}
return
false
;
}
// Rollback function
async
function
performRollback
(
config
,
verbose
=
true
)
{
if
(
verbose
)
{
console
.
log
(
''
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
`
${
colors
.
yellow
}
🔙 ROLLBACK
${
colors
.
reset
}
`
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
`Environment:
${
config
.
ENVIRONMENT
}
`
);
console
.
log
(
`App Name:
${
config
.
APP_NAME
}
`
);
console
.
log
(
'
─
'
.
repeat
(
60
));
}
const
finalPath
=
path
.
join
(
process
.
cwd
(),
config
.
FINAL_DIST_DIR
);
const
backupPath
=
path
.
join
(
process
.
cwd
(),
config
.
BACKUP_DIST_DIR
);
// Check if backup exists
if
(
!
fs
.
existsSync
(
backupPath
))
{
console
.
error
(
`
${
colors
.
red
}
❌ No backup found at:
${
backupPath
}${
colors
.
reset
}
`
);
console
.
log
(
''
);
console
.
log
(
'
Cannot rollback without a backup.
'
);
console
.
log
(
'
═
'
.
repeat
(
60
));
return
false
;
}
try
{
// Remove current dist
if
(
fs
.
existsSync
(
finalPath
))
{
if
(
verbose
)
{
console
.
log
(
`🗑️ Removing current:
${
config
.
FINAL_DIST_DIR
}
`
);
}
fs
.
rmSync
(
finalPath
,
{
recursive
:
true
,
force
:
true
});
}
// Restore backup
if
(
verbose
)
{
console
.
log
(
`📦 Restoring backup:
${
config
.
BACKUP_DIST_DIR
}
→
${
config
.
FINAL_DIST_DIR
}
`
);
}
fs
.
renameSync
(
backupPath
,
finalPath
);
// Reload PM2
if
(
verbose
)
{
console
.
log
(
`🔄 Reloading PM2:
${
config
.
APP_NAME
}
`
);
}
const
reloadResult
=
executeCommand
(
`pm2 reload
${
config
.
APP_NAME
}
`
,
!
verbose
);
if
(
reloadResult
===
null
)
{
console
.
error
(
`
${
colors
.
red
}
❌ Failed to reload PM2
${
colors
.
reset
}
`
);
return
false
;
}
// Wait for app to stabilize
if
(
verbose
)
{
console
.
log
(
'
⏳ Waiting for app to stabilize...
'
);
}
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
3000
));
// Verify rollback with health check
if
(
verbose
)
{
console
.
log
(
'
🏥 Verifying rollback...
'
);
}
const
isHealthy
=
await
performHealthCheck
(
config
,
verbose
);
if
(
isHealthy
)
{
console
.
log
(
'
─
'
.
repeat
(
60
));
console
.
log
(
`
${
colors
.
green
}
✅ ROLLBACK SUCCESSFUL
${
colors
.
reset
}
`
);
console
.
log
(
'
Application has been restored to previous version
'
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
''
);
return
true
;
}
console
.
log
(
'
─
'
.
repeat
(
60
));
console
.
log
(
`
${
colors
.
red
}
❌ ROLLBACK COMPLETED BUT HEALTH CHECK FAILED
${
colors
.
reset
}
`
);
console
.
log
(
'
Previous version restored but application is not responding
'
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
''
);
return
false
;
}
catch
(
error
)
{
console
.
error
(
`
${
colors
.
red
}
❌ Rollback failed:
${
error
.
message
}${
colors
.
reset
}
`
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
''
);
return
false
;
}
}
// Check PM2 app status
async
function
checkAppStatus
(
config
,
verbose
=
true
)
{
if
(
verbose
)
{
console
.
log
(
''
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
`
${
colors
.
blue
}
📊 APPLICATION STATUS
${
colors
.
reset
}
`
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
`Environment:
${
config
.
ENVIRONMENT
}
`
);
console
.
log
(
`App Name:
${
config
.
APP_NAME
}
`
);
console
.
log
(
'
─
'
.
repeat
(
60
));
}
try
{
const
output
=
executeCommand
(
`pm2 jlist`
,
true
);
if
(
!
output
)
{
console
.
log
(
`
${
colors
.
red
}
❌ Failed to get PM2 status
${
colors
.
reset
}
`
);
return
false
;
}
const
processes
=
JSON
.
parse
(
output
);
const
app
=
processes
.
find
((
p
)
=>
p
.
name
===
config
.
APP_NAME
);
if
(
!
app
)
{
console
.
log
(
`
${
colors
.
yellow
}
⚠️ App "
${
config
.
APP_NAME
}
" not found in PM2
${
colors
.
reset
}
`
);
console
.
log
(
''
);
console
.
log
(
'
Available PM2 apps:
'
);
processes
.
forEach
((
p
)
=>
{
console
.
log
(
` -
${
p
.
name
}
(status:
${
p
.
pm2_env
.
status
}
)`
);
});
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
''
);
return
false
;
}
// Display app info
const
{
status
}
=
app
.
pm2_env
;
const
uptime
=
app
.
pm2_env
.
pm_uptime
;
const
memory
=
(
app
.
monit
.
memory
/
1024
/
1024
).
toFixed
(
2
);
const
{
cpu
}
=
app
.
monit
;
const
restarts
=
app
.
pm2_env
.
restart_time
;
const
statusColor
=
status
===
'
online
'
?
colors
.
green
:
colors
.
red
;
console
.
log
(
`Status:
${
statusColor
}${
status
.
toUpperCase
()}${
colors
.
reset
}
`
);
console
.
log
(
`PID:
${
app
.
pid
||
'
N/A
'
}
`
);
console
.
log
(
`Uptime:
${
uptime
?
new
Date
(
uptime
).
toLocaleString
()
:
'
N/A
'
}
`
);
console
.
log
(
`Memory:
${
memory
}
MB`
);
console
.
log
(
`CPU:
${
cpu
}
%`
);
console
.
log
(
`Restarts:
${
restarts
}
`
);
console
.
log
(
`Instances:
${
app
.
pm2_env
.
instances
||
1
}
`
);
console
.
log
(
`Mode:
${
app
.
pm2_env
.
exec_mode
||
'
fork
'
}
`
);
if
(
verbose
)
{
console
.
log
(
'
─
'
.
repeat
(
60
));
// Run health check
const
isHealthy
=
await
performHealthCheck
(
config
,
false
);
if
(
isHealthy
)
{
console
.
log
(
`Health:
${
colors
.
green
}
✅ HEALTHY
${
colors
.
reset
}
`
);
}
else
{
console
.
log
(
`Health:
${
colors
.
red
}
❌ UNHEALTHY
${
colors
.
reset
}
`
);
}
}
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
''
);
return
status
===
'
online
'
;
}
catch
(
error
)
{
console
.
error
(
`
${
colors
.
red
}
❌ Error checking status:
${
error
.
message
}${
colors
.
reset
}
`
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
''
);
return
false
;
}
}
// List all deployed apps
async
function
listApps
(
verbose
=
true
)
{
console
.
log
(
''
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
`
${
colors
.
blue
}
📋 PM2 APPLICATIONS
${
colors
.
reset
}
`
);
console
.
log
(
'
═
'
.
repeat
(
60
));
try
{
const
output
=
executeCommand
(
`pm2 jlist`
,
true
);
if
(
!
output
)
{
console
.
log
(
`
${
colors
.
red
}
❌ Failed to get PM2 list
${
colors
.
reset
}
`
);
return
;
}
const
processes
=
JSON
.
parse
(
output
);
if
(
processes
.
length
===
0
)
{
console
.
log
(
`
${
colors
.
yellow
}
⚠️ No PM2 applications running
${
colors
.
reset
}
`
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
''
);
return
;
}
console
.
log
(
''
);
processes
.
forEach
((
app
,
index
)
=>
{
const
{
status
}
=
app
.
pm2_env
;
const
statusColor
=
status
===
'
online
'
?
colors
.
green
:
colors
.
red
;
const
memory
=
(
app
.
monit
.
memory
/
1024
/
1024
).
toFixed
(
2
);
console
.
log
(
`
${
index
+
1
}
.
${
colors
.
cyan
}${
app
.
name
}${
colors
.
reset
}
`
);
console
.
log
(
` Status:
${
statusColor
}${
status
}${
colors
.
reset
}
`
);
console
.
log
(
` PID:
${
app
.
pid
||
'
N/A
'
}
`
);
console
.
log
(
` Memory:
${
memory
}
MB`
);
console
.
log
(
` CPU:
${
app
.
monit
.
cpu
}
%`
);
console
.
log
(
` Restarts:
${
app
.
pm2_env
.
restart_time
}
`
);
console
.
log
(
''
);
});
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
''
);
}
catch
(
error
)
{
console
.
error
(
`
${
colors
.
red
}
❌ Error listing apps:
${
error
.
message
}${
colors
.
reset
}
`
);
}
}
// Cleanup old backups
function
cleanupBackups
(
config
,
verbose
=
true
)
{
if
(
verbose
)
{
console
.
log
(
''
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
`
${
colors
.
blue
}
🧹 CLEANUP BACKUPS
${
colors
.
reset
}
`
);
console
.
log
(
'
═
'
.
repeat
(
60
));
}
const
backupPath
=
path
.
join
(
process
.
cwd
(),
config
.
BACKUP_DIST_DIR
);
if
(
fs
.
existsSync
(
backupPath
))
{
if
(
verbose
)
{
console
.
log
(
`Removing backup:
${
config
.
BACKUP_DIST_DIR
}
`
);
}
fs
.
rmSync
(
backupPath
,
{
recursive
:
true
,
force
:
true
});
console
.
log
(
`
${
colors
.
green
}
✅ Backup removed successfully
${
colors
.
reset
}
`
);
}
else
{
console
.
log
(
`
${
colors
.
yellow
}
⚠️ No backup found
${
colors
.
reset
}
`
);
}
if
(
verbose
)
{
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
''
);
}
}
// Main CLI handler
async
function
main
()
{
const
args
=
process
.
argv
.
slice
(
2
);
const
command
=
args
[
0
];
const
environment
=
args
[
1
]
||
'
production
'
;
// Validate environment
const
validEnvironments
=
[
'
development
'
,
'
staging
'
,
'
production
'
];
if
(
!
validEnvironments
.
includes
(
environment
))
{
console
.
error
(
`
${
colors
.
red
}
❌ Invalid environment:
${
environment
}${
colors
.
reset
}
`
);
console
.
log
(
`Valid environments:
${
validEnvironments
.
join
(
'
,
'
)}
`
);
process
.
exit
(
1
);
}
// Load environment configuration
loadEnvConfig
(
environment
);
const
config
=
getConfig
(
environment
);
// Execute command
switch
(
command
)
{
case
'
health-check
'
:
case
'
health
'
:
const
healthResult
=
await
performHealthCheck
(
config
);
process
.
exit
(
healthResult
?
0
:
1
);
break
;
case
'
rollback
'
:
const
rollbackResult
=
await
performRollback
(
config
);
process
.
exit
(
rollbackResult
?
0
:
1
);
break
;
case
'
status
'
:
const
statusResult
=
await
checkAppStatus
(
config
);
process
.
exit
(
statusResult
?
0
:
1
);
break
;
case
'
list
'
:
await
listApps
();
break
;
case
'
cleanup
'
:
cleanupBackups
(
config
);
break
;
default
:
console
.
log
(
''
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
`
${
colors
.
blue
}
🛠️ DEPLOYMENT UTILITIES
${
colors
.
reset
}
`
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
''
);
console
.
log
(
'
Usage: node utils.js <command> [environment]
'
);
console
.
log
(
''
);
console
.
log
(
'
Commands:
'
);
console
.
log
(
'
health-check Check application health
'
);
console
.
log
(
'
rollback Rollback to previous version
'
);
console
.
log
(
'
status Show application status
'
);
console
.
log
(
'
list List all PM2 applications
'
);
console
.
log
(
'
cleanup Remove backup files
'
);
console
.
log
(
''
);
console
.
log
(
'
Environments:
'
);
console
.
log
(
'
development
'
);
console
.
log
(
'
staging
'
);
console
.
log
(
'
production (default)
'
);
console
.
log
(
''
);
console
.
log
(
'
Examples:
'
);
console
.
log
(
`
${
colors
.
cyan
}
node utils.js health-check production
${
colors
.
reset
}
`
);
console
.
log
(
`
${
colors
.
cyan
}
node utils.js rollback staging
${
colors
.
reset
}
`
);
console
.
log
(
`
${
colors
.
cyan
}
node utils.js status development
${
colors
.
reset
}
`
);
console
.
log
(
`
${
colors
.
cyan
}
node utils.js list
${
colors
.
reset
}
`
);
console
.
log
(
''
);
console
.
log
(
'
Or use package.json scripts:
'
);
console
.
log
(
`
${
colors
.
cyan
}
yarn health:prod
${
colors
.
reset
}
`
);
console
.
log
(
`
${
colors
.
cyan
}
yarn rollback:prod
${
colors
.
reset
}
`
);
console
.
log
(
`
${
colors
.
cyan
}
yarn status:prod
${
colors
.
reset
}
`
);
console
.
log
(
'
═
'
.
repeat
(
60
));
console
.
log
(
''
);
process
.
exit
(
1
);
}
}
// Run if called directly
if
(
process
.
argv
[
1
]
===
fileURLToPath
(
import
.
meta
.
url
))
{
main
();
}
export
{
performHealthCheck
,
performRollback
,
checkAppStatus
,
listApps
,
cleanupBackups
};
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment