scripts-orchestrator 2.14.0 → 2.15.1
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 +57 -2
- package/index.js +107 -3
- package/lib/index.js +26 -2
- package/lib/orchestrator.js +197 -147
- package/lib/process-manager.js +5 -2
- package/lib/process-manager.test.js +22 -0
- package/lib/recommend-phases.js +340 -0
- package/lib/recommend-phases.test.js +164 -0
- package/lib/report-html.js +308 -0
- package/lib/report-html.test.js +88 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ npm install --save-dev scripts-orchestrator
|
|
|
40
40
|
- **NDJSON event stream**: Machine-readable per-command events for dashboards (v2.14+)
|
|
41
41
|
- **Post-run hook**: Run a shell command after results are written via `post_run` config (v2.14+)
|
|
42
42
|
- **Run-state file**: Library-owned in-progress indicator for live dashboard integration (v2.14+)
|
|
43
|
+
- **Phase recommendations**: Memory-aware `--recommend` mode that proposes an optimal phase layout from a run's time/memory metrics (advisory, v2.15+)
|
|
43
44
|
|
|
44
45
|
## Configuration
|
|
45
46
|
|
|
@@ -450,7 +451,7 @@ Alongside `json_results`, the library writes one NDJSON line per event to
|
|
|
450
451
|
Dashboard tools can `tail -f` this file or watch it with `fs.watch` to get real-time updates
|
|
451
452
|
without parsing human-readable log lines.
|
|
452
453
|
|
|
453
|
-
### Run-state file
|
|
454
|
+
### Run-state file
|
|
454
455
|
|
|
455
456
|
When `--logFolder` is specified, the library writes `{logFolder}/.scripts-orchestrator-run.json`
|
|
456
457
|
at run start and removes it on run end:
|
|
@@ -467,7 +468,7 @@ at run start and removes it on run end:
|
|
|
467
468
|
This file is the authoritative in-progress signal for live dashboards. Its absence means the run
|
|
468
469
|
has finished (or never started).
|
|
469
470
|
|
|
470
|
-
### Post-run hook
|
|
471
|
+
### Post-run hook
|
|
471
472
|
|
|
472
473
|
Add `post_run` to your config to run a shell command **after** `json_results` is written and the
|
|
473
474
|
run-state file is cleared, but **before** `process.exit()`:
|
|
@@ -522,6 +523,59 @@ This is useful when you want to:
|
|
|
522
523
|
- Debug issues without modifying the codebase
|
|
523
524
|
- Override the cache in CI/CD pipelines
|
|
524
525
|
|
|
526
|
+
## Phase Recommendations (advisory)
|
|
527
|
+
|
|
528
|
+
When a run is executed with `metrics: ['time', 'memory']`, the results JSON records each command's
|
|
529
|
+
`durationMs` and peak `memoryKb`. The `--recommend` mode reads that JSON and reports a **memory-aware
|
|
530
|
+
phase recommendation**: it never runs anything and changes no run state.
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
# Analyse an existing results JSON and print a suggested phase layout to the console
|
|
534
|
+
scripts-orchestrator --recommend ./logs/scripts-orchestrator-results.json
|
|
535
|
+
|
|
536
|
+
# Write the report to a plain-text log file instead of the console (only a pointer line is printed)
|
|
537
|
+
scripts-orchestrator --recommend ./logs/results.json --recommend-out ./logs/recommendation.log
|
|
538
|
+
|
|
539
|
+
# Size the budget for a machine running N gates in parallel (each gets 1/N of RAM and cores)
|
|
540
|
+
scripts-orchestrator --recommend ./logs/scripts-orchestrator-results.json --fanout 3
|
|
541
|
+
|
|
542
|
+
# Override the memory budget explicitly (MB) or change the RAM safety fraction
|
|
543
|
+
scripts-orchestrator --recommend ./logs/results.json --budget-mb 8192
|
|
544
|
+
scripts-orchestrator --recommend ./logs/results.json --mem-safety 0.7
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
It reports three things:
|
|
548
|
+
|
|
549
|
+
1. **Observed timeline** — each phase's wall-clock (the longest step in it) and the concurrent peak
|
|
550
|
+
memory (Σ of member peaks), flagging any phase whose concurrent peak exceeds the host budget.
|
|
551
|
+
2. **Recommended layout** — a [First-Fit-Decreasing](https://en.wikipedia.org/wiki/Bin_packing_problem)
|
|
552
|
+
bin-packing by duration that groups steps into sequential phases so each phase's concurrent peak
|
|
553
|
+
memory stays under `budget = totalmem × memSafety ÷ fanout` and its step count stays under
|
|
554
|
+
`coreShare = (cores − 2) ÷ fanout`. Long steps seed phases; short steps fill the gaps beneath them,
|
|
555
|
+
so the estimated makespan stays near the theoretical floor (the single longest step) without
|
|
556
|
+
oversubscribing RAM.
|
|
557
|
+
3. **Verdict** — a single yes/no line: whether re-grouping is worth it (it must trim ≥5% and ≥5s off
|
|
558
|
+
the makespan), or — when one step is ≥95% of the makespan — that the only remaining lever is to
|
|
559
|
+
split that step into smaller commands the orchestrator can schedule separately.
|
|
560
|
+
|
|
561
|
+
The same logic is exported for programmatic use:
|
|
562
|
+
|
|
563
|
+
```js
|
|
564
|
+
import { recommendPhases, formatRecommendationReport } from 'scripts-orchestrator';
|
|
565
|
+
|
|
566
|
+
const payload = JSON.parse(fs.readFileSync('./logs/results.json', 'utf8'));
|
|
567
|
+
const rec = recommendPhases(payload, { fanout: 3 });
|
|
568
|
+
console.log(formatRecommendationReport(rec));
|
|
569
|
+
// rec.verdict.worthwhile, rec.verdict.reason, rec.recommended.bins, rec.observed, rec.budgetBytes, …
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
A natural place to wire it is the `post_run` config hook, so each run prints a recommendation for its
|
|
573
|
+
own results JSON when it finishes.
|
|
574
|
+
|
|
575
|
+
This is advisory only — the budget is conservative (per-process peaks summed as if they coincide) and
|
|
576
|
+
the packing does not model inter-phase data dependencies, so validate any suggested layout against a
|
|
577
|
+
real run before adopting it.
|
|
578
|
+
|
|
525
579
|
## Exit Codes
|
|
526
580
|
|
|
527
581
|
- `0`: All commands executed successfully
|
|
@@ -535,6 +589,7 @@ See [versions](./docs/versions.md)
|
|
|
535
589
|
- Better UX to indicate what is happening
|
|
536
590
|
- Tests to avoid regression
|
|
537
591
|
- Run any shell command rather than assume the command is specified in package.json (? tentative)
|
|
592
|
+
- Promote the advisory `--recommend` phase recommender into an opt-in automatic scheduler that packs each phase under a per-host memory budget at run time
|
|
538
593
|
|
|
539
594
|
|
|
540
595
|
## Disclaimer
|
package/index.js
CHANGED
|
@@ -51,10 +51,103 @@ const argv = yargs(hideBin(process.argv))
|
|
|
51
51
|
type: 'string',
|
|
52
52
|
description: 'Write HTML report to this path; use "-" for stdout only',
|
|
53
53
|
})
|
|
54
|
+
.option('render', {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'Render an existing results JSON file to HTML (no run). Use with --html-results.',
|
|
57
|
+
})
|
|
58
|
+
.option('recommend', {
|
|
59
|
+
type: 'string',
|
|
60
|
+
description:
|
|
61
|
+
'Analyse an existing results JSON and print a memory-aware phase recommendation (no run).',
|
|
62
|
+
})
|
|
63
|
+
.option('fanout', {
|
|
64
|
+
type: 'number',
|
|
65
|
+
description: 'Workspace fan-out (parallel gates sharing the host) used to size the --recommend budget. Default 1.',
|
|
66
|
+
})
|
|
67
|
+
.option('mem-safety', {
|
|
68
|
+
type: 'number',
|
|
69
|
+
description: 'Fraction of total RAM the --recommend budget may use (default 0.8).',
|
|
70
|
+
})
|
|
71
|
+
.option('budget-mb', {
|
|
72
|
+
type: 'number',
|
|
73
|
+
description: 'Override the --recommend memory budget with a fixed value in MB.',
|
|
74
|
+
})
|
|
75
|
+
.option('recommend-out', {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: 'Write the --recommend report to this file (plain text) instead of the console.',
|
|
78
|
+
})
|
|
54
79
|
.help()
|
|
55
80
|
.alias('h', 'help')
|
|
56
81
|
.parse();
|
|
57
82
|
|
|
83
|
+
// --render mode: turn an existing results JSON into HTML and exit (no orchestration run).
|
|
84
|
+
// Keeps all HTML rendering in the library so consumers never reimplement it.
|
|
85
|
+
if (argv.render != null) {
|
|
86
|
+
const { renderReportHtml } = await import('./lib/index.js');
|
|
87
|
+
const srcPath = path.resolve(process.cwd(), argv.render);
|
|
88
|
+
if (!fs.existsSync(srcPath)) {
|
|
89
|
+
log.error(`Error: --render source not found at ${srcPath}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
let payload;
|
|
93
|
+
try {
|
|
94
|
+
payload = JSON.parse(fs.readFileSync(srcPath, 'utf8'));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
log.error(`Error: failed to parse --render JSON: ${err.message}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const html = renderReportHtml(payload);
|
|
100
|
+
const out = argv.htmlResults ?? null;
|
|
101
|
+
if (out == null || out === '-') {
|
|
102
|
+
console.log(html);
|
|
103
|
+
} else {
|
|
104
|
+
const outPath = path.resolve(process.cwd(), out);
|
|
105
|
+
const tmpPath = outPath + '.tmp';
|
|
106
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
107
|
+
fs.writeFileSync(tmpPath, html, 'utf8');
|
|
108
|
+
fs.renameSync(tmpPath, outPath);
|
|
109
|
+
log.info(`📄 Rendered ${path.relative(process.cwd(), srcPath)} → ${path.relative(process.cwd(), outPath)}`);
|
|
110
|
+
}
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --recommend mode: analyse an existing results JSON and print a memory-aware phase
|
|
115
|
+
// recommendation. Advisory only — no orchestration run. Output goes to the console, or to a
|
|
116
|
+
// plain-text log file when --recommend-out is given.
|
|
117
|
+
if (argv.recommend != null) {
|
|
118
|
+
const { recommendPhases, formatRecommendationReport } = await import('./lib/index.js');
|
|
119
|
+
const srcPath = path.resolve(process.cwd(), argv.recommend);
|
|
120
|
+
if (!fs.existsSync(srcPath)) {
|
|
121
|
+
log.error(`Error: --recommend source not found at ${srcPath}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
let payload;
|
|
125
|
+
try {
|
|
126
|
+
payload = JSON.parse(fs.readFileSync(srcPath, 'utf8'));
|
|
127
|
+
} catch (err) {
|
|
128
|
+
log.error(`Error: failed to parse --recommend JSON: ${err.message}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const rec = recommendPhases(payload, {
|
|
132
|
+
fanout: argv.fanout,
|
|
133
|
+
memSafety: argv.memSafety,
|
|
134
|
+
budgetMb: argv.budgetMb,
|
|
135
|
+
});
|
|
136
|
+
const report = formatRecommendationReport(rec, { sourcePath: path.relative(process.cwd(), srcPath) });
|
|
137
|
+
if (argv.recommendOut != null && argv.recommendOut !== '-') {
|
|
138
|
+
// Strip ANSI colour codes so the log file stays plain text. The escape char is built at runtime
|
|
139
|
+
// so the regex carries no literal control character (keeps eslint's no-control-regex happy).
|
|
140
|
+
const ansi = new RegExp(String.fromCharCode(27) + '\\[[0-9;]*m', 'g');
|
|
141
|
+
const outPath = path.resolve(process.cwd(), argv.recommendOut);
|
|
142
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
143
|
+
fs.writeFileSync(outPath, report.replace(ansi, '') + '\n', 'utf8');
|
|
144
|
+
log.info(`📄 Phase recommendation written to ${path.relative(process.cwd(), outPath)}`);
|
|
145
|
+
} else {
|
|
146
|
+
console.log(report);
|
|
147
|
+
}
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
58
151
|
// Extract arguments
|
|
59
152
|
const args = argv._;
|
|
60
153
|
const configPath = args[0] || './scripts-orchestrator.config.js';
|
|
@@ -111,9 +204,16 @@ const htmlResultsPath =
|
|
|
111
204
|
? argv.htmlResults
|
|
112
205
|
: (commandsConfig.html_results ?? commandsConfig.html_results_path ?? null);
|
|
113
206
|
|
|
114
|
-
//
|
|
207
|
+
// post-run hook — shell command run after json_results written
|
|
115
208
|
const postRun = commandsConfig.post_run ?? null;
|
|
116
209
|
|
|
210
|
+
// Periodic hook — shell command run on an interval WHILE the run is in flight (e.g. to roll up
|
|
211
|
+
// results into an aggregate report). Library owns only the cadence; the command is project-specific.
|
|
212
|
+
const periodicHook = commandsConfig.periodic_hook ?? null;
|
|
213
|
+
const periodicIntervalMs = Number(commandsConfig.periodic_interval_ms) > 0
|
|
214
|
+
? Number(commandsConfig.periodic_interval_ms)
|
|
215
|
+
: 45000;
|
|
216
|
+
|
|
117
217
|
// Set the log folder for the main orchestrator logs if specified
|
|
118
218
|
if (logFolder) {
|
|
119
219
|
log.setLogFolder(logFolder);
|
|
@@ -131,18 +231,22 @@ const orchestrator = new Orchestrator(
|
|
|
131
231
|
jsonResultsPath,
|
|
132
232
|
htmlResultsPath,
|
|
133
233
|
);
|
|
134
|
-
//
|
|
234
|
+
// wire post-run hook from config
|
|
135
235
|
orchestrator.postRun = postRun;
|
|
236
|
+
// Wire periodic hook (cadence owned by the library)
|
|
237
|
+
orchestrator.periodicHook = periodicHook;
|
|
238
|
+
orchestrator.periodicIntervalMs = periodicIntervalMs;
|
|
136
239
|
|
|
137
240
|
// Enhanced signal handlers
|
|
138
241
|
const handleSignal = async (signal) => {
|
|
139
242
|
log.warn(`\nReceived ${signal} signal. Cleaning up...`);
|
|
243
|
+
orchestrator._stopPeriodicHook();
|
|
140
244
|
try {
|
|
141
245
|
await orchestrator.processManager.cleanup();
|
|
142
246
|
} catch (error) {
|
|
143
247
|
log.error(`Cleanup failed: ${error.message}`);
|
|
144
248
|
}
|
|
145
|
-
//
|
|
249
|
+
// clear run-state so dashboards know the run ended
|
|
146
250
|
orchestrator._clearRunState();
|
|
147
251
|
process.exit(1);
|
|
148
252
|
};
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,30 @@ import { ProcessManager } from './process-manager.js';
|
|
|
3
3
|
import { HealthCheck } from './health-check.js';
|
|
4
4
|
import { Logger } from './logger.js';
|
|
5
5
|
import { GitCache } from './git-cache.js';
|
|
6
|
+
import { renderReportHtml } from './report-html.js';
|
|
7
|
+
import {
|
|
8
|
+
recommendPhases,
|
|
9
|
+
decideVerdict,
|
|
10
|
+
formatRecommendationReport,
|
|
11
|
+
computeBudget,
|
|
12
|
+
usableSteps,
|
|
13
|
+
observedTimeline,
|
|
14
|
+
packPhases,
|
|
15
|
+
} from './recommend-phases.js';
|
|
6
16
|
|
|
7
|
-
export {
|
|
8
|
-
|
|
17
|
+
export {
|
|
18
|
+
Orchestrator,
|
|
19
|
+
ProcessManager,
|
|
20
|
+
HealthCheck,
|
|
21
|
+
Logger,
|
|
22
|
+
GitCache,
|
|
23
|
+
renderReportHtml,
|
|
24
|
+
recommendPhases,
|
|
25
|
+
decideVerdict,
|
|
26
|
+
formatRecommendationReport,
|
|
27
|
+
computeBudget,
|
|
28
|
+
usableSteps,
|
|
29
|
+
observedTimeline,
|
|
30
|
+
packPhases,
|
|
31
|
+
};
|
|
32
|
+
export default Orchestrator;
|