scripts-orchestrator 1.2.1 → 1.2.3

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
@@ -56,6 +56,144 @@ Create a configuration file (default: `scripts-orchestrator.config.js`) that def
56
56
  }
57
57
  ```
58
58
 
59
+ ## Example Configurations
60
+
61
+ Here are some practical examples of how to configure the orchestrator for different scenarios:
62
+
63
+ ### Basic Build and Test Pipeline
64
+ ```javascript
65
+ export default [
66
+ {
67
+ command: 'build',
68
+ description: 'Build the project',
69
+ status: 'enabled',
70
+ attempts: 1
71
+ },
72
+ {
73
+ command: 'test',
74
+ description: 'Run unit tests',
75
+ status: 'enabled',
76
+ attempts: 2,
77
+ should_retry: (output) => {
78
+ // Only retry if there are actual test failures
79
+ const testSummaryMatch = output.match(/Test Suites:.*?(\d+) failed/);
80
+ return testSummaryMatch && parseInt(testSummaryMatch[1]) > 0;
81
+ }
82
+ },
83
+ {
84
+ command: 'lint',
85
+ description: 'Run lint checks',
86
+ status: 'enabled'
87
+ }
88
+ ];
89
+ ```
90
+
91
+ ### Storybook Testing with Background Process
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
108
+ }
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
131
+ }
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;
159
+ }
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
+ ];
195
+ ```
196
+
59
197
  ## Command Types
60
198
 
61
199
  The orchestrator is completely agnostic to what commands it runs. It can execute any npm scripts. Common use cases include:
@@ -10,6 +10,18 @@ export class Orchestrator {
10
10
  this.logger = log;
11
11
  this.failedCommands = [];
12
12
  this.skippedCommands = [];
13
+ this.commandTimings = new Map();
14
+ }
15
+
16
+ formatDuration(ms) {
17
+ if (ms < 1000) return `${ms}ms`;
18
+ const seconds = Math.floor(ms / 1000);
19
+ const minutes = Math.floor(seconds / 60);
20
+ const remainingSeconds = seconds % 60;
21
+ if (minutes > 0) {
22
+ return `${minutes}m ${remainingSeconds}s`;
23
+ }
24
+ return `${seconds}s`;
13
25
  }
14
26
 
15
27
  async executeCommand(commandConfig, visited = new Set()) {
@@ -26,12 +38,15 @@ export class Orchestrator {
26
38
  health_check,
27
39
  } = commandConfig;
28
40
 
41
+ const startTime = Date.now();
42
+
29
43
  // Check for circular dependencies
30
44
  if (visited.has(command)) {
31
45
  this.logger.error(
32
46
  `Circular dependency detected: ${Array.from(visited).join(' -> ')} -> ${command}`,
33
47
  );
34
48
  this.failedCommands.push(command);
49
+ this.commandTimings.set(command, Date.now() - startTime);
35
50
  return false;
36
51
  }
37
52
  visited.add(command);
@@ -40,6 +55,7 @@ export class Orchestrator {
40
55
  if (status === 'disabled') {
41
56
  this.logger.warn(`Skipping: npm run ${command} (status: disabled)`);
42
57
  this.skippedCommands.push(command);
58
+ this.commandTimings.set(command, Date.now() - startTime);
43
59
  visited.delete(command);
44
60
  return true;
45
61
  }
@@ -56,6 +72,7 @@ export class Orchestrator {
56
72
  startedByScript: false,
57
73
  process_tracking,
58
74
  });
75
+ this.commandTimings.set(command, Date.now() - startTime);
59
76
  visited.delete(command);
60
77
  return true;
61
78
  }
@@ -67,6 +84,7 @@ export class Orchestrator {
67
84
  if (!dependencySuccess) {
68
85
  this.logger.error(`Skipping ${command} due to failed dependency`);
69
86
  this.skippedCommands.push(command);
87
+ this.commandTimings.set(command, Date.now() - startTime);
70
88
  visited.delete(command);
71
89
  return false;
72
90
  }
@@ -83,6 +101,7 @@ export class Orchestrator {
83
101
  `URL ${dependency.health_check.url} is not available after maximum attempts`,
84
102
  );
85
103
  this.skippedCommands.push(command);
104
+ this.commandTimings.set(command, Date.now() - startTime);
86
105
  visited.delete(command);
87
106
  return false;
88
107
  }
@@ -101,6 +120,8 @@ export class Orchestrator {
101
120
  // Execute the main command with retries
102
121
  let result = false;
103
122
  let commandOutput = '';
123
+ let commandFailed = false;
124
+
104
125
  for (let attempt = 1; attempt <= attempts; attempt++) {
105
126
  if (attempt > 1) {
106
127
  this.logger.warn(`Retrying ${command} (attempt ${attempt}/${attempts})`);
@@ -117,36 +138,54 @@ export class Orchestrator {
117
138
  result = success;
118
139
 
119
140
  if (result) {
141
+ // Remove from failed commands if it was there
120
142
  this.failedCommands = this.failedCommands.filter(cmd => cmd !== command);
143
+ commandFailed = false;
121
144
  break;
122
145
  } else if (attempt < attempts) {
123
146
  if (should_retry && !should_retry(commandOutput)) {
124
147
  this.logger.warn(
125
148
  `${command} failed but doesn't meet retry criteria. Skipping retry.`,
126
149
  );
150
+ commandFailed = true;
127
151
  break;
128
152
  }
129
153
  this.logger.error(`Attempt ${attempt}/${attempts} failed for ${command}`);
154
+ commandFailed = true;
155
+ } else {
156
+ commandFailed = true;
130
157
  }
131
158
  }
132
159
 
160
+ if (commandFailed) {
161
+ this.failedCommands.push(command);
162
+ }
163
+
164
+ this.commandTimings.set(command, Date.now() - startTime);
133
165
  visited.delete(command);
134
166
  return result;
135
167
  }
136
168
 
137
169
  summarizeResults() {
138
170
  this.logger.info('\nCommand Summary:');
171
+ let hasFailures = false;
172
+
139
173
  this.config.forEach(({ command }) => {
174
+ const duration = this.commandTimings.get(command);
175
+ const durationStr = duration ? ` (${this.formatDuration(duration)})` : '';
176
+
140
177
  if (this.failedCommands.includes(command)) {
141
- this.logger.error(`- ${command}: ❌ (See logs/scripts-orchestrator_${command}.log)`);
178
+ hasFailures = true;
179
+ this.logger.error(`- ${command}: ❌${durationStr} (See logs/scripts-orchestrator_${command}.log)`);
142
180
  } else if (this.skippedCommands.includes(command)) {
143
- this.logger.warn(`- ${command}: ⚠️ (Skipped due to failed dependency)`);
181
+ hasFailures = true;
182
+ this.logger.warn(`- ${command}: ⚠️${durationStr} (Skipped due to failed dependency)`);
144
183
  } else {
145
- this.logger.success(`- ${command}: ✅`);
184
+ this.logger.success(`- ${command}: ✅${durationStr}`);
146
185
  }
147
186
  });
148
187
 
149
- if (this.failedCommands.length > 0 || this.skippedCommands.length > 0) {
188
+ if (hasFailures) {
150
189
  this.logger.error('\n❌ Some commands failed or were skipped. See details above.');
151
190
  } else {
152
191
  this.logger.success('\n🎉 All commands executed successfully!');
@@ -161,21 +200,28 @@ export class Orchestrator {
161
200
  );
162
201
 
163
202
  // Wait for all top-level commands to complete
164
- await Promise.all(tasks);
203
+ const results = await Promise.all(tasks);
204
+
205
+ // Check if any command failed
206
+ const hasFailures = results.some(result => !result) ||
207
+ this.failedCommands.length > 0 ||
208
+ this.skippedCommands.length > 0;
165
209
 
166
210
  // Add a small delay to ensure all processes have finished
167
211
  await new Promise((resolve) => setTimeout(resolve, 1000));
168
212
 
169
213
  this.summarizeResults();
214
+
215
+ // Exit with appropriate status
216
+ if (hasFailures) {
217
+ process.exit(1);
218
+ }
170
219
  } finally {
171
220
  try {
172
221
  await this.processManager.cleanup();
173
222
  } catch (error) {
174
223
  this.logger.error(`Cleanup failed: ${error.message}`);
175
224
  }
176
- if (this.failedCommands.length > 0 || this.skippedCommands.length > 0) {
177
- process.exit(1);
178
- }
179
225
  }
180
226
  }
181
227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scripts-orchestrator",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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",