scripts-orchestrator 2.0.0 → 2.3.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 +8 -8
- package/lib/health-check.js +35 -12
- package/lib/orchestrator.js +18 -4
- package/lib/process-manager.js +224 -92
- package/package.json +1 -1
- package/scripts-orchestrator.config.js +110 -78
package/README.md
CHANGED
|
@@ -39,13 +39,14 @@ Create a configuration file (default: `scripts-orchestrator.config.js`) that def
|
|
|
39
39
|
|
|
40
40
|
```javascript
|
|
41
41
|
{
|
|
42
|
-
command: 'command_name',
|
|
43
|
-
description: 'Description',
|
|
44
|
-
status: 'enabled',
|
|
45
|
-
attempts: 1,
|
|
46
|
-
dependencies: [],
|
|
47
|
-
background: false,
|
|
48
|
-
|
|
42
|
+
command: 'command_name', // The npm script to run
|
|
43
|
+
description: 'Description', // Optional description
|
|
44
|
+
status: 'enabled', // 'enabled' or 'disabled'
|
|
45
|
+
attempts: 1, // Number of retry attempts
|
|
46
|
+
dependencies: [], // Array of dependent commands
|
|
47
|
+
background: false, // Whether to run in background
|
|
48
|
+
kill_command: 'kill_storybook', // Optional kill command to kill the process
|
|
49
|
+
health_check: { // Health check configuration
|
|
49
50
|
url: 'http://localhost:port',
|
|
50
51
|
max_attempts: 20,
|
|
51
52
|
interval: 2000
|
|
@@ -191,7 +192,6 @@ See [versions](./docs/versions.md)
|
|
|
191
192
|
## Roadmap
|
|
192
193
|
- Better UX to indicate what is happening
|
|
193
194
|
- Tests to avoid regression
|
|
194
|
-
- Retry should append to the log file
|
|
195
195
|
- Run any shell command rather than assume the command is specified in package.json (? tentative)
|
|
196
196
|
|
|
197
197
|
|
package/lib/health-check.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
1
|
import { log } from './logger.js';
|
|
3
2
|
|
|
4
3
|
export class HealthCheck {
|
|
@@ -6,22 +5,46 @@ export class HealthCheck {
|
|
|
6
5
|
this.logger = log;
|
|
7
6
|
}
|
|
8
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Static method to make a simple HTTP/HTTPS request
|
|
10
|
+
* @param {string} url - The URL to check
|
|
11
|
+
* @param {number} timeout - Request timeout in milliseconds (default: 5000)
|
|
12
|
+
* @returns {Promise<{statusCode: number|null, success: boolean, error?: string}>}
|
|
13
|
+
*/
|
|
14
|
+
static async makeHttpRequest(url, timeout = 5000) {
|
|
15
|
+
try {
|
|
16
|
+
// Use Node.js http/https module instead of curl for cross-platform compatibility
|
|
17
|
+
const urlObj = new URL(url);
|
|
18
|
+
const isHttps = urlObj.protocol === 'https:';
|
|
19
|
+
const httpModule = isHttps ? await import('https') : await import('http');
|
|
20
|
+
|
|
21
|
+
return await new Promise((resolve) => {
|
|
22
|
+
const req = httpModule.default.get(url, (res) => {
|
|
23
|
+
resolve({ statusCode: res.statusCode, success: true });
|
|
24
|
+
res.destroy(); // Close the response stream
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
req.on('error', (error) => {
|
|
28
|
+
resolve({ statusCode: null, success: false, error: error.message });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
req.setTimeout(timeout, () => {
|
|
32
|
+
req.destroy();
|
|
33
|
+
resolve({ statusCode: null, success: false, error: 'Timeout' });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return { statusCode: null, success: false, error: error.message };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
9
41
|
async waitForUrl({url, maxAttempts = 20, interval = 2000, silent=false}) {
|
|
10
42
|
!silent && this.logger.info(`Waiting for ${url} to be available...`);
|
|
11
43
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
12
44
|
try {
|
|
13
|
-
const result = await
|
|
14
|
-
const curl = spawn('curl', ['-s', '-o', '/dev/null', '-w', '%{http_code}', url]);
|
|
15
|
-
let output = '';
|
|
16
|
-
curl.stdout.on('data', (data) => {
|
|
17
|
-
output += data.toString();
|
|
18
|
-
});
|
|
19
|
-
curl.on('close', (code) => {
|
|
20
|
-
resolve({ code, output });
|
|
21
|
-
});
|
|
22
|
-
});
|
|
45
|
+
const result = await HealthCheck.makeHttpRequest(url, 5000);
|
|
23
46
|
|
|
24
|
-
if (result.
|
|
47
|
+
if (result.success && result.statusCode === 200) {
|
|
25
48
|
!silent && this.logger.success(`${url} is available`);
|
|
26
49
|
return true;
|
|
27
50
|
}
|
package/lib/orchestrator.js
CHANGED
|
@@ -52,6 +52,7 @@ export class Orchestrator {
|
|
|
52
52
|
should_retry,
|
|
53
53
|
process_tracking = false,
|
|
54
54
|
health_check,
|
|
55
|
+
kill_command,
|
|
55
56
|
} = commandConfig;
|
|
56
57
|
|
|
57
58
|
const startTime = Date.now();
|
|
@@ -87,6 +88,7 @@ export class Orchestrator {
|
|
|
87
88
|
url: checkUrl,
|
|
88
89
|
startedByScript: false,
|
|
89
90
|
process_tracking,
|
|
91
|
+
kill_command,
|
|
90
92
|
});
|
|
91
93
|
this.commandTimings.set(command, Date.now() - startTime);
|
|
92
94
|
visited.delete(command);
|
|
@@ -144,12 +146,14 @@ export class Orchestrator {
|
|
|
144
146
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
145
147
|
}
|
|
146
148
|
|
|
147
|
-
const { success, output } = await this.processManager.runCommand(
|
|
148
|
-
attempt === 1 ? command : retry_command || command,
|
|
149
|
+
const { success, output } = await this.processManager.runCommand({
|
|
150
|
+
cmd: attempt === 1 ? command : retry_command || command,
|
|
149
151
|
logFile,
|
|
150
152
|
background,
|
|
151
|
-
health_check,
|
|
152
|
-
|
|
153
|
+
healthCheck: health_check,
|
|
154
|
+
kill_command,
|
|
155
|
+
isRetry: attempt > 1,
|
|
156
|
+
});
|
|
153
157
|
commandOutput = output;
|
|
154
158
|
result = success;
|
|
155
159
|
|
|
@@ -175,6 +179,16 @@ export class Orchestrator {
|
|
|
175
179
|
|
|
176
180
|
if (commandFailed) {
|
|
177
181
|
this.failedCommands.push(command);
|
|
182
|
+
|
|
183
|
+
// Cleanup any background processes for this failed command
|
|
184
|
+
if (background) {
|
|
185
|
+
this.logger.warn(`Command ${command} failed after all attempts. Cleaning up background processes.`);
|
|
186
|
+
try {
|
|
187
|
+
await this.processManager.cleanupCommand(command);
|
|
188
|
+
} catch (cleanupError) {
|
|
189
|
+
this.logger.error(`Failed to cleanup processes for ${command}: ${cleanupError.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
this.commandTimings.set(command, Date.now() - startTime);
|
package/lib/process-manager.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'child_process';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { log } from './logger.js';
|
|
5
|
+
import { HealthCheck } from './health-check.js';
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
export class ProcessManager {
|
|
@@ -11,17 +12,18 @@ export class ProcessManager {
|
|
|
11
12
|
this.backgroundProcessesDetails = [];
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
addBackgroundProcess({ command, url, startedByScript, process_tracking }) {
|
|
15
|
+
addBackgroundProcess({ command, url, startedByScript, process_tracking, kill_command }) {
|
|
15
16
|
this.logger.verbose(`Adding background process: ${command} (${url})`);
|
|
16
17
|
this.backgroundProcessesDetails.push({
|
|
17
18
|
command,
|
|
18
19
|
url,
|
|
19
20
|
startedByScript,
|
|
20
21
|
process_tracking,
|
|
22
|
+
kill_command,
|
|
21
23
|
});
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
async runCommand(cmd, logFile, background = false, healthCheck = null) {
|
|
26
|
+
async runCommand({ cmd, logFile, background = false, healthCheck = null, kill_command = null, isRetry = false }) {
|
|
25
27
|
const LOGS_DIR = path.resolve(process.cwd(), 'scripts-orchestrator-logs');
|
|
26
28
|
const LOG_FILE = logFile || path.join(LOGS_DIR, `${cmd}.log`);
|
|
27
29
|
|
|
@@ -31,8 +33,12 @@ export class ProcessManager {
|
|
|
31
33
|
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
if (!isRetry) {
|
|
37
|
+
this.logger.verbose(`Clearing log file at ${LOG_FILE}`);
|
|
38
|
+
fs.writeFileSync(LOG_FILE, ''); // Clear the log file
|
|
39
|
+
} else {
|
|
40
|
+
this.logger.verbose(`Appending to existing log file at ${LOG_FILE} (retry attempt)`);
|
|
41
|
+
}
|
|
36
42
|
} catch (error) {
|
|
37
43
|
this.logger.error(`Failed to setup log file: ${error.message}`);
|
|
38
44
|
return Promise.resolve({ success: false, output: '' });
|
|
@@ -42,7 +48,7 @@ export class ProcessManager {
|
|
|
42
48
|
this.logger.info(`Running: npm run ${cmd}`);
|
|
43
49
|
|
|
44
50
|
// Create isolated environment for each process
|
|
45
|
-
const isolatedEnv = this.createIsolatedEnvironment(cmd);
|
|
51
|
+
const isolatedEnv = this.createIsolatedEnvironment({ command: cmd });
|
|
46
52
|
|
|
47
53
|
const options = {
|
|
48
54
|
shell: true,
|
|
@@ -103,6 +109,7 @@ export class ProcessManager {
|
|
|
103
109
|
startTime: Date.now(),
|
|
104
110
|
url: healthCheck?.url,
|
|
105
111
|
startedByScript: true,
|
|
112
|
+
kill_command,
|
|
106
113
|
});
|
|
107
114
|
|
|
108
115
|
this.logger.verbose(`Unreferencing process ${processGroupId}`);
|
|
@@ -172,7 +179,7 @@ export class ProcessManager {
|
|
|
172
179
|
});
|
|
173
180
|
}
|
|
174
181
|
|
|
175
|
-
createIsolatedEnvironment(command) {
|
|
182
|
+
createIsolatedEnvironment({ command }) {
|
|
176
183
|
// Create a deep copy to avoid any reference sharing
|
|
177
184
|
const baseEnv = JSON.parse(JSON.stringify(process.env));
|
|
178
185
|
|
|
@@ -201,121 +208,246 @@ export class ProcessManager {
|
|
|
201
208
|
|
|
202
209
|
async cleanup() {
|
|
203
210
|
this.logger.info('\nCleaning up background processes...');
|
|
211
|
+
|
|
212
|
+
// Debug: Log the number of processes we're tracking
|
|
213
|
+
this.logger.info(`- Found ${this.backgroundProcessesDetails.length} background processes to clean up`);
|
|
214
|
+
|
|
215
|
+
// Debug: Log each process details
|
|
216
|
+
this.backgroundProcessesDetails.forEach(({ command, pgid, url, startedByScript, kill_command }, index) => {
|
|
217
|
+
this.logger.verbose(`- Process ${index + 1}: command=${command}, pgid=${pgid}, url=${url}, startedByScript=${startedByScript}, kill_command=${kill_command}`);
|
|
218
|
+
});
|
|
219
|
+
|
|
204
220
|
const killPromises = this.backgroundProcessesDetails.map(
|
|
205
|
-
async ({ command, pgid, url, startedByScript }) => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
221
|
+
async ({ command, pgid, url, startedByScript, kill_command }) => {
|
|
222
|
+
await this.cleanupProcess({ command, pgid, url, startedByScript, kill_command });
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
await Promise.all(killPromises);
|
|
227
|
+
this.backgroundProcesses = [];
|
|
228
|
+
this.backgroundProcessesDetails = [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async cleanupCommand(commandName) {
|
|
232
|
+
this.logger.info(`\nCleaning up processes for command: ${commandName}`);
|
|
233
|
+
|
|
234
|
+
// Find processes for this specific command
|
|
235
|
+
const commandProcesses = this.backgroundProcessesDetails.filter(
|
|
236
|
+
({ command }) => command === commandName
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (commandProcesses.length === 0) {
|
|
240
|
+
this.logger.verbose(`- No background processes found for command: ${commandName}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this.logger.verbose(`- Found ${commandProcesses.length} background processes for command: ${commandName}`);
|
|
245
|
+
|
|
246
|
+
const killPromises = commandProcesses.map(
|
|
247
|
+
async ({ command, pgid, url, startedByScript, kill_command }) => {
|
|
248
|
+
await this.cleanupProcess({ command, pgid, url, startedByScript, kill_command });
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
await Promise.all(killPromises);
|
|
253
|
+
|
|
254
|
+
// Remove the cleaned up processes from our tracking arrays
|
|
255
|
+
this.backgroundProcesses = this.backgroundProcesses.filter(pgid =>
|
|
256
|
+
!commandProcesses.some(proc => proc.pgid === pgid)
|
|
257
|
+
);
|
|
258
|
+
this.backgroundProcessesDetails = this.backgroundProcessesDetails.filter(
|
|
259
|
+
({ command }) => command !== commandName
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async cleanupProcess({ command, pgid, url, startedByScript, kill_command }) {
|
|
264
|
+
if (!startedByScript) {
|
|
265
|
+
this.logger.verbose(
|
|
266
|
+
`- Skipping cleanup for ${command} (${url}) as it was not started by this script`,
|
|
267
|
+
);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.logger.verbose(`- Processing cleanup for ${command} (kill_command: ${kill_command})`);
|
|
272
|
+
|
|
273
|
+
// Try custom kill command first if specified
|
|
274
|
+
if (kill_command) {
|
|
275
|
+
try {
|
|
276
|
+
this.logger.verbose(`- Using custom kill command: npm run ${kill_command}`);
|
|
277
|
+
const result = await this.runCommand({ cmd: kill_command, logFile: null, background: false });
|
|
278
|
+
if (result.success) {
|
|
279
|
+
this.logger.verbose(`- Successfully killed ${command} using custom command`);
|
|
210
280
|
return;
|
|
281
|
+
} else {
|
|
282
|
+
this.logger.verbose('- Custom kill command failed, falling back to process signals');
|
|
211
283
|
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
this.logger.verbose(`- Custom kill command error: ${error.message}, falling back`);
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
this.logger.verbose(`- No kill_command specified for ${command}, using process signals`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
// First try to kill the process group
|
|
293
|
+
try {
|
|
294
|
+
process.kill(pgid, 0);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
this.logger.verbose(
|
|
297
|
+
`- Process ${command} (PGID: ${pgid}) already terminated`,
|
|
298
|
+
);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
212
301
|
|
|
302
|
+
// Cross-platform process termination
|
|
303
|
+
const isWindows = process.platform === 'win32';
|
|
304
|
+
|
|
305
|
+
if (isWindows) {
|
|
306
|
+
// Windows: use taskkill to terminate process tree
|
|
213
307
|
try {
|
|
214
|
-
|
|
308
|
+
const killProcess = spawn('taskkill', ['/F', '/T', '/PID', pgid.toString()]);
|
|
309
|
+
await new Promise((resolve) => {
|
|
310
|
+
killProcess.on('close', resolve);
|
|
311
|
+
});
|
|
312
|
+
this.logger.verbose(`- Terminated background process: ${command} (PID: ${pgid})`);
|
|
313
|
+
return;
|
|
314
|
+
} catch (killError) {
|
|
315
|
+
this.logger.verbose(`- Failed to use taskkill, falling back to process.kill: ${killError.message}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Unix/Linux/macOS or Windows fallback: Try SIGTERM first
|
|
320
|
+
process.kill(pgid, 'SIGTERM');
|
|
321
|
+
|
|
322
|
+
await new Promise((resolve, reject) => {
|
|
323
|
+
const timeout = setTimeout(() => {
|
|
324
|
+
clearInterval(checkInterval);
|
|
325
|
+
reject(new Error('Process termination timeout'));
|
|
326
|
+
}, 5000);
|
|
327
|
+
|
|
328
|
+
const checkInterval = setInterval(() => {
|
|
215
329
|
try {
|
|
216
330
|
process.kill(pgid, 0);
|
|
217
331
|
} catch (error) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
);
|
|
221
|
-
return;
|
|
332
|
+
clearInterval(checkInterval);
|
|
333
|
+
clearTimeout(timeout);
|
|
334
|
+
resolve();
|
|
222
335
|
}
|
|
336
|
+
}, 100);
|
|
337
|
+
});
|
|
338
|
+
this.logger.verbose(
|
|
339
|
+
`- Terminated background process: ${command} (PGID: ${pgid})`,
|
|
340
|
+
);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
this.logger.verbose(`- Failed to terminate process group: ${error.message}`);
|
|
343
|
+
}
|
|
223
344
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
clearInterval(checkInterval);
|
|
238
|
-
clearTimeout(timeout);
|
|
239
|
-
resolve();
|
|
240
|
-
}
|
|
241
|
-
}, 100);
|
|
242
|
-
});
|
|
243
|
-
this.logger.verbose(
|
|
244
|
-
`- Terminated background process: ${command} (PGID: ${pgid})`,
|
|
245
|
-
);
|
|
246
|
-
} catch (error) {
|
|
247
|
-
this.logger.verbose(`- Failed to terminate process group: ${error.message}`);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Check if the URL is still responding after termination attempt
|
|
251
|
-
if (url) {
|
|
345
|
+
// Check if the URL is still responding after termination attempt
|
|
346
|
+
if (url) {
|
|
347
|
+
try {
|
|
348
|
+
const urlObj = new URL(url);
|
|
349
|
+
const port = urlObj.port || (urlObj.protocol === 'https:' ? '443' : '80');
|
|
350
|
+
|
|
351
|
+
// Use shared HTTP utility for cross-platform compatibility
|
|
352
|
+
const urlResult = await HealthCheck.makeHttpRequest(url, 2000);
|
|
353
|
+
|
|
354
|
+
if (urlResult.success && urlResult.statusCode === 200) {
|
|
355
|
+
this.logger.verbose(`- URL ${url} is still responding after termination, finding process on port ${port}`);
|
|
356
|
+
|
|
357
|
+
// Find and kill process using the port - cross-platform approach
|
|
252
358
|
try {
|
|
253
|
-
const
|
|
254
|
-
|
|
359
|
+
const isWindows = process.platform === 'win32';
|
|
360
|
+
let findPortCmd, findPortArgs;
|
|
361
|
+
|
|
362
|
+
if (isWindows) {
|
|
363
|
+
// Windows: use netstat
|
|
364
|
+
findPortCmd = 'netstat';
|
|
365
|
+
findPortArgs = ['-ano'];
|
|
366
|
+
} else {
|
|
367
|
+
// Unix/Linux/macOS: use lsof
|
|
368
|
+
findPortCmd = 'lsof';
|
|
369
|
+
findPortArgs = ['-i', `:${port}`, '-t'];
|
|
370
|
+
}
|
|
255
371
|
|
|
256
|
-
const
|
|
372
|
+
const findProcess = spawn(findPortCmd, findPortArgs);
|
|
257
373
|
const result = await new Promise((resolve) => {
|
|
258
374
|
let output = '';
|
|
259
|
-
|
|
375
|
+
findProcess.stdout.on('data', (data) => {
|
|
260
376
|
output += data.toString();
|
|
261
377
|
});
|
|
262
|
-
|
|
378
|
+
findProcess.on('close', (code) => {
|
|
263
379
|
resolve({ code, output });
|
|
264
380
|
});
|
|
265
381
|
});
|
|
266
382
|
|
|
267
|
-
if (result.code === 0 && result.output
|
|
268
|
-
|
|
383
|
+
if (result.code === 0 && result.output.trim()) {
|
|
384
|
+
let pids = [];
|
|
269
385
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
resolve({ code, output });
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
if (result.code === 0 && result.output.trim()) {
|
|
284
|
-
const pids = result.output.trim().split('\n');
|
|
285
|
-
for (const pid of pids) {
|
|
286
|
-
try {
|
|
287
|
-
process.kill(parseInt(pid), 'SIGKILL');
|
|
288
|
-
this.logger.verbose(`- Killed process (PID: ${pid}) using port ${port}`);
|
|
289
|
-
} catch (killError) {
|
|
290
|
-
if (killError.code !== 'ESRCH') {
|
|
291
|
-
this.logger.error(`- Failed to kill process (PID: ${pid}): ${killError.message}`);
|
|
292
|
-
}
|
|
386
|
+
if (isWindows) {
|
|
387
|
+
// Parse netstat output to find PIDs for the specific port
|
|
388
|
+
const lines = result.output.split('\n');
|
|
389
|
+
for (const line of lines) {
|
|
390
|
+
if (line.includes(`:${port} `) && line.includes('LISTENING')) {
|
|
391
|
+
const parts = line.trim().split(/\s+/);
|
|
392
|
+
const pid = parts[parts.length - 1];
|
|
393
|
+
if (pid && !isNaN(pid)) {
|
|
394
|
+
pids.push(pid);
|
|
293
395
|
}
|
|
294
396
|
}
|
|
295
397
|
}
|
|
296
|
-
}
|
|
297
|
-
|
|
398
|
+
} else {
|
|
399
|
+
// lsof output is already just PIDs
|
|
400
|
+
pids = result.output.trim().split('\n');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (const pid of pids) {
|
|
404
|
+
try {
|
|
405
|
+
if (isWindows) {
|
|
406
|
+
// Windows: use taskkill
|
|
407
|
+
const killProcess = spawn('taskkill', ['/F', '/PID', pid]);
|
|
408
|
+
await new Promise((resolve) => {
|
|
409
|
+
killProcess.on('close', resolve);
|
|
410
|
+
});
|
|
411
|
+
} else {
|
|
412
|
+
// Unix/Linux/macOS: use process.kill
|
|
413
|
+
process.kill(parseInt(pid), 'SIGKILL');
|
|
414
|
+
}
|
|
415
|
+
this.logger.verbose(`- Killed process (PID: ${pid}) using port ${port}`);
|
|
416
|
+
} catch (killError) {
|
|
417
|
+
if (killError.code !== 'ESRCH') {
|
|
418
|
+
this.logger.error(`- Failed to kill process (PID: ${pid}): ${killError.message}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
298
421
|
}
|
|
299
422
|
}
|
|
300
|
-
} catch (
|
|
301
|
-
this.logger.
|
|
423
|
+
} catch (portError) {
|
|
424
|
+
this.logger.error(`- Failed to find process using port ${port}: ${portError.message}`);
|
|
302
425
|
}
|
|
303
426
|
}
|
|
427
|
+
} catch (error) {
|
|
428
|
+
this.logger.verbose(`- URL check failed: ${error.message}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
304
431
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
432
|
+
// Final attempt to kill the process group
|
|
433
|
+
try {
|
|
434
|
+
const isWindows = process.platform === 'win32';
|
|
435
|
+
|
|
436
|
+
if (isWindows) {
|
|
437
|
+
// Windows: force kill with taskkill
|
|
438
|
+
const killProcess = spawn('taskkill', ['/F', '/T', '/PID', pgid.toString()]);
|
|
439
|
+
await new Promise((resolve) => {
|
|
440
|
+
killProcess.on('close', resolve);
|
|
441
|
+
});
|
|
442
|
+
} else {
|
|
443
|
+
// Unix/Linux/macOS: use SIGKILL
|
|
444
|
+
process.kill(pgid, 'SIGKILL');
|
|
445
|
+
}
|
|
446
|
+
} catch (error) {
|
|
447
|
+
if (error.code !== 'ESRCH') {
|
|
448
|
+
this.logger.error(`- Failed to kill process group: ${error.message}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
319
451
|
}
|
|
320
452
|
}
|
|
321
453
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scripts-orchestrator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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,83 +1,115 @@
|
|
|
1
|
-
export default
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
export default {
|
|
2
|
+
phases: [
|
|
3
|
+
{
|
|
4
|
+
name: 'build',
|
|
5
|
+
parallel: [
|
|
6
|
+
{
|
|
7
|
+
command: 'build',
|
|
8
|
+
description: 'Build the project',
|
|
9
|
+
status: 'enabled',
|
|
10
|
+
attempts: 1,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
command: 'stylelint',
|
|
14
|
+
description: 'Run stylelint checks',
|
|
15
|
+
status: 'enabled',
|
|
16
|
+
},
|
|
17
|
+
{ command: 'lint', description: 'Run lint checks', status: 'enabled' },
|
|
18
|
+
{
|
|
19
|
+
command: 'jscpd',
|
|
20
|
+
description: 'Run code duplication checks',
|
|
21
|
+
status: 'enabled',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'storybook tests',
|
|
27
|
+
parallel: [
|
|
28
|
+
{
|
|
29
|
+
command: 'test-storybook',
|
|
30
|
+
description: 'Run Storybook tests',
|
|
31
|
+
status: 'enabled',
|
|
32
|
+
attempts: 2,
|
|
33
|
+
dependencies: [
|
|
34
|
+
{
|
|
35
|
+
command: 'storybook_silent',
|
|
36
|
+
background: true,
|
|
37
|
+
wait: 5000,
|
|
38
|
+
kill_command: 'kill_storybook',
|
|
39
|
+
dependencies: [],
|
|
40
|
+
// Add process tracking
|
|
41
|
+
process_tracking: true,
|
|
42
|
+
// Add health check
|
|
43
|
+
health_check: {
|
|
44
|
+
url: 'http://localhost:6006',
|
|
45
|
+
max_attempts: 20,
|
|
46
|
+
interval: 2000,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'unit tests',
|
|
55
|
+
parallel: [
|
|
56
|
+
{
|
|
57
|
+
command: 'test-ci',
|
|
58
|
+
description: 'Run unit tests',
|
|
59
|
+
status: 'enabled',
|
|
60
|
+
attempts: 2,
|
|
61
|
+
should_retry: (output) => {
|
|
62
|
+
// Check for test failures in both formats
|
|
63
|
+
const testSuiteFailureMatch = output.match(
|
|
64
|
+
/Test Suites:.*?\(\d+\) failed/,
|
|
65
|
+
);
|
|
66
|
+
const individualTestFailureMatch =
|
|
67
|
+
output.match(/✘\s*(\d+)\s*failing/);
|
|
68
|
+
|
|
69
|
+
const hasTestSuiteFailures =
|
|
70
|
+
testSuiteFailureMatch && parseInt(testSuiteFailureMatch[1]) > 0;
|
|
71
|
+
const hasIndividualTestFailures =
|
|
72
|
+
individualTestFailureMatch &&
|
|
73
|
+
parseInt(individualTestFailureMatch[1]) > 0;
|
|
74
|
+
|
|
75
|
+
const hasTestFailures =
|
|
76
|
+
hasTestSuiteFailures || hasIndividualTestFailures;
|
|
18
77
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
78
|
+
// Check for "Test suite failed to run" in logs
|
|
79
|
+
if (output.includes('Test suite failed to run')) {
|
|
80
|
+
console.error('Certain tests could not be run');
|
|
81
|
+
return false; // Don't retry if certain tests could not be run
|
|
82
|
+
}
|
|
24
83
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
84
|
+
if (!hasTestFailures) {
|
|
85
|
+
console.log(
|
|
86
|
+
'Tests have passed but coverage thresholds have not been met',
|
|
87
|
+
);
|
|
88
|
+
return false; // Don't retry if only coverage failed
|
|
89
|
+
}
|
|
31
90
|
|
|
32
|
-
|
|
91
|
+
return hasTestFailures; // Only retry if there are actual test failures
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
33
95
|
},
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// Add health check
|
|
50
|
-
health_check: {
|
|
51
|
-
url: 'http://localhost:6006',
|
|
52
|
-
max_attempts: 20,
|
|
53
|
-
interval: 2000,
|
|
96
|
+
{
|
|
97
|
+
name: 'playwright',
|
|
98
|
+
parallel: [
|
|
99
|
+
{
|
|
100
|
+
command: 'playwright_ci',
|
|
101
|
+
description: 'Run Playwright tests',
|
|
102
|
+
status: 'enabled',
|
|
103
|
+
attempts: 1, //Playwright internally retries in CI mode
|
|
104
|
+
dependencies: [
|
|
105
|
+
{
|
|
106
|
+
command: 'dev',
|
|
107
|
+
background: true,
|
|
108
|
+
url: 'http://localhost:5173',
|
|
109
|
+
},
|
|
110
|
+
],
|
|
54
111
|
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
command: 'stylelint',
|
|
60
|
-
description: 'Run stylelint checks',
|
|
61
|
-
status: 'enabled',
|
|
62
|
-
},
|
|
63
|
-
{ command: 'lint', description: 'Run lint checks', status: 'enabled' },
|
|
64
|
-
{
|
|
65
|
-
command: 'jscpd',
|
|
66
|
-
description: 'Run code duplication checks',
|
|
67
|
-
status: 'enabled',
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
command: 'playwright_ci',
|
|
71
|
-
description: 'Run Playwright tests',
|
|
72
|
-
status: 'enabled',
|
|
73
|
-
attempts: 1, //Playwright internally retries in CI mode
|
|
74
|
-
dependencies: [
|
|
75
|
-
{
|
|
76
|
-
command: 'dev',
|
|
77
|
-
background: true,
|
|
78
|
-
url: 'http://localhost:5173',
|
|
79
|
-
kill: 'application_end',
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
},
|
|
83
|
-
];
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|