scripts-orchestrator 2.10.0 → 2.13.0
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/index.js +48 -1
- package/lib/health-check.js +27 -11
- package/lib/logger.js +142 -12
- package/lib/orchestrator.js +387 -72
- package/lib/process-manager.js +336 -100
- package/package.json +1 -1
- package/scripts-orchestrator.config.js +6 -0
package/index.js
CHANGED
|
@@ -39,6 +39,18 @@ const argv = yargs(hideBin(process.argv))
|
|
|
39
39
|
type: 'boolean',
|
|
40
40
|
description: 'Force execution even if git state is unchanged',
|
|
41
41
|
})
|
|
42
|
+
.option('metrics', {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'Comma-separated metrics to collect and report: time, memory',
|
|
45
|
+
})
|
|
46
|
+
.option('json-results', {
|
|
47
|
+
type: 'string',
|
|
48
|
+
description: 'Write results JSON to this path; use "-" for stdout only',
|
|
49
|
+
})
|
|
50
|
+
.option('html-results', {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'Write HTML report to this path; use "-" for stdout only',
|
|
53
|
+
})
|
|
42
54
|
.help()
|
|
43
55
|
.alias('h', 'help')
|
|
44
56
|
.parse();
|
|
@@ -52,6 +64,8 @@ const phases = argv.phases ? argv.phases.split(',').map(p => p.trim()) : null;
|
|
|
52
64
|
const sequential = argv.sequential || false;
|
|
53
65
|
const force = argv.force || false;
|
|
54
66
|
|
|
67
|
+
const validMetrics = ['time', 'memory'];
|
|
68
|
+
|
|
55
69
|
// Validate config file exists
|
|
56
70
|
if (!fs.existsSync(configPath)) {
|
|
57
71
|
log.error(`Error: Config file not found at ${configPath}`);
|
|
@@ -74,13 +88,46 @@ if (!logFolder && commandsConfig.log_folder) {
|
|
|
74
88
|
logFolder = commandsConfig.log_folder;
|
|
75
89
|
}
|
|
76
90
|
|
|
91
|
+
// Metrics: CLI overrides config
|
|
92
|
+
let metrics = [];
|
|
93
|
+
if (argv.metrics != null && argv.metrics !== '') {
|
|
94
|
+
metrics = argv.metrics.split(',').map((m) => m.trim()).filter((m) => validMetrics.includes(m));
|
|
95
|
+
} else if (commandsConfig.metrics != null) {
|
|
96
|
+
const fromConfig = Array.isArray(commandsConfig.metrics)
|
|
97
|
+
? commandsConfig.metrics
|
|
98
|
+
: String(commandsConfig.metrics).split(',').map((m) => m.trim());
|
|
99
|
+
metrics = fromConfig.filter((m) => validMetrics.includes(m));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// JSON results path: CLI overrides config
|
|
103
|
+
const jsonResultsPath =
|
|
104
|
+
argv.jsonResults != null
|
|
105
|
+
? argv.jsonResults
|
|
106
|
+
: (commandsConfig.json_results ?? commandsConfig.json_results_path ?? null);
|
|
107
|
+
|
|
108
|
+
// HTML results path: CLI overrides config (optional)
|
|
109
|
+
const htmlResultsPath =
|
|
110
|
+
argv.htmlResults != null
|
|
111
|
+
? argv.htmlResults
|
|
112
|
+
: (commandsConfig.html_results ?? commandsConfig.html_results_path ?? null);
|
|
113
|
+
|
|
77
114
|
// Set the log folder for the main orchestrator logs if specified
|
|
78
115
|
if (logFolder) {
|
|
79
116
|
log.setLogFolder(logFolder);
|
|
80
117
|
}
|
|
81
118
|
|
|
82
119
|
// Create and run the orchestrator
|
|
83
|
-
const orchestrator = new Orchestrator(
|
|
120
|
+
const orchestrator = new Orchestrator(
|
|
121
|
+
commandsConfig,
|
|
122
|
+
startPhase,
|
|
123
|
+
logFolder,
|
|
124
|
+
phases,
|
|
125
|
+
sequential,
|
|
126
|
+
force,
|
|
127
|
+
metrics,
|
|
128
|
+
jsonResultsPath,
|
|
129
|
+
htmlResultsPath,
|
|
130
|
+
);
|
|
84
131
|
|
|
85
132
|
// Enhanced signal handlers
|
|
86
133
|
const handleSignal = async (signal) => {
|
package/lib/health-check.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { log } from './logger.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
|
|
3
4
|
export class HealthCheck {
|
|
4
5
|
constructor() {
|
|
@@ -17,17 +18,17 @@ export class HealthCheck {
|
|
|
17
18
|
const urlObj = new URL(url);
|
|
18
19
|
const isHttps = urlObj.protocol === 'https:';
|
|
19
20
|
const httpModule = isHttps ? await import('https') : await import('http');
|
|
20
|
-
|
|
21
|
+
|
|
21
22
|
return await new Promise((resolve) => {
|
|
22
23
|
const req = httpModule.default.get(url, (res) => {
|
|
23
24
|
resolve({ statusCode: res.statusCode, success: true });
|
|
24
25
|
res.destroy(); // Close the response stream
|
|
25
26
|
});
|
|
26
|
-
|
|
27
|
+
|
|
27
28
|
req.on('error', (error) => {
|
|
28
29
|
resolve({ statusCode: null, success: false, error: error.message });
|
|
29
30
|
});
|
|
30
|
-
|
|
31
|
+
|
|
31
32
|
req.setTimeout(timeout, () => {
|
|
32
33
|
req.destroy();
|
|
33
34
|
resolve({ statusCode: null, success: false, error: 'Timeout' });
|
|
@@ -38,30 +39,45 @@ export class HealthCheck {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
async waitForUrl({url, maxAttempts = 20, interval = 2000, silent=false}) {
|
|
42
|
-
!silent
|
|
42
|
+
async waitForUrl({ url, maxAttempts = 20, interval = 2000, silent = false }) {
|
|
43
|
+
if (!silent) {
|
|
44
|
+
this.logger.startEphemeral(
|
|
45
|
+
`wait_${url}`,
|
|
46
|
+
chalk.yellow(`[INFO] ⏳ Waiting for ${url} to be available...`),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
43
49
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
44
50
|
try {
|
|
45
51
|
const result = await HealthCheck.makeHttpRequest(url, 5000);
|
|
46
52
|
|
|
47
53
|
if (result.success && result.statusCode === 200) {
|
|
48
|
-
!silent
|
|
54
|
+
if (!silent) {
|
|
55
|
+
this.logger.stopEphemeral(`wait_${url}`, `✅ ${url} is available`);
|
|
56
|
+
}
|
|
49
57
|
return true;
|
|
50
58
|
}
|
|
51
59
|
} catch (error) {
|
|
52
|
-
!silent
|
|
60
|
+
if (!silent) {
|
|
61
|
+
// You could potentially log attempts via verbose if needed, but not necessary.
|
|
62
|
+
}
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
if (attempt < maxAttempts) {
|
|
56
66
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
57
67
|
}
|
|
58
68
|
}
|
|
59
|
-
|
|
60
|
-
!silent
|
|
61
|
-
|
|
69
|
+
|
|
70
|
+
if (!silent) {
|
|
71
|
+
this.logger.stopEphemeral(
|
|
72
|
+
`wait_${url}`,
|
|
73
|
+
`❌ Failed to connect to ${url} after ${maxAttempts} attempts`,
|
|
74
|
+
true,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
return false;
|
|
63
79
|
}
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
// For backward compatibility
|
|
67
|
-
export const healthCheck = new HealthCheck();
|
|
83
|
+
export const healthCheck = new HealthCheck();
|
package/lib/logger.js
CHANGED
|
@@ -30,9 +30,131 @@ class Logger {
|
|
|
30
30
|
this.logFolder = argv.logFolder || 'scripts-orchestrator-logs';
|
|
31
31
|
this.logFile = null;
|
|
32
32
|
this.logStream = null;
|
|
33
|
+
|
|
34
|
+
// For TTY dynamic output
|
|
35
|
+
this.isTTY = process.stdout.isTTY;
|
|
36
|
+
this.activeTasks = new Map();
|
|
37
|
+
this.linesRendered = 0;
|
|
38
|
+
|
|
33
39
|
this.initializeLogFile();
|
|
34
40
|
}
|
|
35
41
|
|
|
42
|
+
// --- Dynamic Output Handling ---
|
|
43
|
+
|
|
44
|
+
clearActiveTasks() {
|
|
45
|
+
if (!this.isTTY || this.linesRendered === 0) return;
|
|
46
|
+
// Move cursor up and clear lines
|
|
47
|
+
for (let i = 0; i < this.linesRendered; i++) {
|
|
48
|
+
process.stdout.write('\x1b[1A\x1b[2K'); // Up one line, clear entire line
|
|
49
|
+
}
|
|
50
|
+
this.linesRendered = 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
renderActiveTasks() {
|
|
54
|
+
if (!this.isTTY) return;
|
|
55
|
+
if (this.activeTasks.size === 0) return;
|
|
56
|
+
|
|
57
|
+
// Render active tasks with a spinner or prefix
|
|
58
|
+
for (const [, text] of this.activeTasks) {
|
|
59
|
+
process.stdout.write(`${text}\n`);
|
|
60
|
+
this.linesRendered++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
startTask(id, text) {
|
|
65
|
+
if (this.isTTY) {
|
|
66
|
+
this.clearActiveTasks();
|
|
67
|
+
this.activeTasks.set(id, chalk.cyan(`[INFO] ⏳ Running: ${text}`));
|
|
68
|
+
this.renderActiveTasks();
|
|
69
|
+
} else {
|
|
70
|
+
this.info(`Running: ${text}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
updateTask(id, text) {
|
|
75
|
+
if (this.isTTY && this.activeTasks.has(id)) {
|
|
76
|
+
this.clearActiveTasks();
|
|
77
|
+
this.activeTasks.set(id, chalk.cyan(`[INFO] ⏳ Running: ${text}`));
|
|
78
|
+
this.renderActiveTasks();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
stopTask(id) {
|
|
83
|
+
if (this.isTTY && this.activeTasks.has(id)) {
|
|
84
|
+
this.clearActiveTasks();
|
|
85
|
+
this.activeTasks.delete(id);
|
|
86
|
+
this.renderActiveTasks();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- Wrapper for output methods ---
|
|
91
|
+
|
|
92
|
+
printMessage(logFn) {
|
|
93
|
+
if (this.isTTY) {
|
|
94
|
+
this.clearActiveTasks();
|
|
95
|
+
logFn();
|
|
96
|
+
this.renderActiveTasks();
|
|
97
|
+
} else {
|
|
98
|
+
logFn();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
startEphemeral(id, message) {
|
|
103
|
+
if (this.isTTY) {
|
|
104
|
+
this.clearActiveTasks();
|
|
105
|
+
this.activeTasks.set(id, message);
|
|
106
|
+
this.renderActiveTasks();
|
|
107
|
+
} else {
|
|
108
|
+
this.printMessage(() => console.log(message));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
stopEphemeral(id, finalMessage = '', isError = false) {
|
|
113
|
+
if (this.isTTY && this.activeTasks.has(id)) {
|
|
114
|
+
this.clearActiveTasks();
|
|
115
|
+
this.activeTasks.delete(id);
|
|
116
|
+
this.renderActiveTasks();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (finalMessage) {
|
|
120
|
+
if (isError) {
|
|
121
|
+
this.printMessage(() =>
|
|
122
|
+
console.error(chalk.red(`[ERROR] ${finalMessage}`)),
|
|
123
|
+
);
|
|
124
|
+
} else {
|
|
125
|
+
this.printMessage(() =>
|
|
126
|
+
console.log(chalk.green(`[SUCCESS] ${finalMessage}`)),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
stopPhase(phaseName, success, durationStr) {
|
|
133
|
+
if (success) {
|
|
134
|
+
this.printMessage(() =>
|
|
135
|
+
console.log(
|
|
136
|
+
chalk.green(
|
|
137
|
+
`[SUCCESS] ✅ Phase "${phaseName}" completed successfully ${durationStr}`,
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
// Add a blank line after phase logs for readability
|
|
142
|
+
this.printMessage(() => console.log(''));
|
|
143
|
+
this.writeToFile('');
|
|
144
|
+
} else {
|
|
145
|
+
this.printMessage(() =>
|
|
146
|
+
console.error(
|
|
147
|
+
chalk.red(
|
|
148
|
+
`[ERROR] ❌ Phase "${phaseName}" completed with failures ${durationStr}`,
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
// Add a blank line after phase logs for readability
|
|
153
|
+
this.printMessage(() => console.log(''));
|
|
154
|
+
this.writeToFile('');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
36
158
|
initializeLogFile() {
|
|
37
159
|
try {
|
|
38
160
|
// Create log directory if it doesn't exist
|
|
@@ -41,12 +163,18 @@ class Logger {
|
|
|
41
163
|
}
|
|
42
164
|
|
|
43
165
|
// Create main log file with timestamp
|
|
44
|
-
const timestamp = new Date()
|
|
45
|
-
|
|
46
|
-
|
|
166
|
+
const timestamp = new Date()
|
|
167
|
+
.toISOString()
|
|
168
|
+
.replace(/:/g, '-')
|
|
169
|
+
.replace(/\..+/, '');
|
|
170
|
+
this.logFile = path.join(
|
|
171
|
+
this.logFolder,
|
|
172
|
+
`orchestrator-main-${timestamp}.log`,
|
|
173
|
+
);
|
|
174
|
+
|
|
47
175
|
// Create write stream
|
|
48
176
|
this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
|
|
49
|
-
|
|
177
|
+
|
|
50
178
|
// Handle stream errors
|
|
51
179
|
this.logStream.on('error', (err) => {
|
|
52
180
|
console.error(`Error writing to log file: ${err.message}`);
|
|
@@ -60,7 +188,9 @@ class Logger {
|
|
|
60
188
|
});
|
|
61
189
|
|
|
62
190
|
// Write initial log entry
|
|
63
|
-
this.writeToFile(
|
|
191
|
+
this.writeToFile(
|
|
192
|
+
`[START] Orchestrator started at ${new Date().toISOString()}\n`,
|
|
193
|
+
);
|
|
64
194
|
} catch (error) {
|
|
65
195
|
console.error(`Failed to initialize log file: ${error.message}`);
|
|
66
196
|
}
|
|
@@ -74,7 +204,7 @@ class Logger {
|
|
|
74
204
|
|
|
75
205
|
// Update log folder
|
|
76
206
|
this.logFolder = newLogFolder;
|
|
77
|
-
|
|
207
|
+
|
|
78
208
|
// Reinitialize with new folder
|
|
79
209
|
this.initializeLogFile();
|
|
80
210
|
}
|
|
@@ -89,28 +219,28 @@ class Logger {
|
|
|
89
219
|
}
|
|
90
220
|
|
|
91
221
|
info(message) {
|
|
92
|
-
console.log(chalk.blue(`[INFO] ${message}`));
|
|
222
|
+
this.printMessage(() => console.log(chalk.blue(`[INFO] ${message}`)));
|
|
93
223
|
this.writeToFile(`[INFO] ${message}`);
|
|
94
224
|
}
|
|
95
225
|
|
|
96
226
|
success(message) {
|
|
97
|
-
console.log(chalk.green(`[SUCCESS] ${message}`));
|
|
227
|
+
this.printMessage(() => console.log(chalk.green(`[SUCCESS] ${message}`)));
|
|
98
228
|
this.writeToFile(`[SUCCESS] ${message}`);
|
|
99
229
|
}
|
|
100
230
|
|
|
101
231
|
error(message) {
|
|
102
|
-
console.error(chalk.red(`[ERROR] ${message}`));
|
|
232
|
+
this.printMessage(() => console.error(chalk.red(`[ERROR] ${message}`)));
|
|
103
233
|
this.writeToFile(`[ERROR] ${message}`);
|
|
104
234
|
}
|
|
105
235
|
|
|
106
236
|
warn(message) {
|
|
107
|
-
console.warn(chalk.yellow(`[WARN] ${message}`));
|
|
237
|
+
this.printMessage(() => console.warn(chalk.yellow(`[WARN] ${message}`)));
|
|
108
238
|
this.writeToFile(`[WARN] ${message}`);
|
|
109
239
|
}
|
|
110
240
|
|
|
111
241
|
verbose(message) {
|
|
112
242
|
if (this.isVerbose) {
|
|
113
|
-
console.log(chalk.gray(`[VERBOSE] ${message}`));
|
|
243
|
+
this.printMessage(() => console.log(chalk.gray(`[VERBOSE] ${message}`)));
|
|
114
244
|
this.writeToFile(`[VERBOSE] ${message}`);
|
|
115
245
|
}
|
|
116
246
|
}
|
|
@@ -120,4 +250,4 @@ class Logger {
|
|
|
120
250
|
const logger = new Logger();
|
|
121
251
|
|
|
122
252
|
// Export both the class and the instance
|
|
123
|
-
export { Logger, logger as log };
|
|
253
|
+
export { Logger, logger as log };
|