scripts-orchestrator 2.4.1 ā 2.5.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 +33 -0
- package/index.js +24 -4
- package/lib/orchestrator.js +45 -5
- package/lib/process-manager.js +27 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,8 +165,41 @@ The orchestrator doesn't care what the commands do - it just ensures they run (i
|
|
|
165
165
|
|
|
166
166
|
# Or specify a custom config file
|
|
167
167
|
npm run scripts-orchestrator -- ./path/to/your/config.js
|
|
168
|
+
|
|
169
|
+
# Start from a specific phase
|
|
170
|
+
npm run scripts-orchestrator -- --phase "unit tests"
|
|
171
|
+
|
|
172
|
+
# Start from a specific phase with custom config
|
|
173
|
+
npm run scripts-orchestrator -- ./path/to/your/config.js --phase "playwright"
|
|
168
174
|
```
|
|
169
175
|
|
|
176
|
+
### Starting from a Specific Phase
|
|
177
|
+
|
|
178
|
+
You can start the orchestrator from a specific phase instead of running all phases from the beginning. This is useful for debugging or when you want to skip earlier phases that have already been completed.
|
|
179
|
+
|
|
180
|
+
#### Method 1: Command Line Argument
|
|
181
|
+
```bash
|
|
182
|
+
# Start from the "unit tests" phase
|
|
183
|
+
npm run scripts-orchestrator -- --phase "unit tests"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### Method 2: Configuration File
|
|
187
|
+
```javascript
|
|
188
|
+
export default {
|
|
189
|
+
start_phase: "unit tests", // Start from this phase
|
|
190
|
+
phases: [
|
|
191
|
+
// ... your phases
|
|
192
|
+
]
|
|
193
|
+
};
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Note**: Command line arguments take precedence over configuration file settings.
|
|
197
|
+
|
|
198
|
+
When starting from a specific phase:
|
|
199
|
+
- All phases before the specified phase are skipped
|
|
200
|
+
- Commands in skipped phases are marked as "skipped" in the final summary
|
|
201
|
+
- The orchestrator validates that the specified phase exists and shows available phases if not found
|
|
202
|
+
|
|
170
203
|
## Error Handling
|
|
171
204
|
|
|
172
205
|
- The script tracks failed and skipped commands
|
package/index.js
CHANGED
|
@@ -10,13 +10,28 @@ import fs from 'fs';
|
|
|
10
10
|
import { Orchestrator } from './lib/index.js';
|
|
11
11
|
import { log } from './lib/logger.js';
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
const
|
|
13
|
+
// Parse command line arguments
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
let configPath = './scripts-orchestrator.config.js';
|
|
16
|
+
let startPhase = null;
|
|
17
|
+
|
|
18
|
+
// Parse arguments
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
const arg = args[i];
|
|
21
|
+
|
|
22
|
+
if (arg === '--phase' && i + 1 < args.length) {
|
|
23
|
+
startPhase = args[i + 1];
|
|
24
|
+
i++; // Skip the next argument since we consumed it
|
|
25
|
+
} else if (!arg.startsWith('--') && !configPath) {
|
|
26
|
+
// First non-flag argument is the config path
|
|
27
|
+
configPath = arg;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
15
30
|
|
|
16
31
|
// Validate config file exists
|
|
17
32
|
if (!fs.existsSync(configPath)) {
|
|
18
33
|
log.error(`Error: Config file not found at ${configPath}`);
|
|
19
|
-
log.error('Usage: scripts-orchestrator [path-to-config-file]');
|
|
34
|
+
log.error('Usage: scripts-orchestrator [path-to-config-file] [--phase <phase-name>]');
|
|
20
35
|
process.exit(1);
|
|
21
36
|
}
|
|
22
37
|
|
|
@@ -25,8 +40,13 @@ const configFilePath = path.resolve(process.cwd(), configPath);
|
|
|
25
40
|
const fileUrl = new URL(`file://${configFilePath}`).href;
|
|
26
41
|
const commandsConfig = (await import(fileUrl)).default;
|
|
27
42
|
|
|
43
|
+
// Check for start_phase in config if not provided via command line
|
|
44
|
+
if (!startPhase && commandsConfig.start_phase) {
|
|
45
|
+
startPhase = commandsConfig.start_phase;
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
// Create and run the orchestrator
|
|
29
|
-
const orchestrator = new Orchestrator(commandsConfig);
|
|
49
|
+
const orchestrator = new Orchestrator(commandsConfig, startPhase);
|
|
30
50
|
|
|
31
51
|
// Enhanced signal handlers
|
|
32
52
|
const handleSignal = async (signal) => {
|
package/lib/orchestrator.js
CHANGED
|
@@ -3,8 +3,9 @@ import { healthCheck } from './health-check.js';
|
|
|
3
3
|
import { log } from './logger.js';
|
|
4
4
|
|
|
5
5
|
export class Orchestrator {
|
|
6
|
-
constructor(config) {
|
|
6
|
+
constructor(config, startPhase = null) {
|
|
7
7
|
this.config = config;
|
|
8
|
+
this.startPhase = startPhase;
|
|
8
9
|
this.processManager = processManager;
|
|
9
10
|
this.healthCheck = healthCheck;
|
|
10
11
|
this.logger = log;
|
|
@@ -226,6 +227,7 @@ export class Orchestrator {
|
|
|
226
227
|
try {
|
|
227
228
|
let hasFailures = false;
|
|
228
229
|
let phaseFailed = false;
|
|
230
|
+
let startPhaseFound = false;
|
|
229
231
|
|
|
230
232
|
// Handle both old array format and new phases format
|
|
231
233
|
if (Array.isArray(this.config)) {
|
|
@@ -238,6 +240,21 @@ export class Orchestrator {
|
|
|
238
240
|
} else if (this.config.phases) {
|
|
239
241
|
// New: Run phases sequentially, commands within phases in parallel
|
|
240
242
|
for (const phase of this.config.phases) {
|
|
243
|
+
// Check if we should start from this phase
|
|
244
|
+
if (this.startPhase && !startPhaseFound) {
|
|
245
|
+
if (phase.name === this.startPhase) {
|
|
246
|
+
startPhaseFound = true;
|
|
247
|
+
this.logger.info(`\nšÆ Starting from phase: ${phase.name}`);
|
|
248
|
+
} else {
|
|
249
|
+
// Mark all commands in previous phases as skipped
|
|
250
|
+
phase.parallel.forEach(({ command }) => {
|
|
251
|
+
this.skippedCommands.push(command);
|
|
252
|
+
this.commandTimings.set(command, 0);
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
241
258
|
if (phaseFailed) {
|
|
242
259
|
// Mark all commands in remaining phases as skipped
|
|
243
260
|
phase.parallel.forEach(({ command }) => {
|
|
@@ -266,6 +283,13 @@ export class Orchestrator {
|
|
|
266
283
|
}
|
|
267
284
|
}
|
|
268
285
|
|
|
286
|
+
// Validate start phase if specified
|
|
287
|
+
if (this.startPhase && !startPhaseFound) {
|
|
288
|
+
const availablePhases = this.config.phases.map(p => p.name).join(', ');
|
|
289
|
+
this.logger.error(`ā Start phase "${this.startPhase}" not found. Available phases: ${availablePhases}`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
269
293
|
// Check final status
|
|
270
294
|
hasFailures = hasFailures ||
|
|
271
295
|
this.failedCommands.length > 0 ||
|
|
@@ -276,16 +300,32 @@ export class Orchestrator {
|
|
|
276
300
|
|
|
277
301
|
this.summarizeResults();
|
|
278
302
|
|
|
279
|
-
//
|
|
303
|
+
// Cleanup before exit since finally blocks don't run after process.exit()
|
|
304
|
+
try {
|
|
305
|
+
await this.processManager.cleanup();
|
|
306
|
+
} catch (error) {
|
|
307
|
+
this.logger.error(`Cleanup failed: ${error.message}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Force exit with appropriate status
|
|
280
311
|
if (hasFailures) {
|
|
312
|
+
this.logger.info('Exiting with failure status...');
|
|
281
313
|
process.exit(1);
|
|
314
|
+
} else {
|
|
315
|
+
this.logger.info('Exiting with success status...');
|
|
316
|
+
process.exit(0);
|
|
282
317
|
}
|
|
283
|
-
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
this.logger.error(`Orchestrator failed: ${error.message}`);
|
|
320
|
+
|
|
321
|
+
// Cleanup on error
|
|
284
322
|
try {
|
|
285
323
|
await this.processManager.cleanup();
|
|
286
|
-
} catch (
|
|
287
|
-
this.logger.error(`Cleanup failed: ${
|
|
324
|
+
} catch (cleanupError) {
|
|
325
|
+
this.logger.error(`Cleanup failed: ${cleanupError.message}`);
|
|
288
326
|
}
|
|
327
|
+
|
|
328
|
+
process.exit(1);
|
|
289
329
|
}
|
|
290
330
|
}
|
|
291
331
|
}
|
package/lib/process-manager.js
CHANGED
|
@@ -246,25 +246,29 @@ export class ProcessManager {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
async cleanup() {
|
|
249
|
-
|
|
249
|
+
try {
|
|
250
|
+
this.logger.info('\nCleaning up background processes...');
|
|
250
251
|
|
|
251
|
-
|
|
252
|
-
|
|
252
|
+
// Debug: Log the number of processes we're tracking
|
|
253
|
+
this.logger.info(`- Found ${this.backgroundProcessesDetails.length} background processes to clean up`);
|
|
253
254
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
// Debug: Log each process details
|
|
256
|
+
this.backgroundProcessesDetails.forEach(({ command, pgid, url, startedByScript, kill_command }, index) => {
|
|
257
|
+
this.logger.verbose(`- Process ${index + 1}: command=${command}, pgid=${pgid}, url=${url}, startedByScript=${startedByScript}, kill_command=${kill_command}`);
|
|
258
|
+
});
|
|
258
259
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
260
|
+
const killPromises = this.backgroundProcessesDetails.map(
|
|
261
|
+
async ({ command, pgid, url, startedByScript, kill_command }) => {
|
|
262
|
+
await this.cleanupProcess({ command, pgid, url, startedByScript, kill_command });
|
|
263
|
+
},
|
|
264
|
+
);
|
|
264
265
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
await Promise.allSettled(killPromises);
|
|
267
|
+
this.backgroundProcesses = [];
|
|
268
|
+
this.backgroundProcessesDetails = [];
|
|
269
|
+
} catch (error) {
|
|
270
|
+
this.logger.error(`Cleanup failed: ${error.message}`);
|
|
271
|
+
}
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
async cleanupCommand(commandName) {
|
|
@@ -288,7 +292,7 @@ export class ProcessManager {
|
|
|
288
292
|
}
|
|
289
293
|
);
|
|
290
294
|
|
|
291
|
-
await Promise.
|
|
295
|
+
await Promise.allSettled(killPromises);
|
|
292
296
|
|
|
293
297
|
// Remove the cleaned up processes from our tracking arrays
|
|
294
298
|
this.backgroundProcesses = this.backgroundProcesses.filter(pgid =>
|
|
@@ -359,17 +363,19 @@ export class ProcessManager {
|
|
|
359
363
|
process.kill(pgid, 'SIGTERM');
|
|
360
364
|
|
|
361
365
|
await new Promise((resolve, reject) => {
|
|
362
|
-
|
|
363
|
-
|
|
366
|
+
let timeout, checkInterval;
|
|
367
|
+
|
|
368
|
+
timeout = setTimeout(() => {
|
|
369
|
+
if (checkInterval) clearInterval(checkInterval);
|
|
364
370
|
reject(new Error('Process termination timeout'));
|
|
365
371
|
}, 5000);
|
|
366
372
|
|
|
367
|
-
|
|
373
|
+
checkInterval = setInterval(() => {
|
|
368
374
|
try {
|
|
369
375
|
process.kill(pgid, 0);
|
|
370
376
|
} catch (error) {
|
|
371
|
-
clearInterval(checkInterval);
|
|
372
|
-
clearTimeout(timeout);
|
|
377
|
+
if (checkInterval) clearInterval(checkInterval);
|
|
378
|
+
if (timeout) clearTimeout(timeout);
|
|
373
379
|
resolve();
|
|
374
380
|
}
|
|
375
381
|
}, 100);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scripts-orchestrator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.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",
|