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 +17 -0
- package/lib/git-cache.js +187 -0
- package/lib/index.js +2 -1
- package/lib/logger.js +53 -0
- package/lib/orchestrator.js +14 -0
- package/package.json +1 -1
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
|
package/lib/git-cache.js
ADDED
|
@@ -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
|
}
|
package/lib/orchestrator.js
CHANGED
|
@@ -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.
|
|
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",
|