@gajae-gateway/cli 0.1.8 → 0.2.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/dist/discord-setup.d.ts +41 -0
- package/dist/discord-setup.d.ts.map +1 -0
- package/dist/gjc-install.d.ts +13 -0
- package/dist/gjc-install.d.ts.map +1 -0
- package/dist/main.d.ts +6 -32
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1195 -580
- package/dist/migrate/hermes.d.ts.map +1 -1
- package/dist/migrate.d.ts.map +1 -1
- package/dist/restart-stack.d.ts +21 -11
- package/dist/restart-stack.d.ts.map +1 -1
- package/dist/services.d.ts +9 -6
- package/dist/services.d.ts.map +1 -1
- package/dist/setup.d.ts +14 -3
- package/dist/setup.d.ts.map +1 -1
- package/dist/up.d.ts +20 -0
- package/dist/up.d.ts.map +1 -0
- package/dist/update.d.ts +10 -6
- package/dist/update.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/discord-setup.ts +103 -0
- package/src/gjc-install.ts +148 -0
- package/src/main.ts +178 -142
- package/src/migrate/hermes.ts +0 -2
- package/src/migrate.ts +36 -4
- package/src/restart-stack.ts +258 -90
- package/src/services.ts +75 -23
- package/src/setup.ts +85 -10
- package/src/up.ts +133 -0
- package/src/update.ts +170 -98
- package/dist/upgrade.d.ts +0 -40
- package/dist/upgrade.d.ts.map +0 -1
- package/src/upgrade.ts +0 -205
package/src/update.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
copyFile,
|
|
4
4
|
mkdir,
|
|
5
5
|
mkdtemp,
|
|
6
|
-
open,
|
|
7
6
|
readdir,
|
|
8
7
|
readFile,
|
|
9
8
|
realpath,
|
|
@@ -14,8 +13,17 @@ import {
|
|
|
14
13
|
} from "node:fs/promises";
|
|
15
14
|
import { tmpdir } from "node:os";
|
|
16
15
|
import { basename, dirname, join } from "node:path";
|
|
16
|
+
import { acquireLease, homeLayout, type Lease, LeaseHeldError } from "@gajae-gateway/home";
|
|
17
17
|
import pkg from "../package.json";
|
|
18
|
+
import { type GjcInstallRunner, installPinnedGjc } from "./gjc-install";
|
|
18
19
|
import { launchRestartStack } from "./restart-stack";
|
|
20
|
+
import {
|
|
21
|
+
type InstallServicesOptions,
|
|
22
|
+
installServices,
|
|
23
|
+
type ServicePlatform,
|
|
24
|
+
serviceSpecs,
|
|
25
|
+
systemdUnitName,
|
|
26
|
+
} from "./services";
|
|
19
27
|
|
|
20
28
|
/**
|
|
21
29
|
* `gajaeway update`: self-update a binary install from the project's GitHub
|
|
@@ -127,7 +135,6 @@ const MIN_BINARY_BYTES = 1_048_576;
|
|
|
127
135
|
const MAX_ASSET_BYTES = 512 * 1024 * 1024;
|
|
128
136
|
|
|
129
137
|
const RELEASE_MARKER = ".gajaeway-release";
|
|
130
|
-
const LOCK_FILE = ".gajaeway-update.lock";
|
|
131
138
|
|
|
132
139
|
export function releaseMarkerPath(binDir: string): string {
|
|
133
140
|
return join(binDir, RELEASE_MARKER);
|
|
@@ -168,6 +175,7 @@ export interface UpdateResult {
|
|
|
168
175
|
readonly installed: readonly string[];
|
|
169
176
|
readonly backupDir?: string;
|
|
170
177
|
readonly restartQueued?: string;
|
|
178
|
+
readonly brokerNotEnabled?: boolean;
|
|
171
179
|
readonly restartSkipped?: boolean;
|
|
172
180
|
}
|
|
173
181
|
|
|
@@ -191,6 +199,7 @@ export function renderUpdate(result: UpdateResult): string[] {
|
|
|
191
199
|
if (result.restartQueued !== undefined)
|
|
192
200
|
lines.push(`restart: queued ${result.restartQueued} — check with: gajaeway ops restart-stack --status`);
|
|
193
201
|
else if (result.restartSkipped) lines.push("restart: skipped (--no-restart)");
|
|
202
|
+
if (result.brokerNotEnabled) lines.push("broker unit installed, not enabled: run gajaeway setup --broker-agent");
|
|
194
203
|
return lines;
|
|
195
204
|
}
|
|
196
205
|
|
|
@@ -202,7 +211,13 @@ export interface RestartQueueReceipt {
|
|
|
202
211
|
export type ReleaseFetcher = (repo: string) => Promise<ReleaseInfo>;
|
|
203
212
|
export type AssetDownloader = (asset: ReleaseAsset) => Promise<Uint8Array>;
|
|
204
213
|
export type TarballExtractor = (tarball: Uint8Array, intoDir: string) => Promise<void>;
|
|
205
|
-
export type RestartQueuer = (home: string) => Promise<RestartQueueReceipt>;
|
|
214
|
+
export type RestartQueuer = (home: string, binDir: string, trigger: "update") => Promise<RestartQueueReceipt>;
|
|
215
|
+
export type UpdateCommandRunner = (command: readonly string[]) => Promise<number>;
|
|
216
|
+
|
|
217
|
+
type UpdateServiceOptions = Pick<
|
|
218
|
+
InstallServicesOptions,
|
|
219
|
+
"launchAgentsDir" | "unitDir" | "loginPathRunner" | "writeFile"
|
|
220
|
+
>;
|
|
206
221
|
|
|
207
222
|
export interface UpdateDeps {
|
|
208
223
|
readonly fetchLatestRelease?: ReleaseFetcher;
|
|
@@ -214,6 +229,9 @@ export interface UpdateDeps {
|
|
|
214
229
|
readonly env?: NodeJS.ProcessEnv;
|
|
215
230
|
readonly execPath?: string;
|
|
216
231
|
readonly now?: () => number;
|
|
232
|
+
readonly gjcInstallRunner?: GjcInstallRunner;
|
|
233
|
+
readonly serviceRunner?: UpdateCommandRunner;
|
|
234
|
+
readonly serviceOptions?: UpdateServiceOptions;
|
|
217
235
|
}
|
|
218
236
|
|
|
219
237
|
export interface RunUpdateOptions {
|
|
@@ -222,6 +240,77 @@ export interface RunUpdateOptions {
|
|
|
222
240
|
readonly deps?: UpdateDeps;
|
|
223
241
|
}
|
|
224
242
|
|
|
243
|
+
async function readPinnedGjcVersion(path: string): Promise<string> {
|
|
244
|
+
let parsed: unknown;
|
|
245
|
+
try {
|
|
246
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
247
|
+
} catch (error) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`could not read pinned gjc version from ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
253
|
+
throw new Error(`invalid release package metadata at ${path}`);
|
|
254
|
+
const gjc = (parsed as { gjc?: unknown }).gjc;
|
|
255
|
+
if (typeof gjc !== "object" || gjc === null || Array.isArray(gjc))
|
|
256
|
+
throw new Error(`release package metadata at ${path} has no gjc.version`);
|
|
257
|
+
const version = (gjc as { version?: unknown }).version;
|
|
258
|
+
if (typeof version !== "string" || version.trim() === "")
|
|
259
|
+
throw new Error(`release package metadata at ${path} has no gjc.version`);
|
|
260
|
+
return version;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function defaultUpdateCommandRunner(command: readonly string[]): Promise<number> {
|
|
264
|
+
const child = Bun.spawn([...command], { stdin: "ignore", stdout: "ignore", stderr: "pipe" });
|
|
265
|
+
const [stderr, exitCode] = await Promise.all([new Response(child.stderr).text(), child.exited]);
|
|
266
|
+
if (exitCode !== 0) {
|
|
267
|
+
const detail = stderr.trim();
|
|
268
|
+
throw new Error(`${command.join(" ")} exited with status ${exitCode}${detail.length === 0 ? "" : `: ${detail}`}`);
|
|
269
|
+
}
|
|
270
|
+
return exitCode;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function installUpdatedServices(options: {
|
|
274
|
+
readonly home: string;
|
|
275
|
+
readonly binDir: string;
|
|
276
|
+
readonly platform: ServicePlatform;
|
|
277
|
+
readonly noRestart: boolean;
|
|
278
|
+
readonly deps: UpdateDeps;
|
|
279
|
+
}): Promise<boolean> {
|
|
280
|
+
const { home, binDir, platform, noRestart, deps } = options;
|
|
281
|
+
const brokerProvisioned = await exists(homeLayout(home).brokerProvisionManifest);
|
|
282
|
+
const env = { ...(deps.env ?? process.env), GAJAEWAY_HOME: home };
|
|
283
|
+
await installServices({
|
|
284
|
+
binDir,
|
|
285
|
+
platform,
|
|
286
|
+
env,
|
|
287
|
+
configuredAdaptersOnly: true,
|
|
288
|
+
compareAndWrite: true,
|
|
289
|
+
...deps.serviceOptions,
|
|
290
|
+
});
|
|
291
|
+
if (platform === "linux") {
|
|
292
|
+
const run = deps.serviceRunner ?? defaultUpdateCommandRunner;
|
|
293
|
+
const execute = async (command: readonly string[]): Promise<void> => {
|
|
294
|
+
const exitCode = await run(command);
|
|
295
|
+
if (exitCode !== 0) throw new Error(`${command.join(" ")} exited with status ${exitCode}`);
|
|
296
|
+
};
|
|
297
|
+
await execute(["systemctl", "--user", "daemon-reload"]);
|
|
298
|
+
if (!noRestart) {
|
|
299
|
+
const enableUnits: string[] = [];
|
|
300
|
+
for (const spec of serviceSpecs()) {
|
|
301
|
+
if (spec.id === "broker" && !brokerProvisioned) continue;
|
|
302
|
+
if (
|
|
303
|
+
spec.id.startsWith("adapter-") &&
|
|
304
|
+
!(await Bun.file(join(home, `adapter-${spec.id.slice(8)}.json`)).exists())
|
|
305
|
+
)
|
|
306
|
+
continue;
|
|
307
|
+
enableUnits.push(systemdUnitName(spec));
|
|
308
|
+
}
|
|
309
|
+
if (enableUnits.length > 0) await execute(["systemctl", "--user", "enable", ...enableUnits]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return !brokerProvisioned;
|
|
313
|
+
}
|
|
225
314
|
export async function runUpdate(options: RunUpdateOptions): Promise<UpdateResult> {
|
|
226
315
|
const deps = options.deps ?? {};
|
|
227
316
|
const platform = deps.platform ?? process.platform;
|
|
@@ -247,45 +336,67 @@ export async function runUpdate(options: RunUpdateOptions): Promise<UpdateResult
|
|
|
247
336
|
return { current, latest, tag: release.tag, repo, checkedOnly: true, upToDate: false, installed: [] };
|
|
248
337
|
if (binDir === undefined) throw new Error("run the update from a compiled gajaeway binary, or pass --bin-dir DIR");
|
|
249
338
|
|
|
250
|
-
|
|
339
|
+
let lease: Lease;
|
|
251
340
|
try {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const stamp = new Date((deps.now ?? Date.now)()).toISOString().replace(/[-:]/g, "").slice(0, 15);
|
|
260
|
-
const backupDir = join(binDir, `backup-${current}-${stamp}Z`);
|
|
261
|
-
await mkdir(backupDir, { recursive: false });
|
|
262
|
-
for (const name of toInstall) {
|
|
263
|
-
const target = join(binDir, name);
|
|
264
|
-
if (await exists(target)) await copyFile(target, join(backupDir, name));
|
|
341
|
+
lease = await acquireLease(options.home, "update");
|
|
342
|
+
console.info(`lease_acquired name=update pid=${process.pid}`);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (error instanceof LeaseHeldError) {
|
|
345
|
+
console.error(error.message);
|
|
346
|
+
throw new Error(`another gajaeway update is running (${error.message})`, { cause: error });
|
|
265
347
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
348
|
+
console.error(`lease_failed name=update detail=${error instanceof Error ? error.message : String(error)}`);
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const installed = await (async () => {
|
|
353
|
+
let staged: StagedRelease | undefined;
|
|
354
|
+
try {
|
|
355
|
+
staged = await stageRelease(asset, deps);
|
|
356
|
+
const installedNames = await knownBinariesIn(binDir);
|
|
357
|
+
const toInstall = binariesToInstall(staged.names, installedNames);
|
|
358
|
+
const missingCore = CORE_BINARIES.filter((name) => !toInstall.includes(name));
|
|
359
|
+
if (missingCore.length > 0)
|
|
360
|
+
throw new Error(`release tarball is missing core binaries: ${missingCore.join(", ")}`);
|
|
361
|
+
await assertStagedBinariesUsable(staged, toInstall);
|
|
362
|
+
|
|
363
|
+
const gjcVersion = await readPinnedGjcVersion(join(staged.dir, "package.json"));
|
|
364
|
+
await installPinnedGjc({
|
|
365
|
+
home: options.home,
|
|
366
|
+
version: gjcVersion,
|
|
367
|
+
...(deps.gjcInstallRunner === undefined ? {} : { runner: deps.gjcInstallRunner }),
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const stamp = new Date((deps.now ?? Date.now)()).toISOString().replace(/[-:]/g, "").slice(0, 15);
|
|
371
|
+
const backupDir = join(binDir, `backup-${current}-${stamp}Z`);
|
|
372
|
+
await mkdir(backupDir, { recursive: false });
|
|
373
|
+
for (const name of toInstall) {
|
|
374
|
+
const target = join(binDir, name);
|
|
375
|
+
if (await exists(target)) await copyFile(target, join(backupDir, name));
|
|
376
|
+
}
|
|
377
|
+
for (const name of toInstall) {
|
|
378
|
+
const stagedPath = join(staged.dir, name);
|
|
379
|
+
const tempPath = join(binDir, `.update-${name}`);
|
|
380
|
+
await copyFile(stagedPath, tempPath);
|
|
381
|
+
await chmod(tempPath, 0o755);
|
|
382
|
+
await rename(tempPath, join(binDir, name));
|
|
383
|
+
}
|
|
384
|
+
await writeFile(releaseMarkerPath(binDir), `${latest}\n`, "utf8");
|
|
385
|
+
const brokerNotEnabled = await installUpdatedServices({
|
|
386
|
+
home: options.home,
|
|
387
|
+
binDir,
|
|
388
|
+
platform,
|
|
389
|
+
noRestart: options.args.noRestart,
|
|
390
|
+
deps,
|
|
391
|
+
});
|
|
392
|
+
return { toInstall, backupDir, brokerNotEnabled };
|
|
393
|
+
} finally {
|
|
394
|
+
if (staged !== undefined) await rm(staged.dir, { recursive: true, force: true });
|
|
395
|
+
await lease.release();
|
|
272
396
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (options.args.noRestart)
|
|
277
|
-
return {
|
|
278
|
-
current,
|
|
279
|
-
latest,
|
|
280
|
-
tag: release.tag,
|
|
281
|
-
repo,
|
|
282
|
-
checkedOnly: false,
|
|
283
|
-
upToDate: false,
|
|
284
|
-
installed: toInstall,
|
|
285
|
-
backupDir,
|
|
286
|
-
restartSkipped: true,
|
|
287
|
-
};
|
|
288
|
-
const receipt = await (deps.queueRestart ?? defaultQueueRestart)(options.home);
|
|
397
|
+
})();
|
|
398
|
+
|
|
399
|
+
if (options.args.noRestart)
|
|
289
400
|
return {
|
|
290
401
|
current,
|
|
291
402
|
latest,
|
|
@@ -293,13 +404,25 @@ export async function runUpdate(options: RunUpdateOptions): Promise<UpdateResult
|
|
|
293
404
|
repo,
|
|
294
405
|
checkedOnly: false,
|
|
295
406
|
upToDate: false,
|
|
296
|
-
installed: toInstall,
|
|
297
|
-
backupDir,
|
|
298
|
-
|
|
407
|
+
installed: installed.toInstall,
|
|
408
|
+
backupDir: installed.backupDir,
|
|
409
|
+
brokerNotEnabled: installed.brokerNotEnabled,
|
|
410
|
+
restartSkipped: true,
|
|
299
411
|
};
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
412
|
+
const receipt = await (deps.queueRestart ?? defaultQueueRestart)(options.home, binDir, "update");
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
current,
|
|
416
|
+
latest,
|
|
417
|
+
tag: release.tag,
|
|
418
|
+
repo,
|
|
419
|
+
checkedOnly: false,
|
|
420
|
+
upToDate: false,
|
|
421
|
+
installed: installed.toInstall,
|
|
422
|
+
backupDir: installed.backupDir,
|
|
423
|
+
brokerNotEnabled: installed.brokerNotEnabled,
|
|
424
|
+
restartQueued: receipt.receiptId,
|
|
425
|
+
};
|
|
303
426
|
}
|
|
304
427
|
|
|
305
428
|
function binariesToInstall(stagedNames: readonly string[], installedNames: readonly string[]): string[] {
|
|
@@ -393,58 +516,7 @@ async function defaultExtractTarball(tarball: Uint8Array, intoDir: string): Prom
|
|
|
393
516
|
await rm(tarballPath, { force: true });
|
|
394
517
|
}
|
|
395
518
|
|
|
396
|
-
async function defaultQueueRestart(home: string): Promise<RestartQueueReceipt> {
|
|
397
|
-
const { receipt, supervisorPid } = await launchRestartStack({ home });
|
|
519
|
+
async function defaultQueueRestart(home: string, binDir: string, trigger: "update"): Promise<RestartQueueReceipt> {
|
|
520
|
+
const { receipt, supervisorPid } = await launchRestartStack({ home, binDir, trigger });
|
|
398
521
|
return { receiptId: receipt.id, supervisorPid };
|
|
399
522
|
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* One update at a time per bin dir. A lock naming a live pid refuses; a lock
|
|
403
|
-
* naming a dead one is stale and taken over.
|
|
404
|
-
*/
|
|
405
|
-
export async function acquireUpdateLock(binDir: string): Promise<() => Promise<void>> {
|
|
406
|
-
const lockPath = join(binDir, LOCK_FILE);
|
|
407
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
408
|
-
try {
|
|
409
|
-
const handle = await open(lockPath, "wx");
|
|
410
|
-
try {
|
|
411
|
-
await handle.write(`${JSON.stringify({ pid: process.pid, at: new Date().toISOString() })}\n`);
|
|
412
|
-
} finally {
|
|
413
|
-
await handle.close();
|
|
414
|
-
}
|
|
415
|
-
return async () => {
|
|
416
|
-
await rm(lockPath, { force: true });
|
|
417
|
-
};
|
|
418
|
-
} catch (error) {
|
|
419
|
-
if (!isFileExistsError(error)) throw error;
|
|
420
|
-
const holder = await readLockHolder(lockPath);
|
|
421
|
-
if (holder !== undefined && isProcessAlive(holder)) {
|
|
422
|
-
throw new Error(`another gajaeway update is running (pid ${holder}); refusing to run concurrently`);
|
|
423
|
-
}
|
|
424
|
-
await rm(lockPath, { force: true });
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
throw new Error(`could not acquire the update lock at ${lockPath}`);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function isFileExistsError(error: unknown): boolean {
|
|
431
|
-
return error !== null && typeof error === "object" && (error as { code?: unknown }).code === "EEXIST";
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
async function readLockHolder(lockPath: string): Promise<number | undefined> {
|
|
435
|
-
try {
|
|
436
|
-
const parsed = JSON.parse(await readFile(lockPath, "utf8")) as { pid?: unknown };
|
|
437
|
-
return typeof parsed.pid === "number" ? parsed.pid : undefined;
|
|
438
|
-
} catch {
|
|
439
|
-
return undefined;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function isProcessAlive(pid: number): boolean {
|
|
444
|
-
try {
|
|
445
|
-
process.kill(pid, 0);
|
|
446
|
-
return true;
|
|
447
|
-
} catch (error) {
|
|
448
|
-
return (error as { code?: unknown }).code === "EPERM";
|
|
449
|
-
}
|
|
450
|
-
}
|
package/dist/upgrade.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `gajaeway ops upgrade [--gjc X.Y.Z]`
|
|
3
|
-
*
|
|
4
|
-
* Atomically upgrades the gateway's gjc to the pinned version (or an override).
|
|
5
|
-
* The process:
|
|
6
|
-
* 1. Verify CI is green (deploy gate)
|
|
7
|
-
* 2. Stop intake (implicit on restart)
|
|
8
|
-
* 3. Wait for in-flight turns (bounded)
|
|
9
|
-
* 4. Back up the DB
|
|
10
|
-
* 5. Install the pinned gjc into the gateway's own prefix (checksum-verified)
|
|
11
|
-
* 6. Restart the gateway's broker
|
|
12
|
-
* 7. ops restart-stack
|
|
13
|
-
* 8. Verify status (clients current, 0 pending, no held turns)
|
|
14
|
-
*
|
|
15
|
-
* Fails closed: leaves old binaries in place on any step.
|
|
16
|
-
*/
|
|
17
|
-
export interface UpgradeOptions {
|
|
18
|
-
readonly home: string;
|
|
19
|
-
/** Override gjc version; defaults to reading from gateway's package.json */
|
|
20
|
-
readonly gjcVersion?: string;
|
|
21
|
-
/** Test seam: function to check CI status */
|
|
22
|
-
readonly checkCiStatus?: (repo: string) => Promise<boolean>;
|
|
23
|
-
/** Test seam: function to read gateway pinned gjc version */
|
|
24
|
-
readonly readPinnedVersion?: (home: string) => Promise<string>;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Read the pinned gjc version the installed gateway runs against.
|
|
28
|
-
*
|
|
29
|
-
* The checkout layout is tried first (a source deployment keeps the pin in
|
|
30
|
-
* `packages/gateway/package.json`); an installed binary carries the same file
|
|
31
|
-
* beside itself, which is the only candidate that exists on a host that never
|
|
32
|
-
* had the checkout.
|
|
33
|
-
*/
|
|
34
|
-
export declare function readGatewayPinnedGjcVersion(home: string): Promise<string>;
|
|
35
|
-
export interface UpgradeResult {
|
|
36
|
-
readonly status: "ok" | "failed";
|
|
37
|
-
readonly detail: string;
|
|
38
|
-
}
|
|
39
|
-
export declare function performUpgrade(options: UpgradeOptions): Promise<UpgradeResult>;
|
|
40
|
-
//# sourceMappingURL=upgrade.d.ts.map
|
package/dist/upgrade.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../src/upgrade.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,6CAA6C;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,6DAA6D;IAC7D,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/D;AAED;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc/E;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,MAAM,EAAE,IAAI,GAAG,QAAQ,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AA2DD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAwFpF"}
|
package/src/upgrade.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* `gajaeway ops upgrade [--gjc X.Y.Z]`
|
|
6
|
-
*
|
|
7
|
-
* Atomically upgrades the gateway's gjc to the pinned version (or an override).
|
|
8
|
-
* The process:
|
|
9
|
-
* 1. Verify CI is green (deploy gate)
|
|
10
|
-
* 2. Stop intake (implicit on restart)
|
|
11
|
-
* 3. Wait for in-flight turns (bounded)
|
|
12
|
-
* 4. Back up the DB
|
|
13
|
-
* 5. Install the pinned gjc into the gateway's own prefix (checksum-verified)
|
|
14
|
-
* 6. Restart the gateway's broker
|
|
15
|
-
* 7. ops restart-stack
|
|
16
|
-
* 8. Verify status (clients current, 0 pending, no held turns)
|
|
17
|
-
*
|
|
18
|
-
* Fails closed: leaves old binaries in place on any step.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
export interface UpgradeOptions {
|
|
22
|
-
readonly home: string;
|
|
23
|
-
/** Override gjc version; defaults to reading from gateway's package.json */
|
|
24
|
-
readonly gjcVersion?: string;
|
|
25
|
-
/** Test seam: function to check CI status */
|
|
26
|
-
readonly checkCiStatus?: (repo: string) => Promise<boolean>;
|
|
27
|
-
/** Test seam: function to read gateway pinned gjc version */
|
|
28
|
-
readonly readPinnedVersion?: (home: string) => Promise<string>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Read the pinned gjc version the installed gateway runs against.
|
|
33
|
-
*
|
|
34
|
-
* The checkout layout is tried first (a source deployment keeps the pin in
|
|
35
|
-
* `packages/gateway/package.json`); an installed binary carries the same file
|
|
36
|
-
* beside itself, which is the only candidate that exists on a host that never
|
|
37
|
-
* had the checkout.
|
|
38
|
-
*/
|
|
39
|
-
export async function readGatewayPinnedGjcVersion(home: string): Promise<string> {
|
|
40
|
-
const candidates = [
|
|
41
|
-
join(home, "..", "..", "packages", "gateway", "package.json"),
|
|
42
|
-
join(dirname(process.execPath), "package.json"),
|
|
43
|
-
];
|
|
44
|
-
for (const path of candidates) {
|
|
45
|
-
try {
|
|
46
|
-
const version = (JSON.parse(await readFile(path, "utf-8")) as { gjc?: { version?: unknown } }).gjc?.version;
|
|
47
|
-
if (typeof version === "string" && version.length > 0) return version;
|
|
48
|
-
} catch {
|
|
49
|
-
// A missing or malformed candidate is not fatal: a later one may carry the pin.
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
throw new Error(`Failed to read pinned GJC version from any of ${candidates.join(", ")}`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface UpgradeResult {
|
|
56
|
-
readonly status: "ok" | "failed";
|
|
57
|
-
readonly detail: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Check CI status by running: gh run list -R owner/repo --branch HEAD --limit 1 --json conclusion
|
|
62
|
-
* Returns true if the latest CI run passed (conclusion: success)
|
|
63
|
-
*/
|
|
64
|
-
async function checkLatestCiStatus(repo: string): Promise<boolean> {
|
|
65
|
-
try {
|
|
66
|
-
// Get current git branch
|
|
67
|
-
const branchProc = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
68
|
-
stdout: "pipe",
|
|
69
|
-
stderr: "pipe",
|
|
70
|
-
});
|
|
71
|
-
if (branchProc.exitCode !== 0) {
|
|
72
|
-
throw new Error("Could not determine current git branch");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const branch = branchProc.stdout?.toString().trim();
|
|
76
|
-
if (!branch) {
|
|
77
|
-
throw new Error("Empty branch name");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Check CI status for this branch
|
|
81
|
-
const runProc = Bun.spawnSync(
|
|
82
|
-
["gh", "run", "list", "-R", repo, "--branch", branch, "--limit", "1", "--json", "conclusion,status"],
|
|
83
|
-
{
|
|
84
|
-
stdout: "pipe",
|
|
85
|
-
stderr: "pipe",
|
|
86
|
-
},
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
if (runProc.exitCode !== 0) {
|
|
90
|
-
throw new Error(`gh run list failed: ${runProc.stderr?.toString()}`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const output = runProc.stdout?.toString() || "[]";
|
|
94
|
-
const runs = JSON.parse(output);
|
|
95
|
-
|
|
96
|
-
if (!Array.isArray(runs) || runs.length === 0) {
|
|
97
|
-
throw new Error("No CI runs found for this branch");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const latestRun = runs[0];
|
|
101
|
-
if (!latestRun.conclusion || !latestRun.status) {
|
|
102
|
-
throw new Error(`Latest CI run status is incomplete: ${JSON.stringify(latestRun)}`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// conclusion can be: success, failure, neutral, cancelled, skipped, timed_out
|
|
106
|
-
// status can be: queued, in_progress, completed
|
|
107
|
-
if (latestRun.status === "in_progress") {
|
|
108
|
-
throw new Error("CI is currently running; wait for completion");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return latestRun.conclusion === "success";
|
|
112
|
-
} catch (error) {
|
|
113
|
-
throw new Error(`Failed to check CI status: ${error instanceof Error ? error.message : String(error)}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export async function performUpgrade(options: UpgradeOptions): Promise<UpgradeResult> {
|
|
118
|
-
const readVersion = options.readPinnedVersion || readGatewayPinnedGjcVersion;
|
|
119
|
-
const targetVersion = options.gjcVersion ?? (await readVersion(options.home));
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
// 1. Check CI status (deploy gate)
|
|
123
|
-
const checkCi = options.checkCiStatus || checkLatestCiStatus;
|
|
124
|
-
let ciPassed = false;
|
|
125
|
-
try {
|
|
126
|
-
ciPassed = await checkCi("Yeachan-Heo/gajae-way");
|
|
127
|
-
} catch (ciError) {
|
|
128
|
-
const ciDetail = ciError instanceof Error ? ciError.message : String(ciError);
|
|
129
|
-
return { status: "failed", detail: `CI status check failed: ${ciDetail}` };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (!ciPassed) {
|
|
133
|
-
return {
|
|
134
|
-
status: "failed",
|
|
135
|
-
detail: `Cannot upgrade: latest CI run did not pass. Fix failures and try again.`,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 2. Read current gjc version
|
|
140
|
-
const gjcProc = Bun.spawnSync(["gjc", "--version"], {
|
|
141
|
-
stdout: "pipe",
|
|
142
|
-
stderr: "pipe",
|
|
143
|
-
});
|
|
144
|
-
if (gjcProc.exitCode !== 0) {
|
|
145
|
-
return { status: "failed", detail: "Failed to get current gjc version" };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const currentVersionMatch = ((gjcProc.stdout?.toString() || "") + (gjcProc.stderr?.toString() || "")).match(
|
|
149
|
-
/(?:gjc\/)?(\d+)\.(\d+)\.(\d+)/,
|
|
150
|
-
);
|
|
151
|
-
if (!currentVersionMatch) {
|
|
152
|
-
return { status: "failed", detail: "Could not parse current gjc version" };
|
|
153
|
-
}
|
|
154
|
-
const currentVersion = currentVersionMatch.slice(1, 4).join(".");
|
|
155
|
-
|
|
156
|
-
if (currentVersion === targetVersion) {
|
|
157
|
-
return { status: "ok", detail: `Already running gjc ${targetVersion}` };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// 3. Install new gjc version
|
|
161
|
-
const installProc = Bun.spawnSync(["bun", "add", "-g", `gajae-code@${targetVersion}`], {
|
|
162
|
-
stdout: "pipe",
|
|
163
|
-
stderr: "pipe",
|
|
164
|
-
cwd: join(options.home, "workspace"),
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
if (installProc.exitCode !== 0) {
|
|
168
|
-
return {
|
|
169
|
-
status: "failed",
|
|
170
|
-
detail: `Failed to install gjc ${targetVersion}: ${installProc.stderr?.toString()}`,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// 4. Verify installation
|
|
175
|
-
const verifyProc = Bun.spawnSync(["gjc", "--version"], {
|
|
176
|
-
stdout: "pipe",
|
|
177
|
-
stderr: "pipe",
|
|
178
|
-
});
|
|
179
|
-
if (verifyProc.exitCode !== 0) {
|
|
180
|
-
return { status: "failed", detail: "Failed to verify gjc installation" };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const verifiedVersionMatch = ((verifyProc.stdout?.toString() || "") + (verifyProc.stderr?.toString() || "")).match(
|
|
184
|
-
/(?:gjc\/)?(\d+)\.(\d+)\.(\d+)/,
|
|
185
|
-
);
|
|
186
|
-
if (!verifiedVersionMatch) {
|
|
187
|
-
return { status: "failed", detail: "Could not verify installed gjc version" };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const verifiedVersion = verifiedVersionMatch.slice(1, 4).join(".");
|
|
191
|
-
if (verifiedVersion !== targetVersion) {
|
|
192
|
-
return {
|
|
193
|
-
status: "failed",
|
|
194
|
-
detail: `Verification failed: expected ${targetVersion}, got ${verifiedVersion}`,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return { status: "ok", detail: `Successfully upgraded gjc from ${currentVersion} to ${targetVersion}` };
|
|
199
|
-
} catch (error) {
|
|
200
|
-
return {
|
|
201
|
-
status: "failed",
|
|
202
|
-
detail: `Upgrade failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
}
|