@m8t-stack/cli 0.2.63 → 0.2.64

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/dist/cli.js CHANGED
@@ -1364,7 +1364,7 @@ var init_enable_hosted_brain = __esm({
1364
1364
  import { Builtins, Cli } from "clipanion";
1365
1365
 
1366
1366
  // src/lib/package-version.ts
1367
- var CLI_VERSION = "0.2.63";
1367
+ var CLI_VERSION = "0.2.64";
1368
1368
 
1369
1369
  // src/lib/render-error.ts
1370
1370
  init_errors();
@@ -19006,15 +19006,35 @@ import { DefaultAzureCredential as DefaultAzureCredential16 } from "@azure/ident
19006
19006
  init_errors();
19007
19007
  var DEFAULT_IMAGE_REPO = "ghcr.io/m8t-labs/m8t";
19008
19008
  function parseImageRef2(ref) {
19009
- const lastColon = ref.lastIndexOf(":");
19010
- const lastSlash = ref.lastIndexOf("/");
19011
- let name = ref;
19009
+ let rest = ref;
19010
+ let digest;
19011
+ const at = rest.lastIndexOf("@");
19012
+ if (at !== -1) {
19013
+ digest = rest.slice(at + 1) || void 0;
19014
+ rest = rest.slice(0, at);
19015
+ }
19016
+ const lastColon = rest.lastIndexOf(":");
19017
+ const lastSlash = rest.lastIndexOf("/");
19018
+ let name = rest;
19012
19019
  let tag;
19013
19020
  if (lastColon > lastSlash) {
19014
- name = ref.slice(0, lastColon);
19015
- tag = ref.slice(lastColon + 1) || void 0;
19021
+ name = rest.slice(0, lastColon);
19022
+ tag = rest.slice(lastColon + 1) || void 0;
19016
19023
  }
19017
- return { registry: name.split("/")[0], repo: name, tag };
19024
+ return { registry: name.split("/")[0], repo: name, tag, digest };
19025
+ }
19026
+ function assertGatewayDigest(c) {
19027
+ if (!c.digest?.startsWith("sha256:")) {
19028
+ throw new LocalCliError({
19029
+ code: "PLATFORM_GATEWAY_DIGEST_MISSING",
19030
+ message: `The release manifest's gateway pin (${c.ref}:${c.tag}) carries no usable image digest.`,
19031
+ hint: "Re-fetch the release channel, or target a release whose manifest pins a gateway digest. Deploying by tag instead is not offered: a mutable tag cannot establish which bytes run."
19032
+ });
19033
+ }
19034
+ return c.digest;
19035
+ }
19036
+ function gatewayRefFromComponent(c) {
19037
+ return `${c.ref}@${assertGatewayDigest(c)}`;
19018
19038
  }
19019
19039
  var SEMVER_TAG = /^v(\d+)\.(\d+)\.(\d+)$/;
19020
19040
  function isSemverTag(tag) {
@@ -19058,7 +19078,7 @@ function parseContainerAppResourceId(id) {
19058
19078
  }
19059
19079
  function planUpdate(opts) {
19060
19080
  const cur = parseImageRef2(opts.currentImage);
19061
- const currentTag = cur.tag ?? "(none)";
19081
+ const currentTag = cur.tag ?? cur.digest ?? "(none)";
19062
19082
  if (cur.repo !== opts.imageRepo) {
19063
19083
  return { kind: "refuse-byoc", current: currentTag, currentRepo: cur.repo, trackedRepo: opts.imageRepo };
19064
19084
  }
@@ -19067,6 +19087,9 @@ function planUpdate(opts) {
19067
19087
  }
19068
19088
  const available = opts.to ?? pickNewestSemver(opts.availableTags);
19069
19089
  if (!available) return { kind: "no-versions", current: currentTag };
19090
+ if (cur.digest && opts.toDigest && cur.digest === opts.toDigest) {
19091
+ return { kind: "noop", current: currentTag, available };
19092
+ }
19070
19093
  if (cur.tag === available) return { kind: "noop", current: currentTag, available };
19071
19094
  if (!opts.to) {
19072
19095
  const currentIsSemver = cur.tag ? isSemverTag(cur.tag) : false;
@@ -20057,14 +20080,14 @@ async function applyGatewayImage(a, ctx, gatewayResourceId) {
20057
20080
  const { resourceGroup, name } = parseContainerAppResourceId(gatewayResourceId);
20058
20081
  const current = (await runAz(["containerapp", "show", "-g", resourceGroup, "-n", name, "--query", "properties.template.containers[0].image", "-o", "tsv"])).trim();
20059
20082
  const tags = await fetchPublicTags(DEFAULT_IMAGE_REPO);
20060
- const plan = planUpdate({ currentImage: current, availableTags: tags, imageRepo: DEFAULT_IMAGE_REPO, to: a.to });
20061
20083
  const digest = ctx.manifest.components.gateway.digest;
20084
+ const plan = planUpdate({ currentImage: current, availableTags: tags, imageRepo: DEFAULT_IMAGE_REPO, to: a.to, toDigest: digest });
20062
20085
  switch (plan.kind) {
20063
20086
  case "refuse-byoc":
20064
20087
  ctx.onProgress?.(`gateway is BYOC (${plan.currentRepo}) \u2014 marking externally managed.`);
20065
20088
  return { tag: a.to, digest, state: "external" };
20066
20089
  case "roll":
20067
- await runAz(["containerapp", "update", "-n", name, "-g", resourceGroup, "--image", `${plan.repo}:${plan.target}`]);
20090
+ await runAz(["containerapp", "update", "-n", name, "-g", resourceGroup, "--image", `${plan.repo}@${assertGatewayDigest(ctx.manifest.components.gateway)}`]);
20068
20091
  return { tag: a.to, digest, state: "managed" };
20069
20092
  case "noop":
20070
20093
  return { tag: a.to, digest, state: "managed" };
@@ -20107,7 +20130,7 @@ async function resolveBicepParamsForConverge(ctx, opts = {}) {
20107
20130
  const location = opts.location ?? (await runAz(["resource", "list", "-g", ctx.resourceGroup, "--query", "[0].location", "-o", "tsv"])).trim();
20108
20131
  const foundryResourceId = await resolveFoundryResourceId(opts.foundryEndpoint ?? "", opts.foundryResourceId);
20109
20132
  const acrPullIdentityResourceId = resolveAcrPullIdentity({ explicit: opts.acrPullIdentity });
20110
- const imageRef = `${ctx.manifest.components.gateway.ref}:${ctx.manifest.components.gateway.tag}`;
20133
+ const imageRef = gatewayRefFromComponent(ctx.manifest.components.gateway);
20111
20134
  const acrResourceId = await resolveAcrResourceId({ imageRef, explicit: opts.acrResourceId });
20112
20135
  return {
20113
20136
  location,
@@ -20243,6 +20266,57 @@ async function applyPersona(a, ctx, opts) {
20243
20266
  return { treeSha: a.to, foundryVersion };
20244
20267
  }
20245
20268
 
20269
+ // src/lib/gateway-verify.ts
20270
+ function compareGatewayBinding(args) {
20271
+ const base = { stampTag: args.stampTag, stampDigest: args.stampDigest };
20272
+ if (!args.liveImage) {
20273
+ return { ...base, live: { kind: "unread" }, comparedBy: "none", match: "unknown" };
20274
+ }
20275
+ const parsed = parseImageRef2(args.liveImage);
20276
+ if (parsed.digest) {
20277
+ const comparable = Boolean(args.stampDigest);
20278
+ return {
20279
+ ...base,
20280
+ live: { kind: "digest", digest: parsed.digest },
20281
+ comparedBy: comparable ? "digest" : "none",
20282
+ match: comparable ? parsed.digest === args.stampDigest : "unknown"
20283
+ };
20284
+ }
20285
+ if (parsed.tag) {
20286
+ const comparable = args.stampTag !== "unknown" && args.stampTag !== "";
20287
+ return {
20288
+ ...base,
20289
+ live: { kind: "tag", tag: parsed.tag },
20290
+ comparedBy: comparable ? "tag" : "none",
20291
+ match: comparable ? parsed.tag === args.stampTag : "unknown"
20292
+ };
20293
+ }
20294
+ return { ...base, live: { kind: "unread" }, comparedBy: "none", match: "unknown" };
20295
+ }
20296
+ var LABEL = (s) => ` ${s}`.padEnd(22, " ");
20297
+ function renderLiveVerify(v, apiVersion, fmt2 = { ok: (s) => s, warn: (s) => s }) {
20298
+ const stamp = v.stampDigest ? `${v.stampTag} \xB7 ${v.stampDigest}` : v.stampTag;
20299
+ let live;
20300
+ switch (v.live.kind) {
20301
+ case "digest":
20302
+ live = v.live.digest;
20303
+ break;
20304
+ case "tag":
20305
+ live = `${v.live.tag} (tag-pinned \u2014 bytes not verified)`;
20306
+ break;
20307
+ default:
20308
+ live = "unknown";
20309
+ }
20310
+ const verdict = v.match === "unknown" ? fmt2.warn("unknown") : v.match ? fmt2.ok("yes") : fmt2.warn("no");
20311
+ const how = v.comparedBy === "none" ? "" : ` (compared by ${v.comparedBy})`;
20312
+ return [
20313
+ `${LABEL("stamp gateway:")}${stamp}`,
20314
+ `${LABEL("live gateway:")}${live}`,
20315
+ `${LABEL("match:")}${verdict}${how}`,
20316
+ `${LABEL("/api/version:")}${apiVersion}`
20317
+ ];
20318
+ }
20319
+
20246
20320
  // src/commands/platform/status.ts
20247
20321
  var PlatformStatusCommand = class extends M8tCommand {
20248
20322
  static paths = [["platform", "status"]];
@@ -20298,7 +20372,12 @@ var PlatformStatusCommand = class extends M8tCommand {
20298
20372
  const rows = statusRows(manifest, stamp, tree);
20299
20373
  let verify;
20300
20374
  if (this.verify) {
20301
- verify = await this.probeLive(gw.containerAppResourceId, gw.gatewayUrl, stamp?.components.gateway.tag);
20375
+ verify = await this.probeLive(
20376
+ gw.containerAppResourceId,
20377
+ gw.gatewayUrl,
20378
+ stamp?.components.gateway.tag,
20379
+ stamp?.components.gateway.digest
20380
+ );
20302
20381
  }
20303
20382
  if (mode === "json") {
20304
20383
  this.context.stdout.write(
@@ -20318,12 +20397,7 @@ var PlatformStatusCommand = class extends M8tCommand {
20318
20397
  if (verify) {
20319
20398
  log("");
20320
20399
  log(colors.field("live verify:"));
20321
- log(` stamp gateway tag: ${verify.stampGatewayTag}`);
20322
- log(` live gateway tag: ${verify.liveGatewayTag}`);
20323
- log(
20324
- ` match: ${verify.gatewayTagMatch === "unknown" ? colors.warn("unknown") : verify.gatewayTagMatch ? colors.success("yes") : colors.warn("no")}`
20325
- );
20326
- log(` /api/version: ${verify.apiVersion}`);
20400
+ for (const line2 of renderLiveVerify(verify.gateway, verify.apiVersion, { ok: colors.success, warn: colors.warn })) log(line2);
20327
20401
  }
20328
20402
  return 0;
20329
20403
  }
@@ -20347,11 +20421,11 @@ var PlatformStatusCommand = class extends M8tCommand {
20347
20421
  * error, etc.) degrades the affected field to "unknown" rather than
20348
20422
  * throwing — a verify probe must never crash the read-only status command.
20349
20423
  */
20350
- async probeLive(gatewayResourceId, gatewayUrl, stampGatewayTag) {
20351
- let liveGatewayTag = "unknown";
20424
+ async probeLive(gatewayResourceId, gatewayUrl, stampGatewayTag, stampGatewayDigest) {
20425
+ let liveImage;
20352
20426
  try {
20353
20427
  const { resourceGroup, name } = parseContainerAppResourceId(gatewayResourceId);
20354
- const image = (await runAz([
20428
+ liveImage = (await runAz([
20355
20429
  "containerapp",
20356
20430
  "show",
20357
20431
  "-g",
@@ -20362,11 +20436,9 @@ var PlatformStatusCommand = class extends M8tCommand {
20362
20436
  "properties.template.containers[0].image",
20363
20437
  "-o",
20364
20438
  "tsv"
20365
- ])).trim();
20366
- const tag = image.split(":").pop();
20367
- if (tag) liveGatewayTag = tag;
20439
+ ])).trim() || void 0;
20368
20440
  } catch {
20369
- liveGatewayTag = "unknown";
20441
+ liveImage = void 0;
20370
20442
  }
20371
20443
  let apiVersion = "unknown";
20372
20444
  try {
@@ -20380,9 +20452,17 @@ var PlatformStatusCommand = class extends M8tCommand {
20380
20452
  } catch {
20381
20453
  apiVersion = "unknown";
20382
20454
  }
20383
- const stampTag = stampGatewayTag ?? "unknown";
20384
- const gatewayTagMatch = liveGatewayTag === "unknown" || stampTag === "unknown" ? "unknown" : liveGatewayTag === stampTag;
20385
- return { liveGatewayTag, stampGatewayTag: stampTag, gatewayTagMatch, apiVersion };
20455
+ return {
20456
+ gateway: compareGatewayBinding({
20457
+ stampTag: stampGatewayTag ?? "unknown",
20458
+ // A stamp records "" for a component whose digest it never established, so
20459
+ // empty and absent are the SAME claim here. `??` would preserve "" and let a
20460
+ // caller treat it as a recorded value.
20461
+ stampDigest: stampGatewayDigest === "" ? void 0 : stampGatewayDigest,
20462
+ liveImage
20463
+ }),
20464
+ apiVersion
20465
+ };
20386
20466
  }
20387
20467
  };
20388
20468
 
@@ -20973,7 +21053,7 @@ async function buildConvergeDeps(args) {
20973
21053
  ...existing,
20974
21054
  bicep: {
20975
21055
  ...existing.bicep,
20976
- imageRef: `${ctx.manifest.components.gateway.ref}:${ctx.manifest.components.gateway.tag}`
21056
+ imageRef: gatewayRefFromComponent(ctx.manifest.components.gateway)
20977
21057
  },
20978
21058
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
20979
21059
  }
@@ -28986,7 +29066,7 @@ ${colors.error("\u2717")} The GitHub App on disk is installed on ${colors.field(
28986
29066
  // src/commands/bootstrap/launch.ts
28987
29067
  var DEFAULT_RG = "rg-m8t-stack";
28988
29068
  var DEFAULT_INSTALLER = "ghcr.io/m8t-labs/m8t-installer";
28989
- var DEFAULT_INSTALLER_TAG = "v0.1.53";
29069
+ var DEFAULT_INSTALLER_TAG = "v0.1.55";
28990
29070
  var ACI_NAME = "m8t-installer";
28991
29071
  var MI_NAME = "m8t-installer-mi";
28992
29072
  var BootstrapLaunchCommand = class extends M8tCommand {