scripts-orchestrator 1.2.2 → 2.0.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 CHANGED
@@ -88,112 +88,48 @@ export default [
88
88
  ];
89
89
  ```
90
90
 
91
- ### Storybook Testing with Background Process
91
+ ### Basic Build and Test Pipeline with Phases
92
92
  ```javascript
93
- export default [
94
- {
95
- command: 'test-storybook',
96
- description: 'Run Storybook tests',
97
- status: 'enabled',
98
- attempts: 2,
99
- dependencies: [
100
- {
101
- command: 'storybook_silent',
102
- background: true,
103
- wait: 5000,
104
- health_check: {
105
- url: 'http://localhost:6006',
106
- max_attempts: 20,
107
- interval: 2000
93
+ export default {
94
+ phases: [
95
+ {
96
+ name: 'build',
97
+ parallel: [
98
+ {
99
+ command: 'build',
100
+ description: 'Build the project',
101
+ status: 'enabled',
102
+ attempts: 1
108
103
  }
109
- }
110
- ]
111
- }
112
- ];
113
- ```
114
-
115
- ### Playwright Testing with Development Server
116
- ```javascript
117
- export default [
118
- {
119
- command: 'playwright_ci',
120
- description: 'Run Playwright tests',
121
- status: 'enabled',
122
- attempts: 1,
123
- dependencies: [
124
- {
125
- command: 'dev',
126
- background: true,
127
- health_check: {
128
- url: 'http://localhost:5173',
129
- max_attempts: 20,
130
- interval: 2000
104
+ ]
105
+ },
106
+ {
107
+ name: 'test',
108
+ parallel: [
109
+ {
110
+ command: 'test',
111
+ description: 'Run unit tests',
112
+ status: 'enabled',
113
+ attempts: 2,
114
+ should_retry: (output) => {
115
+ // Only retry if there are actual test failures
116
+ const testSummaryMatch = output.match(/Test Suites:.*?(\d+) failed/);
117
+ return testSummaryMatch && parseInt(testSummaryMatch[1]) > 0;
118
+ }
119
+ },
120
+ {
121
+ command: 'lint',
122
+ description: 'Run lint checks',
123
+ status: 'enabled'
131
124
  }
132
- }
133
- ]
134
- }
135
- ];
136
- ```
137
-
138
- ### Full CI Pipeline with Multiple Checks
139
- ```javascript
140
- export default [
141
- {
142
- command: 'build',
143
- description: 'Build the project',
144
- status: 'enabled',
145
- attempts: 1
146
- },
147
- {
148
- command: 'test-ci',
149
- description: 'Run unit tests',
150
- status: 'enabled',
151
- attempts: 2,
152
- should_retry: (output) => {
153
- const testSummaryMatch = output.match(/Test Suites:.*?(\d+) failed/);
154
- const hasTestFailures = testSummaryMatch && parseInt(testSummaryMatch[1]) > 0;
155
- const hasCoverageFailures = output.match(/Jest: "global" coverage threshold/);
156
-
157
- // Only retry for actual test failures, not coverage issues
158
- return hasTestFailures;
125
+ ]
159
126
  }
160
- },
161
- {
162
- command: 'test-storybook',
163
- description: 'Run Storybook tests',
164
- status: 'enabled',
165
- attempts: 2,
166
- dependencies: [
167
- {
168
- command: 'storybook_silent',
169
- background: true,
170
- wait: 5000,
171
- health_check: {
172
- url: 'http://localhost:6006',
173
- max_attempts: 20,
174
- interval: 2000
175
- }
176
- }
177
- ]
178
- },
179
- {
180
- command: 'stylelint',
181
- description: 'Run stylelint checks',
182
- status: 'enabled'
183
- },
184
- {
185
- command: 'lint',
186
- description: 'Run lint checks',
187
- status: 'enabled'
188
- },
189
- {
190
- command: 'jscpd',
191
- description: 'Run code duplication checks',
192
- status: 'enabled'
193
- }
194
- ];
127
+ ]
128
+ };
195
129
  ```
196
130
 
131
+ See more examples [here](./docs/samples.md)
132
+
197
133
  ## Command Types
198
134
 
199
135
  The orchestrator is completely agnostic to what commands it runs. It can execute any npm scripts. Common use cases include:
@@ -248,6 +184,17 @@ The orchestrator doesn't care what the commands do - it just ensures they run (i
248
184
  - `0`: All commands executed successfully
249
185
  - `1`: One or more commands failed or were skipped
250
186
 
187
+
188
+ ## History
189
+ See [versions](./docs/versions.md)
190
+
191
+ ## Roadmap
192
+ - Better UX to indicate what is happening
193
+ - Tests to avoid regression
194
+ - Retry should append to the log file
195
+ - Run any shell command rather than assume the command is specified in package.json (? tentative)
196
+
197
+
251
198
  ## Disclaimer
252
199
 
253
200
  This software is provided "as is", without warranty of any kind, express or implied. The author(s) shall not be liable for any claims, damages, or other liabilities arising from the use of this software. Users are responsible for testing and verifying the software in their own environment before using it in production.
@@ -258,3 +205,5 @@ Contributions are welcome! Please feel free to submit a Pull Request.
258
205
 
259
206
  ## License
260
207
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
208
+
209
+
@@ -11,6 +11,22 @@ export class Orchestrator {
11
11
  this.failedCommands = [];
12
12
  this.skippedCommands = [];
13
13
  this.commandTimings = new Map();
14
+
15
+ // Flatten commands for easier tracking
16
+ this.allCommands = this.flattenCommands(config);
17
+ }
18
+
19
+ flattenCommands(config) {
20
+ // Handle both old array format and new phases format
21
+ if (Array.isArray(config)) {
22
+ return config;
23
+ }
24
+
25
+ if (config.phases) {
26
+ return config.phases.flatMap(phase => phase.parallel || []);
27
+ }
28
+
29
+ return [];
14
30
  }
15
31
 
16
32
  formatDuration(ms) {
@@ -120,6 +136,8 @@ export class Orchestrator {
120
136
  // Execute the main command with retries
121
137
  let result = false;
122
138
  let commandOutput = '';
139
+ let commandFailed = false;
140
+
123
141
  for (let attempt = 1; attempt <= attempts; attempt++) {
124
142
  if (attempt > 1) {
125
143
  this.logger.warn(`Retrying ${command} (attempt ${attempt}/${attempts})`);
@@ -136,22 +154,29 @@ export class Orchestrator {
136
154
  result = success;
137
155
 
138
156
  if (result) {
157
+ // Remove from failed commands if it was there
139
158
  this.failedCommands = this.failedCommands.filter(cmd => cmd !== command);
159
+ commandFailed = false;
140
160
  break;
141
161
  } else if (attempt < attempts) {
142
162
  if (should_retry && !should_retry(commandOutput)) {
143
163
  this.logger.warn(
144
164
  `${command} failed but doesn't meet retry criteria. Skipping retry.`,
145
165
  );
146
- this.failedCommands.push(command);
166
+ commandFailed = true;
147
167
  break;
148
168
  }
149
169
  this.logger.error(`Attempt ${attempt}/${attempts} failed for ${command}`);
170
+ commandFailed = true;
150
171
  } else {
151
- this.failedCommands.push(command);
172
+ commandFailed = true;
152
173
  }
153
174
  }
154
175
 
176
+ if (commandFailed) {
177
+ this.failedCommands.push(command);
178
+ }
179
+
155
180
  this.commandTimings.set(command, Date.now() - startTime);
156
181
  visited.delete(command);
157
182
  return result;
@@ -159,20 +184,24 @@ export class Orchestrator {
159
184
 
160
185
  summarizeResults() {
161
186
  this.logger.info('\nCommand Summary:');
162
- this.config.forEach(({ command }) => {
187
+ let hasFailures = false;
188
+
189
+ this.allCommands.forEach(({ command }) => {
163
190
  const duration = this.commandTimings.get(command);
164
191
  const durationStr = duration ? ` (${this.formatDuration(duration)})` : '';
165
192
 
166
193
  if (this.failedCommands.includes(command)) {
194
+ hasFailures = true;
167
195
  this.logger.error(`- ${command}: āŒ${durationStr} (See logs/scripts-orchestrator_${command}.log)`);
168
196
  } else if (this.skippedCommands.includes(command)) {
197
+ hasFailures = true;
169
198
  this.logger.warn(`- ${command}: āš ļø${durationStr} (Skipped due to failed dependency)`);
170
199
  } else {
171
200
  this.logger.success(`- ${command}: āœ…${durationStr}`);
172
201
  }
173
202
  });
174
203
 
175
- if (this.failedCommands.length > 0 || this.skippedCommands.length > 0) {
204
+ if (hasFailures) {
176
205
  this.logger.error('\nāŒ Some commands failed or were skipped. See details above.');
177
206
  } else {
178
207
  this.logger.success('\nšŸŽ‰ All commands executed successfully!');
@@ -181,27 +210,58 @@ export class Orchestrator {
181
210
 
182
211
  async run() {
183
212
  try {
184
- // Run top-level commands in parallel
185
- const tasks = this.config.map((commandConfig) =>
186
- this.executeCommand(commandConfig),
187
- );
213
+ let hasFailures = false;
188
214
 
189
- // Wait for all top-level commands to complete
190
- await Promise.all(tasks);
215
+ // Handle both old array format and new phases format
216
+ if (Array.isArray(this.config)) {
217
+ // Legacy: Run all commands in parallel
218
+ const tasks = this.config.map((commandConfig) =>
219
+ this.executeCommand(commandConfig),
220
+ );
221
+ const results = await Promise.all(tasks);
222
+ hasFailures = results.some(result => !result);
223
+ } else if (this.config.phases) {
224
+ // New: Run phases sequentially, commands within phases in parallel
225
+ for (const phase of this.config.phases) {
226
+ this.logger.info(`\nšŸ”„ Starting phase: ${phase.name}`);
227
+
228
+ const tasks = phase.parallel.map((commandConfig) =>
229
+ this.executeCommand(commandConfig),
230
+ );
231
+
232
+ const results = await Promise.all(tasks);
233
+ const phaseHasFailures = results.some(result => !result);
234
+
235
+ if (phaseHasFailures) {
236
+ hasFailures = true;
237
+ this.logger.error(`āŒ Phase "${phase.name}" completed with failures`);
238
+ break; // Stop executing remaining phases on failure
239
+ } else {
240
+ this.logger.success(`āœ… Phase "${phase.name}" completed successfully`);
241
+ }
242
+ }
243
+ }
244
+
245
+ // Check final status
246
+ hasFailures = hasFailures ||
247
+ this.failedCommands.length > 0 ||
248
+ this.skippedCommands.length > 0;
191
249
 
192
250
  // Add a small delay to ensure all processes have finished
193
251
  await new Promise((resolve) => setTimeout(resolve, 1000));
194
252
 
195
253
  this.summarizeResults();
254
+
255
+ // Exit with appropriate status
256
+ if (hasFailures) {
257
+ process.exit(1);
258
+ }
196
259
  } finally {
197
260
  try {
198
261
  await this.processManager.cleanup();
199
262
  } catch (error) {
200
263
  this.logger.error(`Cleanup failed: ${error.message}`);
201
264
  }
202
- if (this.failedCommands.length > 0 || this.skippedCommands.length > 0) {
203
- process.exit(1);
204
- }
205
265
  }
206
266
  }
207
267
  }
@@ -40,15 +40,16 @@ export class ProcessManager {
40
40
 
41
41
  return new Promise((resolve) => {
42
42
  this.logger.info(`Running: npm run ${cmd}`);
43
+
44
+ // Create isolated environment for each process
45
+ const isolatedEnv = this.createIsolatedEnvironment(cmd);
46
+
43
47
  const options = {
44
48
  shell: true,
45
49
  detached: background,
46
50
  stdio: ['ignore', 'pipe', 'pipe'],
47
51
  cwd: process.cwd(),
48
- env: {
49
- ...process.env,
50
- NODE_ENV: process.env.NODE_ENV || 'development',
51
- },
52
+ env: isolatedEnv,
52
53
  windowsHide: true,
53
54
  ...(background ? { processGroup: true } : {}),
54
55
  };
@@ -171,6 +172,33 @@ export class ProcessManager {
171
172
  });
172
173
  }
173
174
 
175
+ createIsolatedEnvironment(command) {
176
+ // Create a deep copy to avoid any reference sharing
177
+ const baseEnv = JSON.parse(JSON.stringify(process.env));
178
+
179
+ // Set standard environment variables
180
+ const isolatedEnv = {
181
+ ...baseEnv,
182
+ NODE_ENV: process.env.NODE_ENV || 'development',
183
+ // Add command-specific environment isolation
184
+ SCRIPTS_ORCHESTRATOR_COMMAND: command,
185
+ SCRIPTS_ORCHESTRATOR_PID: process.pid.toString(),
186
+ // Force fresh PATH to avoid any dynamic modifications
187
+ PATH: process.env.PATH,
188
+ // Ensure npm/node paths are isolated
189
+ npm_config_cache: path.join(process.cwd(), 'node_modules/.cache/npm'),
190
+ // Prevent npm from sharing config between parallel processes
191
+ npm_config_progress: 'false',
192
+ npm_config_loglevel: 'error',
193
+ };
194
+
195
+ // Remove any potentially problematic environment variables
196
+ delete isolatedEnv.npm_lifecycle_event;
197
+ delete isolatedEnv.npm_lifecycle_script;
198
+
199
+ return isolatedEnv;
200
+ }
201
+
174
202
  async cleanup() {
175
203
  this.logger.info('\nCleaning up background processes...');
176
204
  const killPromises = this.backgroundProcessesDetails.map(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scripts-orchestrator",
3
- "version": "1.2.2",
3
+ "version": "2.0.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",
@@ -8,7 +8,8 @@
8
8
  "scripts-orchestrator": "index.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "echo \"Error: no test specified\" && exit 1",
11
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
12
+ "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --detectOpenHandles --runInBand",
12
13
  "start": "node index.js",
13
14
  "lint": "eslint .",
14
15
  "prepare": "npm run lint"
@@ -29,7 +30,7 @@
29
30
  "license": "MIT",
30
31
  "repository": {
31
32
  "type": "git",
32
- "url": "git+https://github.com/Pratishthan/scripts-orchestrator"
33
+ "url": "git+https://github.com/Pratishthan/scripts-orchestrator.git"
33
34
  },
34
35
  "bugs": {
35
36
  "url": "https://github.com/Pratishthan/scripts-orchestrator/issues"
@@ -39,10 +40,14 @@
39
40
  "node": ">=14.0.0"
40
41
  },
41
42
  "dependencies": {
42
- "chalk": "^4.1.2"
43
+ "chalk": "^4.1.2",
44
+ "yargs": "^17.7.2"
43
45
  },
44
46
  "devDependencies": {
45
- "eslint": "^8.0.0"
47
+ "@types/jest": "^29.5.14",
48
+ "eslint": "^8.0.0",
49
+ "jest": "^29.7.0",
50
+ "ts-jest": "^29.3.3"
46
51
  },
47
52
  "files": [
48
53
  "lib/",