omp-conductor 0.19.5 → 0.19.7
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/REFERENCE.md +17 -1
- package/package.json +1 -1
- package/src/cli.ts +2 -0
- package/src/command-help.ts +18 -8
- package/src/command-manifest.ts +22 -5
- package/src/commands/context.ts +1 -0
- package/src/commands/report.ts +63 -0
- package/src/commands/restore-db.ts +1 -1
- package/src/commands/snapshot-db.ts +67 -0
- package/src/commands/upgrade-install.ts +1 -1
- package/src/commands/upgrade.ts +2 -2
- package/src/commands/verb.ts +1 -0
- package/src/commands/watch.ts +14 -2
- package/src/config.ts +17 -0
- package/src/daemon.ts +146 -35
- package/src/doctor.ts +53 -15
- package/src/escalate.ts +39 -21
- package/src/fleet.ts +979 -143
- package/src/orchestrator-tick.ts +14 -3
- package/src/setup-host.ts +10 -10
- package/src/setup-wizard.ts +1 -1
- package/src/status-render.ts +10 -0
- package/src/store.ts +338 -22
- package/src/types.ts +51 -3
- package/src/upgrade-journal.ts +35 -1
- package/src/upgrade-verify.ts +27 -6
- package/src/upgrade.ts +301 -22
- package/src/verbs/actions.ts +6 -1
- package/src/verbs/server.ts +146 -31
- package/systemd/omp-conductor-recover.sh +55 -7
- package/systemd/recover-unit-test.sh +99 -6
package/src/upgrade.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
3
4
|
import { backupTimestamp, copyToUniqueBackup } from "./backups.ts";
|
|
4
5
|
import { inspectBriefLayout, type BriefLayout } from "./brief-upgrade.ts";
|
|
5
6
|
import { pauseInstance, setPaused, statusSnapshot, type AdmissionAckRecord } from "./daemon.ts";
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
upgradeJournalPath,
|
|
25
26
|
type UpgradeCheck,
|
|
26
27
|
type UpgradeJournalEntry,
|
|
28
|
+
type InstallSnapshotManagement,
|
|
27
29
|
} from "./upgrade-journal.ts";
|
|
28
30
|
import {
|
|
29
31
|
enqueueUpgradeReport,
|
|
@@ -279,7 +281,7 @@ function parseRegistry(raw: string): { version: string; gitHead: string } {
|
|
|
279
281
|
interface ReleaseIdentityPlan {
|
|
280
282
|
version: string;
|
|
281
283
|
gitHead: string;
|
|
282
|
-
/** What
|
|
284
|
+
/** What the discovered CLI root / `omp plugin install` are given for this identity. */
|
|
283
285
|
packageSpec: string;
|
|
284
286
|
/** Named checks already run to earn this identity, journalled as evidence. */
|
|
285
287
|
checks: UpgradeCheck[];
|
|
@@ -370,7 +372,12 @@ async function resolveIdentity(deps: UpgradeDeps, options: UpgradeOptions): Prom
|
|
|
370
372
|
return { version, gitHead: sha, packageSpec: `github:${PACKAGE_SOURCE}#${sha}`, checks, bootstrap: true };
|
|
371
373
|
}
|
|
372
374
|
|
|
373
|
-
|
|
375
|
+
interface OmpPluginEntry {
|
|
376
|
+
version: string;
|
|
377
|
+
path?: string;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function ompPluginInventory(raw: string): unknown[] {
|
|
374
381
|
let parsed: unknown;
|
|
375
382
|
try {
|
|
376
383
|
parsed = JSON.parse(raw);
|
|
@@ -382,15 +389,51 @@ function ompPluginVersion(raw: string): string | undefined {
|
|
|
382
389
|
}
|
|
383
390
|
const npm = Reflect.get(parsed, "npm");
|
|
384
391
|
if (!Array.isArray(npm)) throw new Error("omp plugin list returned no npm inventory");
|
|
385
|
-
|
|
392
|
+
return npm;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function ompPluginEntry(raw: string): OmpPluginEntry | undefined {
|
|
396
|
+
const plugin = ompPluginInventory(raw).find(
|
|
386
397
|
(entry) => entry !== null && typeof entry === "object" && Reflect.get(entry, "name") === PACKAGE,
|
|
387
398
|
);
|
|
388
|
-
if (plugin === undefined) return undefined;
|
|
399
|
+
if (plugin === undefined || plugin === null || typeof plugin !== "object") return undefined;
|
|
389
400
|
const version = Reflect.get(plugin, "version");
|
|
390
401
|
if (typeof version !== "string" || version.length === 0) {
|
|
391
402
|
throw new Error("installed omp-conductor plugin has no version");
|
|
392
403
|
}
|
|
393
|
-
|
|
404
|
+
const path = Reflect.get(plugin, "path");
|
|
405
|
+
return {
|
|
406
|
+
version,
|
|
407
|
+
...(typeof path === "string" && path.length > 0 ? { path } : {}),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function ompPluginVersion(raw: string): string | undefined {
|
|
412
|
+
return ompPluginEntry(raw)?.version;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function nodeModulesRoot(path: string): string | undefined {
|
|
416
|
+
const marker = "/node_modules/";
|
|
417
|
+
const at = path.lastIndexOf(marker);
|
|
418
|
+
return at <= 0 ? undefined : path.slice(0, at);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function defaultOmpPluginRoot(env: NodeJS.ProcessEnv): string {
|
|
422
|
+
const configured = env["OMP_CODING_AGENT_DIR"];
|
|
423
|
+
if (configured !== undefined && configured.length > 0) return join(configured, "plugins");
|
|
424
|
+
return join(env["HOME"] ?? homedir(), ".omp", "plugins");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function ompPluginInstallRoot(raw: string, env: NodeJS.ProcessEnv): string {
|
|
428
|
+
const inventory = ompPluginInventory(raw);
|
|
429
|
+
const ownPath = ompPluginEntry(raw)?.path;
|
|
430
|
+
const fallbackPath = inventory
|
|
431
|
+
.map((entry) =>
|
|
432
|
+
entry !== null && typeof entry === "object" ? Reflect.get(entry, "path") : undefined,
|
|
433
|
+
)
|
|
434
|
+
.find((path): path is string => typeof path === "string" && path.length > 0);
|
|
435
|
+
const root = nodeModulesRoot(ownPath ?? fallbackPath ?? "");
|
|
436
|
+
return root ?? defaultOmpPluginRoot(env);
|
|
394
437
|
}
|
|
395
438
|
|
|
396
439
|
function herdrPluginSource(raw: string): string | undefined {
|
|
@@ -403,12 +446,143 @@ function herdrPluginSource(raw: string): string | undefined {
|
|
|
403
446
|
return source;
|
|
404
447
|
}
|
|
405
448
|
|
|
406
|
-
|
|
407
|
-
|
|
449
|
+
function defaultHerdrInstallRoot(env: NodeJS.ProcessEnv): string {
|
|
450
|
+
const config =
|
|
451
|
+
env["HERDR_CONFIG_DIR"] ??
|
|
452
|
+
join(env["XDG_CONFIG_HOME"] ?? join(env["HOME"] ?? homedir(), ".config"), "herdr");
|
|
453
|
+
return join(config, "plugins", "github");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function herdrPluginManagement(
|
|
457
|
+
raw: string,
|
|
458
|
+
env: NodeJS.ProcessEnv,
|
|
459
|
+
): InstallSnapshotManagement["herdr"] {
|
|
460
|
+
let parsed: unknown;
|
|
461
|
+
try {
|
|
462
|
+
parsed = JSON.parse(raw);
|
|
463
|
+
} catch {
|
|
464
|
+
throw new Error("herdr plugin list returned invalid JSON");
|
|
465
|
+
}
|
|
466
|
+
const result =
|
|
467
|
+
parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
468
|
+
? Reflect.get(parsed, "result")
|
|
469
|
+
: undefined;
|
|
470
|
+
const plugins =
|
|
471
|
+
result !== null && typeof result === "object" && !Array.isArray(result)
|
|
472
|
+
? Reflect.get(result, "plugins")
|
|
473
|
+
: undefined;
|
|
474
|
+
if (!Array.isArray(plugins)) throw new Error("herdr plugin list returned no plugin inventory");
|
|
475
|
+
const plugin = plugins.find(
|
|
476
|
+
(entry) => entry !== null && typeof entry === "object" && Reflect.get(entry, "plugin_id") === HERDR_PLUGIN,
|
|
477
|
+
);
|
|
478
|
+
const root =
|
|
479
|
+
plugin !== undefined && typeof plugin === "object"
|
|
480
|
+
? Reflect.get(plugin, "plugin_root")
|
|
481
|
+
: undefined;
|
|
482
|
+
const source =
|
|
483
|
+
plugin !== undefined && typeof plugin === "object"
|
|
484
|
+
? Reflect.get(plugin, "source")
|
|
485
|
+
: undefined;
|
|
486
|
+
const managedPath =
|
|
487
|
+
source !== null && typeof source === "object" && !Array.isArray(source)
|
|
488
|
+
? Reflect.get(source, "managed_path")
|
|
489
|
+
: undefined;
|
|
490
|
+
const marker = "/plugins/github/";
|
|
491
|
+
const markerAt = typeof root === "string" ? root.indexOf(marker) : -1;
|
|
492
|
+
const installRoot =
|
|
493
|
+
typeof managedPath === "string" && managedPath.length > 0
|
|
494
|
+
? dirname(managedPath)
|
|
495
|
+
: markerAt >= 0 && typeof root === "string"
|
|
496
|
+
? root.slice(0, markerAt + marker.length - 1)
|
|
497
|
+
: defaultHerdrInstallRoot(env);
|
|
498
|
+
return {
|
|
499
|
+
...(typeof root === "string" && root.length > 0 ? { root } : {}),
|
|
500
|
+
installRoot,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function cliInstallRoot(target: string): string {
|
|
505
|
+
const marker = `/node_modules/${PACKAGE}/`;
|
|
506
|
+
const at = target.lastIndexOf(marker);
|
|
507
|
+
if (at <= 0) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
`installed omp-conductor CLI target ${target} is not inside a discoverable node_modules/${PACKAGE} root`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return target.slice(0, at);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Discover the exact managed roots the transaction will mutate (#1019). */
|
|
516
|
+
export async function discoverInstallManagement(
|
|
517
|
+
deps: SurfaceProbeDeps,
|
|
518
|
+
): Promise<InstallSnapshotManagement> {
|
|
519
|
+
const session = resolveHerdrSession(deps.env);
|
|
520
|
+
const executable = (await mustRun(deps, "which", ["omp-conductor"])).stdout.trim();
|
|
521
|
+
if (executable.length === 0) throw new Error("which omp-conductor returned no executable");
|
|
522
|
+
const [resolved, omp, herdr] = await Promise.all([
|
|
523
|
+
mustRun(deps, "readlink", ["-f", executable]),
|
|
524
|
+
mustRun(deps, "omp", ["plugin", "list", "--json"]),
|
|
525
|
+
mustRun(deps, "herdr", ["--session", session, "plugin", "list", "--json"]),
|
|
526
|
+
]);
|
|
527
|
+
const target = resolved.stdout.trim();
|
|
528
|
+
if (target.length === 0) throw new Error(`cannot resolve installed CLI ${executable}`);
|
|
529
|
+
const ompRoot = ompPluginInstallRoot(omp.stdout, deps.env);
|
|
530
|
+
return {
|
|
531
|
+
cli: { executable, target, root: cliInstallRoot(target) },
|
|
532
|
+
omp: { root: ompRoot, lock: join(ompRoot, "omp-plugins.lock.json") },
|
|
533
|
+
herdr: herdrPluginManagement(herdr.stdout, deps.env),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function preflightInstall(
|
|
538
|
+
deps: UpgradeDeps,
|
|
539
|
+
management: InstallSnapshotManagement,
|
|
540
|
+
packageSpec: string,
|
|
541
|
+
): Promise<void> {
|
|
542
|
+
const writable: { label: string; path: string }[] = [
|
|
543
|
+
{ label: "CLI package root", path: management.cli.root },
|
|
544
|
+
{ label: "CLI package manifest", path: join(management.cli.root, "package.json") },
|
|
545
|
+
{ label: "CLI node_modules", path: join(management.cli.root, "node_modules") },
|
|
546
|
+
{ label: "OMP plugin root", path: management.omp.root },
|
|
547
|
+
{ label: "Herdr managed-plugin root", path: management.herdr.installRoot },
|
|
548
|
+
];
|
|
549
|
+
if (existsSync(management.omp.lock)) {
|
|
550
|
+
writable.push({ label: "OMP plugin lock", path: management.omp.lock });
|
|
551
|
+
}
|
|
552
|
+
for (const target of writable) {
|
|
553
|
+
const checked = await deps.run("test", ["-w", target.path]);
|
|
554
|
+
if (checked.code !== 0) {
|
|
555
|
+
const detail = checked.stderr.trim() || checked.stdout.trim() || "missing or not writable";
|
|
556
|
+
throw new Error(
|
|
557
|
+
`upgrade preflight refused: ${target.label} ${target.path} is unusable (${detail}); ` +
|
|
558
|
+
"nothing has been installed or paused",
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
await mustRun(deps, "bun", [
|
|
564
|
+
"add",
|
|
565
|
+
"--cwd",
|
|
566
|
+
management.cli.root,
|
|
567
|
+
"--exact",
|
|
568
|
+
"--dry-run",
|
|
569
|
+
packageSpec,
|
|
570
|
+
]);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`upgrade preflight refused: the CLI install command is unusable at ${management.cli.root} ` +
|
|
574
|
+
`(${err instanceof Error ? err.message : String(err)}); nothing has been installed or paused`,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** The three installed identities one host carries: the discovered CLI package
|
|
580
|
+
* tree, the omp plugin, and the herdr recovery plugin's pin. */
|
|
408
581
|
export interface InstalledSurfaces {
|
|
409
582
|
cliVersion: string;
|
|
410
583
|
ompVersion?: string;
|
|
411
584
|
herdrSource?: string;
|
|
585
|
+
management?: InstallSnapshotManagement;
|
|
412
586
|
}
|
|
413
587
|
|
|
414
588
|
/** What reading the three surfaces needs — deliberately narrower than
|
|
@@ -427,6 +601,7 @@ export async function inspectSurfaces(
|
|
|
427
601
|
deps: SurfaceProbeDeps,
|
|
428
602
|
/** `readHerdr: false` skips the herdr session probe and plugin read
|
|
429
603
|
* entirely, so a host with no herdr on PATH can still be asked what its
|
|
604
|
+
|
|
430
605
|
* CLI and omp plugin are (#904). The upgrade transaction always reads all
|
|
431
606
|
* three — a release pins every surface. */
|
|
432
607
|
opts: { readHerdr?: boolean } = {},
|
|
@@ -455,6 +630,56 @@ export async function inspectSurfaces(
|
|
|
455
630
|
};
|
|
456
631
|
}
|
|
457
632
|
|
|
633
|
+
function packageVersionAt(deps: UpgradeDeps, manifest: string): string {
|
|
634
|
+
let parsed: unknown;
|
|
635
|
+
try {
|
|
636
|
+
parsed = JSON.parse(deps.readFile(manifest));
|
|
637
|
+
} catch (err) {
|
|
638
|
+
throw new Error(
|
|
639
|
+
`cannot read restored package manifest ${manifest}: ${err instanceof Error ? err.message : String(err)}`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
const version =
|
|
643
|
+
parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
644
|
+
? Reflect.get(parsed, "version")
|
|
645
|
+
: undefined;
|
|
646
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
647
|
+
throw new Error(`restored package manifest ${manifest} has no version`);
|
|
648
|
+
}
|
|
649
|
+
return version;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** Verify the snapshot's recorded destinations directly, before PATH-based probes. */
|
|
653
|
+
function verifyRestoredManagement(deps: UpgradeDeps, previous: InstalledSurfaces): void {
|
|
654
|
+
const management = previous.management;
|
|
655
|
+
if (management === undefined) return;
|
|
656
|
+
const cliManifest = join(management.cli.root, "node_modules", PACKAGE, "package.json");
|
|
657
|
+
const cliVersion = packageVersionAt(deps, cliManifest);
|
|
658
|
+
if (cliVersion !== previous.cliVersion) {
|
|
659
|
+
throw new Error(
|
|
660
|
+
`restored CLI root ${management.cli.root} has ${cliVersion}, expected ${previous.cliVersion}`,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
if (previous.ompVersion !== undefined) {
|
|
664
|
+
const ompManifest = join(management.omp.root, "node_modules", PACKAGE, "package.json");
|
|
665
|
+
const ompVersion = packageVersionAt(deps, ompManifest);
|
|
666
|
+
if (ompVersion !== previous.ompVersion) {
|
|
667
|
+
throw new Error(
|
|
668
|
+
`restored OMP plugin root ${management.omp.root} has ${ompVersion}, expected ${previous.ompVersion}`,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (previous.herdrSource !== undefined) {
|
|
673
|
+
if (management.herdr.root === undefined) {
|
|
674
|
+
throw new Error("restored Herdr plugin had no recorded root to verify");
|
|
675
|
+
}
|
|
676
|
+
const manifest = join(management.herdr.root, "herdr-plugin.toml");
|
|
677
|
+
if (deps.readFile(manifest).trim().length === 0) {
|
|
678
|
+
throw new Error(`restored Herdr plugin manifest ${manifest} is empty`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
458
683
|
export function expectedHerdrSource(gitHead: string): string {
|
|
459
684
|
return `github:${HERDR_SOURCE}@${gitHead}`;
|
|
460
685
|
}
|
|
@@ -502,7 +727,7 @@ function previousHerdrInstall(source: string): readonly [string, readonly string
|
|
|
502
727
|
* that represents them.
|
|
503
728
|
*
|
|
504
729
|
* Everything an upgrade or a draining restart replaces is host-wide: the
|
|
505
|
-
*
|
|
730
|
+
* discovered CLI package tree, the omp plugin, the Herdr plugin, and the one
|
|
506
731
|
* daemon. That daemon serves *every* configured project — the generated unit's
|
|
507
732
|
* ExecStart carries no `--project` — so its restart lands on all of them at
|
|
508
733
|
* once. The scope therefore names every project whose workers must drain and
|
|
@@ -1102,13 +1327,22 @@ export async function rollbackUpgrade(
|
|
|
1102
1327
|
// Exact post-rollback identity verification decides whether absence is safe.
|
|
1103
1328
|
}
|
|
1104
1329
|
};
|
|
1105
|
-
|
|
1106
1330
|
if (installTouched) {
|
|
1107
|
-
|
|
1108
|
-
"
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1331
|
+
if (previous.management === undefined) {
|
|
1332
|
+
await restore("legacy Bun-global omp-conductor CLI", "bun", [
|
|
1333
|
+
"add",
|
|
1334
|
+
"-g",
|
|
1335
|
+
`${PACKAGE}@${previous.cliVersion}`,
|
|
1336
|
+
]);
|
|
1337
|
+
} else {
|
|
1338
|
+
await restore(`omp-conductor CLI at ${previous.management.cli.root}`, "bun", [
|
|
1339
|
+
"add",
|
|
1340
|
+
"--cwd",
|
|
1341
|
+
previous.management.cli.root,
|
|
1342
|
+
"--exact",
|
|
1343
|
+
`${PACKAGE}@${previous.cliVersion}`,
|
|
1344
|
+
]);
|
|
1345
|
+
}
|
|
1112
1346
|
if (previous.ompVersion === undefined) {
|
|
1113
1347
|
await removeIfPresent("remove newly installed omp plugin", "omp", [
|
|
1114
1348
|
"plugin",
|
|
@@ -1192,6 +1426,7 @@ export async function rollbackUpgrade(
|
|
|
1192
1426
|
|
|
1193
1427
|
if (installTouched) {
|
|
1194
1428
|
try {
|
|
1429
|
+
verifyRestoredManagement(deps, previous);
|
|
1195
1430
|
const restored = await inspectSurfaces(deps);
|
|
1196
1431
|
if (
|
|
1197
1432
|
restored.cliVersion !== previous.cliVersion ||
|
|
@@ -1272,6 +1507,19 @@ export async function upgradeConductor(
|
|
|
1272
1507
|
// already passed (#908). Everything below reads the same pair either way.
|
|
1273
1508
|
const identity = await resolveIdentity(deps, options);
|
|
1274
1509
|
const release = { version: identity.version, gitHead: identity.gitHead };
|
|
1510
|
+
const requestedProjects = (deps.env["OMP_CONDUCTOR_INSTALL_PROJECTS"] ?? "")
|
|
1511
|
+
.split(",")
|
|
1512
|
+
.filter((name) => name.length > 0);
|
|
1513
|
+
const requestedHolder = deps.env["OMP_CONDUCTOR_INSTALL_HOLDER"];
|
|
1514
|
+
const installHolder =
|
|
1515
|
+
requestedHolder === "human" || requestedHolder === "orchestrator"
|
|
1516
|
+
? requestedHolder
|
|
1517
|
+
: undefined;
|
|
1518
|
+
const installProjects: readonly (string | undefined)[] =
|
|
1519
|
+
requestedProjects.length === 0 ? scope.selectors : requestedProjects;
|
|
1520
|
+
const installAttribution =
|
|
1521
|
+
`Host-global install holder: ${installHolder ?? "unknown"}; affected projects: ` +
|
|
1522
|
+
`${installProjects.map((selector) => selector ?? "(default)").join(", ")}.`;
|
|
1275
1523
|
if (identity.bootstrap) {
|
|
1276
1524
|
// The bootstrap's evidence lands in the journal before the first surface
|
|
1277
1525
|
// moves, so an interrupted bootstrap leaves a record of what was verified
|
|
@@ -1297,15 +1545,24 @@ export async function upgradeConductor(
|
|
|
1297
1545
|
version: release.version,
|
|
1298
1546
|
gitHead: release.gitHead,
|
|
1299
1547
|
unit: deps.env["OMP_CONDUCTOR_UNIT"],
|
|
1548
|
+
selectors: scope.selectors.map((selector) => selector ?? null),
|
|
1549
|
+
...(requestedProjects.length === 0 ? {} : { installProjects: requestedProjects }),
|
|
1550
|
+
...(installHolder === undefined ? {} : { installHolder }),
|
|
1300
1551
|
});
|
|
1301
1552
|
}
|
|
1302
1553
|
const initial = deps.layers(scope.pauseKey);
|
|
1303
|
-
const
|
|
1554
|
+
const observedSurfaces = await inspectSurfaces(deps);
|
|
1304
1555
|
const briefs: ScopedBrief[] = scope.selectors.map((selector) => ({
|
|
1305
1556
|
selector,
|
|
1306
1557
|
...deps.brief(selector),
|
|
1307
1558
|
}));
|
|
1308
|
-
const installNeeded = !surfacesCurrent(
|
|
1559
|
+
const installNeeded = !surfacesCurrent(observedSurfaces, release.version, release.gitHead);
|
|
1560
|
+
const management = installNeeded ? await discoverInstallManagement(deps) : undefined;
|
|
1561
|
+
if (management !== undefined) await preflightInstall(deps, management, identity.packageSpec);
|
|
1562
|
+
const surfaces: InstalledSurfaces = {
|
|
1563
|
+
...observedSurfaces,
|
|
1564
|
+
...(management === undefined ? {} : { management }),
|
|
1565
|
+
};
|
|
1309
1566
|
// The host-unit baseline, read while the OLD package is still the one
|
|
1310
1567
|
// rendering (#905). A destination already differing from the old render was
|
|
1311
1568
|
// edited outside conductor, so the reconcile after the install must not
|
|
@@ -1404,6 +1661,7 @@ export async function upgradeConductor(
|
|
|
1404
1661
|
cliVersion: surfaces.cliVersion,
|
|
1405
1662
|
ompVersion: surfaces.ompVersion,
|
|
1406
1663
|
herdrSource: surfaces.herdrSource,
|
|
1664
|
+
...(surfaces.management === undefined ? {} : { management: surfaces.management }),
|
|
1407
1665
|
},
|
|
1408
1666
|
...(preUpgradeBackup === undefined ? {} : { configBackup: preUpgradeBackup }),
|
|
1409
1667
|
pauseKey: scope.pauseKey,
|
|
@@ -1453,9 +1711,18 @@ export async function upgradeConductor(
|
|
|
1453
1711
|
const reloadStarted = Date.now();
|
|
1454
1712
|
try {
|
|
1455
1713
|
if (installNeeded) {
|
|
1714
|
+
if (management === undefined) {
|
|
1715
|
+
throw new Error("upgrade invariant failed: install needed without discovered surface management");
|
|
1716
|
+
}
|
|
1456
1717
|
installTouched = true;
|
|
1457
|
-
deps.log(`install 1/3:
|
|
1458
|
-
await mustRun(deps, "bun", [
|
|
1718
|
+
deps.log(`install 1/3: omp-conductor CLI at ${management.cli.root} → ${release.version}`);
|
|
1719
|
+
await mustRun(deps, "bun", [
|
|
1720
|
+
"add",
|
|
1721
|
+
"--cwd",
|
|
1722
|
+
management.cli.root,
|
|
1723
|
+
"--exact",
|
|
1724
|
+
identity.packageSpec,
|
|
1725
|
+
]);
|
|
1459
1726
|
journal({ kind: "phase", phase: "install", surface: "cli", ok: true, version: release.version, gitHead: release.gitHead });
|
|
1460
1727
|
deps.log(`install 2/3: omp plugin omp-conductor → ${release.version}`);
|
|
1461
1728
|
await mustRun(deps, "omp", ["plugin", "install", identity.packageSpec]);
|
|
@@ -1641,10 +1908,11 @@ export async function upgradeConductor(
|
|
|
1641
1908
|
journal({ kind: "outcome", phase: "rolled-back", ok: false, version: release.version, detail: failure });
|
|
1642
1909
|
enqueueUpgradeReport(
|
|
1643
1910
|
"tier2",
|
|
1644
|
-
reportProject(
|
|
1911
|
+
reportProject(installProjects),
|
|
1645
1912
|
`fleet upgrade to omp-conductor@${release.version} failed and was rolled back`,
|
|
1646
1913
|
[
|
|
1647
1914
|
`Reason: ${failure}`,
|
|
1915
|
+
installAttribution,
|
|
1648
1916
|
`Restored: cli=${surfaces.cliVersion}, omp=${surfaces.ompVersion ?? "missing"}, ` +
|
|
1649
1917
|
`herdr=${surfaces.herdrSource ?? "missing"}`,
|
|
1650
1918
|
...(preUpgradeBackup === undefined ? [] : [`Pre-upgrade config snapshot: ${preUpgradeBackup}`]),
|
|
@@ -1657,10 +1925,11 @@ export async function upgradeConductor(
|
|
|
1657
1925
|
journal({ kind: "outcome", phase: "rollback-failed", ok: false, version: release.version, detail: rollback });
|
|
1658
1926
|
enqueueUpgradeReport(
|
|
1659
1927
|
"tier2",
|
|
1660
|
-
reportProject(
|
|
1928
|
+
reportProject(installProjects),
|
|
1661
1929
|
`upgrade to omp-conductor@${release.version} failed and ITS ROLLBACK ALSO FAILED`,
|
|
1662
1930
|
[
|
|
1663
1931
|
`Failure: ${failure}`,
|
|
1932
|
+
installAttribution,
|
|
1664
1933
|
`Rollback failure: ${rollback}`,
|
|
1665
1934
|
"The fleet may be at mixed versions; do not resume dispatch.",
|
|
1666
1935
|
"Restore manually from the journal at " + upgradeJournalPath(stateDir()) + ".",
|
|
@@ -1766,6 +2035,9 @@ export async function rollbackFromJournal(
|
|
|
1766
2035
|
cliVersion: snapshot.previous.cliVersion,
|
|
1767
2036
|
ompVersion: snapshot.previous.ompVersion,
|
|
1768
2037
|
herdrSource: snapshot.previous.herdrSource,
|
|
2038
|
+
...(snapshot.previous.management === undefined
|
|
2039
|
+
? {}
|
|
2040
|
+
: { management: snapshot.previous.management }),
|
|
1769
2041
|
};
|
|
1770
2042
|
let configBefore: string | undefined;
|
|
1771
2043
|
if (request.configBackup !== undefined && existsSync(request.configBackup)) {
|
|
@@ -1813,10 +2085,17 @@ export async function rollbackFromJournal(
|
|
|
1813
2085
|
});
|
|
1814
2086
|
enqueueUpgradeReport(
|
|
1815
2087
|
"tier2",
|
|
1816
|
-
reportProject(request.selectors),
|
|
2088
|
+
reportProject(request.installProjects ?? request.selectors),
|
|
1817
2089
|
`fleet restored to omp-conductor@${restoredVersion} after the failed upgrade to ${request.version}`,
|
|
1818
2090
|
[
|
|
1819
2091
|
"The transient rollback unit restored every surface from the durable snapshot;",
|
|
2092
|
+
`Host-global install holder: ${request.installHolder ?? "unknown"}; affected projects: ${
|
|
2093
|
+
(request.installProjects ?? request.selectors).length === 0
|
|
2094
|
+
? "all configured projects"
|
|
2095
|
+
: (request.installProjects ?? request.selectors)
|
|
2096
|
+
.map((selector) => selector ?? "(default)")
|
|
2097
|
+
.join(", ")
|
|
2098
|
+
}.`,
|
|
1820
2099
|
"dispatch stays paused until the first tick after these restarts verifies the old version.",
|
|
1821
2100
|
`Pre-upgrade config snapshot: ${request.configBackup ?? "none recorded"}`,
|
|
1822
2101
|
],
|
package/src/verbs/actions.ts
CHANGED
|
@@ -1008,7 +1008,12 @@ export function githubVerbActions(
|
|
|
1008
1008
|
return { code: ran.ok ? 0 : 1, stdout: ran.stdout, stderr: ran.stderr };
|
|
1009
1009
|
},
|
|
1010
1010
|
env(),
|
|
1011
|
-
{
|
|
1011
|
+
{
|
|
1012
|
+
kind: "install",
|
|
1013
|
+
version: execution.version,
|
|
1014
|
+
holder: execution.holder,
|
|
1015
|
+
projects: execution.projects,
|
|
1016
|
+
},
|
|
1012
1017
|
);
|
|
1013
1018
|
if (!launched.ok) return { ok: false, stderr: launched.stderr };
|
|
1014
1019
|
return { ok: true, detail: launched.unit };
|