scripts-orchestrator 2.6.0 → 2.7.1
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 +137 -0
- package/index.js +32 -22
- package/lib/logger.js +4 -0
- package/lib/orchestrator.js +30 -3
- package/lib/process-manager.js +32 -8
- package/package.json +1 -1
- package/scripts-orchestrator.config.js +3 -0
package/README.md
CHANGED
|
@@ -31,6 +31,8 @@ npm install --save-dev scripts-orchestrator
|
|
|
31
31
|
- **Retry Mechanism**: Configurable retry attempts for failed commands
|
|
32
32
|
- **Process Management**: Proper cleanup of background processes
|
|
33
33
|
- **Health Checks**: Verifies service availability before proceeding
|
|
34
|
+
- **Environment Variables**: Pass custom environment variables to commands
|
|
35
|
+
- **Optional Phases**: Mark phases as optional and run them selectively
|
|
34
36
|
- **Comprehensive Logging**: Detailed logging of command execution and results
|
|
35
37
|
|
|
36
38
|
## Configuration
|
|
@@ -45,6 +47,10 @@ Create a configuration file (default: `scripts-orchestrator.config.js`) that def
|
|
|
45
47
|
attempts: 1, // Number of retry attempts
|
|
46
48
|
dependencies: [], // Array of dependent commands
|
|
47
49
|
background: false, // Whether to run in background
|
|
50
|
+
env: { // Optional environment variables
|
|
51
|
+
PORT: 3000,
|
|
52
|
+
NODE_ENV: 'production'
|
|
53
|
+
},
|
|
48
54
|
kill_command: 'kill_storybook', // Optional kill command to kill the process
|
|
49
55
|
health_check: { // Health check configuration
|
|
50
56
|
url: 'http://localhost:port',
|
|
@@ -57,6 +63,20 @@ Create a configuration file (default: `scripts-orchestrator.config.js`) that def
|
|
|
57
63
|
}
|
|
58
64
|
```
|
|
59
65
|
|
|
66
|
+
### Phase Configuration
|
|
67
|
+
|
|
68
|
+
When using the phases format, each phase can have the following properties:
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
{
|
|
72
|
+
name: 'phase_name', // The name of the phase
|
|
73
|
+
optional: true, // Whether this phase is optional (default: false)
|
|
74
|
+
parallel: [ // Array of commands to run in parallel
|
|
75
|
+
// ... command configurations
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
60
80
|
## Example Configurations
|
|
61
81
|
|
|
62
82
|
Here are some practical examples of how to configure the orchestrator for different scenarios:
|
|
@@ -124,11 +144,69 @@ export default {
|
|
|
124
144
|
status: 'enabled'
|
|
125
145
|
}
|
|
126
146
|
]
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'optional-e2e',
|
|
150
|
+
optional: true,
|
|
151
|
+
parallel: [
|
|
152
|
+
{
|
|
153
|
+
command: 'playwright',
|
|
154
|
+
description: 'Run end-to-end tests',
|
|
155
|
+
status: 'enabled',
|
|
156
|
+
attempts: 1
|
|
157
|
+
}
|
|
158
|
+
]
|
|
127
159
|
}
|
|
128
160
|
]
|
|
129
161
|
};
|
|
130
162
|
```
|
|
131
163
|
|
|
164
|
+
### Using Environment Variables
|
|
165
|
+
|
|
166
|
+
You can pass custom environment variables to commands using the `env` property. This is useful for configuring ports, API endpoints, or any environment-specific settings:
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
export default {
|
|
170
|
+
phases: [
|
|
171
|
+
{
|
|
172
|
+
name: 'playwright',
|
|
173
|
+
parallel: [
|
|
174
|
+
{
|
|
175
|
+
command: 'playwright_ci',
|
|
176
|
+
description: 'Run Playwright tests',
|
|
177
|
+
env: {
|
|
178
|
+
PLAYWRIGHT_PORT: 5173,
|
|
179
|
+
API_URL: 'http://localhost:3000',
|
|
180
|
+
TEST_ENV: 'ci'
|
|
181
|
+
},
|
|
182
|
+
status: 'enabled',
|
|
183
|
+
attempts: 1,
|
|
184
|
+
dependencies: [
|
|
185
|
+
{
|
|
186
|
+
command: 'dev',
|
|
187
|
+
background: true,
|
|
188
|
+
env: {
|
|
189
|
+
PORT: 5173
|
|
190
|
+
},
|
|
191
|
+
health_check: {
|
|
192
|
+
url: 'http://localhost:5173',
|
|
193
|
+
max_attempts: 20,
|
|
194
|
+
interval: 2000
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The command will run with the environment variables set, equivalent to:
|
|
206
|
+
```bash
|
|
207
|
+
PLAYWRIGHT_PORT=5173 API_URL=http://localhost:3000 TEST_ENV=ci npm run playwright_ci
|
|
208
|
+
```
|
|
209
|
+
|
|
132
210
|
See more examples [here](./docs/samples.md)
|
|
133
211
|
|
|
134
212
|
## Command Types
|
|
@@ -171,6 +249,12 @@ The orchestrator doesn't care what the commands do - it just ensures they run (i
|
|
|
171
249
|
|
|
172
250
|
# Start from a specific phase with custom config
|
|
173
251
|
npm run scripts-orchestrator -- ./path/to/your/config.js --phase "playwright"
|
|
252
|
+
|
|
253
|
+
# Run specific optional phases
|
|
254
|
+
npm run scripts-orchestrator -- --phases "optional-e2e,optional-performance"
|
|
255
|
+
|
|
256
|
+
# Run with verbose logging
|
|
257
|
+
npm run scripts-orchestrator -- --verbose
|
|
174
258
|
```
|
|
175
259
|
|
|
176
260
|
### Starting from a Specific Phase
|
|
@@ -200,6 +284,59 @@ When starting from a specific phase:
|
|
|
200
284
|
- Commands in skipped phases are marked as "skipped" in the final summary
|
|
201
285
|
- The orchestrator validates that the specified phase exists and shows available phases if not found
|
|
202
286
|
|
|
287
|
+
### Optional Phases
|
|
288
|
+
|
|
289
|
+
You can mark phases as optional by adding `optional: true` to the phase configuration. Optional phases will only run if explicitly requested via the `--phases` command line argument.
|
|
290
|
+
|
|
291
|
+
#### Configuration
|
|
292
|
+
```javascript
|
|
293
|
+
export default {
|
|
294
|
+
phases: [
|
|
295
|
+
{
|
|
296
|
+
name: 'build',
|
|
297
|
+
parallel: [
|
|
298
|
+
{ command: 'build', description: 'Build the project' }
|
|
299
|
+
]
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'optional-e2e',
|
|
303
|
+
optional: true, // This phase is optional
|
|
304
|
+
parallel: [
|
|
305
|
+
{ command: 'playwright', description: 'Run end-to-end tests' }
|
|
306
|
+
]
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: 'optional-performance',
|
|
310
|
+
optional: true, // This phase is optional
|
|
311
|
+
parallel: [
|
|
312
|
+
{ command: 'lighthouse', description: 'Run performance tests' }
|
|
313
|
+
]
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
};
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### Usage
|
|
320
|
+
```bash
|
|
321
|
+
# Run only the default phases (build, test, etc.)
|
|
322
|
+
npm run scripts-orchestrator
|
|
323
|
+
|
|
324
|
+
# Run specific optional phases
|
|
325
|
+
npm run scripts-orchestrator -- --phases "optional-e2e"
|
|
326
|
+
|
|
327
|
+
# Run multiple optional phases
|
|
328
|
+
npm run scripts-orchestrator -- --phases "optional-e2e,optional-performance"
|
|
329
|
+
|
|
330
|
+
# Run all phases including optional ones
|
|
331
|
+
npm run scripts-orchestrator -- --phases "build,test,optional-e2e,optional-performance"
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Note**:
|
|
335
|
+
- Optional phases are skipped by default unless explicitly requested
|
|
336
|
+
- You can combine `--phase` and `--phases` arguments
|
|
337
|
+
- The orchestrator validates that all specified phases exist
|
|
338
|
+
- Commands in skipped optional phases are marked as "skipped" in the final summary
|
|
339
|
+
|
|
203
340
|
## Error Handling
|
|
204
341
|
|
|
205
342
|
- The script tracks failed and skipped commands
|
package/index.js
CHANGED
|
@@ -9,33 +9,43 @@ import path from 'path';
|
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import { Orchestrator } from './lib/index.js';
|
|
11
11
|
import { log } from './lib/logger.js';
|
|
12
|
+
import yargs from 'yargs';
|
|
13
|
+
import { hideBin } from 'yargs/helpers';
|
|
12
14
|
|
|
13
|
-
// Parse command line arguments
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
// Parse command line arguments using yargs
|
|
16
|
+
const argv = yargs(hideBin(process.argv))
|
|
17
|
+
.option('verbose', {
|
|
18
|
+
alias: 'v',
|
|
19
|
+
type: 'boolean',
|
|
20
|
+
description: 'Run with verbose logging',
|
|
21
|
+
})
|
|
22
|
+
.option('phase', {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'Start execution from a specific phase',
|
|
25
|
+
})
|
|
26
|
+
.option('phases', {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Comma-separated list of phases to run (for optional phases)',
|
|
29
|
+
})
|
|
30
|
+
.option('logFolder', {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Specify the directory for log files',
|
|
33
|
+
})
|
|
34
|
+
.help()
|
|
35
|
+
.alias('h', 'help')
|
|
36
|
+
.parse();
|
|
18
37
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
i++; // Skip the next argument since we consumed it
|
|
26
|
-
} else if (arg === '--logFolder' && i + 1 < args.length) {
|
|
27
|
-
logFolder = args[i + 1];
|
|
28
|
-
i++; // Skip the next argument since we consumed it
|
|
29
|
-
} else if (!arg.startsWith('--') && !configPath) {
|
|
30
|
-
// First non-flag argument is the config path
|
|
31
|
-
configPath = arg;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
38
|
+
// Extract arguments
|
|
39
|
+
const args = argv._;
|
|
40
|
+
const configPath = args[0] || './scripts-orchestrator.config.js';
|
|
41
|
+
let startPhase = argv.phase;
|
|
42
|
+
let logFolder = argv.logFolder;
|
|
43
|
+
const phases = argv.phases ? argv.phases.split(',').map(p => p.trim()) : null;
|
|
34
44
|
|
|
35
45
|
// Validate config file exists
|
|
36
46
|
if (!fs.existsSync(configPath)) {
|
|
37
47
|
log.error(`Error: Config file not found at ${configPath}`);
|
|
38
|
-
log.error('
|
|
48
|
+
log.error('Use --help for usage information');
|
|
39
49
|
process.exit(1);
|
|
40
50
|
}
|
|
41
51
|
|
|
@@ -55,7 +65,7 @@ if (!logFolder && commandsConfig.log_folder) {
|
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
// Create and run the orchestrator
|
|
58
|
-
const orchestrator = new Orchestrator(commandsConfig, startPhase, logFolder);
|
|
68
|
+
const orchestrator = new Orchestrator(commandsConfig, startPhase, logFolder, phases);
|
|
59
69
|
|
|
60
70
|
// Enhanced signal handlers
|
|
61
71
|
const handleSignal = async (signal) => {
|
package/lib/logger.js
CHANGED
|
@@ -12,6 +12,10 @@ const argv = yargs(hideBin(process.argv))
|
|
|
12
12
|
type: 'string',
|
|
13
13
|
description: 'Start execution from a specific phase',
|
|
14
14
|
})
|
|
15
|
+
.option('phases', {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'Comma-separated list of phases to run (for optional phases)',
|
|
18
|
+
})
|
|
15
19
|
.option('logFolder', {
|
|
16
20
|
type: 'string',
|
|
17
21
|
description: 'Specify the directory for log files',
|
package/lib/orchestrator.js
CHANGED
|
@@ -3,10 +3,11 @@ import { healthCheck } from './health-check.js';
|
|
|
3
3
|
import { log } from './logger.js';
|
|
4
4
|
|
|
5
5
|
export class Orchestrator {
|
|
6
|
-
constructor(config, startPhase = null, logFolder = null) {
|
|
6
|
+
constructor(config, startPhase = null, logFolder = null, phases = null) {
|
|
7
7
|
this.config = config;
|
|
8
8
|
this.startPhase = startPhase;
|
|
9
9
|
this.logFolder = logFolder;
|
|
10
|
+
this.phases = phases;
|
|
10
11
|
this.processManager = processManager;
|
|
11
12
|
this.healthCheck = healthCheck;
|
|
12
13
|
this.logger = log;
|
|
@@ -53,6 +54,7 @@ export class Orchestrator {
|
|
|
53
54
|
dependencies = [],
|
|
54
55
|
background = false,
|
|
55
56
|
status = 'enabled',
|
|
57
|
+
log,
|
|
56
58
|
logFile,
|
|
57
59
|
attempts = 1,
|
|
58
60
|
retry_command,
|
|
@@ -60,6 +62,7 @@ export class Orchestrator {
|
|
|
60
62
|
process_tracking = false,
|
|
61
63
|
health_check,
|
|
62
64
|
kill_command,
|
|
65
|
+
env,
|
|
63
66
|
} = commandConfig;
|
|
64
67
|
|
|
65
68
|
const startTime = Date.now();
|
|
@@ -155,11 +158,12 @@ export class Orchestrator {
|
|
|
155
158
|
|
|
156
159
|
const { success, output } = await this.processManager.runCommand({
|
|
157
160
|
cmd: attempt === 1 ? command : retry_command || command,
|
|
158
|
-
logFile,
|
|
161
|
+
logFile: log || logFile, // Prefer 'log' key over 'logFile' for backwards compatibility
|
|
159
162
|
background,
|
|
160
163
|
healthCheck: health_check,
|
|
161
164
|
kill_command,
|
|
162
165
|
isRetry: attempt > 1,
|
|
166
|
+
env,
|
|
163
167
|
});
|
|
164
168
|
commandOutput = output;
|
|
165
169
|
result = success;
|
|
@@ -213,7 +217,9 @@ export class Orchestrator {
|
|
|
213
217
|
|
|
214
218
|
if (this.failedCommands.includes(command)) {
|
|
215
219
|
hasFailures = true;
|
|
216
|
-
|
|
220
|
+
// Get the actual log path from process manager
|
|
221
|
+
const logPath = this.processManager.getLogPath(command);
|
|
222
|
+
this.logger.error(`- ${command}: ❌${durationStr} (See ${logPath})`);
|
|
217
223
|
} else if (this.skippedCommands.includes(command)) {
|
|
218
224
|
hasFailures = true;
|
|
219
225
|
this.logger.warn(`- ${command}: ⚠️${durationStr} (Skipped due to failed dependency)`);
|
|
@@ -261,6 +267,17 @@ export class Orchestrator {
|
|
|
261
267
|
}
|
|
262
268
|
}
|
|
263
269
|
|
|
270
|
+
// Check if this is an optional phase that should be skipped
|
|
271
|
+
if (phase.optional === true && this.phases && !this.phases.includes(phase.name)) {
|
|
272
|
+
this.logger.info(`\n⏭️ Skipping optional phase: ${phase.name} (not explicitly requested)`);
|
|
273
|
+
// Mark all commands in this phase as skipped
|
|
274
|
+
phase.parallel.forEach(({ command }) => {
|
|
275
|
+
this.skippedCommands.push(command);
|
|
276
|
+
this.commandTimings.set(command, 0);
|
|
277
|
+
});
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
264
281
|
if (phaseFailed) {
|
|
265
282
|
// Mark all commands in remaining phases as skipped
|
|
266
283
|
phase.parallel.forEach(({ command }) => {
|
|
@@ -296,6 +313,16 @@ export class Orchestrator {
|
|
|
296
313
|
process.exit(1);
|
|
297
314
|
}
|
|
298
315
|
|
|
316
|
+
// Validate phases if specified
|
|
317
|
+
if (this.phases) {
|
|
318
|
+
const availablePhases = this.config.phases.map(p => p.name);
|
|
319
|
+
const invalidPhases = this.phases.filter(phase => !availablePhases.includes(phase));
|
|
320
|
+
if (invalidPhases.length > 0) {
|
|
321
|
+
this.logger.error(`❌ Invalid phases specified: ${invalidPhases.join(', ')}. Available phases: ${availablePhases.join(', ')}`);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
299
326
|
// Check final status
|
|
300
327
|
hasFailures = hasFailures ||
|
|
301
328
|
this.failedCommands.length > 0 ||
|
package/lib/process-manager.js
CHANGED
|
@@ -18,6 +18,14 @@ export class ProcessManager {
|
|
|
18
18
|
this.logger.verbose(`Log folder set to: ${logFolder}`);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
getLogPath(command) {
|
|
22
|
+
const baseDir = this.logFolder ? path.resolve(this.logFolder) : process.cwd();
|
|
23
|
+
const LOGS_DIR = path.join(baseDir, 'scripts-orchestrator-logs');
|
|
24
|
+
// Use only the first word of the command for the log filename
|
|
25
|
+
const logName = command.split(/\s+/)[0];
|
|
26
|
+
return path.join(LOGS_DIR, `${logName}.log`);
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
addBackgroundProcess({ command, url, startedByScript, process_tracking, kill_command }) {
|
|
22
30
|
this.logger.verbose(`Adding background process: ${command} (${url})`);
|
|
23
31
|
this.backgroundProcessesDetails.push({
|
|
@@ -29,10 +37,12 @@ export class ProcessManager {
|
|
|
29
37
|
});
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
async runCommand({ cmd, logFile, background = false, healthCheck = null, kill_command = null, isRetry = false }) {
|
|
40
|
+
async runCommand({ cmd, logFile, background = false, healthCheck = null, kill_command = null, isRetry = false, env = null }) {
|
|
33
41
|
const baseDir = this.logFolder ? path.resolve(this.logFolder) : process.cwd();
|
|
34
42
|
const LOGS_DIR = path.join(baseDir, 'scripts-orchestrator-logs');
|
|
35
|
-
|
|
43
|
+
// Use only the first word of the command for the log filename
|
|
44
|
+
const logName = cmd.split(/\s+/)[0];
|
|
45
|
+
const LOG_FILE = logFile || path.join(LOGS_DIR, `${logName}.log`);
|
|
36
46
|
|
|
37
47
|
try {
|
|
38
48
|
if (!fs.existsSync(LOGS_DIR)) {
|
|
@@ -52,10 +62,17 @@ export class ProcessManager {
|
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
return new Promise((resolve) => {
|
|
55
|
-
|
|
65
|
+
// Build command with environment variables if provided
|
|
66
|
+
let fullCommand = `npm run ${cmd}`;
|
|
67
|
+
if (env && Object.keys(env).length > 0) {
|
|
68
|
+
const envStr = Object.entries(env).map(([key, value]) => `${key}=${value}`).join(' ');
|
|
69
|
+
fullCommand = `${envStr} npm run ${cmd}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.logger.info(`Running: ${fullCommand}`);
|
|
56
73
|
|
|
57
74
|
// Create isolated environment for each process
|
|
58
|
-
const isolatedEnv = this.createIsolatedEnvironment({ command: cmd });
|
|
75
|
+
const isolatedEnv = this.createIsolatedEnvironment({ command: cmd, env });
|
|
59
76
|
|
|
60
77
|
const options = {
|
|
61
78
|
shell: true,
|
|
@@ -70,8 +87,8 @@ export class ProcessManager {
|
|
|
70
87
|
//this.logger.verbose(`Process options: ${JSON.stringify(options, null, 2)}`);
|
|
71
88
|
|
|
72
89
|
try {
|
|
73
|
-
this.logger.verbose(`Spawning process with command:
|
|
74
|
-
const processInstance = spawn(
|
|
90
|
+
this.logger.verbose(`Spawning process with command: ${fullCommand}`);
|
|
91
|
+
const processInstance = spawn(fullCommand, [], options);
|
|
75
92
|
|
|
76
93
|
processInstance.on('error', (error) => {
|
|
77
94
|
this.logger.error(`Failed to start process: ${error.message}`);
|
|
@@ -225,7 +242,7 @@ export class ProcessManager {
|
|
|
225
242
|
});
|
|
226
243
|
}
|
|
227
244
|
|
|
228
|
-
createIsolatedEnvironment({ command }) {
|
|
245
|
+
createIsolatedEnvironment({ command, env = null }) {
|
|
229
246
|
// Create a deep copy to avoid any reference sharing
|
|
230
247
|
const baseEnv = JSON.parse(JSON.stringify(process.env));
|
|
231
248
|
|
|
@@ -245,6 +262,13 @@ export class ProcessManager {
|
|
|
245
262
|
npm_config_loglevel: 'error',
|
|
246
263
|
};
|
|
247
264
|
|
|
265
|
+
// Merge custom environment variables if provided
|
|
266
|
+
if (env && typeof env === 'object') {
|
|
267
|
+
Object.entries(env).forEach(([key, value]) => {
|
|
268
|
+
isolatedEnv[key] = String(value);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
248
272
|
// Remove any potentially problematic environment variables
|
|
249
273
|
delete isolatedEnv.npm_lifecycle_event;
|
|
250
274
|
delete isolatedEnv.npm_lifecycle_script;
|
|
@@ -504,4 +528,4 @@ export class ProcessManager {
|
|
|
504
528
|
}
|
|
505
529
|
|
|
506
530
|
// For backward compatibility
|
|
507
|
-
export const processManager = new ProcessManager();
|
|
531
|
+
export const processManager = new ProcessManager();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scripts-orchestrator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.1",
|
|
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",
|