scripts-orchestrator 2.7.1 → 2.9.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
@@ -26,6 +26,7 @@ npm install --save-dev scripts-orchestrator
26
26
  ## Features
27
27
 
28
28
  - **Parallel Execution**: Runs multiple commands concurrently for faster execution
29
+ - **Sequential Mode**: Option to run all commands sequentially for low CPU machines
29
30
  - **Dependency Management**: Handles command dependencies and ensures proper execution order
30
31
  - **Background Processes**: Supports running commands in the background with health checks
31
32
  - **Retry Mechanism**: Configurable retry attempts for failed commands
@@ -33,6 +34,7 @@ npm install --save-dev scripts-orchestrator
33
34
  - **Health Checks**: Verifies service availability before proceeding
34
35
  - **Environment Variables**: Pass custom environment variables to commands
35
36
  - **Optional Phases**: Mark phases as optional and run them selectively
37
+ - **Git-Based Caching**: Automatically skips execution when git state is unchanged
36
38
  - **Comprehensive Logging**: Detailed logging of command execution and results
37
39
 
38
40
  ## Configuration
@@ -255,6 +257,12 @@ The orchestrator doesn't care what the commands do - it just ensures they run (i
255
257
 
256
258
  # Run with verbose logging
257
259
  npm run scripts-orchestrator -- --verbose
260
+
261
+ # Run in sequential mode (for low CPU machines)
262
+ npm run scripts-orchestrator -- --sequential
263
+
264
+ # Specify a custom log folder
265
+ npm run scripts-orchestrator -- --logFolder ./custom-logs
258
266
  ```
259
267
 
260
268
  ### Starting from a Specific Phase
@@ -337,6 +345,29 @@ npm run scripts-orchestrator -- --phases "build,test,optional-e2e,optional-perfo
337
345
  - The orchestrator validates that all specified phases exist
338
346
  - Commands in skipped optional phases are marked as "skipped" in the final summary
339
347
 
348
+ ### Sequential Mode
349
+
350
+ By default, the orchestrator runs commands within each phase in parallel for optimal performance. However, you can use the `--sequential` flag to run all commands sequentially, which is useful for low CPU machines or when you need to reduce resource consumption.
351
+
352
+ #### Usage
353
+ ```bash
354
+ # Run all commands sequentially instead of in parallel
355
+ npm run scripts-orchestrator -- --sequential
356
+ ```
357
+
358
+ When running in sequential mode:
359
+ - Commands within each phase are executed one at a time
360
+ - Phases still run sequentially (as they always do)
361
+ - If a command fails, the remaining commands in that phase are skipped
362
+ - Lower CPU and memory usage compared to parallel execution
363
+ - Longer total execution time
364
+
365
+ This is particularly useful for:
366
+ - CI/CD environments with limited resources
367
+ - Development machines with low CPU/memory
368
+ - Debugging individual command failures
369
+ - Avoiding resource contention between commands
370
+
340
371
  ## Error Handling
341
372
 
342
373
  - The script tracks failed and skipped commands
@@ -347,9 +378,50 @@ npm run scripts-orchestrator -- --phases "build,test,optional-e2e,optional-perfo
347
378
  ## Logging
348
379
 
349
380
  - Each command's output is logged to `scripts-orchestrator-logs/<command>.log` in the current working directory
381
+ - Main orchestrator logs are saved to `scripts-orchestrator-logs/orchestrator-main-<timestamp>.log`
382
+ - Git commit hash is cached in `scripts-orchestrator-logs/.git-hash-cache` for skip detection
350
383
  - Provides real-time status updates during execution
351
384
  - Summarizes results at the end of execution
352
385
 
386
+ ### Custom Log Folder
387
+
388
+ You can customize the log folder location using either the command line or configuration file:
389
+
390
+ #### Method 1: Command Line Argument
391
+ ```bash
392
+ # Use a custom log folder
393
+ npm run scripts-orchestrator -- --logFolder ./my-custom-logs
394
+ ```
395
+
396
+ #### Method 2: Configuration File
397
+ ```javascript
398
+ export default {
399
+ log_folder: './my-custom-logs', // Custom log folder
400
+ phases: [
401
+ // ... your phases
402
+ ]
403
+ };
404
+ ```
405
+
406
+ **Note**: Command line arguments take precedence over configuration file settings.
407
+
408
+ All logs (command logs, main orchestrator logs, and git cache) will be stored in the specified folder.
409
+
410
+ ## Git-Based Caching
411
+
412
+ The orchestrator automatically tracks the git commit hash and repository state to optimize execution:
413
+
414
+ - **On first run**: Records the current git commit hash in `scripts-orchestrator-logs/.git-hash-cache`
415
+ - **On subsequent runs**: Checks if:
416
+ - The git commit hash matches the cached hash
417
+ - There are no staged or unstaged changes in the repository
418
+ - **When conditions are met**: Skips execution entirely with message `āœ“ Git state unchanged`
419
+ - **When conditions fail**: Runs normally and updates the cache on successful completion
420
+
421
+ This feature is particularly useful in CI/CD pipelines where the same commit might be processed multiple times, saving time and resources by avoiding redundant executions.
422
+
423
+ **Note**: The cache is only updated on successful execution. Failed runs will not update the cache, ensuring subsequent runs will retry.
424
+
353
425
  ## Exit Codes
354
426
 
355
427
  - `0`: All commands executed successfully
package/index.js CHANGED
@@ -31,6 +31,10 @@ const argv = yargs(hideBin(process.argv))
31
31
  type: 'string',
32
32
  description: 'Specify the directory for log files',
33
33
  })
34
+ .option('sequential', {
35
+ type: 'boolean',
36
+ description: 'Run all commands sequentially instead of in parallel (for low CPU machines)',
37
+ })
34
38
  .help()
35
39
  .alias('h', 'help')
36
40
  .parse();
@@ -41,6 +45,7 @@ const configPath = args[0] || './scripts-orchestrator.config.js';
41
45
  let startPhase = argv.phase;
42
46
  let logFolder = argv.logFolder;
43
47
  const phases = argv.phases ? argv.phases.split(',').map(p => p.trim()) : null;
48
+ const sequential = argv.sequential || false;
44
49
 
45
50
  // Validate config file exists
46
51
  if (!fs.existsSync(configPath)) {
@@ -64,8 +69,13 @@ if (!logFolder && commandsConfig.log_folder) {
64
69
  logFolder = commandsConfig.log_folder;
65
70
  }
66
71
 
72
+ // Set the log folder for the main orchestrator logs if specified
73
+ if (logFolder) {
74
+ log.setLogFolder(logFolder);
75
+ }
76
+
67
77
  // Create and run the orchestrator
68
- const orchestrator = new Orchestrator(commandsConfig, startPhase, logFolder, phases);
78
+ const orchestrator = new Orchestrator(commandsConfig, startPhase, logFolder, phases, sequential);
69
79
 
70
80
  // Enhanced signal handlers
71
81
  const handleSignal = async (signal) => {
@@ -0,0 +1,187 @@
1
+ import { spawn } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { log } from './logger.js';
5
+
6
+ export class GitCache {
7
+ constructor(logFolder = 'scripts-orchestrator-logs') {
8
+ this.logFolder = logFolder;
9
+ this.cacheFileName = '.git-hash-cache';
10
+ }
11
+
12
+ /**
13
+ * Execute a git command and return the output
14
+ * @param {string[]} args - Git command arguments
15
+ * @returns {Promise<{success: boolean, output: string}>}
16
+ */
17
+ async executeGitCommand(args) {
18
+ return new Promise((resolve) => {
19
+ const gitProcess = spawn('git', args, {
20
+ cwd: process.cwd(),
21
+ shell: false,
22
+ });
23
+
24
+ let output = '';
25
+ let errorOutput = '';
26
+
27
+ gitProcess.stdout.on('data', (data) => {
28
+ output += data.toString();
29
+ });
30
+
31
+ gitProcess.stderr.on('data', (data) => {
32
+ errorOutput += data.toString();
33
+ });
34
+
35
+ gitProcess.on('close', (code) => {
36
+ if (code === 0) {
37
+ resolve({ success: true, output: output.trim() });
38
+ } else {
39
+ resolve({ success: false, output: errorOutput.trim() });
40
+ }
41
+ });
42
+
43
+ gitProcess.on('error', (error) => {
44
+ resolve({ success: false, output: error.message });
45
+ });
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Get the current git commit hash
51
+ * @returns {Promise<string|null>}
52
+ */
53
+ async getCurrentCommitHash() {
54
+ const result = await this.executeGitCommand(['rev-parse', 'HEAD']);
55
+ if (result.success) {
56
+ return result.output;
57
+ }
58
+ log.verbose(`Failed to get git commit hash: ${result.output}`);
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Check if there are any staged or unstaged changes
64
+ * @returns {Promise<boolean>}
65
+ */
66
+ async hasGitChanges() {
67
+ // Check for staged and unstaged changes
68
+ const statusResult = await this.executeGitCommand(['status', '--porcelain']);
69
+
70
+ if (!statusResult.success) {
71
+ log.verbose(`Failed to check git status: ${statusResult.output}`);
72
+ // If we can't check git status, assume there are changes to be safe
73
+ return true;
74
+ }
75
+
76
+ // If there's any output, there are changes
77
+ return statusResult.output.length > 0;
78
+ }
79
+
80
+ /**
81
+ * Get the path to the cache file
82
+ * @returns {string}
83
+ */
84
+ getCacheFilePath() {
85
+ const baseDir = this.logFolder ? path.resolve(this.logFolder) : process.cwd();
86
+ const LOGS_DIR = path.join(baseDir, 'scripts-orchestrator-logs');
87
+ return path.join(LOGS_DIR, this.cacheFileName);
88
+ }
89
+
90
+ /**
91
+ * Read the cached git hash
92
+ * @returns {string|null}
93
+ */
94
+ readCachedHash() {
95
+ const cacheFilePath = this.getCacheFilePath();
96
+
97
+ try {
98
+ if (fs.existsSync(cacheFilePath)) {
99
+ const cachedHash = fs.readFileSync(cacheFilePath, 'utf8').trim();
100
+ log.verbose(`Read cached git hash: ${cachedHash}`);
101
+ return cachedHash;
102
+ }
103
+ } catch (error) {
104
+ log.verbose(`Failed to read cached git hash: ${error.message}`);
105
+ }
106
+
107
+ return null;
108
+ }
109
+
110
+ /**
111
+ * Write the current git hash to cache
112
+ * @param {string} hash
113
+ * @returns {boolean}
114
+ */
115
+ writeCachedHash(hash) {
116
+ const cacheFilePath = this.getCacheFilePath();
117
+
118
+ try {
119
+ // Ensure the directory exists
120
+ const cacheDir = path.dirname(cacheFilePath);
121
+ if (!fs.existsSync(cacheDir)) {
122
+ fs.mkdirSync(cacheDir, { recursive: true });
123
+ }
124
+
125
+ fs.writeFileSync(cacheFilePath, hash, 'utf8');
126
+ log.verbose(`Wrote git hash to cache: ${hash}`);
127
+ return true;
128
+ } catch (error) {
129
+ log.error(`Failed to write git hash to cache: ${error.message}`);
130
+ return false;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Check if the orchestrator should skip running based on git state
136
+ * @returns {Promise<boolean>} - true if should skip, false if should run
137
+ */
138
+ async shouldSkipExecution() {
139
+ log.verbose('Checking git state for caching...');
140
+
141
+ // Get current commit hash
142
+ const currentHash = await this.getCurrentCommitHash();
143
+ if (!currentHash) {
144
+ log.verbose('Could not get current git hash, will not skip execution');
145
+ return false;
146
+ }
147
+
148
+ // Get cached hash
149
+ const cachedHash = this.readCachedHash();
150
+ if (!cachedHash) {
151
+ log.verbose('No cached git hash found, will not skip execution');
152
+ return false;
153
+ }
154
+
155
+ // Check if hashes match
156
+ if (currentHash !== cachedHash) {
157
+ log.verbose(`Git hash changed (${cachedHash.substring(0, 7)} -> ${currentHash.substring(0, 7)}), will not skip execution`);
158
+ return false;
159
+ }
160
+
161
+ // Check for uncommitted changes
162
+ const hasChanges = await this.hasGitChanges();
163
+ if (hasChanges) {
164
+ log.verbose('Git repository has uncommitted changes, will not skip execution');
165
+ return false;
166
+ }
167
+
168
+ // All conditions met - can skip
169
+ log.info(`āœ“ Git state unchanged (${currentHash.substring(0, 7)}), skipping execution`);
170
+ return true;
171
+ }
172
+
173
+ /**
174
+ * Update the cached git hash with the current commit
175
+ * @returns {Promise<void>}
176
+ */
177
+ async updateCache() {
178
+ const currentHash = await this.getCurrentCommitHash();
179
+ if (currentHash) {
180
+ this.writeCachedHash(currentHash);
181
+ }
182
+ }
183
+ }
184
+
185
+ // Export a singleton instance
186
+ export const gitCache = new GitCache();
187
+
package/lib/index.js CHANGED
@@ -2,6 +2,7 @@ import { Orchestrator } from './orchestrator.js';
2
2
  import { ProcessManager } from './process-manager.js';
3
3
  import { HealthCheck } from './health-check.js';
4
4
  import { Logger } from './logger.js';
5
+ import { GitCache } from './git-cache.js';
5
6
 
6
- export { Orchestrator, ProcessManager, HealthCheck, Logger };
7
+ export { Orchestrator, ProcessManager, HealthCheck, Logger, GitCache };
7
8
  export default Orchestrator;
package/lib/logger.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import yargs from 'yargs';
3
3
  import { hideBin } from 'yargs/helpers';
4
+ import fs from 'fs';
5
+ import path from 'path';
4
6
 
5
7
  const argv = yargs(hideBin(process.argv))
6
8
  .option('verbose', {
@@ -25,27 +27,91 @@ const argv = yargs(hideBin(process.argv))
25
27
  class Logger {
26
28
  constructor() {
27
29
  this.isVerbose = argv.verbose;
30
+ this.logFolder = argv.logFolder || 'scripts-orchestrator-logs';
31
+ this.logFile = null;
32
+ this.logStream = null;
33
+ this.initializeLogFile();
34
+ }
35
+
36
+ initializeLogFile() {
37
+ try {
38
+ // Create log directory if it doesn't exist
39
+ if (!fs.existsSync(this.logFolder)) {
40
+ fs.mkdirSync(this.logFolder, { recursive: true });
41
+ }
42
+
43
+ // Create main log file with timestamp
44
+ const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
45
+ this.logFile = path.join(this.logFolder, `orchestrator-main-${timestamp}.log`);
46
+
47
+ // Create write stream
48
+ this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
49
+
50
+ // Handle stream errors
51
+ this.logStream.on('error', (err) => {
52
+ console.error(`Error writing to log file: ${err.message}`);
53
+ });
54
+
55
+ // Ensure the stream is closed on process exit
56
+ process.on('exit', () => {
57
+ if (this.logStream) {
58
+ this.logStream.end();
59
+ }
60
+ });
61
+
62
+ // Write initial log entry
63
+ this.writeToFile(`[START] Orchestrator started at ${new Date().toISOString()}\n`);
64
+ } catch (error) {
65
+ console.error(`Failed to initialize log file: ${error.message}`);
66
+ }
67
+ }
68
+
69
+ setLogFolder(newLogFolder) {
70
+ // Close existing stream if it exists
71
+ if (this.logStream) {
72
+ this.logStream.end();
73
+ }
74
+
75
+ // Update log folder
76
+ this.logFolder = newLogFolder;
77
+
78
+ // Reinitialize with new folder
79
+ this.initializeLogFile();
80
+ }
81
+
82
+ writeToFile(message) {
83
+ if (this.logStream) {
84
+ // Strip ANSI color codes for file output
85
+ // eslint-disable-next-line no-control-regex
86
+ const cleanMessage = message.replace(/\x1b\[[0-9;]*m/g, '');
87
+ this.logStream.write(`${cleanMessage}\n`);
88
+ }
28
89
  }
29
90
 
30
91
  info(message) {
31
92
  console.log(chalk.blue(`[INFO] ${message}`));
93
+ this.writeToFile(`[INFO] ${message}`);
32
94
  }
33
95
 
34
96
  success(message) {
35
97
  console.log(chalk.green(`[SUCCESS] ${message}`));
98
+ this.writeToFile(`[SUCCESS] ${message}`);
36
99
  }
37
100
 
38
101
  error(message) {
39
102
  console.error(chalk.red(`[ERROR] ${message}`));
103
+ this.writeToFile(`[ERROR] ${message}`);
40
104
  }
41
105
 
42
106
  warn(message) {
43
107
  console.warn(chalk.yellow(`[WARN] ${message}`));
108
+ this.writeToFile(`[WARN] ${message}`);
44
109
  }
45
110
 
46
111
  verbose(message) {
47
112
  if (this.isVerbose) {
48
113
  console.log(chalk.gray(`[VERBOSE] ${message}`));
114
+ this.writeToFile(`[VERBOSE] ${message}`);
49
115
  }
50
116
  }
51
117
  }
@@ -1,19 +1,22 @@
1
1
  import { processManager } from './process-manager.js';
2
2
  import { healthCheck } from './health-check.js';
3
3
  import { log } from './logger.js';
4
+ import { GitCache } from './git-cache.js';
4
5
 
5
6
  export class Orchestrator {
6
- constructor(config, startPhase = null, logFolder = null, phases = null) {
7
+ constructor(config, startPhase = null, logFolder = null, phases = null, sequential = false) {
7
8
  this.config = config;
8
9
  this.startPhase = startPhase;
9
10
  this.logFolder = logFolder;
10
11
  this.phases = phases;
12
+ this.sequential = sequential;
11
13
  this.processManager = processManager;
12
14
  this.healthCheck = healthCheck;
13
15
  this.logger = log;
14
16
  this.failedCommands = [];
15
17
  this.skippedCommands = [];
16
18
  this.commandTimings = new Map();
19
+ this.gitCache = new GitCache(logFolder);
17
20
 
18
21
  // Set the log folder in process manager
19
22
  if (logFolder) {
@@ -237,20 +240,44 @@ export class Orchestrator {
237
240
 
238
241
  async run() {
239
242
  try {
243
+ // Check if we should skip execution based on git state
244
+ const shouldSkip = await this.gitCache.shouldSkipExecution();
245
+ if (shouldSkip) {
246
+ this.logger.success('šŸŽ‰ No changes detected, skipping execution!');
247
+ process.exit(0);
248
+ }
249
+
240
250
  let hasFailures = false;
241
251
  let phaseFailed = false;
242
252
  let startPhaseFound = false;
243
253
 
244
254
  // Handle both old array format and new phases format
245
255
  if (Array.isArray(this.config)) {
246
- // Legacy: Run all commands in parallel
247
- const tasks = this.config.map((commandConfig) =>
248
- this.executeCommand(commandConfig),
249
- );
250
- const results = await Promise.all(tasks);
251
- hasFailures = results.some(result => !result);
256
+ // Legacy: Run all commands in parallel or sequential based on flag
257
+ if (this.sequential) {
258
+ this.logger.info('šŸ”„ Running in sequential mode');
259
+ const results = [];
260
+ for (const commandConfig of this.config) {
261
+ const result = await this.executeCommand(commandConfig);
262
+ results.push(result);
263
+ if (!result) {
264
+ hasFailures = true;
265
+ break; // Stop on first failure in sequential mode
266
+ }
267
+ }
268
+ } else {
269
+ const tasks = this.config.map((commandConfig) =>
270
+ this.executeCommand(commandConfig),
271
+ );
272
+ const results = await Promise.all(tasks);
273
+ hasFailures = results.some(result => !result);
274
+ }
252
275
  } else if (this.config.phases) {
253
- // New: Run phases sequentially, commands within phases in parallel
276
+ // New: Run phases sequentially, commands within phases in parallel or sequential based on flag
277
+ if (this.sequential) {
278
+ this.logger.info('šŸ”„ Running in sequential mode');
279
+ }
280
+
254
281
  for (const phase of this.config.phases) {
255
282
  // Check if we should start from this phase
256
283
  if (this.startPhase && !startPhaseFound) {
@@ -289,11 +316,26 @@ export class Orchestrator {
289
316
 
290
317
  this.logger.info(`\nšŸ”„ Starting phase: ${phase.name}`);
291
318
 
292
- const tasks = phase.parallel.map((commandConfig) =>
293
- this.executeCommand(commandConfig),
294
- );
319
+ let results;
320
+ if (this.sequential) {
321
+ // Run commands sequentially
322
+ results = [];
323
+ for (const commandConfig of phase.parallel) {
324
+ const result = await this.executeCommand(commandConfig);
325
+ results.push(result);
326
+ if (!result) {
327
+ // In sequential mode, stop phase execution on first failure
328
+ break;
329
+ }
330
+ }
331
+ } else {
332
+ // Run commands in parallel
333
+ const tasks = phase.parallel.map((commandConfig) =>
334
+ this.executeCommand(commandConfig),
335
+ );
336
+ results = await Promise.all(tasks);
337
+ }
295
338
 
296
- const results = await Promise.all(tasks);
297
339
  const phaseHasFailures = results.some(result => !result);
298
340
 
299
341
  if (phaseHasFailures) {
@@ -340,6 +382,11 @@ export class Orchestrator {
340
382
  this.logger.error(`Cleanup failed: ${error.message}`);
341
383
  }
342
384
 
385
+ // Update git cache on successful execution
386
+ if (!hasFailures) {
387
+ await this.gitCache.updateCache();
388
+ }
389
+
343
390
  // Force exit with appropriate status
344
391
  if (hasFailures) {
345
392
  this.logger.info('Exiting with failure status...');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scripts-orchestrator",
3
- "version": "2.7.1",
3
+ "version": "2.9.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",