omp-conductor 0.4.1 → 0.4.2
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/package.json +1 -1
- package/src/config.ts +45 -3
- package/src/upgrade.ts +36 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
|
package/src/config.ts
CHANGED
|
@@ -219,6 +219,18 @@ export function loadConfig(): ConductorConfig {
|
|
|
219
219
|
* a truncated one. Mode 0600 because a config carries chat ids and clone URLs.
|
|
220
220
|
*/
|
|
221
221
|
export function saveConfig(c: ConductorConfig): void {
|
|
222
|
+
writeConfigFile(c);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Atomic write of whatever object is handed in.
|
|
227
|
+
*
|
|
228
|
+
* Takes `unknown` rather than `ConductorConfig` so the credentials migration
|
|
229
|
+
* can persist the operator's own parsed file with one key added, instead of a
|
|
230
|
+
* value that has been through the loader and back out. That round-trip is what
|
|
231
|
+
* rewrote a live fleet's config into a dialect the previous release rejected.
|
|
232
|
+
*/
|
|
233
|
+
export function writeConfigFile(c: unknown): void {
|
|
222
234
|
const dir = stateDir();
|
|
223
235
|
const created = mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
224
236
|
// mkdir's mode is masked by umask; chmod only what we just created so an
|
|
@@ -931,9 +943,39 @@ export function migrateCredentialsOnDisk(): { migrated: string[]; path: string }
|
|
|
931
943
|
}
|
|
932
944
|
const migrated = projectsMissingCredentials(parsed);
|
|
933
945
|
if (migrated.length === 0) return { migrated: [], path };
|
|
934
|
-
|
|
935
|
-
//
|
|
936
|
-
|
|
946
|
+
|
|
947
|
+
// A SURGICAL edit: add the one missing key to the projects that lack it and
|
|
948
|
+
// leave every other byte exactly as the operator wrote it.
|
|
949
|
+
//
|
|
950
|
+
// This was `saveConfig(loadConfig())` once, and that round-trip took a live
|
|
951
|
+
// fleet down. Normalising on the way out rewrites the whole file into the
|
|
952
|
+
// current dialect — `defaults.planUsage`, `releasePolicy` as a per-shape map
|
|
953
|
+
// — and the previous release rejects both. The daemon performing the
|
|
954
|
+
// migration is started *by* an upgrade still running the previous CLI, so
|
|
955
|
+
// that CLI's next `loadConfig` failed, and its rollback reinstalled a version
|
|
956
|
+
// which then could not read the file either. A migration must never make a
|
|
957
|
+
// config unreadable by the release you may have to roll back to.
|
|
958
|
+
// Verified against the real previous release, not just against this parser:
|
|
959
|
+
// omp-conductor@0.3.25 loads a config carrying `credentials` and prints
|
|
960
|
+
// `status` normally, because it ignores unknown *project* keys while
|
|
961
|
+
// rejecting unknown `defaults` keys and a non-string `releasePolicy` — the
|
|
962
|
+
// two things the old round-trip wrote. Re-check that with `npm install
|
|
963
|
+
// omp-conductor@<previous>` before widening what this writes.
|
|
964
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { migrated: [], path };
|
|
965
|
+
const projects = (parsed as Raw)["projects"];
|
|
966
|
+
if (!Array.isArray(projects)) return { migrated: [], path };
|
|
967
|
+
for (const entry of projects) {
|
|
968
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
|
969
|
+
const raw = entry as Raw;
|
|
970
|
+
const credentials = raw["credentials"];
|
|
971
|
+
const explicit =
|
|
972
|
+
typeof credentials === "object" &&
|
|
973
|
+
credentials !== null &&
|
|
974
|
+
!Array.isArray(credentials) &&
|
|
975
|
+
(credentials as Raw)["isolation"] !== undefined;
|
|
976
|
+
if (!explicit) (raw as Record<string, unknown>)["credentials"] = { isolation: "none" };
|
|
977
|
+
}
|
|
978
|
+
writeConfigFile(parsed);
|
|
937
979
|
return { migrated, path };
|
|
938
980
|
}
|
|
939
981
|
|
package/src/upgrade.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { inspectBriefLayout, type BriefLayout } from "./brief-upgrade.ts";
|
|
|
3
3
|
import { setPaused, statusSnapshot } from "./daemon.ts";
|
|
4
4
|
import { fleetLayers, telegramStateDir, type DispatchLayer, type FleetLayers } from "./fleet.ts";
|
|
5
5
|
import { livingDaemon, restartDaemon } from "./lifecycle.ts";
|
|
6
|
-
import { findProject, loadConfig, resolveCaps } from "./config.ts";
|
|
6
|
+
import { configPath, findProject, loadConfig, resolveCaps, writeConfigFile } from "./config.ts";
|
|
7
7
|
import { renderBriefForProject } from "./setup.ts";
|
|
8
8
|
import {
|
|
9
9
|
STAGED_SERVICE_NAME,
|
|
@@ -296,8 +296,30 @@ async function rollbackUpgrade(
|
|
|
296
296
|
briefChanged: boolean,
|
|
297
297
|
herdrReloadStarted: boolean,
|
|
298
298
|
daemonReloadStarted: boolean,
|
|
299
|
+
configBefore?: string,
|
|
299
300
|
): Promise<void> {
|
|
300
301
|
const failures: string[] = [];
|
|
302
|
+
|
|
303
|
+
// Restored FIRST, before the packages that have to read it.
|
|
304
|
+
//
|
|
305
|
+
// Rolling back binaries alone is not a rollback. The new daemon starts during
|
|
306
|
+
// the upgrade and may migrate config.json; the reinstated older release then
|
|
307
|
+
// cannot parse its own config and will not boot — which is exactly how an
|
|
308
|
+
// upgrade failure turned into an outage. The snapshot is taken before the
|
|
309
|
+
// first install, and restoring it is unconditional: writing back a file that
|
|
310
|
+
// never changed is harmless, and detecting "did it change" is one more thing
|
|
311
|
+
// to get wrong while the fleet is down.
|
|
312
|
+
if (configBefore !== undefined) {
|
|
313
|
+
try {
|
|
314
|
+
const path = configPath();
|
|
315
|
+
if (readFileSync(path, "utf8") !== configBefore) {
|
|
316
|
+
deps.log("rollback: conductor config.json");
|
|
317
|
+
writeConfigFile(JSON.parse(configBefore));
|
|
318
|
+
}
|
|
319
|
+
} catch (err) {
|
|
320
|
+
failures.push(`could not restore config.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
301
323
|
const restore = async (label: string, command: string, args: readonly string[]): Promise<void> => {
|
|
302
324
|
deps.log(`rollback: ${label}`);
|
|
303
325
|
try {
|
|
@@ -498,6 +520,18 @@ export async function upgradeConductor(
|
|
|
498
520
|
};
|
|
499
521
|
}
|
|
500
522
|
|
|
523
|
+
// Snapshotted before anything is installed, because the new daemon starts
|
|
524
|
+
// during this transaction and may migrate the file — see the restore in
|
|
525
|
+
// `rollbackUpgrade`. Read as bytes, not through the loader: the point is to
|
|
526
|
+
// put back exactly what was there, dialect and all.
|
|
527
|
+
let configBefore: string | undefined;
|
|
528
|
+
try {
|
|
529
|
+
configBefore = readFileSync(configPath(), "utf8");
|
|
530
|
+
} catch {
|
|
531
|
+
// No readable config is not a reason to refuse an upgrade; there is simply
|
|
532
|
+
// nothing to put back.
|
|
533
|
+
}
|
|
534
|
+
|
|
501
535
|
if (brief.kind === "missing") throw new Error("no ORCHESTRATOR.md exists for the configured project");
|
|
502
536
|
if (initial.herdr === "unknown") throw new Error("cannot determine whether herdr-fleet.service is active");
|
|
503
537
|
|
|
@@ -611,6 +645,7 @@ export async function upgradeConductor(
|
|
|
611
645
|
briefChanged,
|
|
612
646
|
herdrReloadStarted,
|
|
613
647
|
daemonReloadStarted,
|
|
648
|
+
configBefore,
|
|
614
649
|
);
|
|
615
650
|
} catch (rollbackErr) {
|
|
616
651
|
const rollback = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
|