scripts-orchestrator 2.9.0 → 2.12.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/README.md +18 -0
- package/index.js +6 -1
- package/lib/health-check.js +27 -11
- package/lib/logger.js +142 -12
- package/lib/orchestrator.js +145 -65
- package/lib/process-manager.js +229 -88
- package/package.json +1 -1
package/lib/orchestrator.js
CHANGED
|
@@ -2,27 +2,37 @@ import { processManager } from './process-manager.js';
|
|
|
2
2
|
import { healthCheck } from './health-check.js';
|
|
3
3
|
import { log } from './logger.js';
|
|
4
4
|
import { GitCache } from './git-cache.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
5
6
|
|
|
6
7
|
export class Orchestrator {
|
|
7
|
-
constructor(
|
|
8
|
+
constructor(
|
|
9
|
+
config,
|
|
10
|
+
startPhase = null,
|
|
11
|
+
logFolder = null,
|
|
12
|
+
phases = null,
|
|
13
|
+
sequential = false,
|
|
14
|
+
force = false,
|
|
15
|
+
) {
|
|
8
16
|
this.config = config;
|
|
9
17
|
this.startPhase = startPhase;
|
|
10
18
|
this.logFolder = logFolder;
|
|
11
19
|
this.phases = phases;
|
|
12
20
|
this.sequential = sequential;
|
|
21
|
+
this.force = force;
|
|
13
22
|
this.processManager = processManager;
|
|
14
23
|
this.healthCheck = healthCheck;
|
|
15
24
|
this.logger = log;
|
|
16
25
|
this.failedCommands = [];
|
|
17
26
|
this.skippedCommands = [];
|
|
27
|
+
this.skipReasons = new Map(); // Track why commands were skipped
|
|
18
28
|
this.commandTimings = new Map();
|
|
19
29
|
this.gitCache = new GitCache(logFolder);
|
|
20
|
-
|
|
30
|
+
|
|
21
31
|
// Set the log folder in process manager
|
|
22
32
|
if (logFolder) {
|
|
23
33
|
this.processManager.setLogFolder(logFolder);
|
|
24
34
|
}
|
|
25
|
-
|
|
35
|
+
|
|
26
36
|
// Flatten commands for easier tracking
|
|
27
37
|
this.allCommands = this.flattenCommands(config);
|
|
28
38
|
}
|
|
@@ -32,11 +42,11 @@ export class Orchestrator {
|
|
|
32
42
|
if (Array.isArray(config)) {
|
|
33
43
|
return config;
|
|
34
44
|
}
|
|
35
|
-
|
|
45
|
+
|
|
36
46
|
if (config.phases) {
|
|
37
|
-
return config.phases.flatMap(phase => phase.parallel || []);
|
|
47
|
+
return config.phases.flatMap((phase) => phase.parallel || []);
|
|
38
48
|
}
|
|
39
|
-
|
|
49
|
+
|
|
40
50
|
return [];
|
|
41
51
|
}
|
|
42
52
|
|
|
@@ -85,6 +95,7 @@ export class Orchestrator {
|
|
|
85
95
|
if (status === 'disabled') {
|
|
86
96
|
this.logger.warn(`Skipping: npm run ${command} (status: disabled)`);
|
|
87
97
|
this.skippedCommands.push(command);
|
|
98
|
+
this.skipReasons.set(command, 'disabled');
|
|
88
99
|
this.commandTimings.set(command, Date.now() - startTime);
|
|
89
100
|
visited.delete(command);
|
|
90
101
|
return true;
|
|
@@ -92,10 +103,23 @@ export class Orchestrator {
|
|
|
92
103
|
|
|
93
104
|
const checkUrl = health_check?.url;
|
|
94
105
|
if (checkUrl) {
|
|
95
|
-
this.logger.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
this.logger.startEphemeral(
|
|
107
|
+
`check_${checkUrl}`,
|
|
108
|
+
chalk.blue(`[INFO] ⏳ Checking if ${checkUrl} is already available...`),
|
|
109
|
+
);
|
|
110
|
+
const urlAvailable = await this.healthCheck.waitForUrl({
|
|
111
|
+
url: checkUrl,
|
|
112
|
+
maxAttempts: 1,
|
|
113
|
+
silent: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (!urlAvailable) {
|
|
117
|
+
this.logger.stopEphemeral(`check_${checkUrl}`);
|
|
118
|
+
} else {
|
|
119
|
+
this.logger.stopEphemeral(
|
|
120
|
+
`check_${checkUrl}`,
|
|
121
|
+
`✅ ${checkUrl} is already available. Skipping ${command} start.`,
|
|
122
|
+
);
|
|
99
123
|
this.processManager.addBackgroundProcess({
|
|
100
124
|
command,
|
|
101
125
|
url: checkUrl,
|
|
@@ -115,23 +139,21 @@ export class Orchestrator {
|
|
|
115
139
|
if (!dependencySuccess) {
|
|
116
140
|
this.logger.error(`Skipping ${command} due to failed dependency`);
|
|
117
141
|
this.skippedCommands.push(command);
|
|
142
|
+
this.skipReasons.set(command, 'failed_dependency');
|
|
118
143
|
this.commandTimings.set(command, Date.now() - startTime);
|
|
119
144
|
visited.delete(command);
|
|
120
145
|
return false;
|
|
121
146
|
}
|
|
122
147
|
|
|
123
148
|
if (dependency.health_check?.url) {
|
|
124
|
-
this.logger.info(`Waiting for ${dependency.health_check.url} to be available...`);
|
|
125
149
|
const urlAvailable = await this.healthCheck.waitForUrl({
|
|
126
150
|
url: dependency.health_check.url,
|
|
127
151
|
maxAttempts: dependency.health_check?.max_attempts || 20,
|
|
128
152
|
interval: dependency.health_check?.interval || 2000,
|
|
129
153
|
});
|
|
130
154
|
if (!urlAvailable) {
|
|
131
|
-
this.logger.error(
|
|
132
|
-
`URL ${dependency.health_check.url} is not available after maximum attempts`,
|
|
133
|
-
);
|
|
134
155
|
this.skippedCommands.push(command);
|
|
156
|
+
this.skipReasons.set(command, 'failed_dependency');
|
|
135
157
|
this.commandTimings.set(command, Date.now() - startTime);
|
|
136
158
|
visited.delete(command);
|
|
137
159
|
return false;
|
|
@@ -140,7 +162,9 @@ export class Orchestrator {
|
|
|
140
162
|
this.logger.verbose(`Waiting ${dependency.wait}ms`);
|
|
141
163
|
await new Promise((resolve) => {
|
|
142
164
|
setTimeout(() => {
|
|
143
|
-
this.logger.verbose(
|
|
165
|
+
this.logger.verbose(
|
|
166
|
+
`Resolving after a wait of ${dependency.wait}ms`,
|
|
167
|
+
);
|
|
144
168
|
resolve(true);
|
|
145
169
|
}, dependency.wait);
|
|
146
170
|
});
|
|
@@ -152,10 +176,12 @@ export class Orchestrator {
|
|
|
152
176
|
let result = false;
|
|
153
177
|
let commandOutput = '';
|
|
154
178
|
let commandFailed = false;
|
|
155
|
-
|
|
179
|
+
|
|
156
180
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
157
181
|
if (attempt > 1) {
|
|
158
|
-
this.logger.warn(
|
|
182
|
+
this.logger.warn(
|
|
183
|
+
`Retrying ${command} (attempt ${attempt}/${attempts})`,
|
|
184
|
+
);
|
|
159
185
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
160
186
|
}
|
|
161
187
|
|
|
@@ -173,7 +199,9 @@ export class Orchestrator {
|
|
|
173
199
|
|
|
174
200
|
if (result) {
|
|
175
201
|
// Remove from failed commands if it was there
|
|
176
|
-
this.failedCommands = this.failedCommands.filter(
|
|
202
|
+
this.failedCommands = this.failedCommands.filter(
|
|
203
|
+
(cmd) => cmd !== command,
|
|
204
|
+
);
|
|
177
205
|
commandFailed = false;
|
|
178
206
|
break;
|
|
179
207
|
} else if (attempt < attempts) {
|
|
@@ -184,7 +212,9 @@ export class Orchestrator {
|
|
|
184
212
|
commandFailed = true;
|
|
185
213
|
break;
|
|
186
214
|
}
|
|
187
|
-
this.logger.error(
|
|
215
|
+
this.logger.error(
|
|
216
|
+
`Attempt ${attempt}/${attempts} failed for ${command}`,
|
|
217
|
+
);
|
|
188
218
|
commandFailed = true;
|
|
189
219
|
} else {
|
|
190
220
|
commandFailed = true;
|
|
@@ -193,14 +223,18 @@ export class Orchestrator {
|
|
|
193
223
|
|
|
194
224
|
if (commandFailed) {
|
|
195
225
|
this.failedCommands.push(command);
|
|
196
|
-
|
|
226
|
+
|
|
197
227
|
// Cleanup any background processes for this failed command
|
|
198
228
|
if (background) {
|
|
199
|
-
this.logger.warn(
|
|
229
|
+
this.logger.warn(
|
|
230
|
+
`Command ${command} failed after all attempts. Cleaning up background processes.`,
|
|
231
|
+
);
|
|
200
232
|
try {
|
|
201
233
|
await this.processManager.cleanupCommand(command);
|
|
202
234
|
} catch (cleanupError) {
|
|
203
|
-
this.logger.error(
|
|
235
|
+
this.logger.error(
|
|
236
|
+
`Failed to cleanup processes for ${command}: ${cleanupError.message}`,
|
|
237
|
+
);
|
|
204
238
|
}
|
|
205
239
|
}
|
|
206
240
|
}
|
|
@@ -211,40 +245,45 @@ export class Orchestrator {
|
|
|
211
245
|
}
|
|
212
246
|
|
|
213
247
|
summarizeResults() {
|
|
214
|
-
this.logger.info('\nCommand Summary:');
|
|
215
248
|
let hasFailures = false;
|
|
216
|
-
|
|
249
|
+
|
|
250
|
+
// Check if any command failed or was skipped due to failure
|
|
217
251
|
this.allCommands.forEach(({ command }) => {
|
|
218
|
-
const duration = this.commandTimings.get(command);
|
|
219
|
-
const durationStr = duration ? ` (${this.formatDuration(duration)})` : '';
|
|
220
|
-
|
|
221
252
|
if (this.failedCommands.includes(command)) {
|
|
222
253
|
hasFailures = true;
|
|
223
|
-
// Get the actual log path from process manager
|
|
224
|
-
const logPath = this.processManager.getLogPath(command);
|
|
225
|
-
this.logger.error(`- ${command}: ❌${durationStr} (See ${logPath})`);
|
|
226
254
|
} else if (this.skippedCommands.includes(command)) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
255
|
+
const skipReason = this.skipReasons.get(command);
|
|
256
|
+
if (
|
|
257
|
+
skipReason === 'failed_dependency' ||
|
|
258
|
+
skipReason === 'after_phase_failure'
|
|
259
|
+
) {
|
|
260
|
+
hasFailures = true;
|
|
261
|
+
}
|
|
231
262
|
}
|
|
232
263
|
});
|
|
233
264
|
|
|
234
265
|
if (hasFailures) {
|
|
235
|
-
this.logger.error('\n❌ Some commands failed or were skipped.
|
|
266
|
+
this.logger.error('\n❌ Some commands failed or were skipped.');
|
|
236
267
|
} else {
|
|
237
268
|
this.logger.success('\n🎉 All commands executed successfully!');
|
|
238
269
|
}
|
|
239
270
|
}
|
|
240
271
|
|
|
241
272
|
async run() {
|
|
273
|
+
this.startTime = Date.now();
|
|
242
274
|
try {
|
|
243
|
-
// Check if we should skip execution based on git state
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
275
|
+
// Check if we should skip execution based on git state (unless forced)
|
|
276
|
+
if (!this.force) {
|
|
277
|
+
const shouldSkip = await this.gitCache.shouldSkipExecution();
|
|
278
|
+
if (shouldSkip) {
|
|
279
|
+
this.logger.success('🎉 No changes detected, skipping execution!');
|
|
280
|
+
this.logger.info('💡 To force execution, use: --force');
|
|
281
|
+
process.exit(0);
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
this.logger.info(
|
|
285
|
+
'⚡ Force execution enabled, skipping git cache check',
|
|
286
|
+
);
|
|
248
287
|
}
|
|
249
288
|
|
|
250
289
|
let hasFailures = false;
|
|
@@ -270,14 +309,14 @@ export class Orchestrator {
|
|
|
270
309
|
this.executeCommand(commandConfig),
|
|
271
310
|
);
|
|
272
311
|
const results = await Promise.all(tasks);
|
|
273
|
-
hasFailures = results.some(result => !result);
|
|
312
|
+
hasFailures = results.some((result) => !result);
|
|
274
313
|
}
|
|
275
314
|
} else if (this.config.phases) {
|
|
276
315
|
// New: Run phases sequentially, commands within phases in parallel or sequential based on flag
|
|
277
316
|
if (this.sequential) {
|
|
278
317
|
this.logger.info('🔄 Running in sequential mode');
|
|
279
318
|
}
|
|
280
|
-
|
|
319
|
+
|
|
281
320
|
for (const phase of this.config.phases) {
|
|
282
321
|
// Check if we should start from this phase
|
|
283
322
|
if (this.startPhase && !startPhaseFound) {
|
|
@@ -288,6 +327,7 @@ export class Orchestrator {
|
|
|
288
327
|
// Mark all commands in previous phases as skipped
|
|
289
328
|
phase.parallel.forEach(({ command }) => {
|
|
290
329
|
this.skippedCommands.push(command);
|
|
330
|
+
this.skipReasons.set(command, 'before_start_phase');
|
|
291
331
|
this.commandTimings.set(command, 0);
|
|
292
332
|
});
|
|
293
333
|
continue;
|
|
@@ -295,11 +335,18 @@ export class Orchestrator {
|
|
|
295
335
|
}
|
|
296
336
|
|
|
297
337
|
// Check if this is an optional phase that should be skipped
|
|
298
|
-
if (
|
|
299
|
-
|
|
338
|
+
if (
|
|
339
|
+
phase.optional === true &&
|
|
340
|
+
this.phases &&
|
|
341
|
+
!this.phases.includes(phase.name)
|
|
342
|
+
) {
|
|
343
|
+
this.logger.info(
|
|
344
|
+
`\n⏭️ Skipping optional phase: ${phase.name} (not explicitly requested)`,
|
|
345
|
+
);
|
|
300
346
|
// Mark all commands in this phase as skipped
|
|
301
347
|
phase.parallel.forEach(({ command }) => {
|
|
302
348
|
this.skippedCommands.push(command);
|
|
349
|
+
this.skipReasons.set(command, 'optional_phase_not_requested');
|
|
303
350
|
this.commandTimings.set(command, 0);
|
|
304
351
|
});
|
|
305
352
|
continue;
|
|
@@ -309,13 +356,14 @@ export class Orchestrator {
|
|
|
309
356
|
// Mark all commands in remaining phases as skipped
|
|
310
357
|
phase.parallel.forEach(({ command }) => {
|
|
311
358
|
this.skippedCommands.push(command);
|
|
359
|
+
this.skipReasons.set(command, 'after_phase_failure');
|
|
312
360
|
this.commandTimings.set(command, 0);
|
|
313
361
|
});
|
|
314
362
|
continue;
|
|
315
363
|
}
|
|
316
364
|
|
|
317
|
-
|
|
318
|
-
|
|
365
|
+
const phaseStartTime = Date.now();
|
|
366
|
+
|
|
319
367
|
let results;
|
|
320
368
|
if (this.sequential) {
|
|
321
369
|
// Run commands sequentially
|
|
@@ -335,58 +383,90 @@ export class Orchestrator {
|
|
|
335
383
|
);
|
|
336
384
|
results = await Promise.all(tasks);
|
|
337
385
|
}
|
|
338
|
-
|
|
339
|
-
const phaseHasFailures = results.some(result => !result);
|
|
340
|
-
|
|
386
|
+
|
|
387
|
+
const phaseHasFailures = results.some((result) => !result);
|
|
388
|
+
const phaseDurationStr = `(${this.formatDuration(
|
|
389
|
+
Date.now() - phaseStartTime,
|
|
390
|
+
)})`;
|
|
391
|
+
|
|
341
392
|
if (phaseHasFailures) {
|
|
342
393
|
hasFailures = true;
|
|
343
394
|
phaseFailed = true;
|
|
344
|
-
this.logger.
|
|
395
|
+
this.logger.stopPhase(phase.name, false, phaseDurationStr);
|
|
345
396
|
} else {
|
|
346
|
-
this.logger.
|
|
397
|
+
this.logger.stopPhase(phase.name, true, phaseDurationStr);
|
|
347
398
|
}
|
|
348
399
|
}
|
|
349
400
|
}
|
|
350
401
|
|
|
351
402
|
// Validate start phase if specified
|
|
352
403
|
if (this.startPhase && !startPhaseFound) {
|
|
353
|
-
const availablePhases = this.config.phases
|
|
354
|
-
|
|
404
|
+
const availablePhases = this.config.phases
|
|
405
|
+
.map((p) => p.name)
|
|
406
|
+
.join(', ');
|
|
407
|
+
this.logger.error(
|
|
408
|
+
`❌ Start phase "${this.startPhase}" not found. Available phases: ${availablePhases}`,
|
|
409
|
+
);
|
|
355
410
|
process.exit(1);
|
|
356
411
|
}
|
|
357
412
|
|
|
358
413
|
// Validate phases if specified
|
|
359
414
|
if (this.phases) {
|
|
360
|
-
const availablePhases = this.config.phases.map(p => p.name);
|
|
361
|
-
const invalidPhases = this.phases.filter(
|
|
415
|
+
const availablePhases = this.config.phases.map((p) => p.name);
|
|
416
|
+
const invalidPhases = this.phases.filter(
|
|
417
|
+
(phase) => !availablePhases.includes(phase),
|
|
418
|
+
);
|
|
362
419
|
if (invalidPhases.length > 0) {
|
|
363
|
-
this.logger.error(
|
|
420
|
+
this.logger.error(
|
|
421
|
+
`❌ Invalid phases specified: ${invalidPhases.join(', ')}. Available phases: ${availablePhases.join(', ')}`,
|
|
422
|
+
);
|
|
364
423
|
process.exit(1);
|
|
365
424
|
}
|
|
366
425
|
}
|
|
367
426
|
|
|
368
427
|
// Check final status
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
428
|
+
// Only count skipped commands as failures if they're due to dependency issues or phase failures
|
|
429
|
+
const failureSkippedCommands = Array.from(this.skipReasons.entries())
|
|
430
|
+
.filter(
|
|
431
|
+
([, reason]) =>
|
|
432
|
+
reason === 'failed_dependency' || reason === 'after_phase_failure',
|
|
433
|
+
)
|
|
434
|
+
.map(([command]) => command);
|
|
435
|
+
|
|
436
|
+
hasFailures =
|
|
437
|
+
hasFailures ||
|
|
438
|
+
this.failedCommands.length > 0 ||
|
|
439
|
+
failureSkippedCommands.length > 0;
|
|
372
440
|
|
|
373
441
|
// Add a small delay to ensure all processes have finished
|
|
374
442
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
375
443
|
|
|
376
444
|
this.summarizeResults();
|
|
377
|
-
|
|
445
|
+
|
|
378
446
|
// Cleanup before exit since finally blocks don't run after process.exit()
|
|
379
447
|
try {
|
|
380
448
|
await this.processManager.cleanup();
|
|
381
449
|
} catch (error) {
|
|
382
450
|
this.logger.error(`Cleanup failed: ${error.message}`);
|
|
383
451
|
}
|
|
384
|
-
|
|
452
|
+
|
|
453
|
+
// Log overall time after cleanup has finished
|
|
454
|
+
if (this.startTime) {
|
|
455
|
+
const overallDuration = Date.now() - this.startTime;
|
|
456
|
+
this.logger.printMessage(() =>
|
|
457
|
+
console.log(
|
|
458
|
+
chalk.cyan(
|
|
459
|
+
`[INFO] ⏱️ Overall time taken: ${this.formatDuration(overallDuration)}`,
|
|
460
|
+
),
|
|
461
|
+
),
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
385
465
|
// Update git cache on successful execution
|
|
386
466
|
if (!hasFailures) {
|
|
387
467
|
await this.gitCache.updateCache();
|
|
388
468
|
}
|
|
389
|
-
|
|
469
|
+
|
|
390
470
|
// Force exit with appropriate status
|
|
391
471
|
if (hasFailures) {
|
|
392
472
|
this.logger.info('Exiting with failure status...');
|
|
@@ -397,15 +477,15 @@ export class Orchestrator {
|
|
|
397
477
|
}
|
|
398
478
|
} catch (error) {
|
|
399
479
|
this.logger.error(`Orchestrator failed: ${error.message}`);
|
|
400
|
-
|
|
480
|
+
|
|
401
481
|
// Cleanup on error
|
|
402
482
|
try {
|
|
403
483
|
await this.processManager.cleanup();
|
|
404
484
|
} catch (cleanupError) {
|
|
405
485
|
this.logger.error(`Cleanup failed: ${cleanupError.message}`);
|
|
406
486
|
}
|
|
407
|
-
|
|
487
|
+
|
|
408
488
|
process.exit(1);
|
|
409
489
|
}
|
|
410
490
|
}
|
|
411
|
-
}
|
|
491
|
+
}
|