@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.
- package/knip.json +3 -1
- package/package.json +1 -1
- package/src/commands/backup.ts +152 -13
- package/src/commands/hatch.ts +120 -65
- package/src/commands/restore.ts +359 -16
- package/src/commands/retire.ts +5 -5
- package/src/commands/rollback.ts +436 -142
- package/src/commands/upgrade.ts +575 -205
- package/src/index.ts +4 -4
- package/src/lib/assistant-config.ts +33 -6
- package/src/lib/aws.ts +15 -8
- package/src/lib/backup-ops.ts +213 -0
- package/src/lib/cli-error.ts +93 -0
- package/src/lib/config-utils.ts +59 -0
- package/src/lib/docker.ts +99 -50
- package/src/lib/doctor-client.ts +11 -1
- package/src/lib/gcp.ts +19 -10
- package/src/lib/guardian-token.ts +4 -42
- package/src/lib/local.ts +30 -9
- package/src/lib/platform-client.ts +205 -3
- package/src/lib/platform-releases.ts +112 -0
- package/src/lib/upgrade-lifecycle.ts +844 -0
package/src/commands/rollback.ts
CHANGED
|
@@ -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 {
|
|
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 "
|
|
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(
|
|
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>
|
|
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
|
|
68
|
+
" vellum rollback my-assistant # Roll back to previous version (Docker or managed)",
|
|
47
69
|
);
|
|
48
70
|
console.log(
|
|
49
|
-
" vellum rollback my-assistant
|
|
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
|
-
|
|
138
|
+
const msg = "No assistants found. Run 'vellum hatch' first.";
|
|
139
|
+
console.error(msg);
|
|
140
|
+
emitCliError("ASSISTANT_NOT_FOUND", msg);
|
|
103
141
|
} else {
|
|
104
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
capturedEnv
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
}
|