@mutmutco/cli 3.99.0 → 3.100.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/main.cjs +197 -2
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -20923,7 +20923,7 @@ function authorizeBodyHasMismatch(body) {
|
|
|
20923
20923
|
}
|
|
20924
20924
|
|
|
20925
20925
|
// src/project-set.ts
|
|
20926
|
-
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
|
|
20926
|
+
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate", "seedCanary"];
|
|
20927
20927
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
20928
20928
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
20929
20929
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -21177,6 +21177,11 @@ function parseRuntimeVaultOnlyVar(raw) {
|
|
|
21177
21177
|
if (raw === "false") return false;
|
|
21178
21178
|
throw new Error("org project set: runtimeVaultOnly must be true or false");
|
|
21179
21179
|
}
|
|
21180
|
+
function parseSeedCanaryVar(raw) {
|
|
21181
|
+
if (raw === "true") return true;
|
|
21182
|
+
if (raw === "false") return false;
|
|
21183
|
+
throw new Error("org project set: seedCanary must be true or false");
|
|
21184
|
+
}
|
|
21180
21185
|
function parseConsumesDesignSystemVar(raw) {
|
|
21181
21186
|
if (raw === "fofu") return raw;
|
|
21182
21187
|
throw new Error('org project set: consumesDesignSystem must be "fofu"');
|
|
@@ -21266,7 +21271,8 @@ var SETTABLE_VAR_KEYS = [
|
|
|
21266
21271
|
"ci",
|
|
21267
21272
|
"requiredChecks",
|
|
21268
21273
|
"gate",
|
|
21269
|
-
"secrets"
|
|
21274
|
+
"secrets",
|
|
21275
|
+
"seedCanary"
|
|
21270
21276
|
];
|
|
21271
21277
|
var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
21272
21278
|
var SETTABLE_VAR_HINTS = {
|
|
@@ -21277,6 +21283,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
21277
21283
|
fofuEnabled: "true|false",
|
|
21278
21284
|
consumesDesignSystem: '"fofu"',
|
|
21279
21285
|
runtimeVaultOnly: "true|false",
|
|
21286
|
+
seedCanary: "true|false",
|
|
21280
21287
|
repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
|
|
21281
21288
|
oauth: "JSON {subdomains,domains,callbackPath,fofuSubdomain}",
|
|
21282
21289
|
requiredGcpApis: "comma-string",
|
|
@@ -21373,6 +21380,8 @@ function buildProjectSetPatch(input) {
|
|
|
21373
21380
|
patch[key] = parseFofuEnabledVar(raw);
|
|
21374
21381
|
} else if (key === "runtimeVaultOnly") {
|
|
21375
21382
|
patch[key] = parseRuntimeVaultOnlyVar(raw);
|
|
21383
|
+
} else if (key === "seedCanary") {
|
|
21384
|
+
patch[key] = parseSeedCanaryVar(raw);
|
|
21376
21385
|
} else if (key === "consumesDesignSystem") {
|
|
21377
21386
|
patch[key] = parseConsumesDesignSystemVar(raw);
|
|
21378
21387
|
} else if (key === "publishDir") {
|
|
@@ -24017,6 +24026,62 @@ function renderPropagationReport(plan) {
|
|
|
24017
24026
|
return lines.join("\n");
|
|
24018
24027
|
}
|
|
24019
24028
|
|
|
24029
|
+
// src/bootstrap-rollback.ts
|
|
24030
|
+
function resolveRollbackRecord(records, repo, target) {
|
|
24031
|
+
const candidates = records.filter((r) => r.repo === repo && r.target === target && r.mergeSha);
|
|
24032
|
+
if (candidates.length === 0) {
|
|
24033
|
+
return { found: false, reason: `no propagation record resolves for ${repo} + ${target} \u2014 refusing to guess a commit to revert` };
|
|
24034
|
+
}
|
|
24035
|
+
const clean4 = candidates.filter((r) => r.files.length === 1 && r.files[0] === target);
|
|
24036
|
+
if (clean4.length === 0) {
|
|
24037
|
+
return {
|
|
24038
|
+
found: false,
|
|
24039
|
+
reason: `${candidates.length} merged seed-propagate PR(s) found for ${repo} + ${target}, but none is a clean single-file propagation of exactly this target \u2014 refusing to guess a commit to revert`
|
|
24040
|
+
};
|
|
24041
|
+
}
|
|
24042
|
+
const latest = clean4.slice().sort((a, b) => a.mergedAt < b.mergedAt ? 1 : a.mergedAt > b.mergedAt ? -1 : 0)[0];
|
|
24043
|
+
return { found: true, record: latest };
|
|
24044
|
+
}
|
|
24045
|
+
function planRollback(repo, target, slug, records) {
|
|
24046
|
+
const resolution = resolveRollbackRecord(records, repo, target);
|
|
24047
|
+
if (!resolution.found) return { repo, target, resolution };
|
|
24048
|
+
const branch = `seed-rollback-${slug}`;
|
|
24049
|
+
const title = `revert: rollback org-owned ${target} in ${repo} (seed PR #${resolution.record.number})`;
|
|
24050
|
+
const body = renderRollbackPrBody(resolution.record);
|
|
24051
|
+
return { repo, target, resolution, branch, title, body };
|
|
24052
|
+
}
|
|
24053
|
+
function renderRollbackPrBody(record) {
|
|
24054
|
+
return [
|
|
24055
|
+
`Per-repo emergency revert of \`${record.target}\`'s seed-propagate merge (\`mmi-cli bootstrap rollback\`, #4240).`,
|
|
24056
|
+
"",
|
|
24057
|
+
`Reverts ${record.url} (merge ${record.mergeSha}) \u2014 restores this repo's copy of \`${record.target}\` to its content immediately before that merge. Nothing else in this repo changes.`,
|
|
24058
|
+
"",
|
|
24059
|
+
"This is the emergency stop, not the fix: MMI-Hub is still the source of truth during a rollback. Closure is the Hub reverting (or fixing forward) the bad seed commit on `development` \u2014 once it does, this repo matches the Hub again and the drift alarm closes itself. A revert with no Hub-side follow-up re-alarms as drift within a week \u2014 deliberately, so an emergency divergence can never silently become permanent.",
|
|
24060
|
+
"",
|
|
24061
|
+
"Never re-run `bootstrap propagate` with the pre-revert bytes before the Hub itself is fixed \u2014 that reopens exactly what this PR closes."
|
|
24062
|
+
].join("\n");
|
|
24063
|
+
}
|
|
24064
|
+
function renderRollbackReport(plan) {
|
|
24065
|
+
if (!plan.resolution.found) {
|
|
24066
|
+
return `bootstrap rollback \u2014 ${plan.repo} / ${plan.target}: REFUSED \u2014 ${plan.resolution.reason}`;
|
|
24067
|
+
}
|
|
24068
|
+
const r = plan.resolution.record;
|
|
24069
|
+
const opened = plan.prUrl ? ` \u2014 opened ${plan.prUrl}` : "";
|
|
24070
|
+
return `bootstrap rollback \u2014 ${plan.repo} / ${plan.target}: reverting PR#${r.number} (merge ${r.mergeSha}${r.mergedAt ? `, merged ${r.mergedAt}` : ""}) on branch ${plan.branch}${opened}`;
|
|
24071
|
+
}
|
|
24072
|
+
function seedPrRecordFromPropagationRecord(record) {
|
|
24073
|
+
if (!record.mergeSha || record.prNumber == null || !record.prUrl) return null;
|
|
24074
|
+
return {
|
|
24075
|
+
repo: record.repo,
|
|
24076
|
+
target: record.target,
|
|
24077
|
+
number: record.prNumber,
|
|
24078
|
+
url: record.prUrl,
|
|
24079
|
+
mergeSha: record.mergeSha,
|
|
24080
|
+
mergedAt: "",
|
|
24081
|
+
files: [record.target]
|
|
24082
|
+
};
|
|
24083
|
+
}
|
|
24084
|
+
|
|
24020
24085
|
// src/bootstrap-verify.ts
|
|
24021
24086
|
var TRAIN_BRANCHES2 = ["development", "rc", "main"];
|
|
24022
24087
|
var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md", "docs/index.md"];
|
|
@@ -25271,6 +25336,136 @@ Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --exe
|
|
|
25271
25336
|
else console.log(renderPropagationReport(plan));
|
|
25272
25337
|
if (plan.halted) process.exitCode = 1;
|
|
25273
25338
|
});
|
|
25339
|
+
bootstrap.command("rollback <repo>").description("#4240: open a per-repo revert PR of the recorded seed-propagate merge \u2014 never a fleet-wide overwrite; dry-run unless --execute").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).option("--target <path>", "the manifest target to roll back (an ownership:org + source:self seed, e.g. .github/workflows/agent-pr.yml)").option("--record <path>", "read propagation candidates from a persisted `bootstrap propagate --json` report instead of the live seed-propagate PR history").option("--execute", "LIVE revert via gh (master-gated) \u2014 opens/reuses the seed-rollback PR; dry-run prints the plan only").option("--json", "machine-readable output").action(async (repo) => {
|
|
25340
|
+
const o = {
|
|
25341
|
+
class: rawValue("--class", "deployable"),
|
|
25342
|
+
target: rawValue("--target", ""),
|
|
25343
|
+
record: rawValue("--record", ""),
|
|
25344
|
+
execute: rawFlag("--execute"),
|
|
25345
|
+
json: rawFlag("--json")
|
|
25346
|
+
};
|
|
25347
|
+
if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap rollback: --class must be deployable or content");
|
|
25348
|
+
let parsedRepo;
|
|
25349
|
+
try {
|
|
25350
|
+
parsedRepo = parseOwnerRepo(repo);
|
|
25351
|
+
} catch (e) {
|
|
25352
|
+
return fail(`bootstrap rollback: ${e.message}`);
|
|
25353
|
+
}
|
|
25354
|
+
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
25355
|
+
if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap rollback: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the manifest names which targets are org-owned and therefore propagated (and rollback-able)`);
|
|
25356
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
25357
|
+
const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
|
|
25358
|
+
if (!o.target) {
|
|
25359
|
+
return fail(`bootstrap rollback: --target <path> is required \u2014 one of:
|
|
25360
|
+
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
25361
|
+
}
|
|
25362
|
+
const seed = propagatable.find((s) => s.target === o.target);
|
|
25363
|
+
if (!seed) return fail(`bootstrap rollback: --target '${o.target}' names no ownership:org + source:self seed in ${manifestPath}. Propagatable (hence rollback-able) targets:
|
|
25364
|
+
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
25365
|
+
const slug = parsedRepo.slug;
|
|
25366
|
+
const baseBranch = o.class === "content" ? "main" : "development";
|
|
25367
|
+
const branchPrefix = "seed-propagate";
|
|
25368
|
+
const propagateBranch = `${branchPrefix}-${slug}`;
|
|
25369
|
+
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
25370
|
+
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
25371
|
+
let candidates;
|
|
25372
|
+
if (o.record) {
|
|
25373
|
+
if (!(0, import_node_fs31.existsSync)(o.record)) return fail(`bootstrap rollback: --record '${o.record}' not found`);
|
|
25374
|
+
let parsed;
|
|
25375
|
+
try {
|
|
25376
|
+
parsed = JSON.parse((0, import_node_fs31.readFileSync)(o.record, "utf8"));
|
|
25377
|
+
} catch (e) {
|
|
25378
|
+
return fail(`bootstrap rollback: --record '${o.record}' is not valid JSON: ${e.message}`);
|
|
25379
|
+
}
|
|
25380
|
+
const recs = Array.isArray(parsed?.records) ? parsed.records : Array.isArray(parsed) ? parsed : [];
|
|
25381
|
+
candidates = recs.map((r) => {
|
|
25382
|
+
try {
|
|
25383
|
+
return seedPrRecordFromPropagationRecord(r);
|
|
25384
|
+
} catch {
|
|
25385
|
+
return null;
|
|
25386
|
+
}
|
|
25387
|
+
}).filter((r) => r !== null);
|
|
25388
|
+
} else {
|
|
25389
|
+
candidates = [];
|
|
25390
|
+
try {
|
|
25391
|
+
const listed = await gh(["pr", "list", "--repo", repo, "--head", propagateBranch, "--base", baseBranch, "--state", "merged", "--json", "number,url,mergedAt,mergeCommit,files", "--limit", "20"]);
|
|
25392
|
+
const arr = JSON.parse(listed.stdout || "[]");
|
|
25393
|
+
for (const p of arr) {
|
|
25394
|
+
if (!p.mergeCommit?.oid) continue;
|
|
25395
|
+
candidates.push({
|
|
25396
|
+
repo,
|
|
25397
|
+
target: seed.target,
|
|
25398
|
+
number: p.number,
|
|
25399
|
+
url: p.url,
|
|
25400
|
+
mergeSha: p.mergeCommit.oid,
|
|
25401
|
+
mergedAt: p.mergedAt ?? "",
|
|
25402
|
+
files: (p.files ?? []).map((f) => f.path)
|
|
25403
|
+
});
|
|
25404
|
+
}
|
|
25405
|
+
} catch (e) {
|
|
25406
|
+
return fail(`bootstrap rollback: could not read ${repo}'s merged ${propagateBranch} PR history: ${e.message}`);
|
|
25407
|
+
}
|
|
25408
|
+
}
|
|
25409
|
+
const plan = planRollback(repo, seed.target, slug, candidates);
|
|
25410
|
+
if (!plan.resolution.found) {
|
|
25411
|
+
if (o.json) console.log(JSON.stringify(plan, null, 2));
|
|
25412
|
+
else console.log(renderRollbackReport(plan));
|
|
25413
|
+
return fail(`bootstrap rollback: ${plan.resolution.reason}`);
|
|
25414
|
+
}
|
|
25415
|
+
if (o.execute) {
|
|
25416
|
+
const record = plan.resolution.record;
|
|
25417
|
+
const parentResp = await gh(["api", `repos/${repo}/commits/${record.mergeSha}`, "--jq", ".parents[0].sha"]);
|
|
25418
|
+
const parentSha = parentResp.stdout.trim();
|
|
25419
|
+
if (!parentSha) return fail(`bootstrap rollback: could not resolve ${repo}@${record.mergeSha}'s parent commit \u2014 refusing to guess what to restore`);
|
|
25420
|
+
let preSeedContent = null;
|
|
25421
|
+
try {
|
|
25422
|
+
const resp = await gh(["api", `repos/${repo}/contents/${enc(seed.target)}?ref=${parentSha}`]);
|
|
25423
|
+
const parsed = JSON.parse(resp.stdout);
|
|
25424
|
+
preSeedContent = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : null;
|
|
25425
|
+
} catch {
|
|
25426
|
+
preSeedContent = null;
|
|
25427
|
+
}
|
|
25428
|
+
if (preSeedContent == null) return fail(`bootstrap rollback: '${seed.target}' did not exist in ${repo} at ${parentSha} (the commit before the seed merge) \u2014 nothing to restore; refusing to guess`);
|
|
25429
|
+
const baseRef = await gh(["api", `repos/${repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"]);
|
|
25430
|
+
const baseSha = baseRef.stdout.trim();
|
|
25431
|
+
let branchExists = true;
|
|
25432
|
+
try {
|
|
25433
|
+
await gh(["api", `repos/${repo}/git/ref/heads/${plan.branch}`]);
|
|
25434
|
+
} catch {
|
|
25435
|
+
branchExists = false;
|
|
25436
|
+
}
|
|
25437
|
+
if (!branchExists) await gh(["api", `repos/${repo}/git/refs`, "-f", `ref=refs/heads/${plan.branch}`, "-f", `sha=${baseSha}`]);
|
|
25438
|
+
let existingSha;
|
|
25439
|
+
try {
|
|
25440
|
+
const cur = await gh(["api", `repos/${repo}/contents/${enc(seed.target)}?ref=${plan.branch}`]);
|
|
25441
|
+
existingSha = JSON.parse(cur.stdout).sha;
|
|
25442
|
+
} catch {
|
|
25443
|
+
existingSha = void 0;
|
|
25444
|
+
}
|
|
25445
|
+
const tmp = (0, import_node_path29.join)((0, import_node_os11.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
25446
|
+
(0, import_node_fs31.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
|
|
25447
|
+
try {
|
|
25448
|
+
await gh(contentPutInputArgs(repo, seed.target, tmp));
|
|
25449
|
+
} finally {
|
|
25450
|
+
try {
|
|
25451
|
+
(0, import_node_fs31.unlinkSync)(tmp);
|
|
25452
|
+
} catch {
|
|
25453
|
+
}
|
|
25454
|
+
}
|
|
25455
|
+
const openPrs = await gh(["pr", "list", "--repo", repo, "--head", plan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
25456
|
+
const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
25457
|
+
let prUrl;
|
|
25458
|
+
if (prDecision.action === "reuse") {
|
|
25459
|
+
prUrl = prDecision.url;
|
|
25460
|
+
} else {
|
|
25461
|
+
const created = await ghCreate(["pr", "create", "--repo", repo, "--base", baseBranch, "--head", plan.branch, "--title", plan.title, "--body", plan.body]);
|
|
25462
|
+
prUrl = created.url;
|
|
25463
|
+
}
|
|
25464
|
+
plan.prUrl = prUrl;
|
|
25465
|
+
}
|
|
25466
|
+
if (o.json) console.log(JSON.stringify(plan, null, 2));
|
|
25467
|
+
else console.log(renderRollbackReport(plan));
|
|
25468
|
+
});
|
|
25274
25469
|
}
|
|
25275
25470
|
|
|
25276
25471
|
// src/stage-commands.ts
|
package/package.json
CHANGED