scripts-orchestrator 2.9.0 → 2.12.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 +18 -0
- package/index.js +6 -1
- package/lib/health-check.js +27 -11
- package/lib/logger.js +142 -12
- package/lib/orchestrator.js +145 -65
- package/lib/process-manager.js +229 -88
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -263,6 +263,9 @@ The orchestrator doesn't care what the commands do - it just ensures they run (i
|
|
|
263
263
|
|
|
264
264
|
# Specify a custom log folder
|
|
265
265
|
npm run scripts-orchestrator -- --logFolder ./custom-logs
|
|
266
|
+
|
|
267
|
+
# Force execution even if git state is unchanged
|
|
268
|
+
npm run scripts-orchestrator -- --force
|
|
266
269
|
```
|
|
267
270
|
|
|
268
271
|
### Starting from a Specific Phase
|
|
@@ -422,6 +425,21 @@ This feature is particularly useful in CI/CD pipelines where the same commit mig
|
|
|
422
425
|
|
|
423
426
|
**Note**: The cache is only updated on successful execution. Failed runs will not update the cache, ensuring subsequent runs will retry.
|
|
424
427
|
|
|
428
|
+
### Force Execution
|
|
429
|
+
|
|
430
|
+
You can bypass the git cache check and force execution even when the git state is unchanged by using the `--force` flag:
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
# Force execution regardless of git state
|
|
434
|
+
npm run scripts-orchestrator -- --force
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
This is useful when you want to:
|
|
438
|
+
- Re-run commands without making code changes
|
|
439
|
+
- Test configuration changes
|
|
440
|
+
- Debug issues without modifying the codebase
|
|
441
|
+
- Override the cache in CI/CD pipelines
|
|
442
|
+
|
|
425
443
|
## Exit Codes
|
|
426
444
|
|
|
427
445
|
- `0`: All commands executed successfully
|
package/index.js
CHANGED
|
@@ -35,6 +35,10 @@ const argv = yargs(hideBin(process.argv))
|
|
|
35
35
|
type: 'boolean',
|
|
36
36
|
description: 'Run all commands sequentially instead of in parallel (for low CPU machines)',
|
|
37
37
|
})
|
|
38
|
+
.option('force', {
|
|
39
|
+
type: 'boolean',
|
|
40
|
+
description: 'Force execution even if git state is unchanged',
|
|
41
|
+
})
|
|
38
42
|
.help()
|
|
39
43
|
.alias('h', 'help')
|
|
40
44
|
.parse();
|
|
@@ -46,6 +50,7 @@ let startPhase = argv.phase;
|
|
|
46
50
|
let logFolder = argv.logFolder;
|
|
47
51
|
const phases = argv.phases ? argv.phases.split(',').map(p => p.trim()) : null;
|
|
48
52
|
const sequential = argv.sequential || false;
|
|
53
|
+
const force = argv.force || false;
|
|
49
54
|
|
|
50
55
|
// Validate config file exists
|
|
51
56
|
if (!fs.existsSync(configPath)) {
|
|
@@ -75,7 +80,7 @@ if (logFolder) {
|
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
// Create and run the orchestrator
|
|
78
|
-
const orchestrator = new Orchestrator(commandsConfig, startPhase, logFolder, phases, sequential);
|
|
83
|
+
const orchestrator = new Orchestrator(commandsConfig, startPhase, logFolder, phases, sequential, force);
|
|
79
84
|
|
|
80
85
|
// Enhanced signal handlers
|
|
81
86
|
const handleSignal = async (signal) => {
|
package/lib/health-check.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { log } from './logger.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
|
|
3
4
|
export class HealthCheck {
|
|
4
5
|
constructor() {
|
|
@@ -17,17 +18,17 @@ export class HealthCheck {
|
|
|
17
18
|
const urlObj = new URL(url);
|
|
18
19
|
const isHttps = urlObj.protocol === 'https:';
|
|
19
20
|
const httpModule = isHttps ? await import('https') : await import('http');
|
|
20
|
-
|
|
21
|
+
|
|
21
22
|
return await new Promise((resolve) => {
|
|
22
23
|
const req = httpModule.default.get(url, (res) => {
|
|
23
24
|
resolve({ statusCode: res.statusCode, success: true });
|
|
24
25
|
res.destroy(); // Close the response stream
|
|
25
26
|
});
|
|
26
|
-
|
|
27
|
+
|
|
27
28
|
req.on('error', (error) => {
|
|
28
29
|
resolve({ statusCode: null, success: false, error: error.message });
|
|
29
30
|
});
|
|
30
|
-
|
|
31
|
+
|
|
31
32
|
req.setTimeout(timeout, () => {
|
|
32
33
|
req.destroy();
|
|
33
34
|
resolve({ statusCode: null, success: false, error: 'Timeout' });
|
|
@@ -38,30 +39,45 @@ export class HealthCheck {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
async waitForUrl({url, maxAttempts = 20, interval = 2000, silent=false}) {
|
|
42
|
-
!silent
|
|
42
|
+
async waitForUrl({ url, maxAttempts = 20, interval = 2000, silent = false }) {
|
|
43
|
+
if (!silent) {
|
|
44
|
+
this.logger.startEphemeral(
|
|
45
|
+
`wait_${url}`,
|
|
46
|
+
chalk.yellow(`[INFO] ⏳ Waiting for ${url} to be available...`),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
43
49
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
44
50
|
try {
|
|
45
51
|
const result = await HealthCheck.makeHttpRequest(url, 5000);
|
|
46
52
|
|
|
47
53
|
if (result.success && result.statusCode === 200) {
|
|
48
|
-
!silent
|
|
54
|
+
if (!silent) {
|
|
55
|
+
this.logger.stopEphemeral(`wait_${url}`, `✅ ${url} is available`);
|
|
56
|
+
}
|
|
49
57
|
return true;
|
|
50
58
|
}
|
|
51
59
|
} catch (error) {
|
|
52
|
-
!silent
|
|
60
|
+
if (!silent) {
|
|
61
|
+
// You could potentially log attempts via verbose if needed, but not necessary.
|
|
62
|
+
}
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
if (attempt < maxAttempts) {
|
|
56
66
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
57
67
|
}
|
|
58
68
|
}
|
|
59
|
-
|
|
60
|
-
!silent
|
|
61
|
-
|
|
69
|
+
|
|
70
|
+
if (!silent) {
|
|
71
|
+
this.logger.stopEphemeral(
|
|
72
|
+
`wait_${url}`,
|
|
73
|
+
`❌ Failed to connect to ${url} after ${maxAttempts} attempts`,
|
|
74
|
+
true,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
return false;
|
|
63
79
|
}
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
// For backward compatibility
|
|
67
|
-
export const healthCheck = new HealthCheck();
|
|
83
|
+
export const healthCheck = new HealthCheck();
|
package/lib/logger.js
CHANGED
|
@@ -30,9 +30,131 @@ class Logger {
|
|
|
30
30
|
this.logFolder = argv.logFolder || 'scripts-orchestrator-logs';
|
|
31
31
|
this.logFile = null;
|
|
32
32
|
this.logStream = null;
|
|
33
|
+
|
|
34
|
+
// For TTY dynamic output
|
|
35
|
+
this.isTTY = process.stdout.isTTY;
|
|
36
|
+
this.activeTasks = new Map();
|
|
37
|
+
this.linesRendered = 0;
|
|
38
|
+
|
|
33
39
|
this.initializeLogFile();
|
|
34
40
|
}
|
|
35
41
|
|
|
42
|
+
// --- Dynamic Output Handling ---
|
|
43
|
+
|
|
44
|
+
clearActiveTasks() {
|
|
45
|
+
if (!this.isTTY || this.linesRendered === 0) return;
|
|
46
|
+
// Move cursor up and clear lines
|
|
47
|
+
for (let i = 0; i < this.linesRendered; i++) {
|
|
48
|
+
process.stdout.write('\x1b[1A\x1b[2K'); // Up one line, clear entire line
|
|
49
|
+
}
|
|
50
|
+
this.linesRendered = 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
renderActiveTasks() {
|
|
54
|
+
if (!this.isTTY) return;
|
|
55
|
+
if (this.activeTasks.size === 0) return;
|
|
56
|
+
|
|
57
|
+
// Render active tasks with a spinner or prefix
|
|
58
|
+
for (const [, text] of this.activeTasks) {
|
|
59
|
+
process.stdout.write(`${text}\n`);
|
|
60
|
+
this.linesRendered++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
startTask(id, text) {
|
|
65
|
+
if (this.isTTY) {
|
|
66
|
+
this.clearActiveTasks();
|
|
67
|
+
this.activeTasks.set(id, chalk.cyan(`[INFO] ⏳ Running: ${text}`));
|
|
68
|
+
this.renderActiveTasks();
|
|
69
|
+
} else {
|
|
70
|
+
this.info(`Running: ${text}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
updateTask(id, text) {
|
|
75
|
+
if (this.isTTY && this.activeTasks.has(id)) {
|
|
76
|
+
this.clearActiveTasks();
|
|
77
|
+
this.activeTasks.set(id, chalk.cyan(`[INFO] ⏳ Running: ${text}`));
|
|
78
|
+
this.renderActiveTasks();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
stopTask(id) {
|
|
83
|
+
if (this.isTTY && this.activeTasks.has(id)) {
|
|
84
|
+
this.clearActiveTasks();
|
|
85
|
+
this.activeTasks.delete(id);
|
|
86
|
+
this.renderActiveTasks();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- Wrapper for output methods ---
|
|
91
|
+
|
|
92
|
+
printMessage(logFn) {
|
|
93
|
+
if (this.isTTY) {
|
|
94
|
+
this.clearActiveTasks();
|
|
95
|
+
logFn();
|
|
96
|
+
this.renderActiveTasks();
|
|
97
|
+
} else {
|
|
98
|
+
logFn();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
startEphemeral(id, message) {
|
|
103
|
+
if (this.isTTY) {
|
|
104
|
+
this.clearActiveTasks();
|
|
105
|
+
this.activeTasks.set(id, message);
|
|
106
|
+
this.renderActiveTasks();
|
|
107
|
+
} else {
|
|
108
|
+
this.printMessage(() => console.log(message));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
stopEphemeral(id, finalMessage = '', isError = false) {
|
|
113
|
+
if (this.isTTY && this.activeTasks.has(id)) {
|
|
114
|
+
this.clearActiveTasks();
|
|
115
|
+
this.activeTasks.delete(id);
|
|
116
|
+
this.renderActiveTasks();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (finalMessage) {
|
|
120
|
+
if (isError) {
|
|
121
|
+
this.printMessage(() =>
|
|
122
|
+
console.error(chalk.red(`[ERROR] ${finalMessage}`)),
|
|
123
|
+
);
|
|
124
|
+
} else {
|
|
125
|
+
this.printMessage(() =>
|
|
126
|
+
console.log(chalk.green(`[SUCCESS] ${finalMessage}`)),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
stopPhase(phaseName, success, durationStr) {
|
|
133
|
+
if (success) {
|
|
134
|
+
this.printMessage(() =>
|
|
135
|
+
console.log(
|
|
136
|
+
chalk.green(
|
|
137
|
+
`[SUCCESS] ✅ Phase "${phaseName}" completed successfully ${durationStr}`,
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
// Add a blank line after phase logs for readability
|
|
142
|
+
this.printMessage(() => console.log(''));
|
|
143
|
+
this.writeToFile('');
|
|
144
|
+
} else {
|
|
145
|
+
this.printMessage(() =>
|
|
146
|
+
console.error(
|
|
147
|
+
chalk.red(
|
|
148
|
+
`[ERROR] ❌ Phase "${phaseName}" completed with failures ${durationStr}`,
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
// Add a blank line after phase logs for readability
|
|
153
|
+
this.printMessage(() => console.log(''));
|
|
154
|
+
this.writeToFile('');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
36
158
|
initializeLogFile() {
|
|
37
159
|
try {
|
|
38
160
|
// Create log directory if it doesn't exist
|
|
@@ -41,12 +163,18 @@ class Logger {
|
|
|
41
163
|
}
|
|
42
164
|
|
|
43
165
|
// Create main log file with timestamp
|
|
44
|
-
const timestamp = new Date()
|
|
45
|
-
|
|
46
|
-
|
|
166
|
+
const timestamp = new Date()
|
|
167
|
+
.toISOString()
|
|
168
|
+
.replace(/:/g, '-')
|
|
169
|
+
.replace(/\..+/, '');
|
|
170
|
+
this.logFile = path.join(
|
|
171
|
+
this.logFolder,
|
|
172
|
+
`orchestrator-main-${timestamp}.log`,
|
|
173
|
+
);
|
|
174
|
+
|
|
47
175
|
// Create write stream
|
|
48
176
|
this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
|
|
49
|
-
|
|
177
|
+
|
|
50
178
|
// Handle stream errors
|
|
51
179
|
this.logStream.on('error', (err) => {
|
|
52
180
|
console.error(`Error writing to log file: ${err.message}`);
|
|
@@ -60,7 +188,9 @@ class Logger {
|
|
|
60
188
|
});
|
|
61
189
|
|
|
62
190
|
// Write initial log entry
|
|
63
|
-
this.writeToFile(
|
|
191
|
+
this.writeToFile(
|
|
192
|
+
`[START] Orchestrator started at ${new Date().toISOString()}\n`,
|
|
193
|
+
);
|
|
64
194
|
} catch (error) {
|
|
65
195
|
console.error(`Failed to initialize log file: ${error.message}`);
|
|
66
196
|
}
|
|
@@ -74,7 +204,7 @@ class Logger {
|
|
|
74
204
|
|
|
75
205
|
// Update log folder
|
|
76
206
|
this.logFolder = newLogFolder;
|
|
77
|
-
|
|
207
|
+
|
|
78
208
|
// Reinitialize with new folder
|
|
79
209
|
this.initializeLogFile();
|
|
80
210
|
}
|
|
@@ -89,28 +219,28 @@ class Logger {
|
|
|
89
219
|
}
|
|
90
220
|
|
|
91
221
|
info(message) {
|
|
92
|
-
console.log(chalk.blue(`[INFO] ${message}`));
|
|
222
|
+
this.printMessage(() => console.log(chalk.blue(`[INFO] ${message}`)));
|
|
93
223
|
this.writeToFile(`[INFO] ${message}`);
|
|
94
224
|
}
|
|
95
225
|
|
|
96
226
|
success(message) {
|
|
97
|
-
console.log(chalk.green(`[SUCCESS] ${message}`));
|
|
227
|
+
this.printMessage(() => console.log(chalk.green(`[SUCCESS] ${message}`)));
|
|
98
228
|
this.writeToFile(`[SUCCESS] ${message}`);
|
|
99
229
|
}
|
|
100
230
|
|
|
101
231
|
error(message) {
|
|
102
|
-
console.error(chalk.red(`[ERROR] ${message}`));
|
|
232
|
+
this.printMessage(() => console.error(chalk.red(`[ERROR] ${message}`)));
|
|
103
233
|
this.writeToFile(`[ERROR] ${message}`);
|
|
104
234
|
}
|
|
105
235
|
|
|
106
236
|
warn(message) {
|
|
107
|
-
console.warn(chalk.yellow(`[WARN] ${message}`));
|
|
237
|
+
this.printMessage(() => console.warn(chalk.yellow(`[WARN] ${message}`)));
|
|
108
238
|
this.writeToFile(`[WARN] ${message}`);
|
|
109
239
|
}
|
|
110
240
|
|
|
111
241
|
verbose(message) {
|
|
112
242
|
if (this.isVerbose) {
|
|
113
|
-
console.log(chalk.gray(`[VERBOSE] ${message}`));
|
|
243
|
+
this.printMessage(() => console.log(chalk.gray(`[VERBOSE] ${message}`)));
|
|
114
244
|
this.writeToFile(`[VERBOSE] ${message}`);
|
|
115
245
|
}
|
|
116
246
|
}
|
|
@@ -120,4 +250,4 @@ class Logger {
|
|
|
120
250
|
const logger = new Logger();
|
|
121
251
|
|
|
122
252
|
// Export both the class and the instance
|
|
123
|
-
export { Logger, logger as log };
|
|
253
|
+
export { Logger, logger as log };
|