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 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 args = process.argv.slice(2);
15
- let configPath = './scripts-orchestrator.config.js';
16
- let startPhase = null;
17
- let logFolder = null;
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
- // Parse arguments
20
- for (let i = 0; i < args.length; i++) {
21
- const arg = args[i];
22
-
23
- if (arg === '--phase' && i + 1 < args.length) {
24
- startPhase = args[i + 1];
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('Usage: scripts-orchestrator [path-to-config-file] [--phase <phase-name>] [--logFolder <log-directory>]');
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',
@@ -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
- this.logger.error(`- ${command}: ❌${durationStr} (See logs/scripts-orchestrator_${command}.log)`);
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 ||
@@ -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
- const LOG_FILE = logFile || path.join(LOGS_DIR, `${cmd}.log`);
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
- this.logger.info(`Running: npm run ${cmd}`);
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: npm run ${cmd}`);
74
- const processInstance = spawn('npm', ['run', cmd], options);
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.6.0",
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",
@@ -99,6 +99,9 @@ export default {
99
99
  {
100
100
  command: 'playwright_ci',
101
101
  description: 'Run Playwright tests',
102
+ env: {
103
+ PLAYWRIGHT_PORT: 5173,
104
+ },
102
105
  status: 'enabled',
103
106
  attempts: 1, //Playwright internally retries in CI mode
104
107
  dependencies: [