scripts-orchestrator 2.7.1 → 2.8.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
@@ -33,6 +33,7 @@ npm install --save-dev scripts-orchestrator
33
33
  - **Health Checks**: Verifies service availability before proceeding
34
34
  - **Environment Variables**: Pass custom environment variables to commands
35
35
  - **Optional Phases**: Mark phases as optional and run them selectively
36
+ - **Git-Based Caching**: Automatically skips execution when git state is unchanged
36
37
  - **Comprehensive Logging**: Detailed logging of command execution and results
37
38
 
38
39
  ## Configuration
@@ -347,9 +348,25 @@ npm run scripts-orchestrator -- --phases "build,test,optional-e2e,optional-perfo
347
348
  ## Logging
348
349
 
349
350
  - Each command's output is logged to `scripts-orchestrator-logs/<command>.log` in the current working directory
351
+ - Git commit hash is cached in `scripts-orchestrator-logs/.git-hash-cache` for skip detection
350
352
  - Provides real-time status updates during execution
351
353
  - Summarizes results at the end of execution
352
354
 
355
+ ## Git-Based Caching
356
+
357
+ The orchestrator automatically tracks the git commit hash and repository state to optimize execution:
358
+
359
+ - **On first run**: Records the current git commit hash in `scripts-orchestrator-logs/.git-hash-cache`
360
+ - **On subsequent runs**: Checks if:
361
+ - The git commit hash matches the cached hash
362
+ - There are no staged or unstaged changes in the repository
363
+ - **When conditions are met**: Skips execution entirely with message `✓ Git state unchanged`
364
+ - **When conditions fail**: Runs normally and updates the cache on successful completion
365
+
366
+ 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.
367
+
368
+ **Note**: The cache is only updated on successful execution. Failed runs will not update the cache, ensuring subsequent runs will retry.
369
+
353
370
  ## Exit Codes
354
371
 
355
372
  - `0`: All commands executed successfully
@@ -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,78 @@ 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
+ writeToFile(message) {
70
+ if (this.logStream) {
71
+ // Strip ANSI color codes for file output
72
+ // eslint-disable-next-line no-control-regex
73
+ const cleanMessage = message.replace(/\x1b\[[0-9;]*m/g, '');
74
+ this.logStream.write(`${cleanMessage}\n`);
75
+ }
28
76
  }
29
77
 
30
78
  info(message) {
31
79
  console.log(chalk.blue(`[INFO] ${message}`));
80
+ this.writeToFile(`[INFO] ${message}`);
32
81
  }
33
82
 
34
83
  success(message) {
35
84
  console.log(chalk.green(`[SUCCESS] ${message}`));
85
+ this.writeToFile(`[SUCCESS] ${message}`);
36
86
  }
37
87
 
38
88
  error(message) {
39
89
  console.error(chalk.red(`[ERROR] ${message}`));
90
+ this.writeToFile(`[ERROR] ${message}`);
40
91
  }
41
92
 
42
93
  warn(message) {
43
94
  console.warn(chalk.yellow(`[WARN] ${message}`));
95
+ this.writeToFile(`[WARN] ${message}`);
44
96
  }
45
97
 
46
98
  verbose(message) {
47
99
  if (this.isVerbose) {
48
100
  console.log(chalk.gray(`[VERBOSE] ${message}`));
101
+ this.writeToFile(`[VERBOSE] ${message}`);
49
102
  }
50
103
  }
51
104
  }
@@ -1,6 +1,7 @@
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
7
  constructor(config, startPhase = null, logFolder = null, phases = null) {
@@ -14,6 +15,7 @@ export class Orchestrator {
14
15
  this.failedCommands = [];
15
16
  this.skippedCommands = [];
16
17
  this.commandTimings = new Map();
18
+ this.gitCache = new GitCache(logFolder);
17
19
 
18
20
  // Set the log folder in process manager
19
21
  if (logFolder) {
@@ -237,6 +239,13 @@ export class Orchestrator {
237
239
 
238
240
  async run() {
239
241
  try {
242
+ // Check if we should skip execution based on git state
243
+ const shouldSkip = await this.gitCache.shouldSkipExecution();
244
+ if (shouldSkip) {
245
+ this.logger.success('🎉 No changes detected, skipping execution!');
246
+ process.exit(0);
247
+ }
248
+
240
249
  let hasFailures = false;
241
250
  let phaseFailed = false;
242
251
  let startPhaseFound = false;
@@ -340,6 +349,11 @@ export class Orchestrator {
340
349
  this.logger.error(`Cleanup failed: ${error.message}`);
341
350
  }
342
351
 
352
+ // Update git cache on successful execution
353
+ if (!hasFailures) {
354
+ await this.gitCache.updateCache();
355
+ }
356
+
343
357
  // Force exit with appropriate status
344
358
  if (hasFailures) {
345
359
  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.8.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",