@tutti-os/nexight-devops 0.6.0 → 0.6.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/README.md +6 -1
- package/dist/cli.js +171 -31
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -48,6 +48,10 @@ nexight-devops release hotfix
|
|
|
48
48
|
nexight-devops release approve
|
|
49
49
|
nexight-devops release rollback
|
|
50
50
|
nexight-devops release rollback tutti-server
|
|
51
|
+
nexight-devops release rollback platform-web
|
|
52
|
+
nexight-devops release rollback platform-web --previous --yes --reason "entry failure"
|
|
53
|
+
nexight-devops release rollback platform-web --version-id <uuid> --yes
|
|
54
|
+
nexight-devops release rollback platform-web --probe
|
|
51
55
|
```
|
|
52
56
|
|
|
53
57
|
### Function overview
|
|
@@ -66,7 +70,8 @@ nexight-devops release rollback tutti-server
|
|
|
66
70
|
- `ppe-lane destroy` checks existing lane instances and triggers destroy workflows after a lane-name confirmation. `tutti-server` must be selected explicitly for destruction.
|
|
67
71
|
- `ppe-lane status` queries AWS Cloud Map for one lane, or all lanes with `--all`, including `tutti-server`.
|
|
68
72
|
- `release prepare`, `release hotfix`, and `release approve` run the controlled release lifecycle through GitHub Actions and approval issues.
|
|
69
|
-
- `release rollback` lets you choose `tutti-server`
|
|
73
|
+
- `release rollback` lets you choose `tutti-server` or `platform-web` interactively. For `tutti-server`, the command reads the current production image and up to ten recent Helm deployments directly when local cluster read access is available, otherwise through GitHub Actions. The selected image is revalidated and restored by GitHub Actions, followed by health checks. Use `--revision <number> --yes` for scripted selection.
|
|
74
|
+
- `release rollback platform-web` restores a retained, previously deployed Cloudflare Worker version through Tutti's lightweight `platform-web-rollback.yml`. It does not build, test, install packages, upload assets, or roll back ingress/storage. The interactive flow binds your confirmation to the displayed deployment ID; if production changes, execution refuses the stale selection. `--previous --yes` starts only one workflow and selects the latest eligible previous version from live history under the writer lock, excluding versions withdrawn by earlier recorded rollbacks. `--version-id <uuid> --yes` explicitly selects a previously deployed version, including a withdrawn version. `--yes` skips confirmation only. `--reason` records up to 200 UTF-8 bytes; missing historical SHA/run mappings remain unknown. Results distinguish deployment switching, public entry/assets, version/content identity, and unverified Room behavior, and include queue/setup, switch, verification, and total timings. `--probe` measures the same read-only production checks without changing traffic. The CLI needs GitHub access; Cloudflare credentials remain in the production environment's GitHub Secrets.
|
|
70
75
|
|
|
71
76
|
## Controlled Release
|
|
72
77
|
|
package/dist/cli.js
CHANGED
|
@@ -3142,6 +3142,9 @@ var matchesWorkflowRunCorrelation = (run, correlation) => {
|
|
|
3142
3142
|
if (correlation.actorLogin && run.actor?.login !== correlation.actorLogin) {
|
|
3143
3143
|
return false;
|
|
3144
3144
|
}
|
|
3145
|
+
if (correlation.runName && run.display_title !== correlation.runName) {
|
|
3146
|
+
return false;
|
|
3147
|
+
}
|
|
3145
3148
|
if (!isRunCreatedAtOrAfter(run.created_at, correlation.createdAt)) {
|
|
3146
3149
|
return false;
|
|
3147
3150
|
}
|
|
@@ -4148,9 +4151,9 @@ var registerReleasePrepareCommand = (program) => {
|
|
|
4148
4151
|
};
|
|
4149
4152
|
|
|
4150
4153
|
// src/commands/release/rollback.ts
|
|
4151
|
-
import { mkdtemp as
|
|
4152
|
-
import { tmpdir } from "os";
|
|
4153
|
-
import { join as
|
|
4154
|
+
import { mkdtemp as mkdtemp3, readFile as readFile3, rm as rm2 } from "fs/promises";
|
|
4155
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
4156
|
+
import { join as join6 } from "path";
|
|
4154
4157
|
|
|
4155
4158
|
// src/constants/rollback-targets.ts
|
|
4156
4159
|
var tuttiServerRollbackTarget = {
|
|
@@ -4158,7 +4161,12 @@ var tuttiServerRollbackTarget = {
|
|
|
4158
4161
|
label: "tutti-server",
|
|
4159
4162
|
repoFullName: "tutti-lab/tutti-server"
|
|
4160
4163
|
};
|
|
4161
|
-
var
|
|
4164
|
+
var platformWebRollbackTarget = {
|
|
4165
|
+
key: "platform-web",
|
|
4166
|
+
label: "platform-web",
|
|
4167
|
+
repoFullName: "tutti-lab/tutti"
|
|
4168
|
+
};
|
|
4169
|
+
var rollbackTargets = [tuttiServerRollbackTarget, platformWebRollbackTarget];
|
|
4162
4170
|
|
|
4163
4171
|
// src/commands/release/rollback-local.ts
|
|
4164
4172
|
var CLUSTER = "arn:aws:eks:us-east-1:250509935467:cluster/nexight-ojo-us-east-1";
|
|
@@ -4246,9 +4254,137 @@ var fetchLocalRollbackCatalog = async () => {
|
|
|
4246
4254
|
return buildLocalRollbackCatalog(history, deployment, manifests);
|
|
4247
4255
|
};
|
|
4248
4256
|
|
|
4257
|
+
// src/commands/release/rollback-web.ts
|
|
4258
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4259
|
+
import { mkdtemp as mkdtemp2, readFile as readFile2, rm } from "fs/promises";
|
|
4260
|
+
import { tmpdir } from "os";
|
|
4261
|
+
import { join as join5 } from "path";
|
|
4262
|
+
var REPO = platformWebRollbackTarget.repoFullName;
|
|
4263
|
+
var WORKFLOW = "platform-web-rollback.yml";
|
|
4264
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
4265
|
+
var SHA = /^[0-9a-f]{40}$/;
|
|
4266
|
+
var choiceTime = new Intl.DateTimeFormat("sv-SE", { timeZone: "Asia/Singapore", dateStyle: "short", timeStyle: "short" });
|
|
4267
|
+
var validDeployment = (value) => UUID.test(value?.id) && Array.isArray(value.versions) && value.versions.length > 0 && value.versions.every((item) => UUID.test(item.version_id) && Number.isFinite(item.percentage) && item.percentage > 0 && item.percentage <= 100) && Math.abs(value.versions.reduce((sum, item) => sum + item.percentage, 0) - 100) < 1e-3;
|
|
4268
|
+
var parseWebRollbackCatalog = (raw) => {
|
|
4269
|
+
const data = JSON.parse(raw);
|
|
4270
|
+
if (data.schema !== "tutti.web.rollback.catalog.v1" || data.environment !== "production" || !validDeployment(data.currentDeployment) || !Array.isArray(data.candidates) || data.candidates.length > 100) {
|
|
4271
|
+
throw new CliError("Invalid Platform Web rollback catalog", 1);
|
|
4272
|
+
}
|
|
4273
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4274
|
+
const active = new Set(data.currentDeployment.versions.map((item) => item.version_id));
|
|
4275
|
+
for (const candidate of data.candidates) {
|
|
4276
|
+
if (!UUID.test(candidate.versionId) || !UUID.test(candidate.deploymentId) || !Number.isFinite(Date.parse(candidate.deployedAt)) || typeof candidate.current !== "boolean" || candidate.current !== active.has(candidate.versionId) || typeof candidate.withdrawn !== "boolean" || typeof candidate.fullTraffic !== "boolean" || !(candidate.sha === null || SHA.test(candidate.sha)) || !(candidate.runId === null || /^\d+$/.test(candidate.runId)) || seen.has(candidate.versionId)) {
|
|
4277
|
+
throw new CliError("Invalid Platform Web deployment in rollback catalog", 1);
|
|
4278
|
+
}
|
|
4279
|
+
seen.add(candidate.versionId);
|
|
4280
|
+
}
|
|
4281
|
+
return data;
|
|
4282
|
+
};
|
|
4283
|
+
var buildWebRollbackChoices = (catalog) => catalog.candidates.filter((candidate) => !candidate.current && !candidate.withdrawn && candidate.fullTraffic).map((candidate) => ({
|
|
4284
|
+
name: `${choiceTime.format(new Date(candidate.deployedAt))} SGT ${candidate.versionId.slice(0, 8)} ${candidate.sha?.slice(0, 12) ?? "SHA unknown"}${candidate.runId ? ` run ${candidate.runId}` : ""}`,
|
|
4285
|
+
value: candidate.versionId
|
|
4286
|
+
}));
|
|
4287
|
+
var gh = async (args) => {
|
|
4288
|
+
const result = await runCommand("gh", args);
|
|
4289
|
+
if (result.exitCode !== 0) throw new CliError(result.stderr.trim() || result.stdout.trim() || "GitHub command failed", 1);
|
|
4290
|
+
return result.stdout;
|
|
4291
|
+
};
|
|
4292
|
+
var dispatch = async (operation, inputs = {}) => {
|
|
4293
|
+
const operationId = randomUUID3();
|
|
4294
|
+
const requestedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4295
|
+
const knownRunIds = await listWorkflowRunIds({ repoFullName: REPO, workflowFile: WORKFLOW, ref: "main" });
|
|
4296
|
+
const fields = { operation, operation_id: operationId, requested_at: requestedAt, ...inputs };
|
|
4297
|
+
await gh(["workflow", "run", WORKFLOW, "--repo", REPO, "--ref", "main", ...Object.entries(fields).flatMap(([key, value]) => ["-f", `${key}=${value}`])]);
|
|
4298
|
+
const progress = ui.spinner(operation === "execute" ? "\u7B49\u5F85 Platform Web \u56DE\u6EDA\u548C\u5165\u53E3\u9A8C\u8BC1..." : "\u8BFB\u53D6 Platform Web \u751F\u4EA7\u72B6\u6001...").start();
|
|
4299
|
+
try {
|
|
4300
|
+
const run = await pollWorkflowRun({
|
|
4301
|
+
knownRunIds,
|
|
4302
|
+
repoFullName: REPO,
|
|
4303
|
+
workflowFile: WORKFLOW,
|
|
4304
|
+
ref: "main",
|
|
4305
|
+
intervalMs: 1e3,
|
|
4306
|
+
maxAttempts: 600,
|
|
4307
|
+
correlation: { runName: `Platform Web Rollback / ${operation} / ${operationId}`, createdAt: requestedAt },
|
|
4308
|
+
onUpdate: (update) => {
|
|
4309
|
+
if ("htmlUrl" in update) progress.text = update.htmlUrl;
|
|
4310
|
+
}
|
|
4311
|
+
});
|
|
4312
|
+
const directory = await mkdtemp2(join5(tmpdir(), "platform-web-rollback-"));
|
|
4313
|
+
try {
|
|
4314
|
+
try {
|
|
4315
|
+
await gh(["run", "download", String(run.runId), "--repo", REPO, "--name", "platform-web-rollback", "--dir", directory]);
|
|
4316
|
+
} catch (error) {
|
|
4317
|
+
throw new CliError(`Could not retrieve operation outcome; inspect actual production state at ${run.htmlUrl} before retrying: ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
4318
|
+
}
|
|
4319
|
+
const result = JSON.parse(await readFile2(join5(directory, "rollback-result.json"), "utf8"));
|
|
4320
|
+
if (result.schema !== "tutti.web.rollback.result.v1" || result.operationId !== operationId || !["success", "failure", "pending_verification"].includes(result.status) || !["unchanged", "switched", "unknown"].includes(result.deployment) || !result.verification || !result.timings || Object.values(result.timings).some((value) => !Number.isFinite(value) || value < 0) || result.actual && !validDeployment(result.actual)) {
|
|
4321
|
+
throw new CliError(`Invalid rollback result: ${run.htmlUrl}`, 1);
|
|
4322
|
+
}
|
|
4323
|
+
if (run.conclusion !== "success" || result.status !== "success") {
|
|
4324
|
+
const state = result.actual ? `${result.actual.id} / ${JSON.stringify(result.actual.versions)}` : "unknown";
|
|
4325
|
+
throw new CliError(`Platform Web operation failed; deployment ${result.deployment}
|
|
4326
|
+
Actual production: ${state}
|
|
4327
|
+
${result.error ?? run.failureMessage ?? run.conclusion}
|
|
4328
|
+
${run.htmlUrl}`, 1);
|
|
4329
|
+
}
|
|
4330
|
+
if (operation === "execute" && (result.deployment !== "switched" || !result.actual || !UUID.test(result.targetVersionId ?? "") || result.actual.versions.length !== 1 || result.actual.versions[0]?.version_id !== result.targetVersionId || result.actual.versions[0]?.percentage !== 100)) {
|
|
4331
|
+
throw new CliError(`Rollback did not confirm target version at 100%: ${run.htmlUrl}`, 1);
|
|
4332
|
+
}
|
|
4333
|
+
const catalog = operation === "list" ? parseWebRollbackCatalog(await readFile2(join5(directory, "rollback-catalog.json"), "utf8")) : void 0;
|
|
4334
|
+
return { run, result, catalog };
|
|
4335
|
+
} catch (error) {
|
|
4336
|
+
if (error instanceof CliError) throw error;
|
|
4337
|
+
throw new CliError(`Could not read actual operation state; inspect ${run.htmlUrl} before retrying: ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
4338
|
+
} finally {
|
|
4339
|
+
await rm(directory, { recursive: true, force: true });
|
|
4340
|
+
}
|
|
4341
|
+
} finally {
|
|
4342
|
+
progress.stop();
|
|
4343
|
+
}
|
|
4344
|
+
};
|
|
4345
|
+
var runPlatformWebRollback = async (options) => {
|
|
4346
|
+
if (options.previous && options.versionId || options.probe && (options.previous || options.versionId)) throw new CliError("Choose only one of --previous, --version-id, or --probe", 1);
|
|
4347
|
+
if (options.versionId && !UUID.test(options.versionId)) throw new CliError("--version-id must be a Worker version UUID", 1);
|
|
4348
|
+
if (options.reason && Buffer.byteLength(options.reason) > 200) throw new CliError("--reason must be at most 200 UTF-8 bytes", 1);
|
|
4349
|
+
await ensurePrerequisites({ needsGh: true });
|
|
4350
|
+
let inputs = { reason: options.reason ?? "operator rollback" };
|
|
4351
|
+
if (options.probe) {
|
|
4352
|
+
const { run: run2, result: result2 } = await dispatch("probe");
|
|
4353
|
+
logger.info(`Read-only production probe: ${JSON.stringify(result2.timings)}`);
|
|
4354
|
+
logger.info(`Verification: ${JSON.stringify(result2.verification)}`);
|
|
4355
|
+
logger.info(run2.htmlUrl);
|
|
4356
|
+
return;
|
|
4357
|
+
}
|
|
4358
|
+
if (options.yes && (options.previous || options.versionId)) {
|
|
4359
|
+
inputs = { ...inputs, previous: String(Boolean(options.previous)), version_id: options.versionId ?? "" };
|
|
4360
|
+
} else {
|
|
4361
|
+
const { catalog } = await dispatch("list", options.versionId ? { version_id: options.versionId } : {});
|
|
4362
|
+
if (!catalog) throw new CliError("Missing Platform Web rollback catalog", 1);
|
|
4363
|
+
const choices = buildWebRollbackChoices(catalog);
|
|
4364
|
+
if (!options.versionId && choices.length === 0) throw new CliError("No eligible recent production Web version is available", 1);
|
|
4365
|
+
const versionId = options.versionId ?? (options.previous ? choices[0]?.value : await ui.select({ message: "Select a production Web version to restore", choices, pageSize: Math.min(choices.length, 15) }));
|
|
4366
|
+
const selected = catalog.candidates.find((candidate) => candidate.versionId === versionId && !candidate.current);
|
|
4367
|
+
if (!selected) throw new CliError("No eligible recent production Web version is available", 1);
|
|
4368
|
+
logger.info(`Current deployment: ${catalog.currentDeployment.id}`);
|
|
4369
|
+
logger.info(`Target version: ${selected.versionId} / SHA ${selected.sha ?? "unknown"}`);
|
|
4370
|
+
if (selected.runId) logger.info(`Published by: https://github.com/${REPO}/actions/runs/${selected.runId}`);
|
|
4371
|
+
if (selected.withdrawn) logger.warn("This version was previously withdrawn by a rollback.");
|
|
4372
|
+
if (!options.yes && !await ui.confirm({ message: "Restore this Web version to production?", default: false })) {
|
|
4373
|
+
logger.warn("Rollback cancelled");
|
|
4374
|
+
return;
|
|
4375
|
+
}
|
|
4376
|
+
inputs = { ...inputs, previous: "false", version_id: selected.versionId, expected_deployment_id: catalog.currentDeployment.id };
|
|
4377
|
+
}
|
|
4378
|
+
const { run, result } = await dispatch("execute", inputs);
|
|
4379
|
+
logger.success(`Platform Web switched to ${result.targetVersionId} at 100% traffic`);
|
|
4380
|
+
logger.info(`Verification: ${JSON.stringify(result.verification)}`);
|
|
4381
|
+
logger.info(`Timing (ms): ${JSON.stringify(result.timings)}`);
|
|
4382
|
+
logger.info(run.htmlUrl);
|
|
4383
|
+
};
|
|
4384
|
+
|
|
4249
4385
|
// src/commands/release/rollback.ts
|
|
4250
|
-
var
|
|
4251
|
-
var
|
|
4386
|
+
var REPO2 = tuttiServerRollbackTarget.repoFullName;
|
|
4387
|
+
var WORKFLOW2 = "rollback.yml";
|
|
4252
4388
|
var SHA256 = /^sha256:[0-9a-f]{64}$/;
|
|
4253
4389
|
var SINGAPORE_TIME = new Intl.DateTimeFormat("sv-SE", {
|
|
4254
4390
|
timeZone: "Asia/Singapore",
|
|
@@ -4272,7 +4408,7 @@ var buildRollbackChoices = (catalog) => catalog.candidates.filter((candidate) =>
|
|
|
4272
4408
|
name: `${displayChoiceTime(candidate.deployedAt)} SGT #${candidate.revision} ${candidate.version}`,
|
|
4273
4409
|
value: candidate.revision
|
|
4274
4410
|
}));
|
|
4275
|
-
var
|
|
4411
|
+
var gh2 = async (args) => {
|
|
4276
4412
|
const result = await runCommand("gh", args);
|
|
4277
4413
|
if (result.exitCode !== 0) {
|
|
4278
4414
|
throw new CliError(result.stderr.trim() || result.stdout.trim() || `gh ${args[0]} failed`, 1);
|
|
@@ -4292,8 +4428,8 @@ var parseRollbackCatalog = (raw) => {
|
|
|
4292
4428
|
return data;
|
|
4293
4429
|
};
|
|
4294
4430
|
var listActiveDeployments = async () => {
|
|
4295
|
-
const runs = await Promise.all(["deploy.yml",
|
|
4296
|
-
const raw = await
|
|
4431
|
+
const runs = await Promise.all(["deploy.yml", WORKFLOW2].map(async (workflow) => {
|
|
4432
|
+
const raw = await gh2(["api", `repos/${REPO2}/actions/workflows/${workflow}/runs?per_page=30`]);
|
|
4297
4433
|
return JSON.parse(raw).workflow_runs;
|
|
4298
4434
|
}));
|
|
4299
4435
|
return runs.flat().filter(
|
|
@@ -4306,19 +4442,19 @@ var ensureNoActiveDeployment = async () => {
|
|
|
4306
4442
|
throw new CliError(`Production deployment is already running: ${active[0]?.html_url}`, 1);
|
|
4307
4443
|
}
|
|
4308
4444
|
};
|
|
4309
|
-
var
|
|
4310
|
-
const knownRunIds = await listWorkflowRunIds({ repoFullName:
|
|
4311
|
-
const args = ["workflow", "run",
|
|
4445
|
+
var dispatch2 = async (operation, inputs = {}) => {
|
|
4446
|
+
const knownRunIds = await listWorkflowRunIds({ repoFullName: REPO2, workflowFile: WORKFLOW2, ref: "main" });
|
|
4447
|
+
const args = ["workflow", "run", WORKFLOW2, "--repo", REPO2, "--ref", "main", "-f", `operation=${operation}`];
|
|
4312
4448
|
for (const [key, value] of Object.entries(inputs)) {
|
|
4313
4449
|
args.push("-f", `${key}=${value}`);
|
|
4314
4450
|
}
|
|
4315
|
-
await
|
|
4451
|
+
await gh2(args);
|
|
4316
4452
|
const progress = ui.spinner(operation === "list" ? "\u8BFB\u53D6\u751F\u4EA7\u90E8\u7F72\u5386\u53F2..." : "\u7B49\u5F85\u751F\u4EA7\u56DE\u6EDA\u548C\u5065\u5EB7\u68C0\u67E5...").start();
|
|
4317
4453
|
try {
|
|
4318
4454
|
const run = await pollWorkflowRun({
|
|
4319
4455
|
knownRunIds,
|
|
4320
|
-
repoFullName:
|
|
4321
|
-
workflowFile:
|
|
4456
|
+
repoFullName: REPO2,
|
|
4457
|
+
workflowFile: WORKFLOW2,
|
|
4322
4458
|
ref: "main",
|
|
4323
4459
|
maxAttempts: operation === "execute" ? 420 : 180,
|
|
4324
4460
|
onUpdate: (update) => {
|
|
@@ -4328,15 +4464,15 @@ var dispatch = async (operation, inputs = {}) => {
|
|
|
4328
4464
|
if (run.conclusion !== "success") {
|
|
4329
4465
|
let state = "";
|
|
4330
4466
|
if (operation === "execute") {
|
|
4331
|
-
const dir = await
|
|
4467
|
+
const dir = await mkdtemp3(join6(tmpdir2(), "tutti-rollback-state-"));
|
|
4332
4468
|
try {
|
|
4333
|
-
await
|
|
4469
|
+
await gh2(["run", "download", String(run.runId), "--repo", REPO2, "--name", "rollback-state", "--dir", dir]);
|
|
4334
4470
|
state = `
|
|
4335
|
-
${await
|
|
4471
|
+
${await readFile3(join6(dir, "rollback-state.txt"), "utf8")}`;
|
|
4336
4472
|
} catch {
|
|
4337
4473
|
state = "\nActual production state is available in the workflow logs.";
|
|
4338
4474
|
} finally {
|
|
4339
|
-
await
|
|
4475
|
+
await rm2(dir, { recursive: true, force: true });
|
|
4340
4476
|
}
|
|
4341
4477
|
}
|
|
4342
4478
|
throw new CliError(`Workflow failed: ${run.failureMessage ?? run.conclusion}
|
|
@@ -4348,12 +4484,12 @@ ${run.htmlUrl}${state}`, 1);
|
|
|
4348
4484
|
}
|
|
4349
4485
|
};
|
|
4350
4486
|
var fetchCatalog = async (runId) => {
|
|
4351
|
-
const dir = await
|
|
4487
|
+
const dir = await mkdtemp3(join6(tmpdir2(), "tutti-rollback-"));
|
|
4352
4488
|
try {
|
|
4353
|
-
await
|
|
4354
|
-
return parseRollbackCatalog(await
|
|
4489
|
+
await gh2(["run", "download", String(runId), "--repo", REPO2, "--name", "rollback-catalog", "--dir", dir]);
|
|
4490
|
+
return parseRollbackCatalog(await readFile3(join6(dir, "rollback-catalog.json"), "utf8"));
|
|
4355
4491
|
} finally {
|
|
4356
|
-
await
|
|
4492
|
+
await rm2(dir, { recursive: true, force: true });
|
|
4357
4493
|
}
|
|
4358
4494
|
};
|
|
4359
4495
|
var readCatalog = async () => {
|
|
@@ -4365,7 +4501,7 @@ var readCatalog = async () => {
|
|
|
4365
4501
|
progress.stop();
|
|
4366
4502
|
}
|
|
4367
4503
|
logger.warn("\u672C\u673A\u65E0\u6CD5\u5B8C\u6574\u8BFB\u53D6\u751F\u4EA7\u90E8\u7F72\u5386\u53F2\uFF0C\u6539\u7528 GitHub Actions \u67E5\u8BE2\u3002");
|
|
4368
|
-
const listRun = await
|
|
4504
|
+
const listRun = await dispatch2("list");
|
|
4369
4505
|
return fetchCatalog(listRun.runId);
|
|
4370
4506
|
};
|
|
4371
4507
|
var runTuttiRollback = async (options) => {
|
|
@@ -4405,7 +4541,7 @@ var runTuttiRollback = async (options) => {
|
|
|
4405
4541
|
return;
|
|
4406
4542
|
}
|
|
4407
4543
|
await ensureNoActiveDeployment();
|
|
4408
|
-
const run = await
|
|
4544
|
+
const run = await dispatch2("execute", {
|
|
4409
4545
|
revision: String(selected.revision),
|
|
4410
4546
|
digest: selected.digest,
|
|
4411
4547
|
expected_current_digest: catalog.currentDigest,
|
|
@@ -4415,14 +4551,18 @@ var runTuttiRollback = async (options) => {
|
|
|
4415
4551
|
logger.info(run.htmlUrl);
|
|
4416
4552
|
};
|
|
4417
4553
|
var registerReleaseRollbackCommand = (program) => {
|
|
4418
|
-
program.command("rollback").description("Restore a recent production
|
|
4554
|
+
program.command("rollback").description("Restore a recent production deployment").argument("[repository]", "Rollback target; omit to choose interactively").option("--revision <number>", "Select a recent Helm revision without the chooser").option("--previous", "Restore the latest eligible previous Platform Web version").option("--version-id <uuid>", "Select a previously deployed Platform Web Worker version").option("--reason <text>", "Record a Platform Web rollback reason").option("--probe", "Measure a read-only Platform Web production probe").option("--yes", "Skip the final confirmation").action(async (repository, options) => {
|
|
4419
4555
|
const selected = repository ?? await ui.select({
|
|
4420
4556
|
message: "Select a repository to roll back",
|
|
4421
4557
|
choices: rollbackTargets.map((target) => ({ name: target.label, value: target.key }))
|
|
4422
4558
|
});
|
|
4423
|
-
if (selected
|
|
4424
|
-
throw new CliError("
|
|
4559
|
+
if (selected === platformWebRollbackTarget.key) {
|
|
4560
|
+
if (options.revision) throw new CliError("--revision applies only to tutti-server", 1);
|
|
4561
|
+
await runPlatformWebRollback(options);
|
|
4562
|
+
return;
|
|
4425
4563
|
}
|
|
4564
|
+
if (selected !== tuttiServerRollbackTarget.key) throw new CliError(`Unsupported rollback target: ${selected}`, 1);
|
|
4565
|
+
if (options.previous || options.versionId || options.reason || options.probe) throw new CliError("--previous, --version-id, --reason and --probe apply only to platform-web", 1);
|
|
4426
4566
|
await runTuttiRollback(options);
|
|
4427
4567
|
});
|
|
4428
4568
|
};
|
|
@@ -4452,7 +4592,7 @@ var readCliVersion = () => {
|
|
|
4452
4592
|
|
|
4453
4593
|
// src/lib/version-check.ts
|
|
4454
4594
|
import { realpath } from "fs/promises";
|
|
4455
|
-
import { join as
|
|
4595
|
+
import { join as join7, resolve as resolve3, sep } from "path";
|
|
4456
4596
|
var PACKAGE_NAME = "@tutti-os/nexight-devops";
|
|
4457
4597
|
var AUTO_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
4458
4598
|
var DEFAULT_LOCALE = "zh-CN";
|
|
@@ -4521,7 +4661,7 @@ var isInsidePackageRoot = (entryPath, root) => {
|
|
|
4521
4661
|
if (!entryPath || !root) {
|
|
4522
4662
|
return false;
|
|
4523
4663
|
}
|
|
4524
|
-
const packageRoot = resolve3(
|
|
4664
|
+
const packageRoot = resolve3(join7(root, "@tutti-os", "nexight-devops"));
|
|
4525
4665
|
const targetPath = resolve3(entryPath);
|
|
4526
4666
|
return targetPath === packageRoot || targetPath.startsWith(`${packageRoot}${sep}`);
|
|
4527
4667
|
};
|
|
@@ -4531,7 +4671,7 @@ var isInsideBunGlobalBin = (entryPath, bunGlobalBin) => {
|
|
|
4531
4671
|
}
|
|
4532
4672
|
const targetPath = resolve3(entryPath);
|
|
4533
4673
|
const binPath = resolve3(bunGlobalBin);
|
|
4534
|
-
const expectedEntryPath = resolve3(
|
|
4674
|
+
const expectedEntryPath = resolve3(join7(binPath, "nexight-devops"));
|
|
4535
4675
|
return targetPath === expectedEntryPath;
|
|
4536
4676
|
};
|
|
4537
4677
|
var readGlobalPath = async (command, args) => {
|