@gajae-gateway/cli 0.1.8 → 0.2.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/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 +17 -30
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1318 -582
- package/dist/migrate/hermes.d.ts.map +1 -1
- package/dist/migrate.d.ts.map +1 -1
- package/dist/restart-stack.d.ts +27 -13
- 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 +296 -151
- package/src/migrate/hermes.ts +0 -2
- package/src/migrate.ts +36 -4
- package/src/restart-stack.ts +309 -92
- package/src/services.ts +75 -23
- package/src/setup.ts +85 -10
- package/src/up.ts +133 -0
- package/src/update.ts +175 -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
|
|
@@ -31,6 +39,8 @@ export const UPDATE_USAGE = "usage: gajaeway update [--check] [--force] [--bin-d
|
|
|
31
39
|
|
|
32
40
|
export const DEFAULT_RELEASE_REPO = "Yeachan-Heo/gajae-way";
|
|
33
41
|
const RELEASE_REPO_OVERRIDE_ENV = "GAJAEWAY_UPDATE_REPO";
|
|
42
|
+
/** The release manifest carrying the gjc pin; installed beside the binaries. */
|
|
43
|
+
const PIN_MANIFEST = "package.json";
|
|
34
44
|
/** A fork can point updates at its own releases; the declared upstream stays the default. */
|
|
35
45
|
export function releaseRepo(env: NodeJS.ProcessEnv = process.env): string {
|
|
36
46
|
const override = env[RELEASE_REPO_OVERRIDE_ENV]?.trim();
|
|
@@ -127,7 +137,6 @@ const MIN_BINARY_BYTES = 1_048_576;
|
|
|
127
137
|
const MAX_ASSET_BYTES = 512 * 1024 * 1024;
|
|
128
138
|
|
|
129
139
|
const RELEASE_MARKER = ".gajaeway-release";
|
|
130
|
-
const LOCK_FILE = ".gajaeway-update.lock";
|
|
131
140
|
|
|
132
141
|
export function releaseMarkerPath(binDir: string): string {
|
|
133
142
|
return join(binDir, RELEASE_MARKER);
|
|
@@ -168,6 +177,7 @@ export interface UpdateResult {
|
|
|
168
177
|
readonly installed: readonly string[];
|
|
169
178
|
readonly backupDir?: string;
|
|
170
179
|
readonly restartQueued?: string;
|
|
180
|
+
readonly brokerNotEnabled?: boolean;
|
|
171
181
|
readonly restartSkipped?: boolean;
|
|
172
182
|
}
|
|
173
183
|
|
|
@@ -191,6 +201,7 @@ export function renderUpdate(result: UpdateResult): string[] {
|
|
|
191
201
|
if (result.restartQueued !== undefined)
|
|
192
202
|
lines.push(`restart: queued ${result.restartQueued} — check with: gajaeway ops restart-stack --status`);
|
|
193
203
|
else if (result.restartSkipped) lines.push("restart: skipped (--no-restart)");
|
|
204
|
+
if (result.brokerNotEnabled) lines.push("broker unit installed, not enabled: run gajaeway setup --broker-agent");
|
|
194
205
|
return lines;
|
|
195
206
|
}
|
|
196
207
|
|
|
@@ -202,7 +213,13 @@ export interface RestartQueueReceipt {
|
|
|
202
213
|
export type ReleaseFetcher = (repo: string) => Promise<ReleaseInfo>;
|
|
203
214
|
export type AssetDownloader = (asset: ReleaseAsset) => Promise<Uint8Array>;
|
|
204
215
|
export type TarballExtractor = (tarball: Uint8Array, intoDir: string) => Promise<void>;
|
|
205
|
-
export type RestartQueuer = (home: string) => Promise<RestartQueueReceipt>;
|
|
216
|
+
export type RestartQueuer = (home: string, binDir: string, trigger: "update") => Promise<RestartQueueReceipt>;
|
|
217
|
+
export type UpdateCommandRunner = (command: readonly string[]) => Promise<number>;
|
|
218
|
+
|
|
219
|
+
type UpdateServiceOptions = Pick<
|
|
220
|
+
InstallServicesOptions,
|
|
221
|
+
"launchAgentsDir" | "unitDir" | "loginPathRunner" | "writeFile"
|
|
222
|
+
>;
|
|
206
223
|
|
|
207
224
|
export interface UpdateDeps {
|
|
208
225
|
readonly fetchLatestRelease?: ReleaseFetcher;
|
|
@@ -214,6 +231,9 @@ export interface UpdateDeps {
|
|
|
214
231
|
readonly env?: NodeJS.ProcessEnv;
|
|
215
232
|
readonly execPath?: string;
|
|
216
233
|
readonly now?: () => number;
|
|
234
|
+
readonly gjcInstallRunner?: GjcInstallRunner;
|
|
235
|
+
readonly serviceRunner?: UpdateCommandRunner;
|
|
236
|
+
readonly serviceOptions?: UpdateServiceOptions;
|
|
217
237
|
}
|
|
218
238
|
|
|
219
239
|
export interface RunUpdateOptions {
|
|
@@ -222,6 +242,77 @@ export interface RunUpdateOptions {
|
|
|
222
242
|
readonly deps?: UpdateDeps;
|
|
223
243
|
}
|
|
224
244
|
|
|
245
|
+
async function readPinnedGjcVersion(path: string): Promise<string> {
|
|
246
|
+
let parsed: unknown;
|
|
247
|
+
try {
|
|
248
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`could not read pinned gjc version from ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
255
|
+
throw new Error(`invalid release package metadata at ${path}`);
|
|
256
|
+
const gjc = (parsed as { gjc?: unknown }).gjc;
|
|
257
|
+
if (typeof gjc !== "object" || gjc === null || Array.isArray(gjc))
|
|
258
|
+
throw new Error(`release package metadata at ${path} has no gjc.version`);
|
|
259
|
+
const version = (gjc as { version?: unknown }).version;
|
|
260
|
+
if (typeof version !== "string" || version.trim() === "")
|
|
261
|
+
throw new Error(`release package metadata at ${path} has no gjc.version`);
|
|
262
|
+
return version;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function defaultUpdateCommandRunner(command: readonly string[]): Promise<number> {
|
|
266
|
+
const child = Bun.spawn([...command], { stdin: "ignore", stdout: "ignore", stderr: "pipe" });
|
|
267
|
+
const [stderr, exitCode] = await Promise.all([new Response(child.stderr).text(), child.exited]);
|
|
268
|
+
if (exitCode !== 0) {
|
|
269
|
+
const detail = stderr.trim();
|
|
270
|
+
throw new Error(`${command.join(" ")} exited with status ${exitCode}${detail.length === 0 ? "" : `: ${detail}`}`);
|
|
271
|
+
}
|
|
272
|
+
return exitCode;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function installUpdatedServices(options: {
|
|
276
|
+
readonly home: string;
|
|
277
|
+
readonly binDir: string;
|
|
278
|
+
readonly platform: ServicePlatform;
|
|
279
|
+
readonly noRestart: boolean;
|
|
280
|
+
readonly deps: UpdateDeps;
|
|
281
|
+
}): Promise<boolean> {
|
|
282
|
+
const { home, binDir, platform, noRestart, deps } = options;
|
|
283
|
+
const brokerProvisioned = await exists(homeLayout(home).brokerProvisionManifest);
|
|
284
|
+
const env = { ...(deps.env ?? process.env), GAJAEWAY_HOME: home };
|
|
285
|
+
await installServices({
|
|
286
|
+
binDir,
|
|
287
|
+
platform,
|
|
288
|
+
env,
|
|
289
|
+
configuredAdaptersOnly: true,
|
|
290
|
+
compareAndWrite: true,
|
|
291
|
+
...deps.serviceOptions,
|
|
292
|
+
});
|
|
293
|
+
if (platform === "linux") {
|
|
294
|
+
const run = deps.serviceRunner ?? defaultUpdateCommandRunner;
|
|
295
|
+
const execute = async (command: readonly string[]): Promise<void> => {
|
|
296
|
+
const exitCode = await run(command);
|
|
297
|
+
if (exitCode !== 0) throw new Error(`${command.join(" ")} exited with status ${exitCode}`);
|
|
298
|
+
};
|
|
299
|
+
await execute(["systemctl", "--user", "daemon-reload"]);
|
|
300
|
+
if (!noRestart) {
|
|
301
|
+
const enableUnits: string[] = [];
|
|
302
|
+
for (const spec of serviceSpecs()) {
|
|
303
|
+
if (spec.id === "broker" && !brokerProvisioned) continue;
|
|
304
|
+
if (
|
|
305
|
+
spec.id.startsWith("adapter-") &&
|
|
306
|
+
!(await Bun.file(join(home, `adapter-${spec.id.slice(8)}.json`)).exists())
|
|
307
|
+
)
|
|
308
|
+
continue;
|
|
309
|
+
enableUnits.push(systemdUnitName(spec));
|
|
310
|
+
}
|
|
311
|
+
if (enableUnits.length > 0) await execute(["systemctl", "--user", "enable", ...enableUnits]);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return !brokerProvisioned;
|
|
315
|
+
}
|
|
225
316
|
export async function runUpdate(options: RunUpdateOptions): Promise<UpdateResult> {
|
|
226
317
|
const deps = options.deps ?? {};
|
|
227
318
|
const platform = deps.platform ?? process.platform;
|
|
@@ -247,45 +338,70 @@ export async function runUpdate(options: RunUpdateOptions): Promise<UpdateResult
|
|
|
247
338
|
return { current, latest, tag: release.tag, repo, checkedOnly: true, upToDate: false, installed: [] };
|
|
248
339
|
if (binDir === undefined) throw new Error("run the update from a compiled gajaeway binary, or pass --bin-dir DIR");
|
|
249
340
|
|
|
250
|
-
|
|
341
|
+
let lease: Lease;
|
|
251
342
|
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));
|
|
343
|
+
lease = await acquireLease(options.home, "update");
|
|
344
|
+
console.info(`lease_acquired name=update pid=${process.pid}`);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (error instanceof LeaseHeldError) {
|
|
347
|
+
console.error(error.message);
|
|
348
|
+
throw new Error(`another gajaeway update is running (${error.message})`, { cause: error });
|
|
265
349
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
350
|
+
console.error(`lease_failed name=update detail=${error instanceof Error ? error.message : String(error)}`);
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const installed = await (async () => {
|
|
355
|
+
let staged: StagedRelease | undefined;
|
|
356
|
+
try {
|
|
357
|
+
staged = await stageRelease(asset, deps);
|
|
358
|
+
const installedNames = await knownBinariesIn(binDir);
|
|
359
|
+
const toInstall = binariesToInstall(staged.names, installedNames);
|
|
360
|
+
const missingCore = CORE_BINARIES.filter((name) => !toInstall.includes(name));
|
|
361
|
+
if (missingCore.length > 0)
|
|
362
|
+
throw new Error(`release tarball is missing core binaries: ${missingCore.join(", ")}`);
|
|
363
|
+
await assertStagedBinariesUsable(staged, toInstall);
|
|
364
|
+
|
|
365
|
+
const gjcVersion = await readPinnedGjcVersion(join(staged.dir, PIN_MANIFEST));
|
|
366
|
+
await installPinnedGjc({
|
|
367
|
+
home: options.home,
|
|
368
|
+
version: gjcVersion,
|
|
369
|
+
...(deps.gjcInstallRunner === undefined ? {} : { runner: deps.gjcInstallRunner }),
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const stamp = new Date((deps.now ?? Date.now)()).toISOString().replace(/[-:]/g, "").slice(0, 15);
|
|
373
|
+
const backupDir = join(binDir, `backup-${current}-${stamp}Z`);
|
|
374
|
+
await mkdir(backupDir, { recursive: false });
|
|
375
|
+
// The compiled gateway and broker host read the gjc pin from the package.json
|
|
376
|
+
// beside them; a stale one makes the new binaries refuse the gjc just installed.
|
|
377
|
+
const replaced = [...toInstall, PIN_MANIFEST];
|
|
378
|
+
for (const name of replaced) {
|
|
379
|
+
const target = join(binDir, name);
|
|
380
|
+
if (await exists(target)) await copyFile(target, join(backupDir, name));
|
|
381
|
+
}
|
|
382
|
+
for (const name of replaced) {
|
|
383
|
+
const stagedPath = join(staged.dir, name);
|
|
384
|
+
const tempPath = join(binDir, `.update-${name}`);
|
|
385
|
+
await copyFile(stagedPath, tempPath);
|
|
386
|
+
await chmod(tempPath, name === PIN_MANIFEST ? 0o644 : 0o755);
|
|
387
|
+
await rename(tempPath, join(binDir, name));
|
|
388
|
+
}
|
|
389
|
+
await writeFile(releaseMarkerPath(binDir), `${latest}\n`, "utf8");
|
|
390
|
+
const brokerNotEnabled = await installUpdatedServices({
|
|
391
|
+
home: options.home,
|
|
392
|
+
binDir,
|
|
393
|
+
platform,
|
|
394
|
+
noRestart: options.args.noRestart,
|
|
395
|
+
deps,
|
|
396
|
+
});
|
|
397
|
+
return { toInstall, backupDir, brokerNotEnabled };
|
|
398
|
+
} finally {
|
|
399
|
+
if (staged !== undefined) await rm(staged.dir, { recursive: true, force: true });
|
|
400
|
+
await lease.release();
|
|
272
401
|
}
|
|
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);
|
|
402
|
+
})();
|
|
403
|
+
|
|
404
|
+
if (options.args.noRestart)
|
|
289
405
|
return {
|
|
290
406
|
current,
|
|
291
407
|
latest,
|
|
@@ -293,13 +409,25 @@ export async function runUpdate(options: RunUpdateOptions): Promise<UpdateResult
|
|
|
293
409
|
repo,
|
|
294
410
|
checkedOnly: false,
|
|
295
411
|
upToDate: false,
|
|
296
|
-
installed: toInstall,
|
|
297
|
-
backupDir,
|
|
298
|
-
|
|
412
|
+
installed: installed.toInstall,
|
|
413
|
+
backupDir: installed.backupDir,
|
|
414
|
+
brokerNotEnabled: installed.brokerNotEnabled,
|
|
415
|
+
restartSkipped: true,
|
|
299
416
|
};
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
417
|
+
const receipt = await (deps.queueRestart ?? defaultQueueRestart)(options.home, binDir, "update");
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
current,
|
|
421
|
+
latest,
|
|
422
|
+
tag: release.tag,
|
|
423
|
+
repo,
|
|
424
|
+
checkedOnly: false,
|
|
425
|
+
upToDate: false,
|
|
426
|
+
installed: installed.toInstall,
|
|
427
|
+
backupDir: installed.backupDir,
|
|
428
|
+
brokerNotEnabled: installed.brokerNotEnabled,
|
|
429
|
+
restartQueued: receipt.receiptId,
|
|
430
|
+
};
|
|
303
431
|
}
|
|
304
432
|
|
|
305
433
|
function binariesToInstall(stagedNames: readonly string[], installedNames: readonly string[]): string[] {
|
|
@@ -393,58 +521,7 @@ async function defaultExtractTarball(tarball: Uint8Array, intoDir: string): Prom
|
|
|
393
521
|
await rm(tarballPath, { force: true });
|
|
394
522
|
}
|
|
395
523
|
|
|
396
|
-
async function defaultQueueRestart(home: string): Promise<RestartQueueReceipt> {
|
|
397
|
-
const { receipt, supervisorPid } = await launchRestartStack({ home });
|
|
524
|
+
async function defaultQueueRestart(home: string, binDir: string, trigger: "update"): Promise<RestartQueueReceipt> {
|
|
525
|
+
const { receipt, supervisorPid } = await launchRestartStack({ home, binDir, trigger });
|
|
398
526
|
return { receiptId: receipt.id, supervisorPid };
|
|
399
527
|
}
|
|
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
|
-
}
|