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 +138 -0
- package/lib/orchestrator.js +54 -8
- package/package.json +1 -1
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:
|
package/lib/orchestrator.js
CHANGED
|
@@ -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
|
-
|
|
178
|
+
hasFailures = true;
|
|
179
|
+
this.logger.error(`- ${command}: ❌${durationStr} (See logs/scripts-orchestrator_${command}.log)`);
|
|
142
180
|
} else if (this.skippedCommands.includes(command)) {
|
|
143
|
-
|
|
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 (
|
|
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.
|
|
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",
|