@m8t-stack/cli 0.2.22 → 0.2.24
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 +3 -3
- package/dist/cli.js +172 -71
- package/dist/cli.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -17,10 +17,10 @@ It's the operator's day-2 tool for the cloud platform — the successor to the o
|
|
|
17
17
|
npm install -g @m8t-stack/cli
|
|
18
18
|
|
|
19
19
|
# Homebrew (macOS / Linux)
|
|
20
|
-
brew install m8t-
|
|
20
|
+
brew install m8t-labs/tap/m8t
|
|
21
21
|
|
|
22
22
|
# Scoop (Windows)
|
|
23
|
-
scoop bucket add m8t https://github.com/m8t-
|
|
23
|
+
scoop bucket add m8t https://github.com/m8t-labs/scoop-bucket
|
|
24
24
|
scoop install m8t
|
|
25
25
|
|
|
26
26
|
# Verify
|
|
@@ -115,7 +115,7 @@ Provisions (or updates) the gateway/webapp stack via Bicep — the successor to
|
|
|
115
115
|
m8t deploy --client-id <appId> --image-ref <acr-or-ghcr-ref> --location <region>
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
Key flags: `--client-id <appId>` (reuse an existing app reg + skip **all** Microsoft Graph writes), `--image-ref <ref>` (full image ref; default `ghcr.io/m8t-
|
|
118
|
+
Key flags: `--client-id <appId>` (reuse an existing app reg + skip **all** Microsoft Graph writes), `--image-ref <ref>` (full image ref; default `ghcr.io/m8t-labs/m8t:latest` — pass an `*.azurecr.io/...` ref for ACR images), `--resource-group` (default `rg-m8t-stack`), `--location`, `--suffix`, `--foundry-endpoint`, `--foundry-resource-id`.
|
|
119
119
|
|
|
120
120
|
> ⚠️ `m8t deploy` is **declarative** — it makes the live stack match the Bicep template. If a deployment was hand-modified after provisioning (e.g. switched to a private-ACR image with a UserAssigned pull identity), re-running the bicep can revert those changes. **Preview with `az deployment group create --what-if` before applying against a live stack.**
|
|
121
121
|
|
package/dist/cli.js
CHANGED
|
@@ -1231,7 +1231,7 @@ var init_enable_hosted_brain = __esm({
|
|
|
1231
1231
|
import { Builtins, Cli } from "clipanion";
|
|
1232
1232
|
|
|
1233
1233
|
// src/lib/package-version.ts
|
|
1234
|
-
var CLI_VERSION = "0.2.
|
|
1234
|
+
var CLI_VERSION = "0.2.24";
|
|
1235
1235
|
|
|
1236
1236
|
// src/lib/render-error.ts
|
|
1237
1237
|
init_errors();
|
|
@@ -11481,6 +11481,31 @@ import { DefaultAzureCredential } from "@azure/identity";
|
|
|
11481
11481
|
// src/lib/az.ts
|
|
11482
11482
|
init_errors();
|
|
11483
11483
|
import { spawn } from "child_process";
|
|
11484
|
+
var SECRET_VALUE_FLAGS = /* @__PURE__ */ new Set(["--secure-environment-variables"]);
|
|
11485
|
+
var SECRET_KEY_RE = /(pem|secret|token|password|passwd|pwd|credential|private[-_]?key)/i;
|
|
11486
|
+
function redactAzArgs(args) {
|
|
11487
|
+
const out = [];
|
|
11488
|
+
let secretFlagActive = false;
|
|
11489
|
+
for (const arg of args) {
|
|
11490
|
+
if (arg.startsWith("-")) {
|
|
11491
|
+
secretFlagActive = SECRET_VALUE_FLAGS.has(arg);
|
|
11492
|
+
out.push(arg);
|
|
11493
|
+
continue;
|
|
11494
|
+
}
|
|
11495
|
+
const eq = arg.indexOf("=");
|
|
11496
|
+
const key2 = eq > 0 ? arg.slice(0, eq) : "";
|
|
11497
|
+
if (secretFlagActive) {
|
|
11498
|
+
out.push(key2 ? `${key2}=[REDACTED]` : "[REDACTED]");
|
|
11499
|
+
continue;
|
|
11500
|
+
}
|
|
11501
|
+
if (key2 && SECRET_KEY_RE.test(key2)) {
|
|
11502
|
+
out.push(`${key2}=[REDACTED]`);
|
|
11503
|
+
continue;
|
|
11504
|
+
}
|
|
11505
|
+
out.push(arg);
|
|
11506
|
+
}
|
|
11507
|
+
return out;
|
|
11508
|
+
}
|
|
11484
11509
|
function runAz(args) {
|
|
11485
11510
|
return new Promise((resolve3, reject) => {
|
|
11486
11511
|
const proc = spawn("az", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -11517,7 +11542,7 @@ function runAz(args) {
|
|
|
11517
11542
|
reject(
|
|
11518
11543
|
new LocalCliError({
|
|
11519
11544
|
code: "AZ_COMMAND_FAILED",
|
|
11520
|
-
message: `'az ${args.join(" ")}' failed: ${errString.trim()}`
|
|
11545
|
+
message: `'az ${redactAzArgs(args).join(" ")}' failed: ${errString.trim()}`
|
|
11521
11546
|
})
|
|
11522
11547
|
);
|
|
11523
11548
|
return;
|
|
@@ -14272,6 +14297,14 @@ function run(cmd, cmdArgs, cwd, env) {
|
|
|
14272
14297
|
c.stderr.on("data", (d) => {
|
|
14273
14298
|
err += d.toString();
|
|
14274
14299
|
});
|
|
14300
|
+
c.on("error", (e) => {
|
|
14301
|
+
reject(
|
|
14302
|
+
new LocalCliError({
|
|
14303
|
+
code: "GH_APP_PUSH_FAILED",
|
|
14304
|
+
message: `${cmd} ${cmdArgs.join(" ")} could not run: ${e.message}`
|
|
14305
|
+
})
|
|
14306
|
+
);
|
|
14307
|
+
});
|
|
14275
14308
|
c.on("close", (code) => {
|
|
14276
14309
|
if (code === 0) {
|
|
14277
14310
|
resolve3();
|
|
@@ -14602,6 +14635,19 @@ async function promptCollision(ctx, info) {
|
|
|
14602
14635
|
rl.close();
|
|
14603
14636
|
}
|
|
14604
14637
|
}
|
|
14638
|
+
function resolveScaffoldCommit(repoRoot, env) {
|
|
14639
|
+
const baked = env.M8T_BUILD_SHA?.trim();
|
|
14640
|
+
if (baked) return baked;
|
|
14641
|
+
try {
|
|
14642
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
14643
|
+
cwd: repoRoot,
|
|
14644
|
+
encoding: "utf8",
|
|
14645
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
14646
|
+
}).trim() || "(scaffold)";
|
|
14647
|
+
} catch {
|
|
14648
|
+
return "(scaffold)";
|
|
14649
|
+
}
|
|
14650
|
+
}
|
|
14605
14651
|
function stageAsGitRepo(dir) {
|
|
14606
14652
|
const steps = [
|
|
14607
14653
|
{ args: ["init", "-b", "main"], label: "git init" },
|
|
@@ -14745,7 +14791,7 @@ var BrainCreateCommand = class extends M8tCommand {
|
|
|
14745
14791
|
if (needsPush) {
|
|
14746
14792
|
materializeBrainTree({ templateSrc, destDir: tmpDir, seedDir });
|
|
14747
14793
|
if (seedName && seedDir) {
|
|
14748
|
-
const headSha =
|
|
14794
|
+
const headSha = resolveScaffoldCommit(repoRoot, env);
|
|
14749
14795
|
writeSeedMarker({ destDir: tmpDir, seedName, seedDir, refreshedToCommit: headSha, version: "(scaffold)" });
|
|
14750
14796
|
}
|
|
14751
14797
|
this.context.stdout.write(
|
|
@@ -16900,7 +16946,7 @@ var SIZE_PRESETS = {
|
|
|
16900
16946
|
medium: { cpu: "1", memory: "2Gi" },
|
|
16901
16947
|
large: { cpu: "2", memory: "4Gi" }
|
|
16902
16948
|
};
|
|
16903
|
-
var DEFAULT_REGISTRY = "ghcr.io/m8t-
|
|
16949
|
+
var DEFAULT_REGISTRY = "ghcr.io/m8t-labs";
|
|
16904
16950
|
var DEFAULT_IMAGE = "m8t-coding-agent";
|
|
16905
16951
|
var DEFAULT_TAG = "v0.1.0";
|
|
16906
16952
|
var DEFAULT_MODEL = "gpt-4.1-mini";
|
|
@@ -16909,7 +16955,7 @@ var CoderDeployCommand = class extends M8tCommand {
|
|
|
16909
16955
|
static paths = [["coder", "deploy"]];
|
|
16910
16956
|
static usage = Command28.Usage({
|
|
16911
16957
|
description: "Deploy the curated coding agent as a hosted Foundry worker.",
|
|
16912
|
-
details: "Creates a hosted agent version from a container image (defaults to the public GHCR image ghcr.io/m8t-
|
|
16958
|
+
details: "Creates a hosted agent version from a container image (defaults to the public GHCR image ghcr.io/m8t-labs/m8t-coding-agent). Post-2026-06-25 Foundry projects require an authenticated pull, so a public image is staged into the customer's Azure Container Registry first (created if missing, then `az acr import` \u2014 a server-side copy, no local build) and the agent is deployed from that ACR ref. Re-running with a newer --image-tag imports the new tag and rolls out a new agent version \u2014 that is how you update a worker's image. Override --image with a private ACR ref to bring your own registry.",
|
|
16913
16959
|
examples: [
|
|
16914
16960
|
["Deploy a coder with defaults", "$0 coder deploy my-coder"],
|
|
16915
16961
|
["Larger sandbox + a custom model", "$0 coder deploy my-coder --size large --model-deployment gpt-4.1"],
|
|
@@ -17296,7 +17342,7 @@ var SIZE_PRESETS2 = {
|
|
|
17296
17342
|
medium: { cpu: "1", memory: "2Gi" },
|
|
17297
17343
|
large: { cpu: "2", memory: "4Gi" }
|
|
17298
17344
|
};
|
|
17299
|
-
var DEFAULT_REGISTRY2 = "ghcr.io/m8t-
|
|
17345
|
+
var DEFAULT_REGISTRY2 = "ghcr.io/m8t-labs";
|
|
17300
17346
|
var DEFAULT_IMAGE2 = "m8t-azure-executor";
|
|
17301
17347
|
var DEFAULT_TAG2 = "v0.1.0";
|
|
17302
17348
|
var DEFAULT_MODEL2 = "gpt-5-mini";
|
|
@@ -17305,11 +17351,11 @@ var AzureExecDeployCommand = class extends M8tCommand {
|
|
|
17305
17351
|
static paths = [["azure-exec", "deploy"]];
|
|
17306
17352
|
static usage = Command30.Usage({
|
|
17307
17353
|
description: "Deploy the Azure executor as a hosted Foundry worker (az CLI + tiered ops).",
|
|
17308
|
-
details: "Creates a hosted agent version from a container image (defaults to the public GHCR image ghcr.io/m8t-
|
|
17354
|
+
details: "Creates a hosted agent version from a container image (defaults to the public GHCR image ghcr.io/m8t-labs/m8t-azure-executor \u2014 no local build needed), grants its identity Foundry User + Contributor (at --scope) + Key Vault Secrets User (brain KV), polls to active, and a2a-enables it as a target. Override with --image/--image-tag for a bring-your-own-registry image (e.g. a private ACR ref, which must be pushed first). Contributor scope is REQUIRED \u2014 pass --scope or --resource-group. Pass --grant-access-admin to additionally grant User Access Administrator (enables human-approved Tier-2 role/delete ops). Post-2026-06-25 Foundry projects require an authenticated pull, so the public image (default ghcr.io/m8t-labs/m8t-azure-executor) is staged into the customer's Azure Container Registry first (created if missing, then `az acr import` \u2014 server-side, no local build) and deployed from that ACR ref; re-running with a newer --image-tag updates the image.",
|
|
17309
17355
|
examples: [
|
|
17310
17356
|
[
|
|
17311
17357
|
"Deploy scoped to a resource group",
|
|
17312
|
-
"$0 azure-exec deploy azexec --resource-group rg-test --brain m8t-
|
|
17358
|
+
"$0 azure-exec deploy azexec --resource-group rg-test --brain m8t-labs/azure-exec-smoke-brain --gateway-url https://<gw>/api/a2a/mcp"
|
|
17313
17359
|
]
|
|
17314
17360
|
]
|
|
17315
17361
|
});
|
|
@@ -17341,7 +17387,7 @@ var AzureExecDeployCommand = class extends M8tCommand {
|
|
|
17341
17387
|
throw new LocalCliError({
|
|
17342
17388
|
code: "USAGE",
|
|
17343
17389
|
message: "--brain owner/repo is required (the executor delivers proof to a brain).",
|
|
17344
|
-
hint: "Example: --brain m8t-
|
|
17390
|
+
hint: "Example: --brain m8t-labs/azure-exec-smoke-brain"
|
|
17345
17391
|
});
|
|
17346
17392
|
}
|
|
17347
17393
|
const size = (this.size ?? "large").toLowerCase();
|
|
@@ -17573,7 +17619,7 @@ import { DefaultAzureCredential as DefaultAzureCredential16 } from "@azure/ident
|
|
|
17573
17619
|
|
|
17574
17620
|
// src/lib/platform-update.ts
|
|
17575
17621
|
init_errors();
|
|
17576
|
-
var DEFAULT_IMAGE_REPO = "ghcr.io/m8t-
|
|
17622
|
+
var DEFAULT_IMAGE_REPO = "ghcr.io/m8t-labs/m8t";
|
|
17577
17623
|
function parseImageRef2(ref) {
|
|
17578
17624
|
const lastColon = ref.lastIndexOf(":");
|
|
17579
17625
|
const lastSlash = ref.lastIndexOf("/");
|
|
@@ -17806,13 +17852,13 @@ function entityToStamp(e) {
|
|
|
17806
17852
|
}
|
|
17807
17853
|
|
|
17808
17854
|
// ../../packages/platform-release/dist/esm/channel-url.js
|
|
17809
|
-
var CHANNEL_LATEST_URL = "https://github.com/m8t-
|
|
17855
|
+
var CHANNEL_LATEST_URL = "https://github.com/m8t-labs/m8t/releases/latest/download/manifest.json";
|
|
17810
17856
|
function platformTag(version) {
|
|
17811
17857
|
const bare = version.trim().replace(/^platform-/, "").replace(/^v/, "");
|
|
17812
17858
|
return `platform-v${bare}`;
|
|
17813
17859
|
}
|
|
17814
17860
|
function channelUrlForVersion(version) {
|
|
17815
|
-
return `https://github.com/m8t-
|
|
17861
|
+
return `https://github.com/m8t-labs/m8t/releases/download/${platformTag(version)}/manifest.json`;
|
|
17816
17862
|
}
|
|
17817
17863
|
|
|
17818
17864
|
// ../../packages/platform-release/dist/esm/apply-request.js
|
|
@@ -17902,7 +17948,7 @@ import * as os8 from "os";
|
|
|
17902
17948
|
import * as path19 from "path";
|
|
17903
17949
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
17904
17950
|
init_errors();
|
|
17905
|
-
var OWNER_REPO = "m8t-
|
|
17951
|
+
var OWNER_REPO = "m8t-labs/m8t";
|
|
17906
17952
|
function parseTreeResponse(body) {
|
|
17907
17953
|
const map = /* @__PURE__ */ new Map();
|
|
17908
17954
|
for (const e of body.tree ?? []) if (e.type === "tree") map.set(e.path, e.sha);
|
|
@@ -20750,7 +20796,7 @@ function classifyWhatIf(changes) {
|
|
|
20750
20796
|
}
|
|
20751
20797
|
|
|
20752
20798
|
// src/commands/deploy.ts
|
|
20753
|
-
var DEFAULT_IMAGE_REF = "ghcr.io/m8t-
|
|
20799
|
+
var DEFAULT_IMAGE_REF = "ghcr.io/m8t-labs/m8t:latest";
|
|
20754
20800
|
var DeployCommand = class extends M8tCommand {
|
|
20755
20801
|
static paths = [["deploy"]];
|
|
20756
20802
|
static usage = Command36.Usage({
|
|
@@ -21151,7 +21197,7 @@ function resolveJudgeDeployment(flag, env) {
|
|
|
21151
21197
|
warning: "no --deployment and no $EXAM_JUDGE_DEPLOYMENT \u2014 falling back to gpt-5-mini as the judge. Name the DISTINCT judge deployment before any blessing/calibration run (DESIGN \xA75.5)."
|
|
21152
21198
|
};
|
|
21153
21199
|
}
|
|
21154
|
-
var BRAIN_REPO_MARKERS = ["m8t-
|
|
21200
|
+
var BRAIN_REPO_MARKERS = ["m8t-labs/", "/azure-advisor-brain", "/stacey-brain", "exam-arm-"];
|
|
21155
21201
|
function assertOutNotInBrainRepo(out) {
|
|
21156
21202
|
if (BRAIN_REPO_MARKERS.some((m) => out.includes(m))) {
|
|
21157
21203
|
throw new LocalCliError({
|
|
@@ -25363,6 +25409,7 @@ init_errors();
|
|
|
25363
25409
|
|
|
25364
25410
|
// src/lib/bootstrap-mi.ts
|
|
25365
25411
|
var INSTALLER_GRANT_DESCRIPTION = "m8t-installer auto-reap (bootstrap)";
|
|
25412
|
+
var GATEWAY_SUBSCOPE_GRANT_DESCRIPTION = "m8t-gateway auto-reap (bootstrap)";
|
|
25366
25413
|
async function ensureResourceGroup2(opts) {
|
|
25367
25414
|
const existing = (await runAz(["group", "show", "--name", opts.name, "--subscription", opts.subscriptionId, "-o", "json"]).catch(() => "")).trim();
|
|
25368
25415
|
if (existing) return;
|
|
@@ -25605,8 +25652,8 @@ async function writeBootstrapState(state, home = os13.homedir()) {
|
|
|
25605
25652
|
|
|
25606
25653
|
// src/commands/bootstrap/launch.ts
|
|
25607
25654
|
var DEFAULT_RG = "rg-m8t-stack";
|
|
25608
|
-
var DEFAULT_INSTALLER = "ghcr.io/m8t-
|
|
25609
|
-
var DEFAULT_INSTALLER_TAG = "v0.1.
|
|
25655
|
+
var DEFAULT_INSTALLER = "ghcr.io/m8t-labs/m8t-installer";
|
|
25656
|
+
var DEFAULT_INSTALLER_TAG = "v0.1.31";
|
|
25610
25657
|
var ACI_NAME = "m8t-installer";
|
|
25611
25658
|
var MI_NAME = "m8t-installer-mi";
|
|
25612
25659
|
var BootstrapLaunchCommand = class extends M8tCommand {
|
|
@@ -25617,7 +25664,8 @@ var BootstrapLaunchCommand = class extends M8tCommand {
|
|
|
25617
25664
|
examples: [
|
|
25618
25665
|
["Launch in eastus2", "$0 bootstrap launch --location eastus2"],
|
|
25619
25666
|
["BYO app registration", "$0 bootstrap launch --location eastus2 --client-id <appId>"],
|
|
25620
|
-
["Pin a specific installer tag", "$0 bootstrap launch --location eastus2 --installer-tag v0.1.
|
|
25667
|
+
["Pin a specific installer tag", "$0 bootstrap launch --location eastus2 --installer-tag v0.1.31"],
|
|
25668
|
+
["Override the full installer image ref", "$0 bootstrap launch --location eastus2 --installer-image ghcr.io/m8t-labs/m8t-installer:v0.1.31"]
|
|
25621
25669
|
]
|
|
25622
25670
|
});
|
|
25623
25671
|
location = Option46.String("--location");
|
|
@@ -25625,6 +25673,10 @@ var BootstrapLaunchCommand = class extends M8tCommand {
|
|
|
25625
25673
|
clientId = Option46.String("--client-id");
|
|
25626
25674
|
subscription = Option46.String("--subscription");
|
|
25627
25675
|
installerTag = Option46.String("--installer-tag");
|
|
25676
|
+
// Full image ref override (registry + repo + tag) — an escape hatch when the
|
|
25677
|
+
// default org/tag is wrong for the current CLI (e.g. a stale published build).
|
|
25678
|
+
// Wins over --installer-tag / the pinned default.
|
|
25679
|
+
installerImage = Option46.String("--installer-image");
|
|
25628
25680
|
gatewayImageRef = Option46.String("--gateway-image-ref");
|
|
25629
25681
|
githubAppCreds = Option46.String("--github-app-creds");
|
|
25630
25682
|
async executeCommand() {
|
|
@@ -25635,6 +25687,8 @@ var BootstrapLaunchCommand = class extends M8tCommand {
|
|
|
25635
25687
|
const resourceGroup = (typeof this.resourceGroup === "string" ? this.resourceGroup : void 0) ?? DEFAULT_RG;
|
|
25636
25688
|
const clientIdOpt = typeof this.clientId === "string" ? this.clientId : void 0;
|
|
25637
25689
|
const installerTag = (typeof this.installerTag === "string" ? this.installerTag : void 0) ?? DEFAULT_INSTALLER_TAG;
|
|
25690
|
+
const installerImageOverride = typeof this.installerImage === "string" ? this.installerImage : void 0;
|
|
25691
|
+
const installerImage = installerImageOverride ?? `${DEFAULT_INSTALLER}:${installerTag}`;
|
|
25638
25692
|
const gatewayImageRef = typeof this.gatewayImageRef === "string" ? this.gatewayImageRef : void 0;
|
|
25639
25693
|
const account = await getAzAccount();
|
|
25640
25694
|
const subscriptionId = (typeof this.subscription === "string" ? this.subscription : void 0) ?? account.subscriptionId;
|
|
@@ -25685,10 +25739,24 @@ var BootstrapLaunchCommand = class extends M8tCommand {
|
|
|
25685
25739
|
out("granting Owner at subscription scope\u2026");
|
|
25686
25740
|
await grantOwnerAtSubscription({ principalId: mi.principalId, subscriptionId });
|
|
25687
25741
|
const roleAssignmentIds = await listAssignmentIds({ principalId: mi.principalId, subscriptionId });
|
|
25742
|
+
const saName = deriveStatusSaName(resourceGroup, subscriptionId);
|
|
25743
|
+
await writeBootstrapState({
|
|
25744
|
+
subscriptionId,
|
|
25745
|
+
resourceGroup,
|
|
25746
|
+
location,
|
|
25747
|
+
aciName: ACI_NAME,
|
|
25748
|
+
miName: MI_NAME,
|
|
25749
|
+
miClientId: mi.clientId,
|
|
25750
|
+
miPrincipalId: mi.principalId,
|
|
25751
|
+
roleAssignmentIds,
|
|
25752
|
+
appRegClientId: appReg.clientId,
|
|
25753
|
+
statusSaName: saName,
|
|
25754
|
+
statusBlobUrl: statusBlobUrl(saName),
|
|
25755
|
+
installerTag
|
|
25756
|
+
});
|
|
25688
25757
|
out("waiting for the installer identity to be usable (role propagation)\u2026");
|
|
25689
25758
|
const miReady = await waitForMiToken({ clientId: mi.clientId, subscriptionId, onProgress: out });
|
|
25690
25759
|
if (!miReady) out("identity not confirmed yet \u2014 launching anyway (the installer retries its own login)");
|
|
25691
|
-
const saName = deriveStatusSaName(resourceGroup, subscriptionId);
|
|
25692
25760
|
out("launching the cloud installer (ACI)\u2026");
|
|
25693
25761
|
await kickInstaller({
|
|
25694
25762
|
aciName: ACI_NAME,
|
|
@@ -25698,25 +25766,11 @@ var BootstrapLaunchCommand = class extends M8tCommand {
|
|
|
25698
25766
|
miResourceId: mi.resourceId,
|
|
25699
25767
|
miClientId: mi.clientId,
|
|
25700
25768
|
appRegClientId: appReg.clientId,
|
|
25701
|
-
image:
|
|
25769
|
+
image: installerImage,
|
|
25702
25770
|
gatewayImageRef,
|
|
25703
25771
|
foundryTracing: "skip",
|
|
25704
25772
|
githubApp
|
|
25705
25773
|
});
|
|
25706
|
-
await writeBootstrapState({
|
|
25707
|
-
subscriptionId,
|
|
25708
|
-
resourceGroup,
|
|
25709
|
-
location,
|
|
25710
|
-
aciName: ACI_NAME,
|
|
25711
|
-
miName: MI_NAME,
|
|
25712
|
-
miClientId: mi.clientId,
|
|
25713
|
-
miPrincipalId: mi.principalId,
|
|
25714
|
-
roleAssignmentIds,
|
|
25715
|
-
appRegClientId: appReg.clientId,
|
|
25716
|
-
statusSaName: saName,
|
|
25717
|
-
statusBlobUrl: statusBlobUrl(saName),
|
|
25718
|
-
installerTag
|
|
25719
|
-
});
|
|
25720
25774
|
this.context.stdout.write(
|
|
25721
25775
|
`${colors.success("\u2713")} installer launched in ${colors.field(resourceGroup)} (${location}).
|
|
25722
25776
|
${colors.hint("next:")} m8t bootstrap status --watch ${colors.dim("# watch the install to done")}
|
|
@@ -25837,35 +25891,53 @@ init_errors();
|
|
|
25837
25891
|
|
|
25838
25892
|
// src/lib/bootstrap-reap.ts
|
|
25839
25893
|
var ROLE_ASSIGNMENT_API = "2022-04-01";
|
|
25840
|
-
|
|
25841
|
-
|
|
25842
|
-
|
|
25894
|
+
var GATEWAY_SUBSCOPE_ROLES = ["Cost Management Reader", "Monitoring Reader"];
|
|
25895
|
+
async function listSubscriptionAssignments(subscriptionId) {
|
|
25896
|
+
return JSON.parse(
|
|
25843
25897
|
await runAz([
|
|
25844
25898
|
"role",
|
|
25845
25899
|
"assignment",
|
|
25846
25900
|
"list",
|
|
25847
25901
|
"--all",
|
|
25848
25902
|
"--subscription",
|
|
25849
|
-
|
|
25903
|
+
subscriptionId,
|
|
25850
25904
|
"-o",
|
|
25851
25905
|
"json"
|
|
25852
25906
|
]).catch(() => "[]") || "[]"
|
|
25853
25907
|
);
|
|
25908
|
+
}
|
|
25909
|
+
async function deleteAssignmentsById(ids) {
|
|
25910
|
+
for (const id of ids) {
|
|
25911
|
+
await runAz([
|
|
25912
|
+
"rest",
|
|
25913
|
+
"--method",
|
|
25914
|
+
"delete",
|
|
25915
|
+
"--url",
|
|
25916
|
+
`https://management.azure.com${id}?api-version=${ROLE_ASSIGNMENT_API}`
|
|
25917
|
+
]).catch(() => void 0);
|
|
25918
|
+
}
|
|
25919
|
+
}
|
|
25920
|
+
async function sweepOrphanGatewaySubRoles(opts) {
|
|
25921
|
+
const subScope = `/subscriptions/${opts.subscriptionId}`;
|
|
25922
|
+
const all = await listSubscriptionAssignments(opts.subscriptionId);
|
|
25923
|
+
const matched = all.filter((r) => {
|
|
25924
|
+
if (r.principalName) return false;
|
|
25925
|
+
if (r.principalType !== "ServicePrincipal") return false;
|
|
25926
|
+
if (r.scope !== subScope) return false;
|
|
25927
|
+
if (r.description === GATEWAY_SUBSCOPE_GRANT_DESCRIPTION) return true;
|
|
25928
|
+
return GATEWAY_SUBSCOPE_ROLES.includes(r.roleDefinitionName ?? "");
|
|
25929
|
+
});
|
|
25930
|
+
if (opts.execute) await deleteAssignmentsById(matched.map((r) => r.id));
|
|
25931
|
+
return matched.map((r) => ({ id: r.id, description: r.description, roleDefinitionName: r.roleDefinitionName, createdOn: r.createdOn }));
|
|
25932
|
+
}
|
|
25933
|
+
async function sweepOrphanOwnerAssignments(opts) {
|
|
25934
|
+
const subScope = `/subscriptions/${opts.subscriptionId}`;
|
|
25935
|
+
const all = await listSubscriptionAssignments(opts.subscriptionId);
|
|
25854
25936
|
const matched = all.filter((r) => {
|
|
25855
25937
|
if (r.description === INSTALLER_GRANT_DESCRIPTION) return true;
|
|
25856
25938
|
return !r.principalName && r.principalType === "ServicePrincipal" && r.roleDefinitionName === "Owner" && r.scope === subScope;
|
|
25857
25939
|
});
|
|
25858
|
-
if (opts.execute)
|
|
25859
|
-
for (const r of matched) {
|
|
25860
|
-
await runAz([
|
|
25861
|
-
"rest",
|
|
25862
|
-
"--method",
|
|
25863
|
-
"delete",
|
|
25864
|
-
"--url",
|
|
25865
|
-
`https://management.azure.com${r.id}?api-version=${ROLE_ASSIGNMENT_API}`
|
|
25866
|
-
]).catch(() => void 0);
|
|
25867
|
-
}
|
|
25868
|
-
}
|
|
25940
|
+
if (opts.execute) await deleteAssignmentsById(matched.map((r) => r.id));
|
|
25869
25941
|
return matched.map((r) => ({ id: r.id, description: r.description, createdOn: r.createdOn }));
|
|
25870
25942
|
}
|
|
25871
25943
|
async function reapInstaller(opts) {
|
|
@@ -25922,46 +25994,75 @@ var BootstrapReapCommand = class extends M8tCommand {
|
|
|
25922
25994
|
if (!sub) {
|
|
25923
25995
|
throw new LocalCliError({ code: "BOOTSTRAP_SWEEP_NO_SUB", message: "No active subscription.", hint: "Run 'az account set --subscription <id>'." });
|
|
25924
25996
|
}
|
|
25925
|
-
const
|
|
25926
|
-
|
|
25927
|
-
|
|
25997
|
+
const execute = this.yes === true;
|
|
25998
|
+
const owner = await sweepOrphanOwnerAssignments({ subscriptionId: sub, execute });
|
|
25999
|
+
const gateway = await sweepOrphanGatewaySubRoles({ subscriptionId: sub, execute });
|
|
26000
|
+
const total = owner.length + gateway.length;
|
|
26001
|
+
if (total === 0) {
|
|
26002
|
+
this.context.stdout.write(`${colors.success("\u2713")} No orphaned installer Owner@sub or gateway sub-scope assignments found.
|
|
25928
26003
|
`);
|
|
25929
26004
|
return 0;
|
|
25930
26005
|
}
|
|
25931
|
-
for (const f of
|
|
25932
|
-
this.context.stdout.write(` ${f.id}${f.createdOn ? colors.dim(` (created ${f.createdOn})`) : ""}${f.description ? colors.dim(" [tagged]") : ""}
|
|
26006
|
+
for (const f of owner) {
|
|
26007
|
+
this.context.stdout.write(` ${colors.dim("Owner@sub")} ${f.id}${f.createdOn ? colors.dim(` (created ${f.createdOn})`) : ""}${f.description ? colors.dim(" [tagged]") : ""}
|
|
26008
|
+
`);
|
|
26009
|
+
}
|
|
26010
|
+
for (const f of gateway) {
|
|
26011
|
+
this.context.stdout.write(` ${colors.dim(f.roleDefinitionName ?? "gateway sub-scope")} ${f.id}${f.createdOn ? colors.dim(` (created ${f.createdOn})`) : ""}${f.description ? colors.dim(" [tagged]") : ""}
|
|
25933
26012
|
`);
|
|
25934
26013
|
}
|
|
25935
|
-
|
|
25936
|
-
|
|
26014
|
+
const breakdown = `${String(owner.length)} Owner@sub, ${String(gateway.length)} gateway sub-scope`;
|
|
26015
|
+
if (execute) {
|
|
26016
|
+
this.context.stdout.write(`${colors.success("\u2713")} Removed ${String(total)} orphaned assignment(s) (${breakdown}).
|
|
25937
26017
|
`);
|
|
25938
26018
|
} else {
|
|
25939
26019
|
this.context.stdout.write(`
|
|
25940
|
-
Found ${String(
|
|
26020
|
+
Found ${String(total)} orphaned assignment(s) (${breakdown}) (dry-run). ${colors.hint("Re-run with --sweep-orphans --yes to delete them.")}
|
|
25941
26021
|
`);
|
|
25942
26022
|
}
|
|
25943
26023
|
return 0;
|
|
25944
26024
|
}
|
|
25945
26025
|
const state = await readBootstrapState();
|
|
25946
26026
|
if (!state) {
|
|
25947
|
-
|
|
26027
|
+
if (this.force === true) {
|
|
26028
|
+
const { subscriptionId: sub } = await getAzAccount();
|
|
26029
|
+
if (!sub) {
|
|
26030
|
+
throw new LocalCliError({ code: "BOOTSTRAP_SWEEP_NO_SUB", message: "No active subscription.", hint: "Run 'az account set --subscription <id>'." });
|
|
26031
|
+
}
|
|
26032
|
+
const owner = await sweepOrphanOwnerAssignments({ subscriptionId: sub, execute: true });
|
|
26033
|
+
const gateway = await sweepOrphanGatewaySubRoles({ subscriptionId: sub, execute: true });
|
|
26034
|
+
const total = owner.length + gateway.length;
|
|
26035
|
+
this.context.stdout.write(
|
|
26036
|
+
total === 0 ? `${colors.success("\u2713")} No bootstrap state and no orphaned installer Owner@sub or gateway sub-scope assignments. Nothing to reap.
|
|
26037
|
+
` : `${colors.success("\u2713")} No bootstrap state found; swept ${String(total)} orphaned assignment(s) (${String(owner.length)} Owner@sub, ${String(gateway.length)} gateway sub-scope).
|
|
26038
|
+
`
|
|
26039
|
+
);
|
|
26040
|
+
return 0;
|
|
26041
|
+
}
|
|
26042
|
+
throw new LocalCliError({
|
|
26043
|
+
code: "BOOTSTRAP_NO_STATE",
|
|
26044
|
+
message: "No bootstrap state to reap.",
|
|
26045
|
+
hint: "Nothing to do. If a launch failed and its RG was deleted it may have orphaned an installer Owner@sub grant or the gateway's sub-scope reader roles \u2014 run 'm8t bootstrap reap --force' (or 'm8t bootstrap reap --sweep-orphans --yes') to clean them."
|
|
26046
|
+
});
|
|
25948
26047
|
}
|
|
25949
|
-
|
|
25950
|
-
|
|
25951
|
-
if (doc.status
|
|
26048
|
+
if (this.force !== true) {
|
|
26049
|
+
const doc = await readStatusBlob({ saName: state.statusSaName, resourceGroup: state.resourceGroup, subscriptionId: state.subscriptionId });
|
|
26050
|
+
if (doc.status !== "done") {
|
|
26051
|
+
if (doc.status === "failed") {
|
|
26052
|
+
throw new LocalCliError({
|
|
26053
|
+
code: "BOOTSTRAP_REAP_FAILED_INSTALL",
|
|
26054
|
+
message: `The install failed at '${doc.error?.phase ?? doc.phase}'. Leaving the installer up for diagnosis.`,
|
|
26055
|
+
hint: `Inspect: az container logs -n ${state.aciName} -g ${state.resourceGroup}. Reap anyway with --force.`
|
|
26056
|
+
});
|
|
26057
|
+
}
|
|
26058
|
+
const aci = await getAciState({ aciName: state.aciName, resourceGroup: state.resourceGroup, subscriptionId: state.subscriptionId }).catch(() => null);
|
|
26059
|
+
const deadHint = aci?.terminated ? `The installer container has terminated (exit ${String(aci.exitCode ?? "?")}) without reaching done \u2014 reap with --force.` : `Wait for 'm8t bootstrap status --watch' to reach done, or reap with --force if the install is stuck.`;
|
|
25952
26060
|
throw new LocalCliError({
|
|
25953
|
-
code: "
|
|
25954
|
-
message: `The install
|
|
25955
|
-
hint:
|
|
26061
|
+
code: "BOOTSTRAP_REAP_NOT_DONE",
|
|
26062
|
+
message: `The install is not done yet (status: ${doc.status}, phase: ${doc.phase}).`,
|
|
26063
|
+
hint: deadHint
|
|
25956
26064
|
});
|
|
25957
26065
|
}
|
|
25958
|
-
const aci = await getAciState({ aciName: state.aciName, resourceGroup: state.resourceGroup, subscriptionId: state.subscriptionId }).catch(() => null);
|
|
25959
|
-
const deadHint = aci?.terminated ? `The installer container has terminated (exit ${String(aci.exitCode ?? "?")}) without reaching done \u2014 reap with --force.` : `Wait for 'm8t bootstrap status --watch' to reach done, or reap with --force if the install is stuck.`;
|
|
25960
|
-
throw new LocalCliError({
|
|
25961
|
-
code: "BOOTSTRAP_REAP_NOT_DONE",
|
|
25962
|
-
message: `The install is not done yet (status: ${doc.status}, phase: ${doc.phase}).`,
|
|
25963
|
-
hint: deadHint
|
|
25964
|
-
});
|
|
25965
26066
|
}
|
|
25966
26067
|
await reapInstaller({
|
|
25967
26068
|
subscriptionId: state.subscriptionId,
|