@tutti-os/nexight-devops 0.5.2 → 0.5.3
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 +1 -1
- package/dist/cli.js +106 -4
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ nexight-devops release rollback tutti-server
|
|
|
68
68
|
- `ppe-lane destroy` checks existing lane instances and triggers destroy workflows after a lane-name confirmation.
|
|
69
69
|
- `ppe-lane status` queries AWS Cloud Map for one lane, or all lanes with `--all`.
|
|
70
70
|
- `release prepare`, `release hotfix`, and `release approve` run the controlled release lifecycle through GitHub Actions and approval issues.
|
|
71
|
-
- `release rollback tutti-server`
|
|
71
|
+
- `release rollback tutti-server` 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` for scripted selection and confirmation.
|
|
72
72
|
|
|
73
73
|
## Controlled Release
|
|
74
74
|
|
package/dist/cli.js
CHANGED
|
@@ -5085,6 +5085,94 @@ var registerReleasePrepareCommand = (program) => {
|
|
|
5085
5085
|
import { mkdtemp as mkdtemp3, readFile as readFile3, rm as rm2 } from "fs/promises";
|
|
5086
5086
|
import { tmpdir } from "os";
|
|
5087
5087
|
import { join as join5 } from "path";
|
|
5088
|
+
|
|
5089
|
+
// src/commands/release/rollback-local.ts
|
|
5090
|
+
var CLUSTER = "arn:aws:eks:us-east-1:250509935467:cluster/nexight-ojo-us-east-1";
|
|
5091
|
+
var REPOSITORY = "250509935467.dkr.ecr.us-east-1.amazonaws.com/nexight/tutti/tutti-server";
|
|
5092
|
+
var DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
5093
|
+
var MANIFEST_IMAGE = new RegExp(`image:\\s*["']?${REPOSITORY.replaceAll(".", "\\.")}@(sha256:[0-9a-f]{64})`, "g");
|
|
5094
|
+
var read = async (command, args) => {
|
|
5095
|
+
const result = await runCommand(command, args);
|
|
5096
|
+
if (result.exitCode !== 0) {
|
|
5097
|
+
throw new CliError(result.stderr.trim() || `${command} read failed`, 1);
|
|
5098
|
+
}
|
|
5099
|
+
return result.stdout.trim();
|
|
5100
|
+
};
|
|
5101
|
+
var digestFromManifest = (manifest) => {
|
|
5102
|
+
const matches = [...manifest.matchAll(MANIFEST_IMAGE)].map((match) => match[1]);
|
|
5103
|
+
const digests = new Set(matches);
|
|
5104
|
+
if (digests.size !== 1) {
|
|
5105
|
+
throw new CliError("Helm revision has no unambiguous Tutti image", 1);
|
|
5106
|
+
}
|
|
5107
|
+
return [...digests][0];
|
|
5108
|
+
};
|
|
5109
|
+
var buildLocalRollbackCatalog = (history, deployment, manifests) => {
|
|
5110
|
+
if (!Array.isArray(history) || history.length === 0) {
|
|
5111
|
+
throw new CliError("Production Helm history is empty", 1);
|
|
5112
|
+
}
|
|
5113
|
+
const sorted = [...history].sort((left, right) => right.revision - left.revision);
|
|
5114
|
+
const latest = sorted[0];
|
|
5115
|
+
if (!latest || !["deployed", "failed"].includes(latest.status)) {
|
|
5116
|
+
throw new CliError("Latest Helm revision needs inspection", 1);
|
|
5117
|
+
}
|
|
5118
|
+
const resource = deployment;
|
|
5119
|
+
const images = resource?.spec?.template?.spec?.containers?.filter((container) => container.name === "tutti-server").map((container) => container.image);
|
|
5120
|
+
if (images?.length !== 1 || !images[0]?.startsWith(`${REPOSITORY}@`)) {
|
|
5121
|
+
throw new CliError("Production deployment has no unambiguous Tutti image", 1);
|
|
5122
|
+
}
|
|
5123
|
+
const currentDigest = images[0].slice(REPOSITORY.length + 1);
|
|
5124
|
+
if (!DIGEST.test(currentDigest)) {
|
|
5125
|
+
throw new CliError("Production deployment image is not digest-pinned", 1);
|
|
5126
|
+
}
|
|
5127
|
+
const entries = [latest, ...sorted.filter(
|
|
5128
|
+
(item) => item.revision !== latest.revision && ["deployed", "superseded"].includes(item.status)
|
|
5129
|
+
)].slice(0, 10);
|
|
5130
|
+
const candidates = entries.map((item) => {
|
|
5131
|
+
const manifest = manifests.get(item.revision);
|
|
5132
|
+
if (!manifest) {
|
|
5133
|
+
throw new CliError(`Missing manifest for Helm revision ${item.revision}`, 1);
|
|
5134
|
+
}
|
|
5135
|
+
const digest = digestFromManifest(manifest);
|
|
5136
|
+
return {
|
|
5137
|
+
revision: item.revision,
|
|
5138
|
+
deployedAt: item.updated,
|
|
5139
|
+
digest,
|
|
5140
|
+
version: `sha256:${digest.slice(7, 19)}`,
|
|
5141
|
+
current: digest === currentDigest
|
|
5142
|
+
};
|
|
5143
|
+
});
|
|
5144
|
+
if (candidates[0]?.digest !== currentDigest) {
|
|
5145
|
+
throw new CliError("Current image does not match the latest Helm revision", 1);
|
|
5146
|
+
}
|
|
5147
|
+
return { environment: "production", currentDigest, currentRevision: latest.revision, candidates };
|
|
5148
|
+
};
|
|
5149
|
+
var fetchLocalRollbackCatalog = async () => {
|
|
5150
|
+
const cluster = await read("kubectl", ["config", "view", "--minify", "-o", "jsonpath={.clusters[0].name}"]);
|
|
5151
|
+
if (cluster !== CLUSTER) {
|
|
5152
|
+
throw new CliError("Current kubectl context is not the production cluster", 1);
|
|
5153
|
+
}
|
|
5154
|
+
const [historyRaw, deploymentRaw] = await Promise.all([
|
|
5155
|
+
read("helm", ["history", "tutti-server", "-n", "tutti", "--max", "100", "-o", "json"]),
|
|
5156
|
+
read("kubectl", ["get", "deployment", "tutti-server", "-n", "tutti", "-o", "json"])
|
|
5157
|
+
]);
|
|
5158
|
+
const history = JSON.parse(historyRaw);
|
|
5159
|
+
const deployment = JSON.parse(deploymentRaw);
|
|
5160
|
+
if (!Array.isArray(history) || history.length === 0) {
|
|
5161
|
+
throw new CliError("Production Helm history is empty", 1);
|
|
5162
|
+
}
|
|
5163
|
+
const sorted = [...history].sort((left, right) => right.revision - left.revision);
|
|
5164
|
+
const latest = sorted[0];
|
|
5165
|
+
const entries = [latest, ...sorted.filter(
|
|
5166
|
+
(item) => item.revision !== latest.revision && ["deployed", "superseded"].includes(item.status)
|
|
5167
|
+
)].slice(0, 10);
|
|
5168
|
+
const manifests = new Map(await Promise.all(entries.map(async (item) => [
|
|
5169
|
+
item.revision,
|
|
5170
|
+
await read("helm", ["get", "manifest", "tutti-server", "-n", "tutti", "--revision", String(item.revision)])
|
|
5171
|
+
])));
|
|
5172
|
+
return buildLocalRollbackCatalog(history, deployment, manifests);
|
|
5173
|
+
};
|
|
5174
|
+
|
|
5175
|
+
// src/commands/release/rollback.ts
|
|
5088
5176
|
var REPO = "tutti-lab/tutti-server";
|
|
5089
5177
|
var WORKFLOW = "rollback.yml";
|
|
5090
5178
|
var SHA256 = /^sha256:[0-9a-f]{64}$/;
|
|
@@ -5106,7 +5194,7 @@ var displayChoiceTime = (raw) => {
|
|
|
5106
5194
|
const date = new Date(raw);
|
|
5107
5195
|
return Number.isNaN(date.getTime()) ? raw : SINGAPORE_CHOICE_TIME.format(date);
|
|
5108
5196
|
};
|
|
5109
|
-
var buildRollbackChoices = (catalog) => catalog.candidates.filter((candidate) => !candidate.current).sort((left, right) => Date.parse(right.deployedAt) - Date.parse(left.deployedAt)).map((candidate) => ({
|
|
5197
|
+
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
5198
|
name: `${displayChoiceTime(candidate.deployedAt)} SGT #${candidate.revision} ${candidate.version}`,
|
|
5111
5199
|
value: candidate.revision
|
|
5112
5200
|
}));
|
|
@@ -5194,11 +5282,22 @@ var fetchCatalog = async (runId) => {
|
|
|
5194
5282
|
await rm2(dir, { recursive: true, force: true });
|
|
5195
5283
|
}
|
|
5196
5284
|
};
|
|
5285
|
+
var readCatalog = async () => {
|
|
5286
|
+
const progress = ui.spinner("\u76F4\u63A5\u8BFB\u53D6\u751F\u4EA7 Helm \u5386\u53F2...").start();
|
|
5287
|
+
try {
|
|
5288
|
+
return await fetchLocalRollbackCatalog();
|
|
5289
|
+
} catch {
|
|
5290
|
+
} finally {
|
|
5291
|
+
progress.stop();
|
|
5292
|
+
}
|
|
5293
|
+
logger.warn("\u672C\u673A\u65E0\u6CD5\u5B8C\u6574\u8BFB\u53D6\u751F\u4EA7\u90E8\u7F72\u5386\u53F2\uFF0C\u6539\u7528 GitHub Actions \u67E5\u8BE2\u3002");
|
|
5294
|
+
const listRun = await dispatch("list");
|
|
5295
|
+
return fetchCatalog(listRun.runId);
|
|
5296
|
+
};
|
|
5197
5297
|
var runTuttiRollback = async (options) => {
|
|
5198
5298
|
await ensurePrerequisites({ needsGh: true });
|
|
5199
5299
|
await ensureNoActiveDeployment();
|
|
5200
|
-
const
|
|
5201
|
-
const catalog = await fetchCatalog(listRun.runId);
|
|
5300
|
+
const catalog = await readCatalog();
|
|
5202
5301
|
const choices = buildRollbackChoices(catalog);
|
|
5203
5302
|
if (choices.length === 0) {
|
|
5204
5303
|
throw new CliError("No previous retained production image is available to restore", 1);
|
|
@@ -5215,7 +5314,9 @@ var runTuttiRollback = async (options) => {
|
|
|
5215
5314
|
pageSize: choices.length
|
|
5216
5315
|
});
|
|
5217
5316
|
}
|
|
5218
|
-
const selected = catalog.candidates.find(
|
|
5317
|
+
const selected = catalog.candidates.find(
|
|
5318
|
+
(candidate) => candidate.revision === revision && !candidate.current && candidate.digest !== catalog.currentDigest
|
|
5319
|
+
);
|
|
5219
5320
|
if (!selected) {
|
|
5220
5321
|
throw new CliError(`Revision ${revision} is not an eligible recent deployment`, 1);
|
|
5221
5322
|
}
|
|
@@ -5224,6 +5325,7 @@ var runTuttiRollback = async (options) => {
|
|
|
5224
5325
|
logger.info(` ${catalog.currentDigest}`);
|
|
5225
5326
|
logger.info(`Target: Helm revision ${selected.revision} / ${displayTime(selected.deployedAt)} / ${selected.version}`);
|
|
5226
5327
|
logger.info(` ${selected.digest}`);
|
|
5328
|
+
logger.info("GitHub Actions will recheck the live deployment and target image before changing production.");
|
|
5227
5329
|
if (!options.yes && !await ui.confirm({ message: "Restore this image to production?", default: false })) {
|
|
5228
5330
|
logger.warn("Rollback cancelled");
|
|
5229
5331
|
return;
|