scripts-orchestrator 2.13.0 → 2.15.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 +128 -0
- package/index.js +95 -0
- package/lib/index.js +24 -2
- package/lib/orchestrator.js +347 -130
- package/lib/process-manager.js +5 -2
- package/lib/process-manager.test.js +22 -0
- package/lib/recommend-phases.js +251 -0
- package/lib/recommend-phases.test.js +107 -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
|
@@ -36,6 +36,11 @@ npm install --save-dev scripts-orchestrator
|
|
|
36
36
|
- **Optional Phases**: Mark phases as optional and run them selectively
|
|
37
37
|
- **Git-Based Caching**: Automatically skips execution when git state is unchanged
|
|
38
38
|
- **Comprehensive Logging**: Detailed logging of command execution and results
|
|
39
|
+
- **Incremental JSON results**: Live-updating `json_results` file as commands complete (v2.14+)
|
|
40
|
+
- **NDJSON event stream**: Machine-readable per-command events for dashboards (v2.14+)
|
|
41
|
+
- **Post-run hook**: Run a shell command after results are written via `post_run` config (v2.14+)
|
|
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+)
|
|
39
44
|
|
|
40
45
|
## Configuration
|
|
41
46
|
|
|
@@ -410,6 +415,84 @@ export default {
|
|
|
410
415
|
|
|
411
416
|
All logs (command logs, main orchestrator logs, and git cache) will be stored in the specified folder.
|
|
412
417
|
|
|
418
|
+
## Live Dashboard Integration (v2.14+)
|
|
419
|
+
|
|
420
|
+
### Incremental JSON results
|
|
421
|
+
|
|
422
|
+
By default `json_results` is written only at the end of a run. From v2.14 onward the file is
|
|
423
|
+
updated atomically (write-to-temp + rename) after **each command starts or completes**, so
|
|
424
|
+
watchers always see a consistent snapshot:
|
|
425
|
+
|
|
426
|
+
```json
|
|
427
|
+
{
|
|
428
|
+
"success": null,
|
|
429
|
+
"timestamp": "2026-06-04T07:13:21.000Z",
|
|
430
|
+
"commands": [
|
|
431
|
+
{ "command": "lint-ci", "phase": "lint", "success": true, "durationMs": 4200 },
|
|
432
|
+
{ "command": "playwright_ci", "phase": "tests", "success": null, "startedAt": "2026-06-04T07:13:25.000Z" }
|
|
433
|
+
]
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
`"success": null` at the top level is the in-progress sentinel. It is replaced with `true` or
|
|
438
|
+
`false` when `writeJsonResults` writes the final result.
|
|
439
|
+
|
|
440
|
+
### NDJSON event stream
|
|
441
|
+
|
|
442
|
+
Alongside `json_results`, the library writes one NDJSON line per event to
|
|
443
|
+
`<json_results_basename>-events.ndjson`:
|
|
444
|
+
|
|
445
|
+
```jsonl
|
|
446
|
+
{"type":"command_start","timestamp":"...","command":"lint-ci","phase":"lint","scope":"workspace"}
|
|
447
|
+
{"type":"command_end","timestamp":"...","command":"lint-ci","phase":"lint","success":true,"durationMs":4200}
|
|
448
|
+
{"type":"run_end","timestamp":"...","success":true,"durationMs":12800}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Dashboard tools can `tail -f` this file or watch it with `fs.watch` to get real-time updates
|
|
452
|
+
without parsing human-readable log lines.
|
|
453
|
+
|
|
454
|
+
### Run-state file (A4)
|
|
455
|
+
|
|
456
|
+
When `--logFolder` is specified, the library writes `{logFolder}/.scripts-orchestrator-run.json`
|
|
457
|
+
at run start and removes it on run end:
|
|
458
|
+
|
|
459
|
+
```json
|
|
460
|
+
{
|
|
461
|
+
"startedAt": "2026-06-04T07:13:17.000Z",
|
|
462
|
+
"pid": 12345,
|
|
463
|
+
"phase": "tests",
|
|
464
|
+
"activeCommand": "playwright_ci"
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
This file is the authoritative in-progress signal for live dashboards. Its absence means the run
|
|
469
|
+
has finished (or never started).
|
|
470
|
+
|
|
471
|
+
### Post-run hook (A7)
|
|
472
|
+
|
|
473
|
+
Add `post_run` to your config to run a shell command **after** `json_results` is written and the
|
|
474
|
+
run-state file is cleared, but **before** `process.exit()`:
|
|
475
|
+
|
|
476
|
+
```javascript
|
|
477
|
+
export default {
|
|
478
|
+
json_results: './logs/scripts-orchestrator-results.json',
|
|
479
|
+
post_run: 'node scripts/generate-report.js', // called after every run
|
|
480
|
+
phases: [ /* ... */ ]
|
|
481
|
+
};
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
The hook receives two environment variables:
|
|
485
|
+
- `SCRIPTS_ORCHESTRATOR_SUCCESS=1` (or `0`) — whether the run succeeded
|
|
486
|
+
- `SCRIPTS_ORCHESTRATOR_EXIT_CODE=0` (or `1`) — same, as a numeric exit code
|
|
487
|
+
|
|
488
|
+
The hook runs synchronously and its exit code is logged but does not change the orchestrator's
|
|
489
|
+
own exit code.
|
|
490
|
+
|
|
491
|
+
**Typical use case:** trigger a monorepo rollup report after each workspace finishes:
|
|
492
|
+
```javascript
|
|
493
|
+
post_run: 'node ../../scripts/merge-orchestrator-report.js'
|
|
494
|
+
```
|
|
495
|
+
|
|
413
496
|
## Git-Based Caching
|
|
414
497
|
|
|
415
498
|
The orchestrator automatically tracks the git commit hash and repository state to optimize execution:
|
|
@@ -440,6 +523,50 @@ This is useful when you want to:
|
|
|
440
523
|
- Debug issues without modifying the codebase
|
|
441
524
|
- Override the cache in CI/CD pipelines
|
|
442
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 prints a **memory-aware
|
|
530
|
+
phase recommendation**: it never runs anything and writes no files.
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
# Analyse an existing results JSON and print a suggested phase layout
|
|
534
|
+
scripts-orchestrator --recommend ./logs/scripts-orchestrator-results.json
|
|
535
|
+
|
|
536
|
+
# Size the budget for a machine running N gates in parallel (each gets 1/N of RAM and cores)
|
|
537
|
+
scripts-orchestrator --recommend ./logs/scripts-orchestrator-results.json --fanout 3
|
|
538
|
+
|
|
539
|
+
# Override the memory budget explicitly (MB) or change the RAM safety fraction
|
|
540
|
+
scripts-orchestrator --recommend ./logs/results.json --budget-mb 8192
|
|
541
|
+
scripts-orchestrator --recommend ./logs/results.json --mem-safety 0.7
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
It reports two things:
|
|
545
|
+
|
|
546
|
+
1. **Observed timeline** — each phase's wall-clock (the longest step in it) and the concurrent peak
|
|
547
|
+
memory (Σ of member peaks), flagging any phase whose concurrent peak exceeds the host budget.
|
|
548
|
+
2. **Recommended layout** — a [First-Fit-Decreasing](https://en.wikipedia.org/wiki/Bin_packing_problem)
|
|
549
|
+
bin-packing by duration that groups steps into sequential phases so each phase's concurrent peak
|
|
550
|
+
memory stays under `budget = totalmem × memSafety ÷ fanout` and its step count stays under
|
|
551
|
+
`coreShare = (cores − 2) ÷ fanout`. Long steps seed phases; short steps fill the gaps beneath them,
|
|
552
|
+
so the estimated makespan stays near the theoretical floor (the single longest step) without
|
|
553
|
+
oversubscribing RAM.
|
|
554
|
+
|
|
555
|
+
The same logic is exported for programmatic use:
|
|
556
|
+
|
|
557
|
+
```js
|
|
558
|
+
import { recommendPhases, formatRecommendationReport } from 'scripts-orchestrator';
|
|
559
|
+
|
|
560
|
+
const payload = JSON.parse(fs.readFileSync('./logs/results.json', 'utf8'));
|
|
561
|
+
const rec = recommendPhases(payload, { fanout: 3 });
|
|
562
|
+
console.log(formatRecommendationReport(rec));
|
|
563
|
+
// rec.recommended.bins, rec.observed, rec.observedMakespanMs, rec.budgetBytes, …
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
This is advisory only — the budget is conservative (per-process peaks summed as if they coincide) and
|
|
567
|
+
the packing does not model inter-phase data dependencies, so validate any suggested layout against a
|
|
568
|
+
real run before adopting it.
|
|
569
|
+
|
|
443
570
|
## Exit Codes
|
|
444
571
|
|
|
445
572
|
- `0`: All commands executed successfully
|
|
@@ -453,6 +580,7 @@ See [versions](./docs/versions.md)
|
|
|
453
580
|
- Better UX to indicate what is happening
|
|
454
581
|
- Tests to avoid regression
|
|
455
582
|
- Run any shell command rather than assume the command is specified in package.json (? tentative)
|
|
583
|
+
- 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
|
|
456
584
|
|
|
457
585
|
|
|
458
586
|
## Disclaimer
|
package/index.js
CHANGED
|
@@ -51,10 +51,87 @@ 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 (R12, 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
|
+
})
|
|
54
75
|
.help()
|
|
55
76
|
.alias('h', 'help')
|
|
56
77
|
.parse();
|
|
57
78
|
|
|
79
|
+
// --render mode: turn an existing results JSON into HTML and exit (no orchestration run).
|
|
80
|
+
// Keeps all HTML rendering in the library so consumers never reimplement it.
|
|
81
|
+
if (argv.render != null) {
|
|
82
|
+
const { renderReportHtml } = await import('./lib/index.js');
|
|
83
|
+
const srcPath = path.resolve(process.cwd(), argv.render);
|
|
84
|
+
if (!fs.existsSync(srcPath)) {
|
|
85
|
+
log.error(`Error: --render source not found at ${srcPath}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
let payload;
|
|
89
|
+
try {
|
|
90
|
+
payload = JSON.parse(fs.readFileSync(srcPath, 'utf8'));
|
|
91
|
+
} catch (err) {
|
|
92
|
+
log.error(`Error: failed to parse --render JSON: ${err.message}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
const html = renderReportHtml(payload);
|
|
96
|
+
const out = argv.htmlResults ?? null;
|
|
97
|
+
if (out == null || out === '-') {
|
|
98
|
+
console.log(html);
|
|
99
|
+
} else {
|
|
100
|
+
const outPath = path.resolve(process.cwd(), out);
|
|
101
|
+
const tmpPath = outPath + '.tmp';
|
|
102
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
103
|
+
fs.writeFileSync(tmpPath, html, 'utf8');
|
|
104
|
+
fs.renameSync(tmpPath, outPath);
|
|
105
|
+
log.info(`📄 Rendered ${path.relative(process.cwd(), srcPath)} → ${path.relative(process.cwd(), outPath)}`);
|
|
106
|
+
}
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --recommend mode: analyse an existing results JSON and print a memory-aware phase
|
|
111
|
+
// recommendation (R12). Advisory only — no orchestration run, no files written.
|
|
112
|
+
if (argv.recommend != null) {
|
|
113
|
+
const { recommendPhases, formatRecommendationReport } = await import('./lib/index.js');
|
|
114
|
+
const srcPath = path.resolve(process.cwd(), argv.recommend);
|
|
115
|
+
if (!fs.existsSync(srcPath)) {
|
|
116
|
+
log.error(`Error: --recommend source not found at ${srcPath}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
let payload;
|
|
120
|
+
try {
|
|
121
|
+
payload = JSON.parse(fs.readFileSync(srcPath, 'utf8'));
|
|
122
|
+
} catch (err) {
|
|
123
|
+
log.error(`Error: failed to parse --recommend JSON: ${err.message}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
const rec = recommendPhases(payload, {
|
|
127
|
+
fanout: argv.fanout,
|
|
128
|
+
memSafety: argv.memSafety,
|
|
129
|
+
budgetMb: argv.budgetMb,
|
|
130
|
+
});
|
|
131
|
+
console.log(formatRecommendationReport(rec, { sourcePath: path.relative(process.cwd(), srcPath) }));
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
|
|
58
135
|
// Extract arguments
|
|
59
136
|
const args = argv._;
|
|
60
137
|
const configPath = args[0] || './scripts-orchestrator.config.js';
|
|
@@ -111,6 +188,16 @@ const htmlResultsPath =
|
|
|
111
188
|
? argv.htmlResults
|
|
112
189
|
: (commandsConfig.html_results ?? commandsConfig.html_results_path ?? null);
|
|
113
190
|
|
|
191
|
+
// A7: post-run hook — shell command run after json_results written
|
|
192
|
+
const postRun = commandsConfig.post_run ?? null;
|
|
193
|
+
|
|
194
|
+
// Periodic hook — shell command run on an interval WHILE the run is in flight (e.g. to roll up
|
|
195
|
+
// results into an aggregate report). Library owns only the cadence; the command is project-specific.
|
|
196
|
+
const periodicHook = commandsConfig.periodic_hook ?? null;
|
|
197
|
+
const periodicIntervalMs = Number(commandsConfig.periodic_interval_ms) > 0
|
|
198
|
+
? Number(commandsConfig.periodic_interval_ms)
|
|
199
|
+
: 45000;
|
|
200
|
+
|
|
114
201
|
// Set the log folder for the main orchestrator logs if specified
|
|
115
202
|
if (logFolder) {
|
|
116
203
|
log.setLogFolder(logFolder);
|
|
@@ -128,15 +215,23 @@ const orchestrator = new Orchestrator(
|
|
|
128
215
|
jsonResultsPath,
|
|
129
216
|
htmlResultsPath,
|
|
130
217
|
);
|
|
218
|
+
// A7: wire post-run hook from config
|
|
219
|
+
orchestrator.postRun = postRun;
|
|
220
|
+
// Wire periodic hook (cadence owned by the library)
|
|
221
|
+
orchestrator.periodicHook = periodicHook;
|
|
222
|
+
orchestrator.periodicIntervalMs = periodicIntervalMs;
|
|
131
223
|
|
|
132
224
|
// Enhanced signal handlers
|
|
133
225
|
const handleSignal = async (signal) => {
|
|
134
226
|
log.warn(`\nReceived ${signal} signal. Cleaning up...`);
|
|
227
|
+
orchestrator._stopPeriodicHook();
|
|
135
228
|
try {
|
|
136
229
|
await orchestrator.processManager.cleanup();
|
|
137
230
|
} catch (error) {
|
|
138
231
|
log.error(`Cleanup failed: ${error.message}`);
|
|
139
232
|
}
|
|
233
|
+
// A4: clear run-state so dashboards know the run ended
|
|
234
|
+
orchestrator._clearRunState();
|
|
140
235
|
process.exit(1);
|
|
141
236
|
};
|
|
142
237
|
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,28 @@ 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
|
+
formatRecommendationReport,
|
|
10
|
+
computeBudget,
|
|
11
|
+
usableSteps,
|
|
12
|
+
observedTimeline,
|
|
13
|
+
packPhases,
|
|
14
|
+
} from './recommend-phases.js';
|
|
6
15
|
|
|
7
|
-
export {
|
|
8
|
-
|
|
16
|
+
export {
|
|
17
|
+
Orchestrator,
|
|
18
|
+
ProcessManager,
|
|
19
|
+
HealthCheck,
|
|
20
|
+
Logger,
|
|
21
|
+
GitCache,
|
|
22
|
+
renderReportHtml,
|
|
23
|
+
recommendPhases,
|
|
24
|
+
formatRecommendationReport,
|
|
25
|
+
computeBudget,
|
|
26
|
+
usableSteps,
|
|
27
|
+
observedTimeline,
|
|
28
|
+
packPhases,
|
|
29
|
+
};
|
|
30
|
+
export default Orchestrator;
|