@mutmutco/cli 2.17.0 → 2.18.0
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/index.cjs +903 -128
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3525,7 +3525,7 @@ function parseHookInput(stdin) {
|
|
|
3525
3525
|
}
|
|
3526
3526
|
|
|
3527
3527
|
// src/index.ts
|
|
3528
|
-
var
|
|
3528
|
+
var import_node_child_process7 = require("node:child_process");
|
|
3529
3529
|
var import_node_util6 = require("node:util");
|
|
3530
3530
|
var import_node_path8 = require("node:path");
|
|
3531
3531
|
var import_node_os3 = require("node:os");
|
|
@@ -5106,6 +5106,42 @@ function ghError(e) {
|
|
|
5106
5106
|
}
|
|
5107
5107
|
|
|
5108
5108
|
// src/gc.ts
|
|
5109
|
+
var WORKTREE_LOCK_RE = /EPERM|EBUSY|EACCES|ENOTEMPTY|permission denied|access is denied|used by another process|resource busy|directory not empty/i;
|
|
5110
|
+
function isWorktreeLockError(error) {
|
|
5111
|
+
return WORKTREE_LOCK_RE.test(error instanceof Error ? error.message : String(error));
|
|
5112
|
+
}
|
|
5113
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5114
|
+
async function removeWorktreeWithRecovery(wtPath, deps) {
|
|
5115
|
+
const maxAttempts = deps.maxAttempts ?? 3;
|
|
5116
|
+
const backoff = deps.backoffMs ?? [250, 1e3];
|
|
5117
|
+
let attempts = 0;
|
|
5118
|
+
let lastError;
|
|
5119
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
5120
|
+
attempts++;
|
|
5121
|
+
try {
|
|
5122
|
+
await deps.git(["worktree", "remove", "--force", wtPath]);
|
|
5123
|
+
return { status: "removed", attempts, recovery: i > 0 ? "retry" : void 0 };
|
|
5124
|
+
} catch (e) {
|
|
5125
|
+
lastError = e;
|
|
5126
|
+
const retriesLeft = i < maxAttempts - 1;
|
|
5127
|
+
if (retriesLeft && isWorktreeLockError(e)) {
|
|
5128
|
+
await deps.sleep(backoff[Math.min(i, backoff.length - 1)]);
|
|
5129
|
+
continue;
|
|
5130
|
+
}
|
|
5131
|
+
break;
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
if (deps.removeWorktreeDir) {
|
|
5135
|
+
try {
|
|
5136
|
+
await deps.removeWorktreeDir(wtPath);
|
|
5137
|
+
await deps.git(["worktree", "prune"]).catch(() => "");
|
|
5138
|
+
return { status: "removed", attempts, recovery: "fallback" };
|
|
5139
|
+
} catch (fallbackError) {
|
|
5140
|
+
lastError = fallbackError;
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
return { status: "failed", attempts, error: errorMessage(lastError) };
|
|
5144
|
+
}
|
|
5109
5145
|
function buildRemoteBranchCleanupReport(branch, input) {
|
|
5110
5146
|
if (!input.attempted) return { name: branch, status: "not-attempted", reason: input.reason };
|
|
5111
5147
|
if (input.existsAfter === true) return { name: branch, status: "failed", reason: "still-present-after-delete" };
|
|
@@ -5145,6 +5181,14 @@ function buildPrMergeResultPayload(input) {
|
|
|
5145
5181
|
worktree: input.localCleanup.worktree
|
|
5146
5182
|
};
|
|
5147
5183
|
}
|
|
5184
|
+
function buildPrMergeArgs(input) {
|
|
5185
|
+
const args = ["pr", "merge", input.number, ...input.repoArgs, input.method, "--delete-branch"];
|
|
5186
|
+
if (input.auto) args.push("--auto");
|
|
5187
|
+
return args;
|
|
5188
|
+
}
|
|
5189
|
+
function basePolicyBlocksImmediateMerge(message) {
|
|
5190
|
+
return /base branch policy prohibits|protected branch|required status check|merge queue/i.test(message);
|
|
5191
|
+
}
|
|
5148
5192
|
async function checkRemoteBranchExists(branch, deps, options = {}) {
|
|
5149
5193
|
if (!branch) return void 0;
|
|
5150
5194
|
try {
|
|
@@ -5394,14 +5438,18 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
5394
5438
|
stageTeardown = { status: "failed", error: errorMessage(e) };
|
|
5395
5439
|
}
|
|
5396
5440
|
}
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5441
|
+
const outcome = await removeWorktreeWithRecovery(wtPath, {
|
|
5442
|
+
git,
|
|
5443
|
+
sleep: options.sleep ?? defaultSleep,
|
|
5444
|
+
removeWorktreeDir: options.removeWorktreeDir
|
|
5445
|
+
});
|
|
5446
|
+
if (outcome.status === "removed") {
|
|
5447
|
+
report.worktree = { path: wtPath, status: "removed", stageTeardown, recovery: outcome.recovery };
|
|
5448
|
+
} else {
|
|
5401
5449
|
report.worktree = {
|
|
5402
5450
|
path: wtPath,
|
|
5403
5451
|
status: "failed",
|
|
5404
|
-
error:
|
|
5452
|
+
error: outcome.error,
|
|
5405
5453
|
safeCleanupCommand: safeWorktreeRemoveCommand(safeCwd, wtPath),
|
|
5406
5454
|
stageTeardown
|
|
5407
5455
|
};
|
|
@@ -5529,10 +5577,11 @@ function rcandVersionStep(targets) {
|
|
|
5529
5577
|
}
|
|
5530
5578
|
function trainPlan(command, options = {}) {
|
|
5531
5579
|
const isHub = options.repo?.toLowerCase() === "mutmutco/mmi-hub";
|
|
5580
|
+
const isDirect = options.releaseTrack === "direct" || options.releaseTrack === void 0 && isHub;
|
|
5532
5581
|
if (command === "rcand") {
|
|
5533
|
-
if (
|
|
5582
|
+
if (isDirect) {
|
|
5534
5583
|
return [
|
|
5535
|
-
{ label: "
|
|
5584
|
+
{ label: "direct-track repos skip rc; use /release from development instead", command: "mmi-cli release --apply", gated: true }
|
|
5536
5585
|
];
|
|
5537
5586
|
}
|
|
5538
5587
|
return [
|
|
@@ -5548,16 +5597,16 @@ function trainPlan(command, options = {}) {
|
|
|
5548
5597
|
];
|
|
5549
5598
|
}
|
|
5550
5599
|
if (command === "release") {
|
|
5551
|
-
if (
|
|
5600
|
+
if (isDirect) {
|
|
5552
5601
|
return [
|
|
5553
5602
|
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
5554
5603
|
{ label: "verify current branch is development", gated: true },
|
|
5555
5604
|
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
5556
5605
|
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
5557
|
-
{ label: "
|
|
5606
|
+
{ label: "direct-track release skips rc; verify any distribution bump is already landed on development", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
|
|
5558
5607
|
{ label: "merge development to main", gated: true },
|
|
5559
5608
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
5560
|
-
{ label: "trigger the
|
|
5609
|
+
{ label: "trigger the repo deploy path from the release event", command: "hub-serverless: deploy.yml + publish.yml auto-fire on the release; other models deploy via their own workflow", gated: true },
|
|
5561
5610
|
{ label: "roll development forward", gated: true }
|
|
5562
5611
|
];
|
|
5563
5612
|
}
|
|
@@ -5566,7 +5615,7 @@ function trainPlan(command, options = {}) {
|
|
|
5566
5615
|
{ label: "verify current branch is rc", gated: true },
|
|
5567
5616
|
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
5568
5617
|
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
5569
|
-
{ label: "verify every main-only hotfix commit is covered by the rc candidate", command: "
|
|
5618
|
+
{ label: "verify every main-only hotfix commit is covered by the rc candidate (the guard runs automatically inside the apply step below; --ack <sha> overrides a verified, trailer-less port)", command: "mmi-cli release --apply [--ack <sha>]", gated: true },
|
|
5570
5619
|
{ label: "merge rc to main", gated: true },
|
|
5571
5620
|
{ label: "for Hub distribution changes, verify the promoted SHA carries the release bump", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view; if missing: mmi-cli train prep --apply", gated: true },
|
|
5572
5621
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
@@ -5599,10 +5648,13 @@ function bootstrapPlan(repo, repoClass) {
|
|
|
5599
5648
|
function shellFor(platform = process.platform) {
|
|
5600
5649
|
return platform === "win32" ? "powershell" : "bash";
|
|
5601
5650
|
}
|
|
5651
|
+
function isCentralContainerModel(model) {
|
|
5652
|
+
return model === "tenant-container" || model === "solo-container";
|
|
5653
|
+
}
|
|
5602
5654
|
function deriveStageGap(inputs) {
|
|
5603
5655
|
const missing = [];
|
|
5604
|
-
if (inputs.deployModel
|
|
5605
|
-
return `local stage default applies to
|
|
5656
|
+
if (!isCentralContainerModel(inputs.deployModel)) {
|
|
5657
|
+
return `local stage default applies to central-container repos only (tenant-container/solo-container; registry deployModel = ${inputs.deployModel ?? "unset"})`;
|
|
5606
5658
|
}
|
|
5607
5659
|
if (!inputs.hasCompose) missing.push("docker-compose.yml");
|
|
5608
5660
|
if (!inputs.hasEnvExample) missing.push(".env.example");
|
|
@@ -6395,16 +6447,21 @@ async function runStage(config = {}, opts = {}) {
|
|
|
6395
6447
|
}
|
|
6396
6448
|
|
|
6397
6449
|
// src/project-model.ts
|
|
6398
|
-
var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-game", "non-deployable"];
|
|
6399
|
-
var DEPLOY_MODELS = ["hub-serverless", "serverless", "tenant-container", "content", "none"];
|
|
6450
|
+
var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-game", "non-deployable", "cli-tool", "worker"];
|
|
6451
|
+
var DEPLOY_MODELS = ["hub-serverless", "serverless", "tenant-container", "solo-container", "registry-publish", "content", "none"];
|
|
6452
|
+
var RELEASE_TRACKS = ["full", "direct", "trunk"];
|
|
6400
6453
|
var PROJECT_TYPE_SET = new Set(PROJECT_TYPES);
|
|
6401
6454
|
var DEPLOY_MODEL_SET = new Set(DEPLOY_MODELS);
|
|
6455
|
+
var RELEASE_TRACK_SET = new Set(RELEASE_TRACKS);
|
|
6402
6456
|
function isProjectType(value) {
|
|
6403
6457
|
return Boolean(value && PROJECT_TYPE_SET.has(value));
|
|
6404
6458
|
}
|
|
6405
6459
|
function isDeployModel(value) {
|
|
6406
6460
|
return Boolean(value && DEPLOY_MODEL_SET.has(value));
|
|
6407
6461
|
}
|
|
6462
|
+
function isReleaseTrack(value) {
|
|
6463
|
+
return Boolean(value && RELEASE_TRACK_SET.has(value));
|
|
6464
|
+
}
|
|
6408
6465
|
function repoIsHub(repo) {
|
|
6409
6466
|
return repo.toLowerCase().endsWith("/mmi-hub") || repo.toLowerCase() === "mmi-hub";
|
|
6410
6467
|
}
|
|
@@ -6413,6 +6470,8 @@ function resolveProjectTypeConfident(meta, repo) {
|
|
|
6413
6470
|
if (isProjectType(rawType)) return rawType;
|
|
6414
6471
|
if (meta?.class === "content" || meta?.deployModel === "content") return "content";
|
|
6415
6472
|
if (meta?.deployModel === "hub-serverless" || repoIsHub(repo)) return "hub-service";
|
|
6473
|
+
if (meta?.deployModel === "registry-publish") return "cli-tool";
|
|
6474
|
+
if (meta?.deployModel === "solo-container") return "worker";
|
|
6416
6475
|
if (meta?.deployModel === "none") return "non-deployable";
|
|
6417
6476
|
return void 0;
|
|
6418
6477
|
}
|
|
@@ -6426,10 +6485,22 @@ function resolveDeployModel(meta, repo) {
|
|
|
6426
6485
|
if (projectType === "content" || meta?.class === "content") return "content";
|
|
6427
6486
|
if (projectType === "hub-service" || repoIsHub(repo)) return "hub-serverless";
|
|
6428
6487
|
if (projectType === "desktop-game" || projectType === "non-deployable") return "none";
|
|
6488
|
+
if (projectType === "cli-tool") return "registry-publish";
|
|
6429
6489
|
return "tenant-container";
|
|
6430
6490
|
}
|
|
6431
6491
|
function projectTypeClearsWebProfile(projectType, deployModel) {
|
|
6432
|
-
return projectType === "content" || projectType === "desktop-game" || projectType === "non-deployable" || deployModel === "content" || deployModel === "none";
|
|
6492
|
+
return projectType === "content" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
|
|
6493
|
+
}
|
|
6494
|
+
function resolveReleaseTrack(meta) {
|
|
6495
|
+
const raw = typeof meta?.releaseTrack === "string" ? meta.releaseTrack : void 0;
|
|
6496
|
+
if (isReleaseTrack(raw)) return raw;
|
|
6497
|
+
if (meta?.class === "content" || meta?.deployModel === "content") return "trunk";
|
|
6498
|
+
return "full";
|
|
6499
|
+
}
|
|
6500
|
+
function branchesForTrack(track) {
|
|
6501
|
+
if (track === "trunk") return ["main"];
|
|
6502
|
+
if (track === "direct") return ["development", "main"];
|
|
6503
|
+
return ["development", "rc", "main"];
|
|
6433
6504
|
}
|
|
6434
6505
|
|
|
6435
6506
|
// src/train-apply.ts
|
|
@@ -6552,6 +6623,13 @@ var HUB_REPO2 = "mutmutco/MMI-Hub";
|
|
|
6552
6623
|
function isHubControlRepo(repo) {
|
|
6553
6624
|
return repo.toLowerCase() === HUB_REPO2.toLowerCase();
|
|
6554
6625
|
}
|
|
6626
|
+
async function loadProjectMeta(deps, ctx) {
|
|
6627
|
+
try {
|
|
6628
|
+
return JSON.parse(await deps.runSelf(["project", "get", ctx.repo]));
|
|
6629
|
+
} catch {
|
|
6630
|
+
return null;
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6555
6633
|
var CORRELATE_ATTEMPTS = 5;
|
|
6556
6634
|
var CORRELATE_DELAY_MS = 1500;
|
|
6557
6635
|
var CORRELATE_SKEW_SLACK_MS = 1e4;
|
|
@@ -6561,7 +6639,7 @@ var TRAIN_PROTECTION_CONTEXTS_JQ = "[.contexts[]]";
|
|
|
6561
6639
|
var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]';
|
|
6562
6640
|
var TRAIN_CHECK_ATTEMPTS = 40;
|
|
6563
6641
|
var TRAIN_CHECK_DELAY_MS = 15e3;
|
|
6564
|
-
async function
|
|
6642
|
+
async function correlateDispatchedRun(deps, workflow, since) {
|
|
6565
6643
|
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
6566
6644
|
const threshold = since - CORRELATE_SKEW_SLACK_MS;
|
|
6567
6645
|
for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
|
|
@@ -6574,7 +6652,7 @@ async function correlateTenantRun(deps, since) {
|
|
|
6574
6652
|
"--repo",
|
|
6575
6653
|
HUB_REPO2,
|
|
6576
6654
|
"--workflow",
|
|
6577
|
-
|
|
6655
|
+
workflow,
|
|
6578
6656
|
"--limit",
|
|
6579
6657
|
"10",
|
|
6580
6658
|
"--json",
|
|
@@ -6589,6 +6667,12 @@ async function correlateTenantRun(deps, since) {
|
|
|
6589
6667
|
}
|
|
6590
6668
|
return {};
|
|
6591
6669
|
}
|
|
6670
|
+
function correlateTenantRun(deps, since) {
|
|
6671
|
+
return correlateDispatchedRun(deps, "tenant-deploy.yml", since);
|
|
6672
|
+
}
|
|
6673
|
+
function correlatePublishRun(deps, since) {
|
|
6674
|
+
return correlateDispatchedRun(deps, "tenant-publish.yml", since);
|
|
6675
|
+
}
|
|
6592
6676
|
async function correlateWorkflowRun(deps, args) {
|
|
6593
6677
|
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
6594
6678
|
const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
|
|
@@ -6763,12 +6847,19 @@ async function resolveRcResumeTag(deps, base, sha) {
|
|
|
6763
6847
|
return { tag: newest, note };
|
|
6764
6848
|
}
|
|
6765
6849
|
async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha) {
|
|
6766
|
-
if (model === "tenant-container") {
|
|
6850
|
+
if (model === "tenant-container" || model === "solo-container") {
|
|
6851
|
+
const since = (deps.now ?? Date.now)();
|
|
6852
|
+
await deps.dispatchTenantDeploy({ repo: ctx.repo, slug: ctx.slug, ref, stage: stage2 });
|
|
6853
|
+
const { runId, runUrl } = await correlateTenantRun(deps, since);
|
|
6854
|
+
const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
6855
|
+
return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
6856
|
+
}
|
|
6857
|
+
if (model === "registry-publish") {
|
|
6767
6858
|
const since = (deps.now ?? Date.now)();
|
|
6768
6859
|
await deps.run("gh", [
|
|
6769
6860
|
"workflow",
|
|
6770
6861
|
"run",
|
|
6771
|
-
"tenant-
|
|
6862
|
+
"tenant-publish.yml",
|
|
6772
6863
|
"--repo",
|
|
6773
6864
|
HUB_REPO2,
|
|
6774
6865
|
"-f",
|
|
@@ -6780,9 +6871,9 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
|
|
|
6780
6871
|
"-f",
|
|
6781
6872
|
`stage=${stage2}`
|
|
6782
6873
|
]);
|
|
6783
|
-
const { runId, runUrl } = await
|
|
6874
|
+
const { runId, runUrl } = await correlatePublishRun(deps, since);
|
|
6784
6875
|
const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
6785
|
-
return { note: `dispatched tenant-
|
|
6876
|
+
return { note: `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
6786
6877
|
}
|
|
6787
6878
|
if (model === "hub-serverless") {
|
|
6788
6879
|
const note = ref === "rc" ? "no manual dispatch: deploy.yml auto-fires on the rc push (rc stage)" : "no manual dispatch: deploy.yml + publish.yml auto-fire on the published Release (prod)";
|
|
@@ -6813,13 +6904,7 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
|
|
|
6813
6904
|
}
|
|
6814
6905
|
return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
|
|
6815
6906
|
}
|
|
6816
|
-
async function preflight(deps, ctx, stage2) {
|
|
6817
|
-
let meta = null;
|
|
6818
|
-
try {
|
|
6819
|
-
meta = JSON.parse(await deps.runSelf(["project", "get", ctx.repo]));
|
|
6820
|
-
} catch {
|
|
6821
|
-
meta = null;
|
|
6822
|
-
}
|
|
6907
|
+
async function preflight(deps, ctx, stage2, meta) {
|
|
6823
6908
|
const model = resolveDeployModel2(meta, ctx.repo);
|
|
6824
6909
|
if (model === "content") {
|
|
6825
6910
|
throw new Error(`${ctx.repo} is a content repo (deployModel=content) \u2014 the release train does not apply (trunk-based; PR to main)`);
|
|
@@ -6835,17 +6920,19 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6835
6920
|
const ctx = await buildTrainApplyContext(deps);
|
|
6836
6921
|
await requireCleanTree(deps);
|
|
6837
6922
|
await deps.run("git", ["fetch", "origin"]);
|
|
6923
|
+
const meta = await loadProjectMeta(deps, ctx);
|
|
6924
|
+
const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta) === "direct";
|
|
6838
6925
|
if (command === "rcand") {
|
|
6839
6926
|
await requireBranch(deps, "development");
|
|
6927
|
+
if (directTrack) {
|
|
6928
|
+
throw new Error("direct-track repos release straight to main (no rc); run `mmi-cli release --apply` from development instead of rcand");
|
|
6929
|
+
}
|
|
6840
6930
|
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
6841
6931
|
ensurePositiveCount(
|
|
6842
6932
|
await deps.run("git", ["rev-list", "--count", "origin/rc..origin/development"]),
|
|
6843
6933
|
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
6844
6934
|
);
|
|
6845
|
-
const deployModel2 = await preflight(deps, ctx, "rc");
|
|
6846
|
-
if (isHubControlRepo(ctx.repo) && deployModel2 === "hub-serverless") {
|
|
6847
|
-
throw new Error("MMI-Hub releases directly from development to main; run mmi-cli release --apply from development instead of rcand");
|
|
6848
|
-
}
|
|
6935
|
+
const deployModel2 = await preflight(deps, ctx, "rc", meta);
|
|
6849
6936
|
const releaseBase = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
|
|
6850
6937
|
await verifyHubDistributionVersion(deps, deployModel2, releaseBase);
|
|
6851
6938
|
await deps.run("git", ["checkout", "rc"]);
|
|
@@ -6863,17 +6950,14 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6863
6950
|
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince2, rcSha);
|
|
6864
6951
|
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, workflowRuns: d2.workflowRuns, deployStatus: d2.deployStatus };
|
|
6865
6952
|
}
|
|
6866
|
-
if (
|
|
6953
|
+
if (directTrack) {
|
|
6867
6954
|
await requireBranch(deps, "development");
|
|
6868
6955
|
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
6869
6956
|
ensurePositiveCount(
|
|
6870
6957
|
await deps.run("git", ["rev-list", "--count", "origin/main..origin/development"]),
|
|
6871
6958
|
"nothing to release: origin/development is not ahead of origin/main"
|
|
6872
6959
|
);
|
|
6873
|
-
const deployModel2 = await preflight(deps, ctx, "main");
|
|
6874
|
-
if (deployModel2 !== "hub-serverless") {
|
|
6875
|
-
throw new Error(`MMI-Hub direct release requires deployModel=hub-serverless, got ${deployModel2}`);
|
|
6876
|
-
}
|
|
6960
|
+
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
6877
6961
|
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
6878
6962
|
await verifyHubDistributionVersion(deps, deployModel2, tag2);
|
|
6879
6963
|
const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
|
|
@@ -6918,7 +7002,7 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6918
7002
|
workflowRuns: d2.workflowRuns,
|
|
6919
7003
|
deployStatus: d2.deployStatus,
|
|
6920
7004
|
rcRetirement: "not-applicable",
|
|
6921
|
-
rcRetirementNote: "
|
|
7005
|
+
rcRetirementNote: "direct-track release skips rc; no rc runtime to retire",
|
|
6922
7006
|
announceNote: announceNote2,
|
|
6923
7007
|
release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 }
|
|
6924
7008
|
};
|
|
@@ -6928,7 +7012,7 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6928
7012
|
await deps.run("git", ["rev-list", "--count", "origin/main..origin/rc"]),
|
|
6929
7013
|
"nothing to release: origin/rc is not ahead of origin/main"
|
|
6930
7014
|
);
|
|
6931
|
-
const deployModel = await preflight(deps, ctx, "main");
|
|
7015
|
+
const deployModel = await preflight(deps, ctx, "main", meta);
|
|
6932
7016
|
const predicted = await predictMergeConflicts(deps, "origin/main", "origin/rc");
|
|
6933
7017
|
const predictedNonSpine = predicted.filter((f) => !isSpinePath(f));
|
|
6934
7018
|
if (predictedNonSpine.length > 0) {
|
|
@@ -6936,6 +7020,13 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6936
7020
|
`rc -> main merge would conflict on non-spine path(s): ${predictedNonSpine.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and rc via an approved alignment PR (do not hand-resolve on main), then rerun release.`
|
|
6937
7021
|
);
|
|
6938
7022
|
}
|
|
7023
|
+
const coverage = deps.hotfixCoverage({ mainRef: "origin/main", rcRef: "origin/rc", ack: options.ack ?? [] });
|
|
7024
|
+
if (!coverage.ok) {
|
|
7025
|
+
const list = coverage.uncovered.map((c) => `${c.sha.slice(0, 8)} ${c.subject}`).join("; ");
|
|
7026
|
+
throw new Error(
|
|
7027
|
+
`hotfix-coverage: ${coverage.uncovered.length} main-only commit(s) not proven in origin/rc \u2014 the candidate would revert a prod hotfix: ${list}. Re-cut /rcand from development, or have the authorized human verify the content is in the candidate and rerun release with --ack <sha>[,<sha>\u2026].`
|
|
7028
|
+
);
|
|
7029
|
+
}
|
|
6939
7030
|
const releasedRcSha = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
6940
7031
|
await deps.run("git", ["checkout", "main"]);
|
|
6941
7032
|
await deps.run("git", ["pull", "--ff-only", "origin", "main"]);
|
|
@@ -7015,6 +7106,49 @@ function retireCategoryFrom(text) {
|
|
|
7015
7106
|
return void 0;
|
|
7016
7107
|
}
|
|
7017
7108
|
}
|
|
7109
|
+
var TRANSPORT_REASONS = /* @__PURE__ */ new Set(["invalid-instance", "invalid-document", "throttled", "timeout", "other"]);
|
|
7110
|
+
function retireReasonFrom(text) {
|
|
7111
|
+
try {
|
|
7112
|
+
const r = JSON.parse(text).reason;
|
|
7113
|
+
return typeof r === "string" && TRANSPORT_REASONS.has(r) ? r : void 0;
|
|
7114
|
+
} catch {
|
|
7115
|
+
return void 0;
|
|
7116
|
+
}
|
|
7117
|
+
}
|
|
7118
|
+
function isRetryableTransport(reason) {
|
|
7119
|
+
return reason === void 0 || reason === "throttled" || reason === "timeout" || reason === "other";
|
|
7120
|
+
}
|
|
7121
|
+
var RETIRE_MAX_ATTEMPTS = 3;
|
|
7122
|
+
var RETIRE_BACKOFF_MS = 1500;
|
|
7123
|
+
async function attemptRetire(deps, ctx) {
|
|
7124
|
+
try {
|
|
7125
|
+
const out = await deps.runSelf(["tenant", "control", ctx.repo, "rc", "retire"]);
|
|
7126
|
+
let commandId = "";
|
|
7127
|
+
let category = retireCategoryFrom(out);
|
|
7128
|
+
try {
|
|
7129
|
+
commandId = String(JSON.parse(out).commandId ?? "");
|
|
7130
|
+
} catch {
|
|
7131
|
+
}
|
|
7132
|
+
if (category === "retired-edge-pending") {
|
|
7133
|
+
return { result: {
|
|
7134
|
+
status: "retired",
|
|
7135
|
+
category,
|
|
7136
|
+
note: `rc runtime retired; edge vhost reconcile pending (tenant control retire${commandId ? `, command ${commandId}` : ""}) \u2014 registry coords kept`
|
|
7137
|
+
} };
|
|
7138
|
+
}
|
|
7139
|
+
category = category ?? "retired";
|
|
7140
|
+
return { result: {
|
|
7141
|
+
status: "retired",
|
|
7142
|
+
category,
|
|
7143
|
+
note: `rc runtime retired (tenant control retire${commandId ? `, command ${commandId}` : ""}) \u2014 registry coords kept; /rcand or tenant redeploy recreates rc next cycle`
|
|
7144
|
+
} };
|
|
7145
|
+
} catch (e) {
|
|
7146
|
+
const err = e;
|
|
7147
|
+
const category = retireCategoryFrom(err.stdout ?? "") ?? "transport-failed";
|
|
7148
|
+
const reason = retireReasonFrom(err.stdout ?? "");
|
|
7149
|
+
return { result: { status: "failed", category, note: `rc retirement failed (the release itself succeeded): ${err.message}` }, reason, message: err.message };
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7018
7152
|
async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
7019
7153
|
if (model !== "tenant-container") {
|
|
7020
7154
|
return { status: "not-applicable", note: `${model} has no co-resident rc runtime to retire` };
|
|
@@ -7037,30 +7171,22 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
7037
7171
|
note: `origin/rc moved past the released candidate (${releasedRcSha.slice(0, 7)} -> ${rcNow.slice(0, 7)}) \u2014 a new candidate is in flight; rc runtime left untouched`
|
|
7038
7172
|
};
|
|
7039
7173
|
}
|
|
7040
|
-
const
|
|
7041
|
-
let
|
|
7042
|
-
let
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
}
|
|
7054
|
-
category = category ?? "retired";
|
|
7055
|
-
return {
|
|
7056
|
-
status: "retired",
|
|
7057
|
-
category,
|
|
7058
|
-
note: `rc runtime retired (tenant control retire${commandId ? `, command ${commandId}` : ""}) \u2014 registry coords kept; /rcand or tenant redeploy recreates rc next cycle`
|
|
7059
|
-
};
|
|
7174
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
7175
|
+
let last;
|
|
7176
|
+
for (let attempt = 1; attempt <= RETIRE_MAX_ATTEMPTS; attempt++) {
|
|
7177
|
+
last = await attemptRetire(deps, ctx);
|
|
7178
|
+
if (last.result.status === "retired") return last.result;
|
|
7179
|
+
const retryable = last.result.category === "transport-failed" && isRetryableTransport(last.reason);
|
|
7180
|
+
if (!retryable || attempt === RETIRE_MAX_ATTEMPTS) break;
|
|
7181
|
+
await sleep(RETIRE_BACKOFF_MS * attempt);
|
|
7182
|
+
}
|
|
7183
|
+
const f = last;
|
|
7184
|
+
const reasonSuffix = f.reason ? ` [reason: ${f.reason}]` : "";
|
|
7185
|
+
const note = `rc retirement failed (the release itself succeeded)${reasonSuffix}: ${f.message ?? f.result.note}. The rc runtime may be orphaned on the box \u2014 retire it with: mmi-cli tenant control ${ctx.repo} rc retire (or sweep all: mmi-cli tenant sweep-rc --retire --yes)`;
|
|
7186
|
+
return { status: "failed", category: f.result.category, note };
|
|
7060
7187
|
} catch (e) {
|
|
7061
7188
|
const err = e;
|
|
7062
|
-
|
|
7063
|
-
return { status: "failed", category, note: `rc retirement failed (the release itself succeeded): ${err.message}` };
|
|
7189
|
+
return { status: "failed", category: "transport-failed", note: `rc retirement failed (the release itself succeeded): ${err.message}` };
|
|
7064
7190
|
}
|
|
7065
7191
|
}
|
|
7066
7192
|
async function runTenantRedeploy(deps, options) {
|
|
@@ -7082,13 +7208,86 @@ async function runTenantRedeploy(deps, options) {
|
|
|
7082
7208
|
meta = null;
|
|
7083
7209
|
}
|
|
7084
7210
|
const deployModel = resolveDeployModel2(meta, repo);
|
|
7085
|
-
if (deployModel !== "tenant-container") {
|
|
7086
|
-
throw new Error(`${repo} is ${deployModel}, not tenant-container \u2014 there is no central tenant-deploy run to retry (its deploy fires from its own workflow)`);
|
|
7211
|
+
if (deployModel !== "tenant-container" && deployModel !== "solo-container") {
|
|
7212
|
+
throw new Error(`${repo} is ${deployModel}, not a central-container model (tenant-container/solo-container) \u2014 there is no central tenant-deploy run to retry (its deploy fires from its own workflow)`);
|
|
7087
7213
|
}
|
|
7088
7214
|
const d = await dispatchDeploy(deps, ctx, stage2, ref, deployModel, watch);
|
|
7089
7215
|
return { ...ctx, command: "tenant-redeploy", stage: stage2, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, workflowRuns: d.workflowRuns, deployStatus: d.deployStatus };
|
|
7090
7216
|
}
|
|
7091
7217
|
|
|
7218
|
+
// src/hotfix-coverage.ts
|
|
7219
|
+
var import_node_child_process6 = require("node:child_process");
|
|
7220
|
+
var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
7221
|
+
function checkHotfixCoverage(options = {}) {
|
|
7222
|
+
const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
|
|
7223
|
+
const ack = (options.ack ?? []).filter(Boolean);
|
|
7224
|
+
const git = options.git ?? ((args, opts) => (0, import_node_child_process6.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
|
|
7225
|
+
const revList = (range) => {
|
|
7226
|
+
const out = git(["rev-list", "--no-merges", range]).trim();
|
|
7227
|
+
return out ? out.split("\n") : [];
|
|
7228
|
+
};
|
|
7229
|
+
const isAncestor = (sha, ref) => {
|
|
7230
|
+
try {
|
|
7231
|
+
git(["merge-base", "--is-ancestor", sha, ref]);
|
|
7232
|
+
return true;
|
|
7233
|
+
} catch {
|
|
7234
|
+
return false;
|
|
7235
|
+
}
|
|
7236
|
+
};
|
|
7237
|
+
const patchId = (sha) => {
|
|
7238
|
+
const diff = git(["show", "--no-color", "--pretty=format:", sha]);
|
|
7239
|
+
if (!diff.trim()) return null;
|
|
7240
|
+
const out = git(["patch-id", "--stable"], { input: diff }).trim();
|
|
7241
|
+
return out ? out.split(" ")[0] : null;
|
|
7242
|
+
};
|
|
7243
|
+
const changedPaths = (sha) => {
|
|
7244
|
+
const out = git(["show", "--no-color", "--name-only", "--pretty=format:", sha]).trim();
|
|
7245
|
+
return out ? out.split("\n") : [];
|
|
7246
|
+
};
|
|
7247
|
+
const cherrySources = (sha) => {
|
|
7248
|
+
const message = git(["log", "-1", "--format=%B", sha]);
|
|
7249
|
+
return [...message.matchAll(CHERRY_TRAILER)].map((m) => m[1]);
|
|
7250
|
+
};
|
|
7251
|
+
const mainOnly = revList(`${rcRef}..${mainRef}`);
|
|
7252
|
+
const rcSidePatchIds = /* @__PURE__ */ new Map();
|
|
7253
|
+
let rcSideBuilt = false;
|
|
7254
|
+
const buildRcSide = () => {
|
|
7255
|
+
if (rcSideBuilt) return;
|
|
7256
|
+
rcSideBuilt = true;
|
|
7257
|
+
for (const sha of revList(`${mainRef}..${rcRef}`)) {
|
|
7258
|
+
const id = patchId(sha);
|
|
7259
|
+
if (id) rcSidePatchIds.set(id, sha);
|
|
7260
|
+
}
|
|
7261
|
+
};
|
|
7262
|
+
const commits = mainOnly.map((sha) => {
|
|
7263
|
+
const subject = git(["log", "-1", "--format=%s", sha]).trim();
|
|
7264
|
+
const paths = changedPaths(sha);
|
|
7265
|
+
if (paths.length > 0 && paths.every((p) => manifestPaths.includes(p))) {
|
|
7266
|
+
return { sha, subject, coverage: "exempt-distribution" };
|
|
7267
|
+
}
|
|
7268
|
+
const sources = cherrySources(sha);
|
|
7269
|
+
if (sources.length > 0) {
|
|
7270
|
+
if (sources.every((s) => isAncestor(s, rcRef))) {
|
|
7271
|
+
return { sha, subject, coverage: "trailer", sources };
|
|
7272
|
+
}
|
|
7273
|
+
if (ack.some((a) => sha.startsWith(a))) return { sha, subject, coverage: "acked", sources };
|
|
7274
|
+
return { sha, subject, coverage: "uncovered", sources };
|
|
7275
|
+
}
|
|
7276
|
+
const id = patchId(sha);
|
|
7277
|
+
if (id) {
|
|
7278
|
+
buildRcSide();
|
|
7279
|
+
const matchedRcSha = rcSidePatchIds.get(id);
|
|
7280
|
+
if (matchedRcSha) {
|
|
7281
|
+
return { sha, subject, coverage: "patch-id", matchedRcSha };
|
|
7282
|
+
}
|
|
7283
|
+
}
|
|
7284
|
+
if (ack.some((a) => sha.startsWith(a))) return { sha, subject, coverage: "acked" };
|
|
7285
|
+
return { sha, subject, coverage: "uncovered" };
|
|
7286
|
+
});
|
|
7287
|
+
const uncovered = commits.filter((c) => c.coverage === "uncovered");
|
|
7288
|
+
return { ok: uncovered.length === 0, mainRef, rcRef, commits, uncovered };
|
|
7289
|
+
}
|
|
7290
|
+
|
|
7092
7291
|
// src/train-prep.ts
|
|
7093
7292
|
function clean2(text) {
|
|
7094
7293
|
return text.trim();
|
|
@@ -7117,6 +7316,69 @@ async function runTrainPrep(deps, options = {}) {
|
|
|
7117
7316
|
return { target, version, applied: true, command: "mmi-cli pr create --base development --head <current-branch>", stagedFiles };
|
|
7118
7317
|
}
|
|
7119
7318
|
|
|
7319
|
+
// src/tenant-sweep.ts
|
|
7320
|
+
function isRcBearingTenant(p) {
|
|
7321
|
+
if ((p.deployModel ?? "") !== "tenant-container") return false;
|
|
7322
|
+
return (p.releaseTrack ?? "full") === "full";
|
|
7323
|
+
}
|
|
7324
|
+
function pickRepo(p) {
|
|
7325
|
+
const repos = p.repos ?? [];
|
|
7326
|
+
if (repos.length === 0) return null;
|
|
7327
|
+
const bySlug = repos.find((r) => r.split("/")[1]?.toLowerCase() === (p.slug ?? "").toLowerCase());
|
|
7328
|
+
return bySlug ?? repos[0];
|
|
7329
|
+
}
|
|
7330
|
+
async function sweepRcOrphans(deps, opts) {
|
|
7331
|
+
const projects = await deps.listProjects();
|
|
7332
|
+
if (!projects) throw new Error("project list unavailable \u2014 Hub unreachable or this repo is not bootstrapped");
|
|
7333
|
+
const targets = projects.filter(isRcBearingTenant);
|
|
7334
|
+
const stages = [];
|
|
7335
|
+
for (const p of targets) {
|
|
7336
|
+
const repo = pickRepo(p);
|
|
7337
|
+
if (!repo) continue;
|
|
7338
|
+
const slug = p.slug ?? "";
|
|
7339
|
+
let serviceState;
|
|
7340
|
+
try {
|
|
7341
|
+
serviceState = (await deps.status(repo)).serviceState || "unknown";
|
|
7342
|
+
} catch (e) {
|
|
7343
|
+
stages.push({ repo, slug, serviceState: "error", orphanCandidate: false, detail: e.message });
|
|
7344
|
+
continue;
|
|
7345
|
+
}
|
|
7346
|
+
const orphanCandidate = serviceState === "running";
|
|
7347
|
+
const stage2 = { repo, slug, serviceState, orphanCandidate };
|
|
7348
|
+
if (orphanCandidate && opts.retire) {
|
|
7349
|
+
try {
|
|
7350
|
+
const r = await deps.retire(repo);
|
|
7351
|
+
stage2.retired = r.ok ? "retired" : "failed";
|
|
7352
|
+
stage2.category = r.category;
|
|
7353
|
+
stage2.reason = r.reason;
|
|
7354
|
+
} catch (e) {
|
|
7355
|
+
stage2.retired = "failed";
|
|
7356
|
+
stage2.detail = e.message;
|
|
7357
|
+
}
|
|
7358
|
+
}
|
|
7359
|
+
stages.push(stage2);
|
|
7360
|
+
}
|
|
7361
|
+
return {
|
|
7362
|
+
scanned: stages.length,
|
|
7363
|
+
running: stages.filter((s) => s.orphanCandidate && s.retired !== "retired").length,
|
|
7364
|
+
stages,
|
|
7365
|
+
retireAttempted: !!opts.retire
|
|
7366
|
+
};
|
|
7367
|
+
}
|
|
7368
|
+
function renderSweep(r) {
|
|
7369
|
+
const lines = [`tenant sweep-rc: scanned ${r.scanned} tenant-container(s); ${r.running} rc runtime(s) running`];
|
|
7370
|
+
for (const s of r.stages) {
|
|
7371
|
+
let line = ` ${s.repo} rc: ${s.orphanCandidate ? "RUNNING" : s.serviceState}`;
|
|
7372
|
+
if (s.retired) line += ` -> ${s.retired}${s.category ? ` (${s.category}${s.reason ? `/${s.reason}` : ""})` : ""}`;
|
|
7373
|
+
if (s.detail) line += ` \u2014 ${s.detail}`;
|
|
7374
|
+
lines.push(line);
|
|
7375
|
+
}
|
|
7376
|
+
if (r.running > 0 && !r.retireAttempted) {
|
|
7377
|
+
lines.push("Retire an orphan with: mmi-cli tenant control <owner/repo> rc retire (or sweep all running: mmi-cli tenant sweep-rc --retire --yes)");
|
|
7378
|
+
}
|
|
7379
|
+
return lines.join("\n");
|
|
7380
|
+
}
|
|
7381
|
+
|
|
7120
7382
|
// src/hotfix-apply.ts
|
|
7121
7383
|
var HOTFIX_RELEASE_WORKFLOWS = ["deploy.yml", "publish.yml"];
|
|
7122
7384
|
var HOTFIX_RUN_FIND_ATTEMPTS = 10;
|
|
@@ -7605,6 +7867,18 @@ function ensurePortRange(repo, path2) {
|
|
|
7605
7867
|
function portCursorSeed(registry2) {
|
|
7606
7868
|
return nextPortBlock(registry2)[0];
|
|
7607
7869
|
}
|
|
7870
|
+
function metaPortRange(meta) {
|
|
7871
|
+
const r = meta?.portRange;
|
|
7872
|
+
if (r && typeof r.start === "number" && typeof r.end === "number") return [r.start, r.end];
|
|
7873
|
+
return null;
|
|
7874
|
+
}
|
|
7875
|
+
function decidePortRange(input) {
|
|
7876
|
+
if (!input.metaReadOk) {
|
|
7877
|
+
return { action: "fail", reason: "could not verify the existing port block (Hub registry read failed) \u2014 retry; NOT allocating (a re-allocation on an unverified read would advance the cursor and hand out a duplicate block)" };
|
|
7878
|
+
}
|
|
7879
|
+
if (input.metaPortRange) return { action: "return", range: input.metaPortRange };
|
|
7880
|
+
return { action: "allocate" };
|
|
7881
|
+
}
|
|
7608
7882
|
function existingPortRange(repo, registry2) {
|
|
7609
7883
|
return registry2[repo] ?? null;
|
|
7610
7884
|
}
|
|
@@ -7884,7 +8158,8 @@ var requiredProjectWorkflows = [
|
|
|
7884
8158
|
];
|
|
7885
8159
|
var requiredOrgRulesetTypes = ["pull_request", "non_fast_forward", "deletion"];
|
|
7886
8160
|
var requiredHubStatusChecks = ["cli", "infra", "docs"];
|
|
7887
|
-
function expectedBranches(repoClass) {
|
|
8161
|
+
function expectedBranches(repoClass, releaseTrack) {
|
|
8162
|
+
if (isReleaseTrack(releaseTrack)) return branchesForTrack(releaseTrack);
|
|
7888
8163
|
return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
|
|
7889
8164
|
}
|
|
7890
8165
|
function gcpProjectForSlug(slug) {
|
|
@@ -7977,9 +8252,9 @@ function localRegistryCheck(deps, path2, predicate) {
|
|
|
7977
8252
|
if (text == null) return null;
|
|
7978
8253
|
return predicate(safeJson2(text, null));
|
|
7979
8254
|
}
|
|
7980
|
-
async function verifyBootstrap(repo, repoClass, deps) {
|
|
7981
|
-
const branchesWanted = expectedBranches(repoClass);
|
|
7982
|
-
const baseBranch = repoClass === "content" ? "main" : "development";
|
|
8255
|
+
async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
8256
|
+
const branchesWanted = expectedBranches(repoClass, releaseTrack);
|
|
8257
|
+
const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
|
|
7983
8258
|
const checks = [];
|
|
7984
8259
|
const repoInfo = await restJson2(deps, `repos/${repo}`, {});
|
|
7985
8260
|
checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
|
|
@@ -8281,6 +8556,15 @@ function parseOwnerRepo(repo) {
|
|
|
8281
8556
|
if (owner.includes("\\") || name.includes("\\")) throw new Error("repo must be owner/repo");
|
|
8282
8557
|
return { owner, name, slug: name.toLowerCase(), fullName: `${owner}/${name}` };
|
|
8283
8558
|
}
|
|
8559
|
+
var DEFAULT_INSTALL_CMD = "npm ci";
|
|
8560
|
+
function withDerivedRepoVars(vars, parsed, cls) {
|
|
8561
|
+
const out = { ...vars };
|
|
8562
|
+
out.REPO_NAME ??= parsed.name;
|
|
8563
|
+
out.REPO_SLUG ??= parsed.slug;
|
|
8564
|
+
out.CLASS ??= cls;
|
|
8565
|
+
out.INSTALL_CMD ??= DEFAULT_INSTALL_CMD;
|
|
8566
|
+
return out;
|
|
8567
|
+
}
|
|
8284
8568
|
function planSeedAction(seed, exists) {
|
|
8285
8569
|
if (seed.source === "fanout") {
|
|
8286
8570
|
return { target: seed.target, action: "skip", ownership: "fanout", reason: "delivered by the fanout pipeline" };
|
|
@@ -8293,6 +8577,18 @@ function planSeedAction(seed, exists) {
|
|
|
8293
8577
|
}
|
|
8294
8578
|
return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-owned, refresh to current" } : { target: seed.target, action: "create", ownership: "org", reason: "org-owned, missing" };
|
|
8295
8579
|
}
|
|
8580
|
+
function reconcileSeedAction(action, content, isManagedBlock) {
|
|
8581
|
+
if (action.action === "skip") return action;
|
|
8582
|
+
if (content == null) {
|
|
8583
|
+
return { ...action, action: "skip", reason: "no resolvable content" };
|
|
8584
|
+
}
|
|
8585
|
+
if (isManagedBlock) return action;
|
|
8586
|
+
const unfilled = missingPlaceholders(content);
|
|
8587
|
+
if (unfilled.length) {
|
|
8588
|
+
return { ...action, action: "skip", reason: `unfilled: ${unfilled.join(", ")} \u2014 pass --var` };
|
|
8589
|
+
}
|
|
8590
|
+
return action;
|
|
8591
|
+
}
|
|
8296
8592
|
function renderSeedPlan(actions) {
|
|
8297
8593
|
const lines = ["bootstrap apply \u2014 seed plan (dry-run; no mutations):"];
|
|
8298
8594
|
for (const a of actions) {
|
|
@@ -8302,6 +8598,20 @@ function renderSeedPlan(actions) {
|
|
|
8302
8598
|
lines.push(` \u2014 ${order.map((k) => `${actions.filter((a) => a.action === k).length} ${k}`).join(", ")}`);
|
|
8303
8599
|
return lines.join("\n");
|
|
8304
8600
|
}
|
|
8601
|
+
var GITHUB_DEFAULT_LABELS = [
|
|
8602
|
+
"documentation",
|
|
8603
|
+
"duplicate",
|
|
8604
|
+
"enhancement",
|
|
8605
|
+
"good first issue",
|
|
8606
|
+
"help wanted",
|
|
8607
|
+
"invalid",
|
|
8608
|
+
"question",
|
|
8609
|
+
"wontfix"
|
|
8610
|
+
];
|
|
8611
|
+
function labelsToPrune(orgLabelNames) {
|
|
8612
|
+
const org = new Set(orgLabelNames);
|
|
8613
|
+
return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
|
|
8614
|
+
}
|
|
8305
8615
|
function resolveSeedContent(seed, vars, readFile2) {
|
|
8306
8616
|
if (seed.source === "self") return readFile2(seed.target);
|
|
8307
8617
|
if (seed.source.startsWith("seed:")) {
|
|
@@ -8314,10 +8624,13 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
8314
8624
|
const parsedRepo = parseOwnerRepo(repo);
|
|
8315
8625
|
const slug = parsedRepo.slug;
|
|
8316
8626
|
if (options.projectType && !isProjectType(options.projectType)) {
|
|
8317
|
-
throw new Error(
|
|
8627
|
+
throw new Error(`projectType must be one of: ${PROJECT_TYPES.join(", ")}`);
|
|
8318
8628
|
}
|
|
8319
8629
|
if (options.deployModel && !isDeployModel(options.deployModel)) {
|
|
8320
|
-
throw new Error(
|
|
8630
|
+
throw new Error(`deployModel must be one of: ${DEPLOY_MODELS.join(", ")}`);
|
|
8631
|
+
}
|
|
8632
|
+
if (options.releaseTrack && !isReleaseTrack(options.releaseTrack)) {
|
|
8633
|
+
throw new Error("releaseTrack must be full, direct, or trunk");
|
|
8321
8634
|
}
|
|
8322
8635
|
const shape = {
|
|
8323
8636
|
class: cls,
|
|
@@ -8327,7 +8640,7 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
8327
8640
|
const projectType = resolveProjectTypeConfident(shape, parsedRepo.fullName);
|
|
8328
8641
|
if (!projectType) {
|
|
8329
8642
|
throw new Error(
|
|
8330
|
-
`Project type for ${parsedRepo.fullName} is unset and not derivable \u2014 pass --project-type
|
|
8643
|
+
`Project type for ${parsedRepo.fullName} is unset and not derivable \u2014 pass --project-type <${PROJECT_TYPES.join("|")}> and --deploy-model <${DEPLOY_MODELS.join("|")}> (prevents defaulting a non-web repo to tenant-container).`
|
|
8331
8644
|
);
|
|
8332
8645
|
}
|
|
8333
8646
|
const deployModel = resolveDeployModel({ ...shape, projectType }, parsedRepo.fullName);
|
|
@@ -8359,6 +8672,8 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
8359
8672
|
class: cls,
|
|
8360
8673
|
projectType,
|
|
8361
8674
|
deployModel,
|
|
8675
|
+
// #917: only emit when explicitly direct/trunk; absent → registry resolves to `full` (no clobber).
|
|
8676
|
+
releaseTrack: isReleaseTrack(options.releaseTrack) ? options.releaseTrack : void 0,
|
|
8362
8677
|
// Board coords (from GraphQL at bootstrap, passed as --var by the skill).
|
|
8363
8678
|
projectOwner: vars.PROJECT_OWNER || void 0,
|
|
8364
8679
|
projectNumber: num(vars.PROJECT_NUMBER),
|
|
@@ -8374,6 +8689,83 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
8374
8689
|
for (const k of Object.keys(payload)) if (payload[k] === void 0) delete payload[k];
|
|
8375
8690
|
return payload;
|
|
8376
8691
|
}
|
|
8692
|
+
var BOARD_FIELD_VAR_MAP = {
|
|
8693
|
+
Status: {
|
|
8694
|
+
fieldIdVar: "STATUS_FIELD_ID",
|
|
8695
|
+
options: { Todo: "STATUS_TODO", "In Progress": "STATUS_IN_PROGRESS", "In Review": "STATUS_IN_REVIEW", Done: "STATUS_DONE" }
|
|
8696
|
+
},
|
|
8697
|
+
Priority: {
|
|
8698
|
+
fieldIdVar: "PRIORITY_FIELD_ID",
|
|
8699
|
+
options: { Urgent: "PRIORITY_URGENT", High: "PRIORITY_HIGH", Medium: "PRIORITY_MEDIUM", Low: "PRIORITY_LOW" }
|
|
8700
|
+
}
|
|
8701
|
+
};
|
|
8702
|
+
function extractBoardFieldVars(fieldsJson) {
|
|
8703
|
+
const out = {};
|
|
8704
|
+
const nodes = Array.isArray(fieldsJson) ? fieldsJson : fieldsJson?.data?.node?.fields?.nodes ?? fieldsJson?.node?.fields?.nodes;
|
|
8705
|
+
if (!Array.isArray(nodes)) return out;
|
|
8706
|
+
for (const node of nodes) {
|
|
8707
|
+
const field = node;
|
|
8708
|
+
const name = typeof field.name === "string" ? field.name : void 0;
|
|
8709
|
+
const map = name ? BOARD_FIELD_VAR_MAP[name] : void 0;
|
|
8710
|
+
if (!map || typeof field.id !== "string" || !field.id) continue;
|
|
8711
|
+
out[map.fieldIdVar] = field.id;
|
|
8712
|
+
const options = Array.isArray(field.options) ? field.options : [];
|
|
8713
|
+
for (const opt of options) {
|
|
8714
|
+
const o = opt;
|
|
8715
|
+
const varName = typeof o.name === "string" ? map.options[o.name] : void 0;
|
|
8716
|
+
if (varName && typeof o.id === "string" && o.id) out[varName] = o.id;
|
|
8717
|
+
}
|
|
8718
|
+
}
|
|
8719
|
+
return out;
|
|
8720
|
+
}
|
|
8721
|
+
function boardFieldsQueryArgs(projectId) {
|
|
8722
|
+
const query = "query($id: ID!) { node(id: $id) { ... on ProjectV2 { fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }";
|
|
8723
|
+
return ["api", "graphql", "-f", `query=${query}`, "-f", `id=${projectId}`];
|
|
8724
|
+
}
|
|
8725
|
+
function serializeRegistry(obj) {
|
|
8726
|
+
return `${JSON.stringify(obj, null, 2)}
|
|
8727
|
+
`;
|
|
8728
|
+
}
|
|
8729
|
+
function planFanoutRegistration(fanoutTargetsRaw, projectsRaw, entry) {
|
|
8730
|
+
const fanout = JSON.parse(fanoutTargetsRaw);
|
|
8731
|
+
const projects = JSON.parse(projectsRaw);
|
|
8732
|
+
const fanoutRepos = Array.isArray(fanout.repos) ? fanout.repos : [];
|
|
8733
|
+
const projectEntries = Array.isArray(projects.projects) ? projects.projects : [];
|
|
8734
|
+
const name = entry.name ?? entry.repo;
|
|
8735
|
+
const canonName = entry.repo.toLowerCase();
|
|
8736
|
+
const inFanout = fanoutRepos.some((r) => typeof r.repo === "string" && r.repo.toLowerCase() === canonName);
|
|
8737
|
+
const inProjects = projectEntries.some(
|
|
8738
|
+
(p) => p.slug === entry.slug || Array.isArray(p.repos) && p.repos.some((full) => String(full).split("/").pop()?.toLowerCase() === canonName)
|
|
8739
|
+
);
|
|
8740
|
+
if (inFanout || inProjects) {
|
|
8741
|
+
return { changed: false, fanoutTargets: fanoutTargetsRaw, projects: projectsRaw };
|
|
8742
|
+
}
|
|
8743
|
+
const projectEntry = {
|
|
8744
|
+
name,
|
|
8745
|
+
slug: entry.slug,
|
|
8746
|
+
projectId: entry.projectId,
|
|
8747
|
+
wikiRepo: entry.wikiRepo,
|
|
8748
|
+
repos: [`mutmutco/${entry.repo}`]
|
|
8749
|
+
};
|
|
8750
|
+
if (entry.cls === "content") projectEntry.branch = "main";
|
|
8751
|
+
for (const k of Object.keys(projectEntry)) if (projectEntry[k] === void 0) delete projectEntry[k];
|
|
8752
|
+
const nextProjects = { ...projects, projects: [...projectEntries, projectEntry] };
|
|
8753
|
+
const fanoutEntry = { repo: entry.repo, branch: entry.branch, class: entry.cls };
|
|
8754
|
+
const nextFanout = { ...fanout, repos: [...fanoutRepos, fanoutEntry] };
|
|
8755
|
+
return {
|
|
8756
|
+
changed: true,
|
|
8757
|
+
fanoutTargets: serializeRegistry(nextFanout),
|
|
8758
|
+
projects: serializeRegistry(nextProjects)
|
|
8759
|
+
};
|
|
8760
|
+
}
|
|
8761
|
+
function decideFanoutPrAction(openPrs) {
|
|
8762
|
+
const list = Array.isArray(openPrs) ? openPrs : [];
|
|
8763
|
+
for (const pr2 of list) {
|
|
8764
|
+
const url = pr2?.url;
|
|
8765
|
+
if (typeof url === "string" && url.trim()) return { action: "reuse", url: url.trim() };
|
|
8766
|
+
}
|
|
8767
|
+
return { action: "create" };
|
|
8768
|
+
}
|
|
8377
8769
|
function contentPutArgs(repo, path2, content, branch, sha) {
|
|
8378
8770
|
const args = [
|
|
8379
8771
|
"api",
|
|
@@ -8537,6 +8929,33 @@ async function tenantControl(payload, deps) {
|
|
|
8537
8929
|
const timeoutMs = payload.wait ? WAITED_TENANT_CONTROL_TIMEOUT_MS : void 0;
|
|
8538
8930
|
return postJson("/tenant-control", payload, deps, "POST", { noRetry, timeoutMs });
|
|
8539
8931
|
}
|
|
8932
|
+
async function tenantDeploy(payload, deps) {
|
|
8933
|
+
return postJson("/tenant-deploy", payload, deps, "POST", { noRetry: true });
|
|
8934
|
+
}
|
|
8935
|
+
|
|
8936
|
+
// src/tenant-verify-secrets.ts
|
|
8937
|
+
function tenantControlWait(action) {
|
|
8938
|
+
return action === "status" || action === "retire" || action === "verify-secrets";
|
|
8939
|
+
}
|
|
8940
|
+
function renderVerifySecrets(body) {
|
|
8941
|
+
const secrets2 = body?.secrets ?? [];
|
|
8942
|
+
const counts = {
|
|
8943
|
+
match: secrets2.filter((s) => s.status === "match").length,
|
|
8944
|
+
mismatch: secrets2.filter((s) => s.status === "mismatch").length,
|
|
8945
|
+
missing: secrets2.filter((s) => s.status === "missing").length
|
|
8946
|
+
};
|
|
8947
|
+
const lines = secrets2.map((s) => `${s.key}: ${s.status}`);
|
|
8948
|
+
lines.push(`verify-secrets: ${counts.match} match, ${counts.mismatch} mismatch, ${counts.missing} missing`);
|
|
8949
|
+
const ssmStatus = body?.ssmStatus ?? "pending";
|
|
8950
|
+
if (ssmStatus !== "Success") {
|
|
8951
|
+
return { lines, failure: `verify-secrets did not complete (ssm status ${ssmStatus})${body?.commandId ? ` \u2014 command ${body.commandId}` : ""}` };
|
|
8952
|
+
}
|
|
8953
|
+
const bad = counts.mismatch + counts.missing;
|
|
8954
|
+
if (bad > 0) {
|
|
8955
|
+
return { lines, failure: `${bad} of ${secrets2.length} required secret(s) not matching the vault (${counts.mismatch} mismatch, ${counts.missing} missing)` };
|
|
8956
|
+
}
|
|
8957
|
+
return { lines, failure: null };
|
|
8958
|
+
}
|
|
8540
8959
|
|
|
8541
8960
|
// src/project-readiness.ts
|
|
8542
8961
|
function dnsErrorToResolution(code) {
|
|
@@ -8563,11 +8982,17 @@ function declaresNoPublicEdge(meta) {
|
|
|
8563
8982
|
const ed = meta?.edgeDomains;
|
|
8564
8983
|
return Boolean(ed && typeof ed === "object" && !Array.isArray(ed) && Object.keys(ed).length === 0);
|
|
8565
8984
|
}
|
|
8985
|
+
function declaresWorker(meta) {
|
|
8986
|
+
return meta?.projectType === "worker";
|
|
8987
|
+
}
|
|
8988
|
+
function isCentralContainerModel2(model) {
|
|
8989
|
+
return model === "tenant-container" || model === "solo-container";
|
|
8990
|
+
}
|
|
8566
8991
|
function isNoEdgeTenantWorker(meta, model) {
|
|
8567
|
-
return model
|
|
8992
|
+
return isCentralContainerModel2(model) && (declaresNoPublicEdge(meta) || declaresWorker(meta));
|
|
8568
8993
|
}
|
|
8569
8994
|
function projectRequiresDeployCoords(model, stage2, meta) {
|
|
8570
|
-
if (model
|
|
8995
|
+
if (!isCentralContainerModel2(model)) return false;
|
|
8571
8996
|
if (stage2 && isNoEdgeTenantWorker(meta, model)) return stage2 === "main";
|
|
8572
8997
|
return true;
|
|
8573
8998
|
}
|
|
@@ -8600,7 +9025,7 @@ function attestedLine(att) {
|
|
|
8600
9025
|
var CONTRACT_UNDECLARED_LINE = "No runtime secrets declared \u2014 declare requiredRuntimeSecrets (a per-stage name map) in the registry META, or attest the app needs none with an explicit empty stage map ({ dev: [], rc: [], main: [] }).";
|
|
8601
9026
|
function appGapsFor(meta, model, slug, projectType) {
|
|
8602
9027
|
const attested = appAttestationOf(meta);
|
|
8603
|
-
const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-game" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless";
|
|
9028
|
+
const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-game" && projectType !== "cli-tool" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless" && model !== "registry-publish";
|
|
8604
9029
|
const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
|
|
8605
9030
|
if (attested) return contractUndeclared ? [attestedLine(attested), CONTRACT_UNDECLARED_LINE] : [attestedLine(attested)];
|
|
8606
9031
|
if (projectType === "content" || model === "content") return ["Content/KB repo: keep app-owned work to docs/content changes; release train does not apply."];
|
|
@@ -8629,13 +9054,20 @@ function appGapsFor(meta, model, slug, projectType) {
|
|
|
8629
9054
|
"Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
|
|
8630
9055
|
];
|
|
8631
9056
|
}
|
|
9057
|
+
if (projectType === "cli-tool" || model === "registry-publish") {
|
|
9058
|
+
return [
|
|
9059
|
+
"Distributable CLI/plugin: ship a publishable package.json (name, version, bin/exports) \u2014 release publishes to npm (plugin-marketplace publish is future work when a plugin tenant lands), not a box deploy; no Hub deploy coords, edge, or OAuth.",
|
|
9060
|
+
"Keep the published version in lockstep with the release tag; make the publish idempotent (skip when the version is already on the registry).",
|
|
9061
|
+
"Keep app-owned README.md and architecture.md aligned with the registry-publish release path."
|
|
9062
|
+
];
|
|
9063
|
+
}
|
|
8632
9064
|
const gaps = [
|
|
8633
9065
|
"Ensure the compose file carries env_file: .env and the app reads plain environment variables \u2014 the box injects coords + declared runtime secrets into the release .env; the app never self-loads SSM.",
|
|
8634
9066
|
"Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
|
|
8635
9067
|
"Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
|
|
8636
9068
|
];
|
|
8637
9069
|
if (isNoEdgeTenantWorker(meta, model)) {
|
|
8638
|
-
gaps.unshift("No public edge
|
|
9070
|
+
gaps.unshift("No public edge (worker/outbound-only tenant \u2014 `projectType: worker` or `edgeDomains: {}`): skip Cloudflare edge auto-heal, OAuth defaults, and dev/rc remote deploy coords unless META explicitly adds them.");
|
|
8639
9071
|
}
|
|
8640
9072
|
if (contractUndeclared) {
|
|
8641
9073
|
gaps.unshift(CONTRACT_UNDECLARED_LINE);
|
|
@@ -8701,7 +9133,7 @@ function buildV2HealPatch(repoOrSlug, meta) {
|
|
|
8701
9133
|
}
|
|
8702
9134
|
if (!meta?.vaultPath) patch.vaultPath = `/mmi-future/${slug}`;
|
|
8703
9135
|
if (!meta?.kbPointer) patch.kbPointer = `kb/projects/${slug}.md`;
|
|
8704
|
-
if (confidentType && !meta?.edgeDomains && model === "tenant-container") {
|
|
9136
|
+
if (confidentType && !meta?.edgeDomains && model === "tenant-container" && projectType !== "worker") {
|
|
8705
9137
|
patch.edgeDomains = {
|
|
8706
9138
|
dev: `dev.${sub}.mutatismutandis.co`,
|
|
8707
9139
|
rc: `rc.${sub}.mutatismutandis.co`,
|
|
@@ -8724,7 +9156,7 @@ function buildV2HealPatch(repoOrSlug, meta) {
|
|
|
8724
9156
|
patch.requiredRuntimeSecrets = next;
|
|
8725
9157
|
}
|
|
8726
9158
|
}
|
|
8727
|
-
const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-game|non-deployable> --deploy-model <tenant-container|hub-serverless|serverless|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
|
|
9159
|
+
const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
|
|
8728
9160
|
return { slug, patch, appOwnedGaps };
|
|
8729
9161
|
}
|
|
8730
9162
|
async function runV2Heal(repoOrSlug, opts, deps) {
|
|
@@ -8923,6 +9355,158 @@ function parseEdgeDomainsVar(raw) {
|
|
|
8923
9355
|
}
|
|
8924
9356
|
return out;
|
|
8925
9357
|
}
|
|
9358
|
+
var PRIORITY_OPTION_NAMES = ["Urgent", "High", "Medium", "Low"];
|
|
9359
|
+
function parsePriorityOptionsVar(raw) {
|
|
9360
|
+
let parsed;
|
|
9361
|
+
try {
|
|
9362
|
+
parsed = JSON.parse(raw);
|
|
9363
|
+
} catch {
|
|
9364
|
+
throw new Error('project set: priorityOptions must be JSON, e.g. {"Urgent":"<id>","High":"<id>","Medium":"<id>","Low":"<id>"}');
|
|
9365
|
+
}
|
|
9366
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
9367
|
+
throw new Error("project set: priorityOptions must be a {Urgent,High,Medium,Low} map of option-id strings");
|
|
9368
|
+
}
|
|
9369
|
+
const map = parsed;
|
|
9370
|
+
const out = {};
|
|
9371
|
+
for (const [name, id] of Object.entries(map)) {
|
|
9372
|
+
if (!PRIORITY_OPTION_NAMES.includes(name)) {
|
|
9373
|
+
throw new Error(`project set: priorityOptions "${name}" \u2014 expected only ${PRIORITY_OPTION_NAMES.join("/")}`);
|
|
9374
|
+
}
|
|
9375
|
+
if (typeof id !== "string" || !id.trim()) {
|
|
9376
|
+
throw new Error(`project set: priorityOptions.${name} must be a non-empty option-id string`);
|
|
9377
|
+
}
|
|
9378
|
+
out[name] = id.trim();
|
|
9379
|
+
}
|
|
9380
|
+
return out;
|
|
9381
|
+
}
|
|
9382
|
+
function parsePortRangeVar(raw) {
|
|
9383
|
+
let parsed;
|
|
9384
|
+
try {
|
|
9385
|
+
parsed = JSON.parse(raw);
|
|
9386
|
+
} catch {
|
|
9387
|
+
throw new Error('project set: portRange must be JSON, e.g. {"start":3700,"end":3710} or [3700,3710]');
|
|
9388
|
+
}
|
|
9389
|
+
let start;
|
|
9390
|
+
let end;
|
|
9391
|
+
if (Array.isArray(parsed) && parsed.length === 2) {
|
|
9392
|
+
[start, end] = parsed;
|
|
9393
|
+
} else if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
9394
|
+
({ start, end } = parsed);
|
|
9395
|
+
} else {
|
|
9396
|
+
throw new Error("project set: portRange must be {start,end} or a [start,end] tuple");
|
|
9397
|
+
}
|
|
9398
|
+
if (typeof start !== "number" || typeof end !== "number" || !Number.isFinite(start) || !Number.isFinite(end)) {
|
|
9399
|
+
throw new Error("project set: portRange start/end must be finite numbers");
|
|
9400
|
+
}
|
|
9401
|
+
if (start > end) throw new Error("project set: portRange start must be <= end");
|
|
9402
|
+
return { start, end };
|
|
9403
|
+
}
|
|
9404
|
+
function parseStatusOptionsVar(raw) {
|
|
9405
|
+
let parsed;
|
|
9406
|
+
try {
|
|
9407
|
+
parsed = JSON.parse(raw);
|
|
9408
|
+
} catch {
|
|
9409
|
+
throw new Error('project set: statusOptions must be JSON, e.g. {"Todo":"<id>","In Progress":"<id>","In Review":"<id>","Done":"<id>"}');
|
|
9410
|
+
}
|
|
9411
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
9412
|
+
throw new Error("project set: statusOptions must be a map of status name to option-id string");
|
|
9413
|
+
}
|
|
9414
|
+
const map = parsed;
|
|
9415
|
+
const out = {};
|
|
9416
|
+
for (const [name, id] of Object.entries(map)) {
|
|
9417
|
+
if (typeof id !== "string" || !id.trim()) {
|
|
9418
|
+
throw new Error(`project set: statusOptions.${name} must be a non-empty option-id string`);
|
|
9419
|
+
}
|
|
9420
|
+
out[name] = id.trim();
|
|
9421
|
+
}
|
|
9422
|
+
return out;
|
|
9423
|
+
}
|
|
9424
|
+
function parseOauthVar(raw) {
|
|
9425
|
+
let parsed;
|
|
9426
|
+
try {
|
|
9427
|
+
parsed = JSON.parse(raw);
|
|
9428
|
+
} catch {
|
|
9429
|
+
throw new Error('project set: oauth must be JSON, e.g. {"subdomains":["app"],"domains":["example.co"],"callbackPath":"/auth/callback"}');
|
|
9430
|
+
}
|
|
9431
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
9432
|
+
throw new Error("project set: oauth must be a {subdomains,domains,callbackPath} object");
|
|
9433
|
+
}
|
|
9434
|
+
const map = parsed;
|
|
9435
|
+
const out = {};
|
|
9436
|
+
for (const [key, value] of Object.entries(map)) {
|
|
9437
|
+
if (key === "subdomains" || key === "domains") {
|
|
9438
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== "string" || !v.trim())) {
|
|
9439
|
+
throw new Error(`project set: oauth.${key} must be an array of non-empty strings`);
|
|
9440
|
+
}
|
|
9441
|
+
out[key] = value.map((v) => v.trim());
|
|
9442
|
+
} else if (key === "callbackPath") {
|
|
9443
|
+
if (typeof value !== "string" || !value.trim()) throw new Error("project set: oauth.callbackPath must be a non-empty string");
|
|
9444
|
+
out.callbackPath = value.trim();
|
|
9445
|
+
} else {
|
|
9446
|
+
throw new Error(`project set: oauth key "${key}" \u2014 expected only subdomains/domains/callbackPath`);
|
|
9447
|
+
}
|
|
9448
|
+
}
|
|
9449
|
+
return out;
|
|
9450
|
+
}
|
|
9451
|
+
function parseReposVar(raw) {
|
|
9452
|
+
let parsed;
|
|
9453
|
+
try {
|
|
9454
|
+
parsed = JSON.parse(raw);
|
|
9455
|
+
} catch {
|
|
9456
|
+
throw new Error('project set: repos must be a JSON array, e.g. ["mutmutco/mm-foo"]');
|
|
9457
|
+
}
|
|
9458
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || parsed.some((r) => typeof r !== "string" || !r.trim())) {
|
|
9459
|
+
throw new Error("project set: repos must be a non-empty array of owner/name strings");
|
|
9460
|
+
}
|
|
9461
|
+
return parsed.map((r) => r.trim());
|
|
9462
|
+
}
|
|
9463
|
+
function parsePublishRequiredVar(raw) {
|
|
9464
|
+
if (raw === "true") return true;
|
|
9465
|
+
if (raw === "false") return false;
|
|
9466
|
+
throw new Error("project set: publishRequired must be true or false");
|
|
9467
|
+
}
|
|
9468
|
+
var SETTABLE_VAR_KEYS = [
|
|
9469
|
+
"name",
|
|
9470
|
+
"division",
|
|
9471
|
+
"projectId",
|
|
9472
|
+
"projectOwner",
|
|
9473
|
+
"projectNumber",
|
|
9474
|
+
"branch",
|
|
9475
|
+
"wikiRepo",
|
|
9476
|
+
"vaultPath",
|
|
9477
|
+
"kbPointer",
|
|
9478
|
+
"repos",
|
|
9479
|
+
"oauth",
|
|
9480
|
+
"publishRequired",
|
|
9481
|
+
"requiredGcpApis",
|
|
9482
|
+
"requiredRuntimeSecrets",
|
|
9483
|
+
"edgeDomains",
|
|
9484
|
+
"statusFieldId",
|
|
9485
|
+
"statusOptions",
|
|
9486
|
+
"priorityFieldId",
|
|
9487
|
+
"priorityOptions",
|
|
9488
|
+
"portRange"
|
|
9489
|
+
];
|
|
9490
|
+
var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
9491
|
+
var SETTABLE_VAR_HINTS = {
|
|
9492
|
+
projectNumber: "numeric",
|
|
9493
|
+
publishRequired: "true|false",
|
|
9494
|
+
repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
|
|
9495
|
+
oauth: "JSON {subdomains,domains,callbackPath}",
|
|
9496
|
+
requiredGcpApis: "comma-string",
|
|
9497
|
+
requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
|
|
9498
|
+
edgeDomains: "JSON {dev,rc,main} domain map",
|
|
9499
|
+
statusOptions: "JSON name\u2192id map",
|
|
9500
|
+
priorityOptions: "JSON {Urgent,High,Medium,Low}\u2192id map",
|
|
9501
|
+
portRange: "JSON {start,end} or [start,end]"
|
|
9502
|
+
};
|
|
9503
|
+
function settableVarHelp() {
|
|
9504
|
+
const keys = SETTABLE_VAR_KEYS.map((k) => {
|
|
9505
|
+
const hint = SETTABLE_VAR_HINTS[k];
|
|
9506
|
+
return hint ? `${k} (${hint})` : k;
|
|
9507
|
+
});
|
|
9508
|
+
return `META field to set (repeatable): ${keys.join(", ")}`;
|
|
9509
|
+
}
|
|
8926
9510
|
function buildProjectSetPatch(input) {
|
|
8927
9511
|
const patch = {};
|
|
8928
9512
|
if (input.class) {
|
|
@@ -8943,22 +9527,42 @@ function buildProjectSetPatch(input) {
|
|
|
8943
9527
|
}
|
|
8944
9528
|
patch.deployModel = input.deployModel;
|
|
8945
9529
|
}
|
|
9530
|
+
if (input.releaseTrack) {
|
|
9531
|
+
if (!isReleaseTrack(input.releaseTrack)) {
|
|
9532
|
+
throw new Error(`project set: --release-track must be one of: ${RELEASE_TRACKS.join(", ")}`);
|
|
9533
|
+
}
|
|
9534
|
+
patch.releaseTrack = input.releaseTrack;
|
|
9535
|
+
}
|
|
8946
9536
|
for (const value of input.vars) {
|
|
8947
9537
|
const eq = value.indexOf("=");
|
|
8948
|
-
if (eq
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
9538
|
+
if (eq <= 0) continue;
|
|
9539
|
+
const key = value.slice(0, eq);
|
|
9540
|
+
const raw = value.slice(eq + 1);
|
|
9541
|
+
if (!SETTABLE_VAR_KEY_SET.has(key)) {
|
|
9542
|
+
throw new Error(`project set: --var KEY "${key}" is not settable \u2014 allowed keys: ${SETTABLE_VAR_KEYS.join(", ")}`);
|
|
9543
|
+
}
|
|
9544
|
+
if (key === "projectNumber") {
|
|
9545
|
+
const n = Number(raw);
|
|
9546
|
+
if (!Number.isFinite(n)) throw new Error("project set: projectNumber must be numeric");
|
|
9547
|
+
patch[key] = n;
|
|
9548
|
+
} else if (key === "requiredRuntimeSecrets") {
|
|
9549
|
+
patch[key] = parseRuntimeSecretsVar(raw);
|
|
9550
|
+
} else if (key === "edgeDomains") {
|
|
9551
|
+
patch[key] = parseEdgeDomainsVar(raw);
|
|
9552
|
+
} else if (key === "priorityOptions") {
|
|
9553
|
+
patch[key] = parsePriorityOptionsVar(raw);
|
|
9554
|
+
} else if (key === "statusOptions") {
|
|
9555
|
+
patch[key] = parseStatusOptionsVar(raw);
|
|
9556
|
+
} else if (key === "portRange") {
|
|
9557
|
+
patch[key] = parsePortRangeVar(raw);
|
|
9558
|
+
} else if (key === "oauth") {
|
|
9559
|
+
patch[key] = parseOauthVar(raw);
|
|
9560
|
+
} else if (key === "repos") {
|
|
9561
|
+
patch[key] = parseReposVar(raw);
|
|
9562
|
+
} else if (key === "publishRequired") {
|
|
9563
|
+
patch[key] = parsePublishRequiredVar(raw);
|
|
9564
|
+
} else {
|
|
9565
|
+
patch[key] = raw;
|
|
8962
9566
|
}
|
|
8963
9567
|
}
|
|
8964
9568
|
for (const key of input.unsets) {
|
|
@@ -8972,7 +9576,7 @@ function buildProjectSetPatch(input) {
|
|
|
8972
9576
|
patch.edgeDomains = null;
|
|
8973
9577
|
}
|
|
8974
9578
|
if (Object.keys(patch).length === 0) {
|
|
8975
|
-
throw new Error("project set: nothing to set - pass --class, --project-type, --deploy-model, --var KEY=VALUE, --unset KEY, and/or --clear-web-profile");
|
|
9579
|
+
throw new Error("project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, and/or --clear-web-profile");
|
|
8976
9580
|
}
|
|
8977
9581
|
return patch;
|
|
8978
9582
|
}
|
|
@@ -9807,7 +10411,7 @@ function authorizeBodyHasMismatch(body) {
|
|
|
9807
10411
|
}
|
|
9808
10412
|
|
|
9809
10413
|
// src/index.ts
|
|
9810
|
-
var rawExecFileP3 = (0, import_node_util6.promisify)(
|
|
10414
|
+
var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process7.execFile);
|
|
9811
10415
|
var DEFAULT_EXEC_TIMEOUT_MS = 1e4;
|
|
9812
10416
|
var execFileP4 = (file, args, options = {}) => (
|
|
9813
10417
|
// encoding 'utf8' guarantees string stdout/stderr at runtime; the cast pins the type because
|
|
@@ -10236,7 +10840,7 @@ saga.command("head-update").option("--run", "detached worker: fetch state, run t
|
|
|
10236
10840
|
if (!headGateDue(tsPath)) return;
|
|
10237
10841
|
markHeadRun(tsPath);
|
|
10238
10842
|
try {
|
|
10239
|
-
(0,
|
|
10843
|
+
(0, import_node_child_process7.spawn)(process.execPath, [process.argv[1], "saga", "head-update", "--run"], {
|
|
10240
10844
|
detached: true,
|
|
10241
10845
|
stdio: "ignore",
|
|
10242
10846
|
windowsHide: true
|
|
@@ -10456,7 +11060,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
10456
11060
|
try {
|
|
10457
11061
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
|
|
10458
11062
|
if (o.repo) args.push("--repo", o.repo);
|
|
10459
|
-
(0,
|
|
11063
|
+
(0, import_node_child_process7.spawn)(process.execPath, [process.argv[1], ...args], {
|
|
10460
11064
|
detached: true,
|
|
10461
11065
|
stdio: "ignore",
|
|
10462
11066
|
windowsHide: true,
|
|
@@ -10512,7 +11116,7 @@ function openInEditor(path2) {
|
|
|
10512
11116
|
return;
|
|
10513
11117
|
}
|
|
10514
11118
|
try {
|
|
10515
|
-
(0,
|
|
11119
|
+
(0, import_node_child_process7.spawn)(editor, [path2], { stdio: "inherit" });
|
|
10516
11120
|
} catch {
|
|
10517
11121
|
console.log(`open ${path2} manually`);
|
|
10518
11122
|
}
|
|
@@ -10671,15 +11275,22 @@ function reportWrite(label, res) {
|
|
|
10671
11275
|
fail(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
10672
11276
|
}
|
|
10673
11277
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
10674
|
-
tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart, plus rc-only retire) for a tenant; project-admin dev/rc, master main").option("--json", "machine-readable output").action(async (repo, stage2, action) => {
|
|
11278
|
+
tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart, plus rc-only retire and read-only verify-secrets) for a tenant; project-admin dev/rc, master main").option("--json", "machine-readable output").action(async (repo, stage2, action, o) => {
|
|
10675
11279
|
const cfg = await loadConfig();
|
|
10676
|
-
const wait = action
|
|
11280
|
+
const wait = tenantControlWait(action);
|
|
10677
11281
|
const res = await tenantControl({ repo, stage: stage2, action, wait }, registryClientDeps(cfg));
|
|
10678
11282
|
const body = res.body;
|
|
10679
11283
|
if (!res.ok && body?.category) {
|
|
10680
11284
|
console.log(JSON.stringify(body));
|
|
10681
11285
|
return fail(`tenant control ${stage2} ${action}: ${body.category}`);
|
|
10682
11286
|
}
|
|
11287
|
+
if (res.ok && action === "verify-secrets") {
|
|
11288
|
+
const { lines, failure } = renderVerifySecrets(res.body);
|
|
11289
|
+
if (o.json) console.log(JSON.stringify(res.body));
|
|
11290
|
+
else lines.forEach((l) => console.log(l));
|
|
11291
|
+
if (failure) return fail(`tenant control ${stage2} verify-secrets: ${failure}`);
|
|
11292
|
+
return;
|
|
11293
|
+
}
|
|
10683
11294
|
reportWrite("tenant control", res);
|
|
10684
11295
|
});
|
|
10685
11296
|
tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main \u2014 the promoted ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage2, o) => {
|
|
@@ -10691,6 +11302,31 @@ tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the cen
|
|
|
10691
11302
|
return fail(`tenant redeploy: ${e.message}`);
|
|
10692
11303
|
}
|
|
10693
11304
|
});
|
|
11305
|
+
tenant.command("sweep-rc").description("discover (and optionally retire) running rc tenant runtimes across tenant-containers \u2014 orphan cleanup after a failed post-release retire (#942)").option("--retire", "retire every running rc runtime found (requires --yes) \u2014 WARNING: tears down a legitimately-staged rc too").option("--yes", "confirm the destructive --retire").option("--json", "machine-readable output").action(async (o) => {
|
|
11306
|
+
if (o.retire && !o.yes) {
|
|
11307
|
+
return fail("tenant sweep-rc --retire is destructive (it tears down EVERY running rc, including one legitimately staged between /rcand and /release) \u2014 re-run with --yes to confirm");
|
|
11308
|
+
}
|
|
11309
|
+
const cfg = await loadConfig();
|
|
11310
|
+
const cdeps = registryClientDeps(cfg);
|
|
11311
|
+
try {
|
|
11312
|
+
const result = await sweepRcOrphans({
|
|
11313
|
+
listProjects: () => fetchProjectsList(cdeps),
|
|
11314
|
+
status: async (repo) => {
|
|
11315
|
+
const res = await tenantControl({ repo, stage: "rc", action: "status", wait: true }, cdeps);
|
|
11316
|
+
const b = res.body;
|
|
11317
|
+
return { serviceState: b?.serviceState ?? "unknown" };
|
|
11318
|
+
},
|
|
11319
|
+
retire: async (repo) => {
|
|
11320
|
+
const res = await tenantControl({ repo, stage: "rc", action: "retire", wait: true }, cdeps);
|
|
11321
|
+
const b = res.body;
|
|
11322
|
+
return { ok: res.ok, category: b?.category, reason: b?.reason };
|
|
11323
|
+
}
|
|
11324
|
+
}, { retire: !!o.retire });
|
|
11325
|
+
return printLine(o.json ? JSON.stringify(result) : renderSweep(result));
|
|
11326
|
+
} catch (e) {
|
|
11327
|
+
return fail(`tenant sweep-rc: ${e.message}`);
|
|
11328
|
+
}
|
|
11329
|
+
});
|
|
10694
11330
|
async function resolveDnsBounded(host, timeoutMs = 3e3) {
|
|
10695
11331
|
const { lookup } = await import("node:dns/promises");
|
|
10696
11332
|
const probe = lookup(host).then(() => true).catch((e) => dnsErrorToResolution(e?.code));
|
|
@@ -10859,7 +11495,7 @@ project.command("attest [owner/repo]").description("attest this repo's app-owned
|
|
|
10859
11495
|
const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
|
|
10860
11496
|
reportWrite("project attest", res);
|
|
10861
11497
|
});
|
|
10862
|
-
project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project META (idempotent merge; defaults to the current repo; no clobber of unspecified fields)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--
|
|
11498
|
+
project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project META (idempotent merge; defaults to the current repo; no clobber of unspecified fields)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
10863
11499
|
const cfg = await loadConfig();
|
|
10864
11500
|
let target;
|
|
10865
11501
|
try {
|
|
@@ -10874,6 +11510,7 @@ project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project M
|
|
|
10874
11510
|
class: o.class,
|
|
10875
11511
|
projectType: o.projectType,
|
|
10876
11512
|
deployModel: o.deployModel,
|
|
11513
|
+
releaseTrack: o.releaseTrack,
|
|
10877
11514
|
vars: rawValues("--var"),
|
|
10878
11515
|
unsets: rawValues("--unset"),
|
|
10879
11516
|
clearWebProfile: Boolean(o.clearWebProfile)
|
|
@@ -11151,7 +11788,7 @@ function teardownWorktreeStage(worktreePath) {
|
|
|
11151
11788
|
}
|
|
11152
11789
|
});
|
|
11153
11790
|
}
|
|
11154
|
-
pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
|
|
11791
|
+
pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").action(async (number, o) => {
|
|
11155
11792
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
11156
11793
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
11157
11794
|
const headRef = (await execFileP4("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq", ".headRefName"], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
|
|
@@ -11162,7 +11799,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
11162
11799
|
const remoteBefore = repoArgs.length ? void 0 : await remoteBranchExists(headRef);
|
|
11163
11800
|
let remoteDeleteAttempted = false;
|
|
11164
11801
|
let remoteNotAttemptedReason = repoArgs.length ? "repo-option" : void 0;
|
|
11165
|
-
await execFileP4("gh",
|
|
11802
|
+
await execFileP4("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e) => {
|
|
11166
11803
|
const message = String(e.message || "");
|
|
11167
11804
|
if (/already been merged/i.test(message)) {
|
|
11168
11805
|
remoteNotAttemptedReason = "pr-already-merged";
|
|
@@ -11170,8 +11807,18 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
11170
11807
|
}
|
|
11171
11808
|
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
11172
11809
|
if (note) throw new Error(`gh pr merge ${number}: ${note}`);
|
|
11810
|
+
if (!o.auto && basePolicyBlocksImmediateMerge(message)) {
|
|
11811
|
+
throw new Error(`gh pr merge ${number}: the base-branch policy blocks an immediate merge \u2014 re-run with --auto to merge once required checks pass.`);
|
|
11812
|
+
}
|
|
11173
11813
|
if (!/used by worktree|cannot delete branch/i.test(message)) throw e;
|
|
11174
11814
|
});
|
|
11815
|
+
if (o.auto) {
|
|
11816
|
+
const state = (await execFileP4("gh", ["pr", "view", number, ...repoArgs, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
11817
|
+
if (state !== "MERGED") {
|
|
11818
|
+
console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", pr: number, branch: headRef, state: state || "unknown" }));
|
|
11819
|
+
return;
|
|
11820
|
+
}
|
|
11821
|
+
}
|
|
11175
11822
|
if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
|
|
11176
11823
|
const remoteBranch = repoArgs.length ? buildRemoteBranchCleanupReport(headRef, {
|
|
11177
11824
|
attempted: false,
|
|
@@ -11191,7 +11838,11 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
11191
11838
|
beforeWorktrees,
|
|
11192
11839
|
startingPath,
|
|
11193
11840
|
execGit: async (args) => (await execFileP4("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
11194
|
-
teardownWorktreeStage
|
|
11841
|
+
teardownWorktreeStage,
|
|
11842
|
+
// Hardened fallback when retried `git worktree remove` still hits a Windows file lock (#967).
|
|
11843
|
+
// `fs.rm` recursive removes a junction as a link (it does not traverse into the target) and its
|
|
11844
|
+
// own maxRetries/retryDelay rides out a handle that an indexer/antivirus releases a moment later.
|
|
11845
|
+
removeWorktreeDir: async (worktreePath) => (0, import_promises2.rm)(worktreePath, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
11195
11846
|
});
|
|
11196
11847
|
console.log(JSON.stringify(buildPrMergeResultPayload({
|
|
11197
11848
|
number,
|
|
@@ -11370,6 +12021,19 @@ function reportedStageUrl(res, result) {
|
|
|
11370
12021
|
return result.port != null ? stageUrlForPort(result.port) : res.derived.url;
|
|
11371
12022
|
}
|
|
11372
12023
|
program2.command("port-range <repo>").description("assign (idempotently) + print the repo's local stage port block via the atomic ORG#config.portCursor allocator (committed-file fallback)").option("--json", "machine-readable output").action(async (repo, o) => {
|
|
12024
|
+
const cfg = await loadConfig();
|
|
12025
|
+
const reg = registryClientDeps(cfg);
|
|
12026
|
+
const slug = slugOf(repo);
|
|
12027
|
+
const read = await fetchProjectBySlugChecked(slug, reg);
|
|
12028
|
+
const decision = decidePortRange({ metaReadOk: read.ok, metaPortRange: read.ok ? metaPortRange(read.project) : null });
|
|
12029
|
+
if (decision.action === "fail") {
|
|
12030
|
+
return fail(`port-range: ${decision.reason}${read.ok ? "" : ` (${read.error})`}`);
|
|
12031
|
+
}
|
|
12032
|
+
if (decision.action === "return") {
|
|
12033
|
+
const [start2, end2] = decision.range;
|
|
12034
|
+
printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
|
|
12035
|
+
return;
|
|
12036
|
+
}
|
|
11373
12037
|
const path2 = (0, import_node_path8.join)(process.cwd(), "infra", "port-ranges.json");
|
|
11374
12038
|
const allocate = async (seed) => {
|
|
11375
12039
|
const { stdout } = await execFileP4("node", [(0, import_node_path8.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
|
|
@@ -11377,8 +12041,16 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
|
|
|
11377
12041
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
11378
12042
|
return parsed.range;
|
|
11379
12043
|
};
|
|
11380
|
-
const { range: [start, end] } = await ensurePortRangeAtomic(repo, path2, allocate);
|
|
11381
|
-
|
|
12044
|
+
const { range: [start, end], source } = await ensurePortRangeAtomic(repo, path2, allocate);
|
|
12045
|
+
const write = await upsertProject(slug, { portRange: { start, end } }, reg);
|
|
12046
|
+
if (!write.ok && source === "ddb") {
|
|
12047
|
+
return fail(`port-range: block [${start}, ${end}] was allocated (cursor advanced) but NOT recorded in the registry META (${write.error ?? `HTTP ${write.status}`}) \u2014 fix auth/connectivity and retry so the block is persisted; do not re-run blind`);
|
|
12048
|
+
}
|
|
12049
|
+
if (o.json) {
|
|
12050
|
+
printLine(JSON.stringify({ repo, portRange: [start, end], source: "allocated", persisted: write.ok, ...write.ok ? {} : { persistError: write.error ?? `HTTP ${write.status}` } }));
|
|
12051
|
+
} else {
|
|
12052
|
+
printLine(`${repo}: stage.portRange [${start}, ${end}]${write.ok ? "" : ` (META not persisted: ${write.error ?? `HTTP ${write.status}`})`}`);
|
|
12053
|
+
}
|
|
11382
12054
|
});
|
|
11383
12055
|
var stage = program2.command("stage").description("plan or run the repo local stage environment").option("--json", "machine-readable output").option("--apply", "run the full local stage: stop previous, build, start, health-check").option("--timeout-ms <ms>", "bounded build/health timeout", "60000").action(async (o) => {
|
|
11384
12056
|
const res = await resolveStage();
|
|
@@ -11502,6 +12174,19 @@ function trainApplyDeps() {
|
|
|
11502
12174
|
const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
|
|
11503
12175
|
return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
|
|
11504
12176
|
},
|
|
12177
|
+
// Hub-App-authority dispatch of the central tenant deploy (#953) — the Hub fires the
|
|
12178
|
+
// workflow_dispatch with its App token, so the caller needs no MMI-Hub Actions write.
|
|
12179
|
+
dispatchTenantDeploy: async ({ repo, stage: stage2, ref }) => {
|
|
12180
|
+
const res = await tenantDeploy({ repo, stage: stage2, ref }, registryClientDeps(await loadConfig()));
|
|
12181
|
+
if (!res.ok) {
|
|
12182
|
+
const detail = res.body?.error ?? res.error ?? `HTTP ${res.status}`;
|
|
12183
|
+
throw new Error(`tenant deploy dispatch failed: ${detail}`);
|
|
12184
|
+
}
|
|
12185
|
+
},
|
|
12186
|
+
// Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths is empty —
|
|
12187
|
+
// product repos carry no main-only distribution bump (that exemption is Hub-distribution-specific, and
|
|
12188
|
+
// the Hub is direct-track, so it never reaches this guard).
|
|
12189
|
+
hotfixCoverage: (input) => checkHotfixCoverage({ ...input, manifestPaths: [] }),
|
|
11505
12190
|
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
11506
12191
|
announce: (args) => announceRelease({
|
|
11507
12192
|
run: async (file, cmdArgs) => (await execFileP4(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
@@ -11576,15 +12261,19 @@ trainCmd.command("prep").description("prepare and stage the Hub distribution bum
|
|
|
11576
12261
|
}
|
|
11577
12262
|
});
|
|
11578
12263
|
for (const commandName of ["rcand", "release"]) {
|
|
11579
|
-
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").action(async (o) => {
|
|
12264
|
+
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").option("--ack <shas>", "release only: comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)").action(async (o) => {
|
|
11580
12265
|
try {
|
|
11581
12266
|
await requireFreshTrainCli(commandName);
|
|
11582
12267
|
} catch (e) {
|
|
11583
12268
|
return fail(`${commandName}: ${e.message}`);
|
|
11584
12269
|
}
|
|
12270
|
+
if (o.ack && commandName !== "release") {
|
|
12271
|
+
return fail("--ack applies only to release: it overrides the rc -> main hotfix-coverage guard, which rcand does not run");
|
|
12272
|
+
}
|
|
11585
12273
|
if (o.apply) {
|
|
11586
12274
|
try {
|
|
11587
|
-
const
|
|
12275
|
+
const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
12276
|
+
const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack });
|
|
11588
12277
|
return printLine(o.json ? JSON.stringify(result, null, 2) : renderTrainApply(commandName, result));
|
|
11589
12278
|
} catch (e) {
|
|
11590
12279
|
return fail(`${commandName}: ${e.message}`);
|
|
@@ -11683,15 +12372,16 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
11683
12372
|
return null;
|
|
11684
12373
|
}
|
|
11685
12374
|
}
|
|
11686
|
-
});
|
|
12375
|
+
}, resolveReleaseTrack(meta));
|
|
11687
12376
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
|
|
11688
12377
|
if (!report.ok) process.exitCode = 1;
|
|
11689
12378
|
});
|
|
11690
|
-
bootstrap.command("apply <repo>").description("idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
|
|
12379
|
+
bootstrap.command("apply <repo>").description("idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
|
|
11691
12380
|
const o = {
|
|
11692
12381
|
class: rawValue("--class", "deployable"),
|
|
11693
12382
|
projectType: rawValue("--project-type", ""),
|
|
11694
12383
|
deployModel: rawValue("--deploy-model", ""),
|
|
12384
|
+
releaseTrack: rawValue("--release-track", ""),
|
|
11695
12385
|
execute: rawFlag("--execute"),
|
|
11696
12386
|
json: rawFlag("--json")
|
|
11697
12387
|
};
|
|
@@ -11710,10 +12400,19 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
11710
12400
|
const gh = async (args) => execFileP4("gh", args, { timeout: 2e4 });
|
|
11711
12401
|
const readFile2 = (p) => (0, import_node_fs7.existsSync)(p) ? (0, import_node_fs7.readFileSync)(p, "utf8") : null;
|
|
11712
12402
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
11713
|
-
const
|
|
12403
|
+
const rawVars = {};
|
|
11714
12404
|
for (const value of rawValues("--var")) {
|
|
11715
12405
|
const eq = value.indexOf("=");
|
|
11716
|
-
if (eq > 0)
|
|
12406
|
+
if (eq > 0) rawVars[value.slice(0, eq)] = value.slice(eq + 1);
|
|
12407
|
+
}
|
|
12408
|
+
const vars = withDerivedRepoVars(rawVars, parsedRepo, o.class);
|
|
12409
|
+
if (vars.PROJECT_ID) {
|
|
12410
|
+
try {
|
|
12411
|
+
const r = await gh(boardFieldsQueryArgs(vars.PROJECT_ID));
|
|
12412
|
+
const boardVars = extractBoardFieldVars(JSON.parse(r.stdout));
|
|
12413
|
+
for (const [k, v] of Object.entries(boardVars)) if (vars[k] == null) vars[k] = v;
|
|
12414
|
+
} catch {
|
|
12415
|
+
}
|
|
11717
12416
|
}
|
|
11718
12417
|
const actions = [];
|
|
11719
12418
|
const applied = [];
|
|
@@ -11739,22 +12438,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
11739
12438
|
exists = false;
|
|
11740
12439
|
}
|
|
11741
12440
|
}
|
|
11742
|
-
const
|
|
12441
|
+
const planned = planSeedAction(resolved, exists);
|
|
12442
|
+
const isBlock = resolved.source === "managed-block";
|
|
12443
|
+
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile2) : null;
|
|
12444
|
+
const action = reconcileSeedAction(planned, content, isBlock);
|
|
11743
12445
|
actions.push(action);
|
|
11744
12446
|
if (o.execute && (action.action === "create" || action.action === "update")) {
|
|
11745
|
-
const isBlock = resolved.source === "managed-block";
|
|
11746
|
-
const content = isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile2);
|
|
11747
|
-
if (content == null) {
|
|
11748
|
-
applied.push(`skip ${resolved.target} (no resolvable content)`);
|
|
11749
|
-
continue;
|
|
11750
|
-
}
|
|
11751
|
-
if (!isBlock) {
|
|
11752
|
-
const missing = missingPlaceholders(content);
|
|
11753
|
-
if (missing.length) {
|
|
11754
|
-
applied.push(`skip ${resolved.target} (unfilled: ${missing.join(", ")} \u2014 pass --var)`);
|
|
11755
|
-
continue;
|
|
11756
|
-
}
|
|
11757
|
-
}
|
|
11758
12447
|
await gh(contentPutArgs(repo, resolved.target, content, baseBranch, action.action === "update" ? sha : void 0));
|
|
11759
12448
|
applied.push(`${action.action} ${resolved.target}`);
|
|
11760
12449
|
}
|
|
@@ -11768,13 +12457,23 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
11768
12457
|
applied.push(`label ${l.name} (failed)`);
|
|
11769
12458
|
}
|
|
11770
12459
|
}
|
|
12460
|
+
for (const name of labelsToPrune(manifest.labels.map((l) => l.name))) {
|
|
12461
|
+
try {
|
|
12462
|
+
await gh(["label", "delete", name, "-R", repo, "--yes"]);
|
|
12463
|
+
applied.push(`label ${name} (pruned)`);
|
|
12464
|
+
} catch (e) {
|
|
12465
|
+
if (/not found/i.test(e.message ?? "")) continue;
|
|
12466
|
+
applied.push(`label ${name} (prune failed)`);
|
|
12467
|
+
}
|
|
12468
|
+
}
|
|
11771
12469
|
}
|
|
11772
12470
|
const ddbWrites = [];
|
|
11773
12471
|
let registerPayload;
|
|
11774
12472
|
try {
|
|
11775
12473
|
registerPayload = buildRegisterPayload(repo, o.class, vars, {
|
|
11776
12474
|
projectType: o.projectType || void 0,
|
|
11777
|
-
deployModel: o.deployModel || void 0
|
|
12475
|
+
deployModel: o.deployModel || void 0,
|
|
12476
|
+
releaseTrack: o.releaseTrack || void 0
|
|
11778
12477
|
});
|
|
11779
12478
|
} catch (e) {
|
|
11780
12479
|
return fail(`bootstrap apply: ${e.message}`);
|
|
@@ -11790,7 +12489,83 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
11790
12489
|
applied.push(`ddb register ${registerPayload.slug} (failed: ${why})`);
|
|
11791
12490
|
}
|
|
11792
12491
|
}
|
|
11793
|
-
|
|
12492
|
+
let fanoutPrUrl;
|
|
12493
|
+
if (o.execute) {
|
|
12494
|
+
const fanoutEntry = {
|
|
12495
|
+
repo: parsedRepo.name,
|
|
12496
|
+
slug,
|
|
12497
|
+
projectId: vars.PROJECT_ID || void 0,
|
|
12498
|
+
wikiRepo: vars.WIKI_REPO || parsedRepo.fullName,
|
|
12499
|
+
branch: baseBranch,
|
|
12500
|
+
cls: o.class,
|
|
12501
|
+
name: vars.NAME || parsedRepo.name
|
|
12502
|
+
};
|
|
12503
|
+
const readHubFile = async (path2) => {
|
|
12504
|
+
const r = await gh(["api", `repos/${HUB_REPO}/contents/${enc(path2)}?ref=development`]);
|
|
12505
|
+
const parsed = JSON.parse(r.stdout);
|
|
12506
|
+
if (parsed.encoding !== "base64" || typeof parsed.content !== "string" || !parsed.sha) {
|
|
12507
|
+
throw new Error(`could not read ${HUB_REPO}/${path2}`);
|
|
12508
|
+
}
|
|
12509
|
+
return { content: Buffer.from(parsed.content, "base64").toString("utf8"), sha: parsed.sha };
|
|
12510
|
+
};
|
|
12511
|
+
try {
|
|
12512
|
+
const fanoutFile = await readHubFile(".github/fanout-targets.json");
|
|
12513
|
+
const projectsFile = await readHubFile("projects.json");
|
|
12514
|
+
const plan2 = planFanoutRegistration(fanoutFile.content, projectsFile.content, fanoutEntry);
|
|
12515
|
+
if (!plan2.changed) {
|
|
12516
|
+
applied.push(`fanout: already registered (${parsedRepo.name})`);
|
|
12517
|
+
} else {
|
|
12518
|
+
const branchName = `bootstrap-register-fanout-${slug}`;
|
|
12519
|
+
const headSha = (await gh(["api", `repos/${HUB_REPO}/git/ref/heads/development`, "--jq", ".object.sha"])).stdout.trim();
|
|
12520
|
+
try {
|
|
12521
|
+
await gh(["api", "-X", "POST", `repos/${HUB_REPO}/git/refs`, "-f", `ref=refs/heads/${branchName}`, "-f", `sha=${headSha}`]);
|
|
12522
|
+
} catch (e) {
|
|
12523
|
+
if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
|
|
12524
|
+
}
|
|
12525
|
+
const branchFileSha = async (path2) => {
|
|
12526
|
+
try {
|
|
12527
|
+
const r = await gh(["api", `repos/${HUB_REPO}/contents/${enc(path2)}?ref=${branchName}`, "--jq", ".sha"]);
|
|
12528
|
+
return r.stdout.trim() || void 0;
|
|
12529
|
+
} catch (e) {
|
|
12530
|
+
if (/404|Not Found/i.test(String(e.message ?? ""))) return void 0;
|
|
12531
|
+
throw e;
|
|
12532
|
+
}
|
|
12533
|
+
};
|
|
12534
|
+
const fanoutBranchSha = await branchFileSha(".github/fanout-targets.json");
|
|
12535
|
+
const projectsBranchSha = await branchFileSha("projects.json");
|
|
12536
|
+
await gh(contentPutArgs(HUB_REPO, ".github/fanout-targets.json", plan2.fanoutTargets, branchName, fanoutBranchSha));
|
|
12537
|
+
await gh(contentPutArgs(HUB_REPO, "projects.json", plan2.projects, branchName, projectsBranchSha));
|
|
12538
|
+
const openPrs = await gh(["pr", "list", "--repo", HUB_REPO, "--head", branchName, "--base", "development", "--state", "open", "--json", "number,url"]);
|
|
12539
|
+
const prDecision = decideFanoutPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
12540
|
+
if (prDecision.action === "reuse") {
|
|
12541
|
+
fanoutPrUrl = prDecision.url;
|
|
12542
|
+
} else {
|
|
12543
|
+
const created = await ghCreate([
|
|
12544
|
+
"pr",
|
|
12545
|
+
"create",
|
|
12546
|
+
"--repo",
|
|
12547
|
+
HUB_REPO,
|
|
12548
|
+
"--base",
|
|
12549
|
+
"development",
|
|
12550
|
+
"--head",
|
|
12551
|
+
branchName,
|
|
12552
|
+
"--title",
|
|
12553
|
+
`bootstrap: register ${parsedRepo.name} for org-spine fanout`,
|
|
12554
|
+
"--body",
|
|
12555
|
+
`Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#933): adds ${parsedRepo.name} to projects.json + .github/fanout-targets.json so the spine fans out to it.`
|
|
12556
|
+
]);
|
|
12557
|
+
fanoutPrUrl = created.url;
|
|
12558
|
+
}
|
|
12559
|
+
await gh(["pr", "merge", fanoutPrUrl, "--repo", HUB_REPO, "--auto", "--squash"]).catch((e) => {
|
|
12560
|
+
if (!/already/i.test(String(e.message ?? ""))) throw e;
|
|
12561
|
+
});
|
|
12562
|
+
applied.push(`fanout: PR ${fanoutPrUrl} (auto-merge enabled)`);
|
|
12563
|
+
}
|
|
12564
|
+
} catch (e) {
|
|
12565
|
+
return fail(`bootstrap apply: fanout registration failed: ${e.message}`);
|
|
12566
|
+
}
|
|
12567
|
+
}
|
|
12568
|
+
if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, actions, applied, ddbWrites, fanoutPrUrl }, null, 2));
|
|
11794
12569
|
else {
|
|
11795
12570
|
console.log(renderSeedPlan(actions));
|
|
11796
12571
|
if (o.execute) console.log(`
|
|
@@ -12207,7 +12982,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
12207
12982
|
} catch (e) {
|
|
12208
12983
|
console.error(`[mmi-hook] saga session failed: ${e.message}`);
|
|
12209
12984
|
}
|
|
12210
|
-
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn:
|
|
12985
|
+
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process7.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
12211
12986
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
12212
12987
|
rulesSync: (io) => runRulesSync({ quiet: true }, io),
|
|
12213
12988
|
sagaShow: (io) => runSagaShow({ quiet: true }, io),
|