scripts-orchestrator 2.12.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/orchestrator.js +257 -16
- package/lib/process-manager.js +109 -14
- 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/orchestrator.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
1
2
|
import { processManager } from './process-manager.js';
|
|
2
3
|
import { healthCheck } from './health-check.js';
|
|
3
4
|
import { log } from './logger.js';
|
|
@@ -12,6 +13,9 @@ export class Orchestrator {
|
|
|
12
13
|
phases = null,
|
|
13
14
|
sequential = false,
|
|
14
15
|
force = false,
|
|
16
|
+
metrics = [],
|
|
17
|
+
jsonResultsPath = null,
|
|
18
|
+
htmlResultsPath = null,
|
|
15
19
|
) {
|
|
16
20
|
this.config = config;
|
|
17
21
|
this.startPhase = startPhase;
|
|
@@ -19,13 +23,17 @@ export class Orchestrator {
|
|
|
19
23
|
this.phases = phases;
|
|
20
24
|
this.sequential = sequential;
|
|
21
25
|
this.force = force;
|
|
26
|
+
this.metrics = Array.isArray(metrics) ? metrics : [];
|
|
27
|
+
this.jsonResultsPath = jsonResultsPath ?? null;
|
|
28
|
+
this.htmlResultsPath = htmlResultsPath ?? null;
|
|
22
29
|
this.processManager = processManager;
|
|
23
30
|
this.healthCheck = healthCheck;
|
|
24
31
|
this.logger = log;
|
|
25
32
|
this.failedCommands = [];
|
|
26
33
|
this.skippedCommands = [];
|
|
27
34
|
this.skipReasons = new Map(); // Track why commands were skipped
|
|
28
|
-
this.commandTimings = new Map();
|
|
35
|
+
this.commandTimings = new Map(); // command -> { durationMs, memoryKb? }
|
|
36
|
+
this.phaseResults = []; // { name, success, durationMs } per phase run
|
|
29
37
|
this.gitCache = new GitCache(logFolder);
|
|
30
38
|
|
|
31
39
|
// Set the log folder in process manager
|
|
@@ -80,13 +88,17 @@ export class Orchestrator {
|
|
|
80
88
|
|
|
81
89
|
const startTime = Date.now();
|
|
82
90
|
|
|
91
|
+
const setTiming = (durationMs, memoryKb = null) => {
|
|
92
|
+
this.commandTimings.set(command, { durationMs, memoryKb });
|
|
93
|
+
};
|
|
94
|
+
|
|
83
95
|
// Check for circular dependencies
|
|
84
96
|
if (visited.has(command)) {
|
|
85
97
|
this.logger.error(
|
|
86
98
|
`Circular dependency detected: ${Array.from(visited).join(' -> ')} -> ${command}`,
|
|
87
99
|
);
|
|
88
100
|
this.failedCommands.push(command);
|
|
89
|
-
|
|
101
|
+
setTiming(Date.now() - startTime);
|
|
90
102
|
return false;
|
|
91
103
|
}
|
|
92
104
|
visited.add(command);
|
|
@@ -96,7 +108,7 @@ export class Orchestrator {
|
|
|
96
108
|
this.logger.warn(`Skipping: npm run ${command} (status: disabled)`);
|
|
97
109
|
this.skippedCommands.push(command);
|
|
98
110
|
this.skipReasons.set(command, 'disabled');
|
|
99
|
-
|
|
111
|
+
setTiming(Date.now() - startTime);
|
|
100
112
|
visited.delete(command);
|
|
101
113
|
return true;
|
|
102
114
|
}
|
|
@@ -127,7 +139,7 @@ export class Orchestrator {
|
|
|
127
139
|
process_tracking,
|
|
128
140
|
kill_command,
|
|
129
141
|
});
|
|
130
|
-
|
|
142
|
+
setTiming(Date.now() - startTime);
|
|
131
143
|
visited.delete(command);
|
|
132
144
|
return true;
|
|
133
145
|
}
|
|
@@ -140,7 +152,7 @@ export class Orchestrator {
|
|
|
140
152
|
this.logger.error(`Skipping ${command} due to failed dependency`);
|
|
141
153
|
this.skippedCommands.push(command);
|
|
142
154
|
this.skipReasons.set(command, 'failed_dependency');
|
|
143
|
-
|
|
155
|
+
setTiming(Date.now() - startTime);
|
|
144
156
|
visited.delete(command);
|
|
145
157
|
return false;
|
|
146
158
|
}
|
|
@@ -154,7 +166,7 @@ export class Orchestrator {
|
|
|
154
166
|
if (!urlAvailable) {
|
|
155
167
|
this.skippedCommands.push(command);
|
|
156
168
|
this.skipReasons.set(command, 'failed_dependency');
|
|
157
|
-
|
|
169
|
+
setTiming(Date.now() - startTime);
|
|
158
170
|
visited.delete(command);
|
|
159
171
|
return false;
|
|
160
172
|
}
|
|
@@ -176,6 +188,7 @@ export class Orchestrator {
|
|
|
176
188
|
let result = false;
|
|
177
189
|
let commandOutput = '';
|
|
178
190
|
let commandFailed = false;
|
|
191
|
+
let lastRunResult = null;
|
|
179
192
|
|
|
180
193
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
181
194
|
if (attempt > 1) {
|
|
@@ -185,7 +198,7 @@ export class Orchestrator {
|
|
|
185
198
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
186
199
|
}
|
|
187
200
|
|
|
188
|
-
const
|
|
201
|
+
const runResult = await this.processManager.runCommand({
|
|
189
202
|
cmd: attempt === 1 ? command : retry_command || command,
|
|
190
203
|
logFile: log || logFile, // Prefer 'log' key over 'logFile' for backwards compatibility
|
|
191
204
|
background,
|
|
@@ -193,7 +206,11 @@ export class Orchestrator {
|
|
|
193
206
|
kill_command,
|
|
194
207
|
isRetry: attempt > 1,
|
|
195
208
|
env,
|
|
209
|
+
reportTime: this.metrics.includes('time'),
|
|
210
|
+
reportMemory: this.metrics.includes('memory'),
|
|
196
211
|
});
|
|
212
|
+
lastRunResult = runResult;
|
|
213
|
+
const { success, output } = runResult;
|
|
197
214
|
commandOutput = output;
|
|
198
215
|
result = success;
|
|
199
216
|
|
|
@@ -239,7 +256,8 @@ export class Orchestrator {
|
|
|
239
256
|
}
|
|
240
257
|
}
|
|
241
258
|
|
|
242
|
-
|
|
259
|
+
const totalDurationMs = Date.now() - startTime;
|
|
260
|
+
setTiming(totalDurationMs, lastRunResult?.memoryKb ?? null);
|
|
243
261
|
visited.delete(command);
|
|
244
262
|
return result;
|
|
245
263
|
}
|
|
@@ -269,6 +287,217 @@ export class Orchestrator {
|
|
|
269
287
|
}
|
|
270
288
|
}
|
|
271
289
|
|
|
290
|
+
writeJsonResults(hasFailures) {
|
|
291
|
+
const overallDurationMs =
|
|
292
|
+
this.metrics.includes('time') && this.startTime
|
|
293
|
+
? Date.now() - this.startTime
|
|
294
|
+
: undefined;
|
|
295
|
+
|
|
296
|
+
const commands = [];
|
|
297
|
+
if (Array.isArray(this.config)) {
|
|
298
|
+
this.config.forEach(({ command }) => {
|
|
299
|
+
const timing = this.commandTimings.get(command);
|
|
300
|
+
const skipReason = this.skippedCommands.includes(command)
|
|
301
|
+
? this.skipReasons.get(command) ?? null
|
|
302
|
+
: null;
|
|
303
|
+
const success =
|
|
304
|
+
!this.failedCommands.includes(command) &&
|
|
305
|
+
(skipReason === null ||
|
|
306
|
+
skipReason === 'disabled' ||
|
|
307
|
+
skipReason === 'optional_phase_not_requested' ||
|
|
308
|
+
skipReason === 'before_start_phase');
|
|
309
|
+
const entry = {
|
|
310
|
+
command,
|
|
311
|
+
success,
|
|
312
|
+
...(timing?.durationMs != null && this.metrics.includes('time')
|
|
313
|
+
? { durationMs: timing.durationMs }
|
|
314
|
+
: {}),
|
|
315
|
+
...(this.metrics.includes('memory')
|
|
316
|
+
? { memoryKb: timing?.memoryKb ?? null }
|
|
317
|
+
: {}),
|
|
318
|
+
...(skipReason ? { skipReason } : {}),
|
|
319
|
+
};
|
|
320
|
+
commands.push(entry);
|
|
321
|
+
});
|
|
322
|
+
} else if (this.config.phases) {
|
|
323
|
+
this.config.phases.forEach((phase) => {
|
|
324
|
+
(phase.parallel || []).forEach(({ command }) => {
|
|
325
|
+
const timing = this.commandTimings.get(command);
|
|
326
|
+
const skipReason = this.skippedCommands.includes(command)
|
|
327
|
+
? this.skipReasons.get(command) ?? null
|
|
328
|
+
: null;
|
|
329
|
+
const success =
|
|
330
|
+
!this.failedCommands.includes(command) &&
|
|
331
|
+
(skipReason === null ||
|
|
332
|
+
skipReason === 'disabled' ||
|
|
333
|
+
skipReason === 'optional_phase_not_requested' ||
|
|
334
|
+
skipReason === 'before_start_phase');
|
|
335
|
+
const entry = {
|
|
336
|
+
command,
|
|
337
|
+
phase: phase.name,
|
|
338
|
+
success,
|
|
339
|
+
...(timing?.durationMs != null && this.metrics.includes('time')
|
|
340
|
+
? { durationMs: timing.durationMs }
|
|
341
|
+
: {}),
|
|
342
|
+
...(this.metrics.includes('memory')
|
|
343
|
+
? { memoryKb: timing?.memoryKb ?? null }
|
|
344
|
+
: {}),
|
|
345
|
+
...(skipReason ? { skipReason } : {}),
|
|
346
|
+
};
|
|
347
|
+
commands.push(entry);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const payload = {
|
|
353
|
+
success: !hasFailures,
|
|
354
|
+
timestamp: new Date().toISOString(),
|
|
355
|
+
...(overallDurationMs != null ? { overallDurationMs } : {}),
|
|
356
|
+
commands,
|
|
357
|
+
...(this.config.phases && this.phaseResults.length > 0
|
|
358
|
+
? { phases: this.phaseResults }
|
|
359
|
+
: {}),
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const json = JSON.stringify(payload, null, 2);
|
|
363
|
+
if (this.jsonResultsPath === '-') {
|
|
364
|
+
console.log(json);
|
|
365
|
+
} else {
|
|
366
|
+
const outPath = this.jsonResultsPath || './scripts-orchestrator-results.json';
|
|
367
|
+
fs.writeFileSync(outPath, json, 'utf8');
|
|
368
|
+
this.logger.verbose(`Wrote results to ${outPath}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (this.htmlResultsPath != null) {
|
|
372
|
+
this.writeHtmlResults(payload);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
formatMs(ms) {
|
|
377
|
+
if (ms == null || ms === 0) return '—';
|
|
378
|
+
if (ms < 1000) return `${ms}ms`;
|
|
379
|
+
const s = (ms / 1000).toFixed(1);
|
|
380
|
+
return `${s}s`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
writeHtmlResults(payload) {
|
|
384
|
+
const escapeHtml = (s) => {
|
|
385
|
+
if (s == null) return '';
|
|
386
|
+
return String(s)
|
|
387
|
+
.replace(/&/g, '&')
|
|
388
|
+
.replace(/</g, '<')
|
|
389
|
+
.replace(/>/g, '>')
|
|
390
|
+
.replace(/"/g, '"');
|
|
391
|
+
};
|
|
392
|
+
const { success, timestamp, overallDurationMs, commands = [], phases = [] } = payload;
|
|
393
|
+
const maxDuration = Math.max(0, ...commands.map((c) => c.durationMs || 0), ...phases.map((p) => p.durationMs || 0));
|
|
394
|
+
const maxMemory = Math.max(0, ...commands.map((c) => c.memoryKb || 0));
|
|
395
|
+
|
|
396
|
+
const row = (c) => {
|
|
397
|
+
const durationPct = maxDuration > 0 && c.durationMs != null ? (c.durationMs / maxDuration) * 100 : 0;
|
|
398
|
+
const memoryPct = maxMemory > 0 && c.memoryKb != null ? (c.memoryKb / maxMemory) * 100 : 0;
|
|
399
|
+
const statusClass = c.success ? 'ok' : 'fail';
|
|
400
|
+
const statusLabel = c.success ? 'OK' : (c.skipReason || 'Failed');
|
|
401
|
+
return `
|
|
402
|
+
<tr class="${statusClass}">
|
|
403
|
+
<td><code>${escapeHtml(c.command)}</code></td>
|
|
404
|
+
<td>${c.phase != null ? escapeHtml(c.phase) : '—'}</td>
|
|
405
|
+
<td><span class="badge ${statusClass}">${escapeHtml(statusLabel)}</span></td>
|
|
406
|
+
<td>${this.formatMs(c.durationMs)}</td>
|
|
407
|
+
<td>${c.memoryKb != null ? `${(c.memoryKb / 1024).toFixed(1)} MB` : '—'}</td>
|
|
408
|
+
<td class="bar-cell"><div class="bar" style="width:${durationPct}%"></div></td>
|
|
409
|
+
<td class="bar-cell"><div class="bar mem" style="width:${memoryPct}%"></div></td>
|
|
410
|
+
</tr>`;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const phaseRow = (p) => {
|
|
414
|
+
const durationPct = maxDuration > 0 && p.durationMs != null ? (p.durationMs / maxDuration) * 100 : 0;
|
|
415
|
+
const statusClass = p.success ? 'ok' : 'fail';
|
|
416
|
+
return `
|
|
417
|
+
<tr class="${statusClass}">
|
|
418
|
+
<td>${escapeHtml(p.name)}</td>
|
|
419
|
+
<td><span class="badge ${statusClass}">${p.success ? 'OK' : 'Failed'}</span></td>
|
|
420
|
+
<td>${this.formatMs(p.durationMs)}</td>
|
|
421
|
+
<td class="bar-cell"><div class="bar" style="width:${durationPct}%"></div></td>
|
|
422
|
+
</tr>`;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const html = `<!DOCTYPE html>
|
|
426
|
+
<html lang="en">
|
|
427
|
+
<head>
|
|
428
|
+
<meta charset="utf-8">
|
|
429
|
+
<title>Scripts Orchestrator Report</title>
|
|
430
|
+
<style>
|
|
431
|
+
* { box-sizing: border-box; }
|
|
432
|
+
body { font-family: system-ui, sans-serif; margin: 1rem 2rem; background: #1a1a1a; color: #e0e0e0; }
|
|
433
|
+
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
434
|
+
.summary { display: flex; gap: 1.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
|
|
435
|
+
.summary .card { background: #2a2a2a; padding: 1rem 1.25rem; border-radius: 8px; min-width: 140px; }
|
|
436
|
+
.summary .card.success { border-left: 4px solid #22c55e; }
|
|
437
|
+
.summary .card.fail { border-left: 4px solid #ef4444; }
|
|
438
|
+
.summary .label { font-size: 0.75rem; text-transform: uppercase; color: #888; }
|
|
439
|
+
.summary .value { font-size: 1.25rem; font-weight: 600; }
|
|
440
|
+
section { margin-bottom: 1.5rem; }
|
|
441
|
+
section h2 { font-size: 1.1rem; color: #a0a0a0; margin-bottom: 0.5rem; }
|
|
442
|
+
table { width: 100%; border-collapse: collapse; background: #2a2a2a; border-radius: 8px; overflow: hidden; }
|
|
443
|
+
th, td { padding: 0.5rem 0.75rem; text-align: left; }
|
|
444
|
+
th { background: #333; color: #888; font-weight: 600; font-size: 0.8rem; }
|
|
445
|
+
tr.fail { background: rgba(239,68,68,0.08); }
|
|
446
|
+
.badge { padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem; }
|
|
447
|
+
.badge.ok { background: #22c55e; color: #0f0f0f; }
|
|
448
|
+
.badge.fail { background: #ef4444; color: #fff; }
|
|
449
|
+
.bar-cell { width: 120px; }
|
|
450
|
+
.bar { height: 8px; background: #3b82f6; border-radius: 4px; min-width: 2px; }
|
|
451
|
+
.bar.mem { background: #8b5cf6; }
|
|
452
|
+
code { font-size: 0.9em; background: #333; padding: 0.1rem 0.3rem; border-radius: 4px; }
|
|
453
|
+
</style>
|
|
454
|
+
</head>
|
|
455
|
+
<body>
|
|
456
|
+
<h1>Scripts Orchestrator Report</h1>
|
|
457
|
+
<div class="summary">
|
|
458
|
+
<div class="card ${success ? 'success' : 'fail'}">
|
|
459
|
+
<div class="label">Status</div>
|
|
460
|
+
<div class="value">${success ? 'Success' : 'Failed'}</div>
|
|
461
|
+
</div>
|
|
462
|
+
<div class="card">
|
|
463
|
+
<div class="label">Timestamp</div>
|
|
464
|
+
<div class="value" style="font-size:0.9rem">${escapeHtml(timestamp)}</div>
|
|
465
|
+
</div>
|
|
466
|
+
${overallDurationMs != null ? `
|
|
467
|
+
<div class="card">
|
|
468
|
+
<div class="label">Total time</div>
|
|
469
|
+
<div class="value">${this.formatMs(overallDurationMs)}</div>
|
|
470
|
+
</div>` : ''}
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
${phases.length > 0 ? `
|
|
474
|
+
<section>
|
|
475
|
+
<h2>Phases</h2>
|
|
476
|
+
<table>
|
|
477
|
+
<thead><tr><th>Phase</th><th>Status</th><th>Duration</th><th></th></tr></thead>
|
|
478
|
+
<tbody>${phases.map(phaseRow).join('')}</tbody>
|
|
479
|
+
</table>
|
|
480
|
+
</section>` : ''}
|
|
481
|
+
|
|
482
|
+
<section>
|
|
483
|
+
<h2>Commands</h2>
|
|
484
|
+
<table>
|
|
485
|
+
<thead><tr><th>Command</th><th>Phase</th><th>Status</th><th>Duration</th><th>Memory</th><th>Time</th><th>Memory</th></tr></thead>
|
|
486
|
+
<tbody>${commands.map(row).join('')}</tbody>
|
|
487
|
+
</table>
|
|
488
|
+
</section>
|
|
489
|
+
</body>
|
|
490
|
+
</html>`;
|
|
491
|
+
|
|
492
|
+
if (this.htmlResultsPath === '-') {
|
|
493
|
+
console.log(html);
|
|
494
|
+
} else {
|
|
495
|
+
const outPath = this.htmlResultsPath || './scripts-orchestrator-results.html';
|
|
496
|
+
fs.writeFileSync(outPath, html, 'utf8');
|
|
497
|
+
this.logger.verbose(`Wrote HTML report to ${outPath}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
272
501
|
async run() {
|
|
273
502
|
this.startTime = Date.now();
|
|
274
503
|
try {
|
|
@@ -328,7 +557,7 @@ export class Orchestrator {
|
|
|
328
557
|
phase.parallel.forEach(({ command }) => {
|
|
329
558
|
this.skippedCommands.push(command);
|
|
330
559
|
this.skipReasons.set(command, 'before_start_phase');
|
|
331
|
-
this.commandTimings.set(command, 0);
|
|
560
|
+
this.commandTimings.set(command, { durationMs: 0, memoryKb: null });
|
|
332
561
|
});
|
|
333
562
|
continue;
|
|
334
563
|
}
|
|
@@ -347,7 +576,7 @@ export class Orchestrator {
|
|
|
347
576
|
phase.parallel.forEach(({ command }) => {
|
|
348
577
|
this.skippedCommands.push(command);
|
|
349
578
|
this.skipReasons.set(command, 'optional_phase_not_requested');
|
|
350
|
-
this.commandTimings.set(command, 0);
|
|
579
|
+
this.commandTimings.set(command, { durationMs: 0, memoryKb: null });
|
|
351
580
|
});
|
|
352
581
|
continue;
|
|
353
582
|
}
|
|
@@ -357,7 +586,7 @@ export class Orchestrator {
|
|
|
357
586
|
phase.parallel.forEach(({ command }) => {
|
|
358
587
|
this.skippedCommands.push(command);
|
|
359
588
|
this.skipReasons.set(command, 'after_phase_failure');
|
|
360
|
-
this.commandTimings.set(command, 0);
|
|
589
|
+
this.commandTimings.set(command, { durationMs: 0, memoryKb: null });
|
|
361
590
|
});
|
|
362
591
|
continue;
|
|
363
592
|
}
|
|
@@ -385,9 +614,16 @@ export class Orchestrator {
|
|
|
385
614
|
}
|
|
386
615
|
|
|
387
616
|
const phaseHasFailures = results.some((result) => !result);
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
617
|
+
const phaseDurationMs = Date.now() - phaseStartTime;
|
|
618
|
+
const phaseDurationStr = this.metrics.includes('time')
|
|
619
|
+
? `(${this.formatDuration(phaseDurationMs)})`
|
|
620
|
+
: '';
|
|
621
|
+
|
|
622
|
+
this.phaseResults.push({
|
|
623
|
+
name: phase.name,
|
|
624
|
+
success: !phaseHasFailures,
|
|
625
|
+
durationMs: this.metrics.includes('time') ? phaseDurationMs : undefined,
|
|
626
|
+
});
|
|
391
627
|
|
|
392
628
|
if (phaseHasFailures) {
|
|
393
629
|
hasFailures = true;
|
|
@@ -450,8 +686,8 @@ export class Orchestrator {
|
|
|
450
686
|
this.logger.error(`Cleanup failed: ${error.message}`);
|
|
451
687
|
}
|
|
452
688
|
|
|
453
|
-
// Log overall time after cleanup has finished
|
|
454
|
-
if (this.startTime) {
|
|
689
|
+
// Log overall time after cleanup has finished (only when metrics include time)
|
|
690
|
+
if (this.startTime && this.metrics.includes('time')) {
|
|
455
691
|
const overallDuration = Date.now() - this.startTime;
|
|
456
692
|
this.logger.printMessage(() =>
|
|
457
693
|
console.log(
|
|
@@ -462,6 +698,11 @@ export class Orchestrator {
|
|
|
462
698
|
);
|
|
463
699
|
}
|
|
464
700
|
|
|
701
|
+
// Write JSON results if requested
|
|
702
|
+
if (this.jsonResultsPath != null) {
|
|
703
|
+
this.writeJsonResults(hasFailures);
|
|
704
|
+
}
|
|
705
|
+
|
|
465
706
|
// Update git cache on successful execution
|
|
466
707
|
if (!hasFailures) {
|
|
467
708
|
await this.gitCache.updateCache();
|
package/lib/process-manager.js
CHANGED
|
@@ -55,6 +55,32 @@ export class ProcessManager {
|
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
parseGnuTimeOutput(filePath) {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(filePath)) return null;
|
|
61
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
62
|
+
const m = text.match(/Maximum resident set size \(kbytes\):\s*(\d+)/i);
|
|
63
|
+
const kbytes = m ? parseInt(m[1], 10) : null;
|
|
64
|
+
try {
|
|
65
|
+
fs.unlinkSync(filePath);
|
|
66
|
+
} catch {
|
|
67
|
+
// ignore
|
|
68
|
+
}
|
|
69
|
+
return Number.isFinite(kbytes) ? kbytes : null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Parse macOS BSD time -l output (bytes) from text; returns memory in KB or null. */
|
|
76
|
+
parseBsdTimeOutput(text) {
|
|
77
|
+
if (!text || typeof text !== 'string') return null;
|
|
78
|
+
const m = text.match(/(\d+)\s+maximum resident set size/i);
|
|
79
|
+
if (!m) return null;
|
|
80
|
+
const bytes = parseInt(m[1], 10);
|
|
81
|
+
return Number.isFinite(bytes) ? Math.round(bytes / 1024) : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
58
84
|
async runCommand({
|
|
59
85
|
cmd,
|
|
60
86
|
logFile,
|
|
@@ -63,6 +89,8 @@ export class ProcessManager {
|
|
|
63
89
|
kill_command = null,
|
|
64
90
|
isRetry = false,
|
|
65
91
|
env = null,
|
|
92
|
+
reportTime = false,
|
|
93
|
+
reportMemory = false,
|
|
66
94
|
}) {
|
|
67
95
|
const baseDir = this.logFolder
|
|
68
96
|
? path.resolve(this.logFolder)
|
|
@@ -88,11 +116,17 @@ export class ProcessManager {
|
|
|
88
116
|
}
|
|
89
117
|
} catch (error) {
|
|
90
118
|
this.logger.error(`Failed to setup log file: ${error.message}`);
|
|
91
|
-
return Promise.resolve({
|
|
119
|
+
return Promise.resolve({
|
|
120
|
+
success: false,
|
|
121
|
+
output: '',
|
|
122
|
+
durationMs: 0,
|
|
123
|
+
memoryKb: null,
|
|
124
|
+
});
|
|
92
125
|
}
|
|
93
126
|
|
|
94
127
|
return new Promise((resolve) => {
|
|
95
128
|
const startTime = Date.now();
|
|
129
|
+
let timeOutputPath = null;
|
|
96
130
|
// Build command with environment variables if provided
|
|
97
131
|
let fullCommand = `npm run ${cmd}`;
|
|
98
132
|
if (env && Object.keys(env).length > 0) {
|
|
@@ -101,6 +135,14 @@ export class ProcessManager {
|
|
|
101
135
|
.join(' ');
|
|
102
136
|
fullCommand = `${envStr} npm run ${cmd}`;
|
|
103
137
|
}
|
|
138
|
+
const useTimeWrapper =
|
|
139
|
+
reportMemory && !background && (process.platform === 'linux' || process.platform === 'darwin');
|
|
140
|
+
if (useTimeWrapper && process.platform === 'linux') {
|
|
141
|
+
timeOutputPath = path.join(LOGS_DIR, `.time-${logName}-${startTime}.txt`);
|
|
142
|
+
fullCommand = `/usr/bin/time -v -o ${JSON.stringify(timeOutputPath)} sh -c ${JSON.stringify(fullCommand)}`;
|
|
143
|
+
} else if (useTimeWrapper && process.platform === 'darwin') {
|
|
144
|
+
fullCommand = `/usr/bin/time -l sh -c ${JSON.stringify(fullCommand)}`;
|
|
145
|
+
}
|
|
104
146
|
|
|
105
147
|
this.logger.startTask(cmd, fullCommand);
|
|
106
148
|
|
|
@@ -126,8 +168,12 @@ export class ProcessManager {
|
|
|
126
168
|
processInstance.on('error', (error) => {
|
|
127
169
|
this.logger.stopTask(cmd);
|
|
128
170
|
this.logger.error(`Failed to start process: ${error.message}`);
|
|
129
|
-
|
|
130
|
-
|
|
171
|
+
resolve({
|
|
172
|
+
success: false,
|
|
173
|
+
output: '',
|
|
174
|
+
durationMs: Date.now() - startTime,
|
|
175
|
+
memoryKb: null,
|
|
176
|
+
});
|
|
131
177
|
});
|
|
132
178
|
|
|
133
179
|
if (background) {
|
|
@@ -189,7 +235,12 @@ export class ProcessManager {
|
|
|
189
235
|
`Failed to read log file: ${error.message}`,
|
|
190
236
|
);
|
|
191
237
|
}
|
|
192
|
-
return {
|
|
238
|
+
return {
|
|
239
|
+
success: false,
|
|
240
|
+
output,
|
|
241
|
+
durationMs: Date.now() - startTime,
|
|
242
|
+
memoryKb: null,
|
|
243
|
+
};
|
|
193
244
|
}
|
|
194
245
|
|
|
195
246
|
this.logger.verbose(
|
|
@@ -216,7 +267,12 @@ export class ProcessManager {
|
|
|
216
267
|
`Failed to read log file: ${error.message}`,
|
|
217
268
|
);
|
|
218
269
|
}
|
|
219
|
-
return {
|
|
270
|
+
return {
|
|
271
|
+
success: false,
|
|
272
|
+
output,
|
|
273
|
+
durationMs: Date.now() - startTime,
|
|
274
|
+
memoryKb: null,
|
|
275
|
+
};
|
|
220
276
|
}
|
|
221
277
|
|
|
222
278
|
this.backgroundProcesses.push(processGroupId);
|
|
@@ -236,7 +292,12 @@ export class ProcessManager {
|
|
|
236
292
|
this.logger.verbose(
|
|
237
293
|
`Background process started: npm run ${cmd} (PGID: ${processGroupId})`,
|
|
238
294
|
);
|
|
239
|
-
return {
|
|
295
|
+
return {
|
|
296
|
+
success: true,
|
|
297
|
+
output: '',
|
|
298
|
+
durationMs: Date.now() - startTime,
|
|
299
|
+
memoryKb: null,
|
|
300
|
+
};
|
|
240
301
|
} catch (error) {
|
|
241
302
|
if (attempt === maxAttempts) {
|
|
242
303
|
this.logger.error(
|
|
@@ -245,7 +306,12 @@ export class ProcessManager {
|
|
|
245
306
|
this.logger.verbose(
|
|
246
307
|
`Final verification attempt failed: ${error.message}`,
|
|
247
308
|
);
|
|
248
|
-
return {
|
|
309
|
+
return {
|
|
310
|
+
success: false,
|
|
311
|
+
output: '',
|
|
312
|
+
durationMs: Date.now() - startTime,
|
|
313
|
+
memoryKb: null,
|
|
314
|
+
};
|
|
249
315
|
}
|
|
250
316
|
this.logger.verbose(
|
|
251
317
|
`Verification attempt ${attempt} failed: ${error.message}`,
|
|
@@ -258,7 +324,12 @@ export class ProcessManager {
|
|
|
258
324
|
);
|
|
259
325
|
}
|
|
260
326
|
}
|
|
261
|
-
return {
|
|
327
|
+
return {
|
|
328
|
+
success: false,
|
|
329
|
+
output: '',
|
|
330
|
+
durationMs: Date.now() - startTime,
|
|
331
|
+
memoryKb: null,
|
|
332
|
+
};
|
|
262
333
|
};
|
|
263
334
|
|
|
264
335
|
verifyProcess().then(resolve);
|
|
@@ -293,26 +364,50 @@ export class ProcessManager {
|
|
|
293
364
|
|
|
294
365
|
this.logger.stopTask(cmd);
|
|
295
366
|
|
|
296
|
-
const
|
|
297
|
-
const durationStr =
|
|
367
|
+
const durationMs = Date.now() - startTime;
|
|
368
|
+
const durationStr = reportTime
|
|
369
|
+
? ` (${this.formatDuration(durationMs)})`
|
|
370
|
+
: '';
|
|
371
|
+
let memoryKb = null;
|
|
372
|
+
if (reportMemory) {
|
|
373
|
+
if (timeOutputPath) {
|
|
374
|
+
memoryKb = this.parseGnuTimeOutput(timeOutputPath);
|
|
375
|
+
} else if (process.platform === 'darwin') {
|
|
376
|
+
memoryKb = this.parseBsdTimeOutput(output);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
298
379
|
|
|
299
380
|
if (code !== 0) {
|
|
300
381
|
this.logger.error(
|
|
301
382
|
`Failed: npm run ${cmd} ❌${durationStr} (exit code: ${code})`,
|
|
302
383
|
);
|
|
303
384
|
this.logger.verbose(`Process output: ${output}`);
|
|
304
|
-
resolve({
|
|
385
|
+
resolve({
|
|
386
|
+
success: false,
|
|
387
|
+
output,
|
|
388
|
+
durationMs,
|
|
389
|
+
memoryKb,
|
|
390
|
+
});
|
|
305
391
|
} else {
|
|
306
392
|
this.logger.success(`Completed: npm run ${cmd} ✅${durationStr}`);
|
|
307
|
-
resolve({
|
|
393
|
+
resolve({
|
|
394
|
+
success: true,
|
|
395
|
+
output,
|
|
396
|
+
durationMs,
|
|
397
|
+
memoryKb,
|
|
398
|
+
});
|
|
308
399
|
}
|
|
309
400
|
});
|
|
310
401
|
}
|
|
311
402
|
} catch (error) {
|
|
312
403
|
this.logger.stopTask(cmd);
|
|
313
404
|
this.logger.error(`Failed to spawn process: ${error.message}`);
|
|
314
|
-
|
|
315
|
-
|
|
405
|
+
resolve({
|
|
406
|
+
success: false,
|
|
407
|
+
output: '',
|
|
408
|
+
durationMs: Date.now() - startTime,
|
|
409
|
+
memoryKb: null,
|
|
410
|
+
});
|
|
316
411
|
}
|
|
317
412
|
});
|
|
318
413
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scripts-orchestrator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "A powerful script orchestrator for running parallel commands with dependency management, background processes, and health checks",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export default {
|
|
2
|
+
// Optional: metrics to report (time, memory). CLI --metrics overrides.
|
|
3
|
+
// metrics: ['time'],
|
|
4
|
+
// Optional: path for JSON results, or '-' for stdout. CLI --json-results overrides.
|
|
5
|
+
// json_results: './scripts-orchestrator-results.json',
|
|
6
|
+
// Optional: path for HTML report. CLI --html-results overrides.
|
|
7
|
+
// html_results: './scripts-orchestrator-results.html',
|
|
2
8
|
phases: [
|
|
3
9
|
{
|
|
4
10
|
name: 'build',
|