@vellumai/cli 0.5.6 → 0.5.8

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.
@@ -9,7 +9,6 @@ import {
9
9
  import type { AssistantEntry } from "../lib/assistant-config";
10
10
  import {
11
11
  captureImageRefs,
12
- clearSigningKeyBootstrapLock,
13
12
  GATEWAY_INTERNAL_PORT,
14
13
  dockerResourceNames,
15
14
  migrateCesSecurityFiles,
@@ -18,46 +17,79 @@ import {
18
17
  stopContainers,
19
18
  } from "../lib/docker";
20
19
  import type { ServiceName } from "../lib/docker";
21
- import { loadBootstrapSecret } from "../lib/guardian-token";
20
+ import { emitCliError, categorizeUpgradeError } from "../lib/cli-error.js";
21
+ import {
22
+ fetchOrganizationId,
23
+ readPlatformToken,
24
+ rollbackPlatformAssistant,
25
+ } from "../lib/platform-client.js";
22
26
  import {
23
27
  broadcastUpgradeEvent,
28
+ buildCompleteEvent,
29
+ buildProgressEvent,
30
+ buildStartingEvent,
31
+ buildUpgradeCommitMessage,
24
32
  captureContainerEnv,
33
+ commitWorkspaceViaGateway,
34
+ CONTAINER_ENV_EXCLUDE_KEYS,
35
+ performDockerRollback,
36
+ rollbackMigrations,
37
+ UPGRADE_PROGRESS,
25
38
  waitForReady,
26
- } from "./upgrade";
39
+ } from "../lib/upgrade-lifecycle.js";
40
+ import { parseVersion } from "../lib/version-compat.js";
27
41
 
28
- function parseArgs(): { name: string | null } {
42
+ function parseArgs(): { name: string | null; version: string | null } {
29
43
  const args = process.argv.slice(3);
30
44
  let name: string | null = null;
45
+ let version: string | null = null;
31
46
 
32
47
  for (let i = 0; i < args.length; i++) {
33
48
  const arg = args[i];
34
49
  if (arg === "--help" || arg === "-h") {
35
- console.log("Usage: vellum rollback [<name>]");
50
+ console.log("Usage: vellum rollback [<name>] [--version <version>]");
36
51
  console.log("");
37
- console.log("Roll back a Docker assistant to the previous version.");
52
+ console.log(
53
+ "Roll back a Docker or managed assistant to a previous version.",
54
+ );
38
55
  console.log("");
39
56
  console.log("Arguments:");
40
57
  console.log(
41
- " <name> Name of the assistant to roll back (default: active or only assistant)",
58
+ " <name> Name of the assistant (default: active or only assistant)",
59
+ );
60
+ console.log("");
61
+ console.log("Options:");
62
+ console.log(
63
+ " --version <version> Target version (optional for managed — omit to roll back to previous)",
42
64
  );
43
65
  console.log("");
44
66
  console.log("Examples:");
45
67
  console.log(
46
- " vellum rollback # Roll back the active assistant",
68
+ " vellum rollback my-assistant # Roll back to previous version (Docker or managed)",
47
69
  );
48
70
  console.log(
49
- " vellum rollback my-assistant # Roll back a specific assistant by name",
71
+ " vellum rollback my-assistant --version v1.2.3 # Roll back to a specific version",
50
72
  );
51
73
  process.exit(0);
74
+ } else if (arg === "--version") {
75
+ const next = args[i + 1];
76
+ if (!next || next.startsWith("-")) {
77
+ console.error("Error: --version requires a value");
78
+ emitCliError("UNKNOWN", "--version requires a value");
79
+ process.exit(1);
80
+ }
81
+ version = next;
82
+ i++;
52
83
  } else if (!arg.startsWith("-")) {
53
84
  name = arg;
54
85
  } else {
55
86
  console.error(`Error: Unknown option '${arg}'.`);
87
+ emitCliError("UNKNOWN", `Unknown option '${arg}'`);
56
88
  process.exit(1);
57
89
  }
58
90
  }
59
91
 
60
- return { name };
92
+ return { name, version };
61
93
  }
62
94
 
63
95
  function resolveCloud(entry: AssistantEntry): string {
@@ -84,6 +116,10 @@ function resolveTargetAssistant(nameArg: string | null): AssistantEntry {
84
116
  const entry = findAssistantByName(nameArg);
85
117
  if (!entry) {
86
118
  console.error(`No assistant found with name '${nameArg}'.`);
119
+ emitCliError(
120
+ "ASSISTANT_NOT_FOUND",
121
+ `No assistant found with name '${nameArg}'.`,
122
+ );
87
123
  process.exit(1);
88
124
  }
89
125
  return entry;
@@ -99,42 +135,218 @@ function resolveTargetAssistant(nameArg: string | null): AssistantEntry {
99
135
  if (all.length === 1) return all[0];
100
136
 
101
137
  if (all.length === 0) {
102
- console.error("No assistants found. Run 'vellum hatch' first.");
138
+ const msg = "No assistants found. Run 'vellum hatch' first.";
139
+ console.error(msg);
140
+ emitCliError("ASSISTANT_NOT_FOUND", msg);
103
141
  } else {
104
- console.error(
105
- "Multiple assistants found. Specify a name or set an active assistant with 'vellum use <name>'.",
106
- );
142
+ const msg =
143
+ "Multiple assistants found. Specify a name or set an active assistant with 'vellum use <name>'.";
144
+ console.error(msg);
145
+ emitCliError("ASSISTANT_NOT_FOUND", msg);
107
146
  }
108
147
  process.exit(1);
109
148
  }
110
149
 
150
+ async function rollbackPlatformViaEndpoint(
151
+ entry: AssistantEntry,
152
+ version?: string,
153
+ ): Promise<void> {
154
+ const currentVersion = entry.serviceGroupVersion;
155
+
156
+ // Step 1 — Version validation (only if version provided)
157
+ if (version && currentVersion) {
158
+ const current = parseVersion(currentVersion);
159
+ const target = parseVersion(version);
160
+ if (current && target) {
161
+ const isOlder =
162
+ target.major < current.major ||
163
+ (target.major === current.major && target.minor < current.minor) ||
164
+ (target.major === current.major &&
165
+ target.minor === current.minor &&
166
+ target.patch < current.patch);
167
+ if (!isOlder) {
168
+ const msg = `Target version ${version} is not older than the current version ${currentVersion}. Use \`vellum upgrade --version ${version}\` to upgrade.`;
169
+ console.error(msg);
170
+ emitCliError("VERSION_DIRECTION", msg);
171
+ process.exit(1);
172
+ }
173
+ }
174
+ }
175
+
176
+ // Step 2 — Workspace commit (starting)
177
+ await commitWorkspaceViaGateway(
178
+ entry.runtimeUrl,
179
+ entry.assistantId,
180
+ buildUpgradeCommitMessage({
181
+ action: "rollback",
182
+ phase: "starting",
183
+ from: currentVersion ?? "unknown",
184
+ to: version ?? "previous",
185
+ topology: "managed",
186
+ assistantId: entry.assistantId,
187
+ }),
188
+ );
189
+
190
+ // Step 3 — Authenticate
191
+ const token = readPlatformToken();
192
+ if (!token) {
193
+ const msg =
194
+ "Error: Not logged in. Run `vellum login --token <token>` first.";
195
+ console.error(msg);
196
+ emitCliError("AUTH_FAILED", msg);
197
+ process.exit(1);
198
+ }
199
+
200
+ let orgId: string;
201
+ try {
202
+ orgId = await fetchOrganizationId(token);
203
+ } catch (err) {
204
+ const msg = err instanceof Error ? err.message : String(err);
205
+ if (msg.includes("401") || msg.includes("403")) {
206
+ console.error("Authentication failed. Run 'vellum login' to refresh.");
207
+ } else {
208
+ console.error(`Error: ${msg}`);
209
+ }
210
+ emitCliError("AUTH_FAILED", "Failed to authenticate with platform", msg);
211
+ process.exit(1);
212
+ }
213
+
214
+ // Step 4 — Broadcast starting event
215
+ console.log("šŸ“¢ Notifying connected clients...");
216
+ await broadcastUpgradeEvent(
217
+ entry.runtimeUrl,
218
+ entry.assistantId,
219
+ buildStartingEvent(version ?? "previous", 90),
220
+ );
221
+
222
+ // Step 5 — Call rollback endpoint
223
+ if (version) {
224
+ console.log(`Rolling back to ${version}...`);
225
+ } else {
226
+ console.log("Rolling back to previous version...");
227
+ }
228
+
229
+ let result: { detail: string; version: string | null };
230
+ try {
231
+ result = await rollbackPlatformAssistant(token, orgId, version);
232
+ } catch (err) {
233
+ const detail = err instanceof Error ? err.message : String(err);
234
+
235
+ // Map specific server error messages to actionable CLI output
236
+ if (detail.includes("No previous version")) {
237
+ console.error(
238
+ "No previous version available. A successful upgrade must have been performed first.",
239
+ );
240
+ } else if (detail.includes("not older")) {
241
+ console.error(
242
+ `Target version is not older than the current version. Use 'vellum upgrade --version' instead.`,
243
+ );
244
+ } else if (detail.includes("not found")) {
245
+ console.error(
246
+ version
247
+ ? `Version ${version} not found.`
248
+ : `Rollback target not found.`,
249
+ );
250
+ } else if (
251
+ err instanceof TypeError ||
252
+ detail.includes("fetch failed") ||
253
+ detail.includes("ECONNREFUSED")
254
+ ) {
255
+ console.error(
256
+ `Connection error: ${detail}\nIs the platform reachable? Try 'vellum wake' if the assistant is asleep.`,
257
+ );
258
+ } else {
259
+ console.error(`Error: ${detail}`);
260
+ }
261
+
262
+ emitCliError("PLATFORM_API_ERROR", "Platform rollback failed", detail);
263
+ await broadcastUpgradeEvent(
264
+ entry.runtimeUrl,
265
+ entry.assistantId,
266
+ buildCompleteEvent(currentVersion ?? "unknown", false),
267
+ );
268
+ process.exit(1);
269
+ }
270
+
271
+ const rolledBackVersion = result.version ?? version ?? "unknown";
272
+
273
+ // Step 6 — Workspace commit (complete)
274
+ await commitWorkspaceViaGateway(
275
+ entry.runtimeUrl,
276
+ entry.assistantId,
277
+ buildUpgradeCommitMessage({
278
+ action: "rollback",
279
+ phase: "complete",
280
+ from: currentVersion ?? "unknown",
281
+ to: rolledBackVersion,
282
+ topology: "managed",
283
+ assistantId: entry.assistantId,
284
+ result: "success",
285
+ }),
286
+ );
287
+
288
+ // Step 7 — Print success
289
+ console.log(`Rolled back to version ${rolledBackVersion}.`);
290
+ if (!version) {
291
+ console.log("Tip: Run 'vellum rollback' again to undo.");
292
+ }
293
+ }
294
+
111
295
  export async function rollback(): Promise<void> {
112
- const { name } = parseArgs();
296
+ const { name, version } = parseArgs();
113
297
  const entry = resolveTargetAssistant(name);
114
298
  const cloud = resolveCloud(entry);
115
299
 
116
- // Only Docker assistants support rollback
300
+ // ---------- Managed (Vellum platform) rollback ----------
301
+ if (cloud === "vellum") {
302
+ await rollbackPlatformViaEndpoint(entry, version ?? undefined);
303
+ return;
304
+ }
305
+
306
+ // ---------- Unsupported topologies ----------
117
307
  if (cloud !== "docker") {
118
- console.error(
119
- "Rollback is only supported for Docker assistants. For managed assistants, use the version picker to upgrade to the previous version.",
120
- );
308
+ const msg = "Rollback is only supported for Docker and managed assistants.";
309
+ console.error(msg);
310
+ emitCliError("UNSUPPORTED_TOPOLOGY", msg);
121
311
  process.exit(1);
122
312
  }
123
313
 
314
+ // ---------- Docker: Targeted version rollback (--version specified) ----------
315
+ if (version) {
316
+ try {
317
+ await performDockerRollback(entry, { targetVersion: version });
318
+ } catch (err) {
319
+ const detail = err instanceof Error ? err.message : String(err);
320
+ console.error(`\nāŒ Rollback failed: ${detail}`);
321
+ await broadcastUpgradeEvent(
322
+ entry.runtimeUrl,
323
+ entry.assistantId,
324
+ buildCompleteEvent(entry.serviceGroupVersion ?? "unknown", false),
325
+ );
326
+ emitCliError(categorizeUpgradeError(err), "Rollback failed", detail);
327
+ process.exit(1);
328
+ }
329
+ return;
330
+ }
331
+
332
+ // ---------- Docker: Saved-state rollback (no --version) ----------
333
+
124
334
  // Verify rollback state exists
125
335
  if (!entry.previousServiceGroupVersion || !entry.previousContainerInfo) {
126
- console.error(
127
- "No rollback state available. Run `vellum upgrade` first to create a rollback point.",
128
- );
336
+ const msg =
337
+ "No rollback state available. Run `vellum upgrade` first to create a rollback point.";
338
+ console.error(msg);
339
+ emitCliError("ROLLBACK_NO_STATE", msg);
129
340
  process.exit(1);
130
341
  }
131
342
 
132
343
  // Verify all three digest fields are present
133
344
  const prev = entry.previousContainerInfo;
134
345
  if (!prev.assistantDigest || !prev.gatewayDigest || !prev.cesDigest) {
135
- console.error(
136
- "Incomplete rollback state. Previous container digests are missing.",
137
- );
346
+ const msg =
347
+ "Incomplete rollback state. Previous container digests are missing.";
348
+ console.error(msg);
349
+ emitCliError("ROLLBACK_NO_STATE", msg);
138
350
  process.exit(1);
139
351
  }
140
352
 
@@ -148,133 +360,215 @@ export async function rollback(): Promise<void> {
148
360
  const instanceName = entry.assistantId;
149
361
  const res = dockerResourceNames(instanceName);
150
362
 
151
- console.log(
152
- `šŸ”„ Rolling back Docker assistant '${instanceName}' to ${entry.previousServiceGroupVersion}...\n`,
153
- );
363
+ try {
364
+ // Record rollback start in workspace git history
365
+ await commitWorkspaceViaGateway(
366
+ entry.runtimeUrl,
367
+ entry.assistantId,
368
+ buildUpgradeCommitMessage({
369
+ action: "rollback",
370
+ phase: "starting",
371
+ from: entry.serviceGroupVersion ?? "unknown",
372
+ to: entry.previousServiceGroupVersion ?? "unknown",
373
+ topology: "docker",
374
+ assistantId: entry.assistantId,
375
+ }),
376
+ );
154
377
 
155
- // Capture current container env
156
- console.log("šŸ’¾ Capturing existing container environment...");
157
- const capturedEnv = await captureContainerEnv(res.assistantContainer);
158
- console.log(
159
- ` Captured ${Object.keys(capturedEnv).length} env var(s) from ${res.assistantContainer}\n`,
160
- );
378
+ console.log(
379
+ `šŸ”„ Rolling back Docker assistant '${instanceName}' to ${entry.previousServiceGroupVersion}...\n`,
380
+ );
161
381
 
162
- // Extract CES_SERVICE_TOKEN from captured env, or generate fresh one
163
- const cesServiceToken =
164
- capturedEnv["CES_SERVICE_TOKEN"] || randomBytes(32).toString("hex");
165
-
166
- // Retrieve or generate a bootstrap secret for the gateway.
167
- const bootstrapSecret =
168
- loadBootstrapSecret(instanceName) || randomBytes(32).toString("hex");
169
-
170
- // Build extra env vars, excluding keys managed by serviceDockerRunArgs
171
- const envKeysSetByRunArgs = new Set([
172
- "CES_SERVICE_TOKEN",
173
- "VELLUM_ASSISTANT_NAME",
174
- "RUNTIME_HTTP_HOST",
175
- "PATH",
176
- ]);
177
- for (const envVar of ["ANTHROPIC_API_KEY", "VELLUM_PLATFORM_URL"]) {
178
- if (process.env[envVar]) {
179
- envKeysSetByRunArgs.add(envVar);
382
+ // Capture current container env
383
+ console.log("šŸ’¾ Capturing existing container environment...");
384
+ const capturedEnv = await captureContainerEnv(res.assistantContainer);
385
+ console.log(
386
+ ` Captured ${Object.keys(capturedEnv).length} env var(s) from ${res.assistantContainer}\n`,
387
+ );
388
+
389
+ // Extract CES_SERVICE_TOKEN from captured env, or generate fresh one
390
+ const cesServiceToken =
391
+ capturedEnv["CES_SERVICE_TOKEN"] || randomBytes(32).toString("hex");
392
+
393
+ // Extract or generate the shared JWT signing key.
394
+ const signingKey =
395
+ capturedEnv["ACTOR_TOKEN_SIGNING_KEY"] || randomBytes(32).toString("hex");
396
+
397
+ // Build extra env vars, excluding keys managed by serviceDockerRunArgs
398
+ const envKeysSetByRunArgs = new Set(CONTAINER_ENV_EXCLUDE_KEYS);
399
+ for (const envVar of ["ANTHROPIC_API_KEY", "VELLUM_PLATFORM_URL"]) {
400
+ if (process.env[envVar]) {
401
+ envKeysSetByRunArgs.add(envVar);
402
+ }
180
403
  }
181
- }
182
- const extraAssistantEnv: Record<string, string> = {};
183
- for (const [key, value] of Object.entries(capturedEnv)) {
184
- if (!envKeysSetByRunArgs.has(key)) {
185
- extraAssistantEnv[key] = value;
404
+ const extraAssistantEnv: Record<string, string> = {};
405
+ for (const [key, value] of Object.entries(capturedEnv)) {
406
+ if (!envKeysSetByRunArgs.has(key)) {
407
+ extraAssistantEnv[key] = value;
408
+ }
186
409
  }
187
- }
188
410
 
189
- // Parse gateway port from entry's runtimeUrl, fall back to default
190
- let gatewayPort = GATEWAY_INTERNAL_PORT;
191
- try {
192
- const parsed = new URL(entry.runtimeUrl);
193
- const port = parseInt(parsed.port, 10);
194
- if (!isNaN(port)) {
195
- gatewayPort = port;
411
+ // Parse gateway port from entry's runtimeUrl, fall back to default
412
+ let gatewayPort = GATEWAY_INTERNAL_PORT;
413
+ try {
414
+ const parsed = new URL(entry.runtimeUrl);
415
+ const port = parseInt(parsed.port, 10);
416
+ if (!isNaN(port)) {
417
+ gatewayPort = port;
418
+ }
419
+ } catch {
420
+ // use default
196
421
  }
197
- } catch {
198
- // use default
199
- }
200
422
 
201
- // Notify connected clients that a rollback is about to begin (best-effort)
202
- console.log("šŸ“¢ Notifying connected clients...");
203
- await broadcastUpgradeEvent(entry.runtimeUrl, entry.assistantId, {
204
- type: "starting",
205
- targetVersion: entry.previousServiceGroupVersion,
206
- expectedDowntimeSeconds: 60,
207
- });
208
- // Brief pause to allow SSE delivery before containers stop.
209
- await new Promise((r) => setTimeout(r, 500));
210
-
211
- console.log("šŸ›‘ Stopping existing containers...");
212
- await stopContainers(res);
213
- console.log("āœ… Containers stopped\n");
214
-
215
- // Run security file migrations and signing key cleanup
216
- console.log("šŸ”„ Migrating security files to gateway volume...");
217
- await migrateGatewaySecurityFiles(res, (msg) => console.log(msg));
218
-
219
- console.log("šŸ”„ Migrating credential files to CES security volume...");
220
- await migrateCesSecurityFiles(res, (msg) => console.log(msg));
221
-
222
- console.log("šŸ”‘ Clearing signing key bootstrap lock...");
223
- await clearSigningKeyBootstrapLock(res);
224
-
225
- console.log("šŸš€ Starting containers with previous version...");
226
- await startContainers(
227
- {
228
- bootstrapSecret,
229
- cesServiceToken,
230
- extraAssistantEnv,
231
- gatewayPort,
232
- imageTags: previousImageRefs,
233
- instanceName,
234
- res,
235
- },
236
- (msg) => console.log(msg),
237
- );
238
- console.log("āœ… Containers started\n");
239
-
240
- console.log("Waiting for assistant to become ready...");
241
- const ready = await waitForReady(entry.runtimeUrl);
242
-
243
- if (ready) {
244
- // Capture new digests from the rolled-back containers
245
- const newDigests = await captureImageRefs(res);
246
-
247
- // Swap current/previous state to enable "rollback the rollback"
248
- const updatedEntry: AssistantEntry = {
249
- ...entry,
250
- serviceGroupVersion: entry.previousServiceGroupVersion,
251
- containerInfo: {
252
- assistantImage: prev.assistantImage ?? previousImageRefs.assistant,
253
- gatewayImage: prev.gatewayImage ?? previousImageRefs.gateway,
254
- cesImage: prev.cesImage ?? previousImageRefs["credential-executor"],
255
- assistantDigest: newDigests?.assistant,
256
- gatewayDigest: newDigests?.gateway,
257
- cesDigest: newDigests?.["credential-executor"],
258
- networkName: res.network,
423
+ // Notify connected clients that a rollback is about to begin (best-effort)
424
+ console.log("šŸ“¢ Notifying connected clients...");
425
+ await broadcastUpgradeEvent(
426
+ entry.runtimeUrl,
427
+ entry.assistantId,
428
+ buildStartingEvent(entry.previousServiceGroupVersion),
429
+ );
430
+ // Brief pause to allow SSE delivery before containers stop.
431
+ await new Promise((r) => setTimeout(r, 500));
432
+
433
+ // Roll back migrations to pre-upgrade state (must happen before containers stop)
434
+ if (
435
+ entry.previousDbMigrationVersion !== undefined ||
436
+ entry.previousWorkspaceMigrationId !== undefined
437
+ ) {
438
+ console.log("šŸ”„ Reverting database changes...");
439
+ await broadcastUpgradeEvent(
440
+ entry.runtimeUrl,
441
+ entry.assistantId,
442
+ buildProgressEvent(UPGRADE_PROGRESS.REVERTING_MIGRATIONS),
443
+ );
444
+ await rollbackMigrations(
445
+ entry.runtimeUrl,
446
+ entry.assistantId,
447
+ entry.previousDbMigrationVersion,
448
+ entry.previousWorkspaceMigrationId,
449
+ );
450
+ }
451
+
452
+ // Progress: switching version (must be sent BEFORE stopContainers)
453
+ await broadcastUpgradeEvent(
454
+ entry.runtimeUrl,
455
+ entry.assistantId,
456
+ buildProgressEvent(UPGRADE_PROGRESS.SWITCHING),
457
+ );
458
+
459
+ console.log("šŸ›‘ Stopping existing containers...");
460
+ await stopContainers(res);
461
+ console.log("āœ… Containers stopped\n");
462
+
463
+ // Run security file migrations and signing key cleanup
464
+ console.log("šŸ”„ Migrating security files to gateway volume...");
465
+ await migrateGatewaySecurityFiles(res, (msg) => console.log(msg));
466
+
467
+ console.log("šŸ”„ Migrating credential files to CES security volume...");
468
+ await migrateCesSecurityFiles(res, (msg) => console.log(msg));
469
+
470
+ console.log("šŸš€ Starting containers with previous version...");
471
+ await startContainers(
472
+ {
473
+ signingKey,
474
+ cesServiceToken,
475
+ extraAssistantEnv,
476
+ gatewayPort,
477
+ imageTags: previousImageRefs,
478
+ instanceName,
479
+ res,
259
480
  },
260
- previousServiceGroupVersion: entry.serviceGroupVersion,
261
- previousContainerInfo: entry.containerInfo,
262
- };
263
- saveAssistantEntry(updatedEntry);
264
-
265
- // Notify clients that the rollback succeeded
266
- await broadcastUpgradeEvent(entry.runtimeUrl, entry.assistantId, {
267
- type: "complete",
268
- installedVersion: entry.previousServiceGroupVersion,
269
- success: true,
270
- });
481
+ (msg) => console.log(msg),
482
+ );
483
+ console.log("āœ… Containers started\n");
484
+
485
+ console.log("Waiting for assistant to become ready...");
486
+ const ready = await waitForReady(entry.runtimeUrl);
487
+
488
+ if (ready) {
489
+ // Capture new digests from the rolled-back containers
490
+ const newDigests = await captureImageRefs(res);
491
+
492
+ // Swap current/previous state to enable "rollback the rollback"
493
+ const updatedEntry: AssistantEntry = {
494
+ ...entry,
495
+ serviceGroupVersion: entry.previousServiceGroupVersion,
496
+ containerInfo: {
497
+ assistantImage: prev.assistantImage ?? previousImageRefs.assistant,
498
+ gatewayImage: prev.gatewayImage ?? previousImageRefs.gateway,
499
+ cesImage: prev.cesImage ?? previousImageRefs["credential-executor"],
500
+ assistantDigest: newDigests?.assistant,
501
+ gatewayDigest: newDigests?.gateway,
502
+ cesDigest: newDigests?.["credential-executor"],
503
+ networkName: res.network,
504
+ },
505
+ previousServiceGroupVersion: entry.serviceGroupVersion,
506
+ previousContainerInfo: entry.containerInfo,
507
+ // Clear the backup path — it belonged to the upgrade we just rolled back
508
+ preUpgradeBackupPath: undefined,
509
+ previousDbMigrationVersion: undefined,
510
+ previousWorkspaceMigrationId: undefined,
511
+ };
512
+ saveAssistantEntry(updatedEntry);
513
+
514
+ // Notify clients that the rollback succeeded
515
+ await broadcastUpgradeEvent(
516
+ entry.runtimeUrl,
517
+ entry.assistantId,
518
+ buildCompleteEvent(entry.previousServiceGroupVersion, true),
519
+ );
271
520
 
272
- console.log(
273
- `\nāœ… Docker assistant '${instanceName}' rolled back to ${entry.previousServiceGroupVersion}.`,
521
+ // Record successful rollback in workspace git history
522
+ await commitWorkspaceViaGateway(
523
+ entry.runtimeUrl,
524
+ entry.assistantId,
525
+ buildUpgradeCommitMessage({
526
+ action: "rollback",
527
+ phase: "complete",
528
+ from: entry.serviceGroupVersion ?? "unknown",
529
+ to: entry.previousServiceGroupVersion ?? "unknown",
530
+ topology: "docker",
531
+ assistantId: entry.assistantId,
532
+ result: "success",
533
+ }),
534
+ );
535
+
536
+ console.log(
537
+ `\nāœ… Docker assistant '${instanceName}' rolled back to ${entry.previousServiceGroupVersion}.`,
538
+ );
539
+ console.log(
540
+ "\nTip: To also restore data from before the upgrade, use `vellum restore --from <backup-path>`.",
541
+ );
542
+ } else {
543
+ console.error(
544
+ `\nāŒ Containers failed to become ready within the timeout.`,
545
+ );
546
+ console.log(
547
+ ` Check logs with: docker logs -f ${res.assistantContainer}`,
548
+ );
549
+ await broadcastUpgradeEvent(
550
+ entry.runtimeUrl,
551
+ entry.assistantId,
552
+ buildCompleteEvent(
553
+ entry.previousServiceGroupVersion ?? "unknown",
554
+ false,
555
+ ),
556
+ );
557
+ emitCliError(
558
+ "READINESS_TIMEOUT",
559
+ "Rolled-back containers failed to become ready within the timeout.",
560
+ );
561
+ process.exit(1);
562
+ }
563
+ } catch (err) {
564
+ const detail = err instanceof Error ? err.message : String(err);
565
+ console.error(`\nāŒ Rollback failed: ${detail}`);
566
+ await broadcastUpgradeEvent(
567
+ entry.runtimeUrl,
568
+ entry.assistantId,
569
+ buildCompleteEvent(entry.serviceGroupVersion ?? "unknown", false),
274
570
  );
275
- } else {
276
- console.error(`\nāŒ Containers failed to become ready within the timeout.`);
277
- console.log(` Check logs with: docker logs -f ${res.assistantContainer}`);
571
+ emitCliError(categorizeUpgradeError(err), "Rollback failed", detail);
278
572
  process.exit(1);
279
573
  }
280
574
  }