@tutti-os/nexight-devops 0.5.2 → 0.5.4

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 CHANGED
@@ -48,6 +48,7 @@ nexight-devops ppe-lane status --all
48
48
  nexight-devops release prepare
49
49
  nexight-devops release hotfix
50
50
  nexight-devops release approve
51
+ nexight-devops release rollback
51
52
  nexight-devops release rollback tutti-server
52
53
  ```
53
54
 
@@ -64,11 +65,11 @@ nexight-devops release rollback tutti-server
64
65
  - `admin computer log-recall` exposes the lower-level task operations for troubleshooting and recovery.
65
66
  - `datafinder` validates access, reads dashboards/reports, runs trusted local JavaScript queries, and exposes fixed reliability queries.
66
67
  - `diagnose shared-agent` runs a bounded, read-only CloudWatch playbook that correlates room descriptor results, Owner lease observations, client version, and HTTP failures. It verifies the production AWS account before querying and supports stable `--json` output.
67
- - `ppe-lane deploy` deploys selected backend repositories and the optional `zk-web/canvas` staging artifact to a PPE lane.
68
- - `ppe-lane destroy` checks existing lane instances and triggers destroy workflows after a lane-name confirmation.
69
- - `ppe-lane status` queries AWS Cloud Map for one lane, or all lanes with `--all`.
68
+ - `ppe-lane deploy` deploys selected backend repositories, including `tutti-server`, and the optional `zk-web/canvas` staging artifact to a PPE lane.
69
+ - `ppe-lane destroy` checks existing lane instances and triggers destroy workflows after a lane-name confirmation. `tutti-server` must be selected explicitly for destruction.
70
+ - `ppe-lane status` queries AWS Cloud Map for one lane, or all lanes with `--all`, including `tutti-server`.
70
71
  - `release prepare`, `release hotfix`, and `release approve` run the controlled release lifecycle through GitHub Actions and approval issues.
71
- - `release rollback tutti-server` lists the current production image and recent successful Helm deployments (up to ten entries), then restores a selected image through GitHub Actions and waits for health checks. Use `--revision <number> --yes` for scripted selection and confirmation.
72
+ - `release rollback` lets you choose `tutti-server` interactively; `release rollback tutti-server` selects it directly. The command reads the current production image and up to ten recent Helm deployments directly when local read access to the production cluster is available. Otherwise it retrieves the verified list through GitHub Actions. The selected image is always revalidated and restored by GitHub Actions, followed by health checks. Use `--revision <number> --yes` with the explicit repository for scripted selection and confirmation.
72
73
 
73
74
  ## Controlled Release
74
75
 
package/dist/cli.js CHANGED
@@ -4018,6 +4018,17 @@ var ppeLaneCanvasTarget = {
4018
4018
 
4019
4019
  // src/constants/ppe-lane-repos.ts
4020
4020
  var ppeLaneRepos = [
4021
+ {
4022
+ key: "tutti-server",
4023
+ label: "tutti-server",
4024
+ repoFullName: "tutti-lab/tutti-server",
4025
+ deployWorkflowFile: "deploy-lane.yml",
4026
+ destroyWorkflowFile: "destroy-lane.yml",
4027
+ defaultRef: "main",
4028
+ defaultDestroySelection: false,
4029
+ cloudMapServiceId: "srv-izinli73rwxpf2dr",
4030
+ serviceName: "tutti-server"
4031
+ },
4021
4032
  {
4022
4033
  key: "zk-agent-engine",
4023
4034
  label: "zk-agent-engine",
@@ -4463,7 +4474,11 @@ var runPpeLaneDestroy = async () => {
4463
4474
  const lane = validatePpeLaneName(await ui.input({ message: t("ppeLane.destroy.enterLane") }));
4464
4475
  const selectedKeys = await ui.checkbox({
4465
4476
  message: t("ppeLane.destroy.selectRepos"),
4466
- choices: ppeLaneRepos.map((repo) => ({ value: repo.key, name: repo.label, checked: true }))
4477
+ choices: ppeLaneRepos.map((repo) => ({
4478
+ value: repo.key,
4479
+ name: repo.label,
4480
+ checked: repo.defaultDestroySelection !== false
4481
+ }))
4467
4482
  });
4468
4483
  if (selectedKeys.length === 0) {
4469
4484
  throw new CliError(t("ppeLane.destroy.atLeastOneRepo"), 1);
@@ -5085,7 +5100,103 @@ var registerReleasePrepareCommand = (program) => {
5085
5100
  import { mkdtemp as mkdtemp3, readFile as readFile3, rm as rm2 } from "fs/promises";
5086
5101
  import { tmpdir } from "os";
5087
5102
  import { join as join5 } from "path";
5088
- var REPO = "tutti-lab/tutti-server";
5103
+
5104
+ // src/constants/rollback-targets.ts
5105
+ var tuttiServerRollbackTarget = {
5106
+ key: "tutti-server",
5107
+ label: "tutti-server",
5108
+ repoFullName: "tutti-lab/tutti-server"
5109
+ };
5110
+ var rollbackTargets = [tuttiServerRollbackTarget];
5111
+
5112
+ // src/commands/release/rollback-local.ts
5113
+ var CLUSTER = "arn:aws:eks:us-east-1:250509935467:cluster/nexight-ojo-us-east-1";
5114
+ var REPOSITORY = "250509935467.dkr.ecr.us-east-1.amazonaws.com/nexight/tutti/tutti-server";
5115
+ var DIGEST = /^sha256:[0-9a-f]{64}$/;
5116
+ var MANIFEST_IMAGE = new RegExp(`image:\\s*["']?${REPOSITORY.replaceAll(".", "\\.")}@(sha256:[0-9a-f]{64})`, "g");
5117
+ var read = async (command, args) => {
5118
+ const result = await runCommand(command, args);
5119
+ if (result.exitCode !== 0) {
5120
+ throw new CliError(result.stderr.trim() || `${command} read failed`, 1);
5121
+ }
5122
+ return result.stdout.trim();
5123
+ };
5124
+ var digestFromManifest = (manifest) => {
5125
+ const matches = [...manifest.matchAll(MANIFEST_IMAGE)].map((match) => match[1]);
5126
+ const digests = new Set(matches);
5127
+ if (digests.size !== 1) {
5128
+ throw new CliError("Helm revision has no unambiguous Tutti image", 1);
5129
+ }
5130
+ return [...digests][0];
5131
+ };
5132
+ var buildLocalRollbackCatalog = (history, deployment, manifests) => {
5133
+ if (!Array.isArray(history) || history.length === 0) {
5134
+ throw new CliError("Production Helm history is empty", 1);
5135
+ }
5136
+ const sorted = [...history].sort((left, right) => right.revision - left.revision);
5137
+ const latest = sorted[0];
5138
+ if (!latest || !["deployed", "failed"].includes(latest.status)) {
5139
+ throw new CliError("Latest Helm revision needs inspection", 1);
5140
+ }
5141
+ const resource = deployment;
5142
+ const images = resource?.spec?.template?.spec?.containers?.filter((container) => container.name === "tutti-server").map((container) => container.image);
5143
+ if (images?.length !== 1 || !images[0]?.startsWith(`${REPOSITORY}@`)) {
5144
+ throw new CliError("Production deployment has no unambiguous Tutti image", 1);
5145
+ }
5146
+ const currentDigest = images[0].slice(REPOSITORY.length + 1);
5147
+ if (!DIGEST.test(currentDigest)) {
5148
+ throw new CliError("Production deployment image is not digest-pinned", 1);
5149
+ }
5150
+ const entries = [latest, ...sorted.filter(
5151
+ (item) => item.revision !== latest.revision && ["deployed", "superseded"].includes(item.status)
5152
+ )].slice(0, 10);
5153
+ const candidates = entries.map((item) => {
5154
+ const manifest = manifests.get(item.revision);
5155
+ if (!manifest) {
5156
+ throw new CliError(`Missing manifest for Helm revision ${item.revision}`, 1);
5157
+ }
5158
+ const digest = digestFromManifest(manifest);
5159
+ return {
5160
+ revision: item.revision,
5161
+ deployedAt: item.updated,
5162
+ digest,
5163
+ version: `sha256:${digest.slice(7, 19)}`,
5164
+ current: digest === currentDigest
5165
+ };
5166
+ });
5167
+ if (candidates[0]?.digest !== currentDigest) {
5168
+ throw new CliError("Current image does not match the latest Helm revision", 1);
5169
+ }
5170
+ return { environment: "production", currentDigest, currentRevision: latest.revision, candidates };
5171
+ };
5172
+ var fetchLocalRollbackCatalog = async () => {
5173
+ const cluster = await read("kubectl", ["config", "view", "--minify", "-o", "jsonpath={.clusters[0].name}"]);
5174
+ if (cluster !== CLUSTER) {
5175
+ throw new CliError("Current kubectl context is not the production cluster", 1);
5176
+ }
5177
+ const [historyRaw, deploymentRaw] = await Promise.all([
5178
+ read("helm", ["history", "tutti-server", "-n", "tutti", "--max", "100", "-o", "json"]),
5179
+ read("kubectl", ["get", "deployment", "tutti-server", "-n", "tutti", "-o", "json"])
5180
+ ]);
5181
+ const history = JSON.parse(historyRaw);
5182
+ const deployment = JSON.parse(deploymentRaw);
5183
+ if (!Array.isArray(history) || history.length === 0) {
5184
+ throw new CliError("Production Helm history is empty", 1);
5185
+ }
5186
+ const sorted = [...history].sort((left, right) => right.revision - left.revision);
5187
+ const latest = sorted[0];
5188
+ const entries = [latest, ...sorted.filter(
5189
+ (item) => item.revision !== latest.revision && ["deployed", "superseded"].includes(item.status)
5190
+ )].slice(0, 10);
5191
+ const manifests = new Map(await Promise.all(entries.map(async (item) => [
5192
+ item.revision,
5193
+ await read("helm", ["get", "manifest", "tutti-server", "-n", "tutti", "--revision", String(item.revision)])
5194
+ ])));
5195
+ return buildLocalRollbackCatalog(history, deployment, manifests);
5196
+ };
5197
+
5198
+ // src/commands/release/rollback.ts
5199
+ var REPO = tuttiServerRollbackTarget.repoFullName;
5089
5200
  var WORKFLOW = "rollback.yml";
5090
5201
  var SHA256 = /^sha256:[0-9a-f]{64}$/;
5091
5202
  var SINGAPORE_TIME = new Intl.DateTimeFormat("sv-SE", {
@@ -5106,7 +5217,7 @@ var displayChoiceTime = (raw) => {
5106
5217
  const date = new Date(raw);
5107
5218
  return Number.isNaN(date.getTime()) ? raw : SINGAPORE_CHOICE_TIME.format(date);
5108
5219
  };
5109
- var buildRollbackChoices = (catalog) => catalog.candidates.filter((candidate) => !candidate.current).sort((left, right) => Date.parse(right.deployedAt) - Date.parse(left.deployedAt)).map((candidate) => ({
5220
+ var buildRollbackChoices = (catalog) => catalog.candidates.filter((candidate) => !candidate.current && candidate.digest !== catalog.currentDigest).sort((left, right) => Date.parse(right.deployedAt) - Date.parse(left.deployedAt)).map((candidate) => ({
5110
5221
  name: `${displayChoiceTime(candidate.deployedAt)} SGT #${candidate.revision} ${candidate.version}`,
5111
5222
  value: candidate.revision
5112
5223
  }));
@@ -5194,11 +5305,22 @@ var fetchCatalog = async (runId) => {
5194
5305
  await rm2(dir, { recursive: true, force: true });
5195
5306
  }
5196
5307
  };
5308
+ var readCatalog = async () => {
5309
+ const progress = ui.spinner("\u76F4\u63A5\u8BFB\u53D6\u751F\u4EA7 Helm \u5386\u53F2...").start();
5310
+ try {
5311
+ return await fetchLocalRollbackCatalog();
5312
+ } catch {
5313
+ } finally {
5314
+ progress.stop();
5315
+ }
5316
+ logger.warn("\u672C\u673A\u65E0\u6CD5\u5B8C\u6574\u8BFB\u53D6\u751F\u4EA7\u90E8\u7F72\u5386\u53F2\uFF0C\u6539\u7528 GitHub Actions \u67E5\u8BE2\u3002");
5317
+ const listRun = await dispatch("list");
5318
+ return fetchCatalog(listRun.runId);
5319
+ };
5197
5320
  var runTuttiRollback = async (options) => {
5198
5321
  await ensurePrerequisites({ needsGh: true });
5199
5322
  await ensureNoActiveDeployment();
5200
- const listRun = await dispatch("list");
5201
- const catalog = await fetchCatalog(listRun.runId);
5323
+ const catalog = await readCatalog();
5202
5324
  const choices = buildRollbackChoices(catalog);
5203
5325
  if (choices.length === 0) {
5204
5326
  throw new CliError("No previous retained production image is available to restore", 1);
@@ -5215,7 +5337,9 @@ var runTuttiRollback = async (options) => {
5215
5337
  pageSize: choices.length
5216
5338
  });
5217
5339
  }
5218
- const selected = catalog.candidates.find((candidate) => candidate.revision === revision && !candidate.current);
5340
+ const selected = catalog.candidates.find(
5341
+ (candidate) => candidate.revision === revision && !candidate.current && candidate.digest !== catalog.currentDigest
5342
+ );
5219
5343
  if (!selected) {
5220
5344
  throw new CliError(`Revision ${revision} is not an eligible recent deployment`, 1);
5221
5345
  }
@@ -5224,6 +5348,7 @@ var runTuttiRollback = async (options) => {
5224
5348
  logger.info(` ${catalog.currentDigest}`);
5225
5349
  logger.info(`Target: Helm revision ${selected.revision} / ${displayTime(selected.deployedAt)} / ${selected.version}`);
5226
5350
  logger.info(` ${selected.digest}`);
5351
+ logger.info("GitHub Actions will recheck the live deployment and target image before changing production.");
5227
5352
  if (!options.yes && !await ui.confirm({ message: "Restore this image to production?", default: false })) {
5228
5353
  logger.warn("Rollback cancelled");
5229
5354
  return;
@@ -5239,8 +5364,12 @@ var runTuttiRollback = async (options) => {
5239
5364
  logger.info(run.htmlUrl);
5240
5365
  };
5241
5366
  var registerReleaseRollbackCommand = (program) => {
5242
- program.command("rollback").description("Restore a recent production image").argument("<repository>", "Currently supported: tutti-server").option("--revision <number>", "Select a recent Helm revision without the chooser").option("--yes", "Skip the final confirmation").action(async (repository, options) => {
5243
- if (repository !== "tutti-server") {
5367
+ program.command("rollback").description("Restore a recent production image").argument("[repository]", "Rollback target; omit to choose interactively").option("--revision <number>", "Select a recent Helm revision without the chooser").option("--yes", "Skip the final confirmation").action(async (repository, options) => {
5368
+ const selected = repository ?? await ui.select({
5369
+ message: "Select a repository to roll back",
5370
+ choices: rollbackTargets.map((target) => ({ name: target.label, value: target.key }))
5371
+ });
5372
+ if (selected !== tuttiServerRollbackTarget.key) {
5244
5373
  throw new CliError("Only tutti-server is currently supported", 1);
5245
5374
  }
5246
5375
  await runTuttiRollback(options);