@mutmutco/cli 4.5.46 → 4.5.48
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 +200 -74
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -14365,6 +14365,9 @@ async function activateProductRuleset(repo, rulesetBody, client, enforcement = "
|
|
|
14365
14365
|
return { action: "created", enforcement };
|
|
14366
14366
|
}
|
|
14367
14367
|
|
|
14368
|
+
// src/bootstrap-apply.ts
|
|
14369
|
+
var import_node_crypto5 = require("node:crypto");
|
|
14370
|
+
|
|
14368
14371
|
// src/bootstrap-seeds.ts
|
|
14369
14372
|
var PLACEHOLDER_RE = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
14370
14373
|
function loadBootstrapSeeds(manifestJson) {
|
|
@@ -15593,6 +15596,32 @@ function planSeedAction(seed, exists) {
|
|
|
15593
15596
|
}
|
|
15594
15597
|
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" };
|
|
15595
15598
|
}
|
|
15599
|
+
var GATE_RENDER_NOTE = "# Rendered from the registry gate config by mmi-cli devops bootstrap apply <repo> --only gate.yml --from-config; edit the config, not this file.\n";
|
|
15600
|
+
var gateHash = (body) => (0, import_node_crypto5.createHash)("sha256").update(body, "utf8").digest("hex");
|
|
15601
|
+
function parseGateRenderStamp(content) {
|
|
15602
|
+
const match = /^# mmi-gate-render: sha256=([a-f0-9]{64})\n/.exec(content);
|
|
15603
|
+
return match ? { hash: match[1], body: content.slice(match[0].length) } : null;
|
|
15604
|
+
}
|
|
15605
|
+
function verifyGateRenderStamp(content) {
|
|
15606
|
+
const stamp = parseGateRenderStamp(content);
|
|
15607
|
+
return stamp !== null && stamp.body.startsWith(GATE_RENDER_NOTE) && gateHash(stamp.body) === stamp.hash;
|
|
15608
|
+
}
|
|
15609
|
+
function stampGateRender(rendered) {
|
|
15610
|
+
const body = GATE_RENDER_NOTE + rendered;
|
|
15611
|
+
return `# mmi-gate-render: sha256=${gateHash(body)}
|
|
15612
|
+
${body}`;
|
|
15613
|
+
}
|
|
15614
|
+
function planGateFromConfig(current, rendered) {
|
|
15615
|
+
const target = ".github/workflows/gate.yml";
|
|
15616
|
+
const base = { target, ownership: "repo" };
|
|
15617
|
+
if (current === null) return { ...base, action: "create", reason: "registry gate config; review via branch + PR" };
|
|
15618
|
+
const stamp = parseGateRenderStamp(current);
|
|
15619
|
+
if (stamp) {
|
|
15620
|
+
if (!verifyGateRenderStamp(current)) return { ...base, action: "skip", refused: true, reason: "REFUSED: stamped gate was hand-edited since its last render (diff below)" };
|
|
15621
|
+
return stamp.body === GATE_RENDER_NOTE + rendered ? { ...base, action: "skip", reason: "already current from registry gate config" } : { ...base, action: "update", reason: "registry gate config changed; update stamped gate via branch + PR" };
|
|
15622
|
+
}
|
|
15623
|
+
return current === rendered ? { ...base, action: "update", reason: "adopt legacy gate: add registry render stamp via branch + PR" } : { ...base, action: "skip", refused: true, reason: "REFUSED: unstamped gate differs from registry render; make the registry config reproduce the file first, then re-run to adopt (diff below)" };
|
|
15624
|
+
}
|
|
15596
15625
|
function reconcileSeedAction(action, content, isManagedBlock, remoteContent = null) {
|
|
15597
15626
|
if (action.action === "skip") return action;
|
|
15598
15627
|
if (content == null) {
|
|
@@ -17014,10 +17043,10 @@ var rollout_plan_default = {
|
|
|
17014
17043
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
17015
17044
|
},
|
|
17016
17045
|
baseline: {
|
|
17017
|
-
version: "4.5.
|
|
17018
|
-
tag: "v4.5.
|
|
17019
|
-
commit: "
|
|
17020
|
-
npm: "@mutmutco/cli@4.5.
|
|
17046
|
+
version: "4.5.48",
|
|
17047
|
+
tag: "v4.5.48",
|
|
17048
|
+
commit: "92f1d9951b04",
|
|
17049
|
+
npm: "@mutmutco/cli@4.5.48"
|
|
17021
17050
|
},
|
|
17022
17051
|
exitCriterion: "fleet-n-of-n",
|
|
17023
17052
|
hubOnlyShortcut: "forbidden",
|
|
@@ -17034,14 +17063,14 @@ var rollout_plan_default = {
|
|
|
17034
17063
|
repo: "mutmutco/mmi-hub",
|
|
17035
17064
|
role: "canary",
|
|
17036
17065
|
schedule: "train",
|
|
17037
|
-
v3Target: "v4.5.
|
|
17066
|
+
v3Target: "v4.5.48"
|
|
17038
17067
|
}
|
|
17039
17068
|
],
|
|
17040
17069
|
rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
|
|
17041
17070
|
rollback: {
|
|
17042
17071
|
independent: true,
|
|
17043
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
17044
|
-
v3Target: "v4.5.
|
|
17072
|
+
mechanism: "npm dist-tag latest -> 4.5.48 and redeploy the Hub Lambda from tag v4.5.48 (92f1d9951b04); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17073
|
+
v3Target: "v4.5.48 (@mutmutco/cli@4.5.48, tag commit 92f1d9951b04 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
17045
17074
|
}
|
|
17046
17075
|
},
|
|
17047
17076
|
{
|
|
@@ -22319,7 +22348,7 @@ function checkLineEndings(probe) {
|
|
|
22319
22348
|
// src/release-ledger.ts
|
|
22320
22349
|
var import_node_fs22 = require("node:fs");
|
|
22321
22350
|
var import_node_path22 = require("node:path");
|
|
22322
|
-
var
|
|
22351
|
+
var import_node_crypto6 = require("node:crypto");
|
|
22323
22352
|
|
|
22324
22353
|
// src/file-lock.ts
|
|
22325
22354
|
var import_promises2 = require("node:fs/promises");
|
|
@@ -22677,7 +22706,7 @@ function releaseLedgerPath(cwd) {
|
|
|
22677
22706
|
}
|
|
22678
22707
|
function atomicWriteJson(path2, ledger) {
|
|
22679
22708
|
(0, import_node_fs22.mkdirSync)((0, import_node_path22.dirname)(path2), { recursive: true });
|
|
22680
|
-
const tmp = `${path2}.tmp-${process.pid}-${(0,
|
|
22709
|
+
const tmp = `${path2}.tmp-${process.pid}-${(0, import_node_crypto6.randomBytes)(4).toString("hex")}`;
|
|
22681
22710
|
(0, import_node_fs22.writeFileSync)(tmp, `${JSON.stringify(ledger, null, 2)}
|
|
22682
22711
|
`, "utf8");
|
|
22683
22712
|
(0, import_node_fs22.renameSync)(tmp, path2);
|
|
@@ -29092,13 +29121,13 @@ function renderPluginCachePlan(plan, applied) {
|
|
|
29092
29121
|
}
|
|
29093
29122
|
|
|
29094
29123
|
// ../shared/github-quota-scope.ts
|
|
29095
|
-
var
|
|
29124
|
+
var import_node_crypto7 = require("node:crypto");
|
|
29096
29125
|
function appQuotaScope(appId, installationId, origin = "https://api.github.com", resource = "graphql") {
|
|
29097
|
-
return `github-app-v1:${(0,
|
|
29126
|
+
return `github-app-v1:${(0, import_node_crypto7.createHash)("sha256").update(JSON.stringify([new URL(origin).origin, appId.trim(), installationId.trim(), resource])).digest("hex")}`;
|
|
29098
29127
|
}
|
|
29099
29128
|
|
|
29100
29129
|
// src/app-actor.ts
|
|
29101
|
-
var
|
|
29130
|
+
var import_node_crypto8 = require("node:crypto");
|
|
29102
29131
|
var APP_ACTOR_ENV = "MMI_ACTOR";
|
|
29103
29132
|
var APP_VAULT_REPO = "mutmutco/MMI-Hub";
|
|
29104
29133
|
var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
|
|
@@ -29149,7 +29178,7 @@ function mintAppJwt(appId, privateKeyPem, nowSec) {
|
|
|
29149
29178
|
exp: now + APP_JWT_TTL_S,
|
|
29150
29179
|
iss: appId
|
|
29151
29180
|
}));
|
|
29152
|
-
const signer = (0,
|
|
29181
|
+
const signer = (0, import_node_crypto8.createSign)("RSA-SHA256");
|
|
29153
29182
|
signer.update(`${header}.${payload}`);
|
|
29154
29183
|
return `${header}.${payload}.${signer.sign(privateKeyPem, "base64url")}`;
|
|
29155
29184
|
}
|
|
@@ -30045,7 +30074,7 @@ var import_node_os16 = require("node:os");
|
|
|
30045
30074
|
var import_node_path27 = require("node:path");
|
|
30046
30075
|
|
|
30047
30076
|
// src/box-control.ts
|
|
30048
|
-
var
|
|
30077
|
+
var import_node_crypto9 = require("node:crypto");
|
|
30049
30078
|
var MANIFEST_PATH = "infra/box-control/manifest.json";
|
|
30050
30079
|
var ALLOWED_POST = /* @__PURE__ */ new Set(["systemctl daemon-reload"]);
|
|
30051
30080
|
function parseManifest(text) {
|
|
@@ -30092,7 +30121,7 @@ function filesForBox(files, box) {
|
|
|
30092
30121
|
return files.flatMap((file) => file.targets.filter((t) => t.box === box).map((t) => ({ file, dest: t.dest })));
|
|
30093
30122
|
}
|
|
30094
30123
|
function md5(content) {
|
|
30095
|
-
return (0,
|
|
30124
|
+
return (0, import_node_crypto9.createHash)("md5").update(content).digest("hex");
|
|
30096
30125
|
}
|
|
30097
30126
|
function planRemoteScript(dests) {
|
|
30098
30127
|
const lines2 = ["set -u"];
|
|
@@ -31958,7 +31987,7 @@ function renderDeployPortDoctor(report) {
|
|
|
31958
31987
|
}
|
|
31959
31988
|
|
|
31960
31989
|
// src/bootstrap-drift.ts
|
|
31961
|
-
var
|
|
31990
|
+
var import_node_crypto10 = require("node:crypto");
|
|
31962
31991
|
function byteComparableSeeds(manifest, cls) {
|
|
31963
31992
|
return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
|
|
31964
31993
|
}
|
|
@@ -31978,7 +32007,7 @@ function compareSeedBytes(hubContent, repoContent) {
|
|
|
31978
32007
|
return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
|
|
31979
32008
|
}
|
|
31980
32009
|
function seedContentHash(content) {
|
|
31981
|
-
return (0,
|
|
32010
|
+
return (0, import_node_crypto10.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
|
|
31982
32011
|
}
|
|
31983
32012
|
function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
|
|
31984
32013
|
const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
|
|
@@ -32140,7 +32169,7 @@ function renderPropagationReport(plan) {
|
|
|
32140
32169
|
}
|
|
32141
32170
|
|
|
32142
32171
|
// src/bootstrap-propagation-identity.ts
|
|
32143
|
-
var
|
|
32172
|
+
var import_node_crypto11 = require("node:crypto");
|
|
32144
32173
|
var PROPAGATION_BRANCH_PREFIX = "seed-propagate-";
|
|
32145
32174
|
var TARGET_MARKER_NAME = "mmi-bootstrap-propagation-target";
|
|
32146
32175
|
function safeBranchPart(value, maxLength, fallback) {
|
|
@@ -32153,7 +32182,7 @@ function repoSlug2(repo) {
|
|
|
32153
32182
|
function propagationBranch(repo, target) {
|
|
32154
32183
|
const repoPart = safeBranchPart(repoSlug2(repo), 32, "repo");
|
|
32155
32184
|
const targetPart = safeBranchPart(target, 48, "target");
|
|
32156
|
-
const hash = (0,
|
|
32185
|
+
const hash = (0, import_node_crypto11.createHash)("sha256").update(repo.trim().toLowerCase()).update("\0").update(target).digest("hex").slice(0, 12);
|
|
32157
32186
|
return `${PROPAGATION_BRANCH_PREFIX}${repoPart}-${targetPart}-${hash}`;
|
|
32158
32187
|
}
|
|
32159
32188
|
function legacyPropagationBranch(repo) {
|
|
@@ -33309,7 +33338,7 @@ function renderBootstrapVerifyReport(report) {
|
|
|
33309
33338
|
}
|
|
33310
33339
|
|
|
33311
33340
|
// src/bootstrap-seed-delivery.ts
|
|
33312
|
-
var
|
|
33341
|
+
var import_node_crypto12 = require("node:crypto");
|
|
33313
33342
|
init_github_client();
|
|
33314
33343
|
var SHA = /^[a-f0-9]{40}$/;
|
|
33315
33344
|
async function readSeedFile(repo, target, ref, client) {
|
|
@@ -33332,7 +33361,7 @@ async function readTargetedSeedFiles(repo, plan, target, ref, client) {
|
|
|
33332
33361
|
const candidate = ref === plan.baseSha ? base : await readSeedFile(repo, target, ref, client);
|
|
33333
33362
|
return { base, candidate };
|
|
33334
33363
|
}
|
|
33335
|
-
async function targetedSeedPlan(repo, slug, target, baseBranch, sourceSha, client) {
|
|
33364
|
+
async function targetedSeedPlan(repo, slug, target, baseBranch, sourceSha, client, forcePr = false) {
|
|
33336
33365
|
const [rules, base] = await Promise.all([
|
|
33337
33366
|
client.rest("GET", `repos/${repo}/rules/branches/${encodeURIComponent(baseBranch)}`),
|
|
33338
33367
|
client.rest("GET", `repos/${repo}/git/ref/heads/${encodeURIComponent(baseBranch)}`)
|
|
@@ -33342,8 +33371,10 @@ async function targetedSeedPlan(repo, slug, target, baseBranch, sourceSha, clien
|
|
|
33342
33371
|
}
|
|
33343
33372
|
const baseSha = base?.object?.sha;
|
|
33344
33373
|
if (!baseSha || !SHA.test(baseSha) || !SHA.test(sourceSha)) throw new Error("bootstrap apply: seed identity is unreadable");
|
|
33345
|
-
const identity = (0,
|
|
33346
|
-
|
|
33374
|
+
const identity = (0, import_node_crypto12.createHash)("sha256").update(JSON.stringify([repo.toLowerCase(), target, baseSha, sourceSha])).digest("hex").slice(0, 24);
|
|
33375
|
+
const delivery = planSeedDelivery(rules, slug, baseBranch, `bootstrap-seed-${identity}`);
|
|
33376
|
+
const scoped = forcePr && delivery.mode === "direct" ? { mode: "pr", ref: `bootstrap-seed-${identity}-${slug}`, branch: `bootstrap-seed-${identity}-${slug}`, reason: "registry gate render requires review" } : delivery;
|
|
33377
|
+
return { ...scoped, baseSha, baseBranch };
|
|
33347
33378
|
}
|
|
33348
33379
|
async function verifyTargetedSeedBranch(repo, plan, target, client, allowMissing = false) {
|
|
33349
33380
|
let ref;
|
|
@@ -33405,6 +33436,24 @@ async function enableTargetedSeedAutoMerge(repo, number, plan, target, expected,
|
|
|
33405
33436
|
|
|
33406
33437
|
// src/bootstrap-commands.ts
|
|
33407
33438
|
var execGitForSeedSource = async (args) => (await execFileP("git", args, { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
33439
|
+
async function gateRenderDiff(current, rendered) {
|
|
33440
|
+
const dir = (0, import_node_fs28.mkdtempSync)((0, import_node_path28.join)((0, import_node_os17.tmpdir)(), "mmi-gate-diff-"));
|
|
33441
|
+
const before = (0, import_node_path28.join)(dir, "committed-gate.yml");
|
|
33442
|
+
const after = (0, import_node_path28.join)(dir, "registry-render.yml");
|
|
33443
|
+
try {
|
|
33444
|
+
(0, import_node_fs28.writeFileSync)(before, current);
|
|
33445
|
+
(0, import_node_fs28.writeFileSync)(after, rendered);
|
|
33446
|
+
try {
|
|
33447
|
+
return (await execFileP("git", ["diff", "--no-index", "--", before, after], { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
33448
|
+
} catch (e) {
|
|
33449
|
+
const result = e;
|
|
33450
|
+
if (result.code === 1 && result.stdout) return result.stdout;
|
|
33451
|
+
throw e;
|
|
33452
|
+
}
|
|
33453
|
+
} finally {
|
|
33454
|
+
(0, import_node_fs28.rmSync)(dir, { recursive: true, force: true });
|
|
33455
|
+
}
|
|
33456
|
+
}
|
|
33408
33457
|
function bootstrapTrainDoctorDeps() {
|
|
33409
33458
|
return {
|
|
33410
33459
|
run: async (file, args) => (process.platform === "win32" && file === "npm" ? await execFileP("cmd.exe", ["/c", "npm", ...args], { timeout: 3e4 }) : await execFileP(file, args, { timeout: 3e4 })).stdout,
|
|
@@ -33645,18 +33694,21 @@ function registerBootstrapCommands(program3) {
|
|
|
33645
33694
|
}
|
|
33646
33695
|
if (findings.some((f) => f.state !== "waived")) process.exitCode = 1;
|
|
33647
33696
|
});
|
|
33648
|
-
bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--box <name>", "resolve this live box and plan free registry DEPLOY# ports (container deploy models)").option("--domains <stages>", "optional comma-separated dev=host,rc=host,main=host deploy domains (requires --box)").option("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes; or the special target environments (#5035) to run the deployment-environment reconcile alone (dry-run classifies, --execute writes)").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
|
|
33697
|
+
bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--box <name>", "resolve this live box and plan free registry DEPLOY# ports (container deploy models)").option("--domains <stages>", "optional comma-separated dev=host,rc=host,main=host deploy domains (requires --box)").option("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes; or the special target environments (#5035) to run the deployment-environment reconcile alone (dry-run classifies, --execute writes)").option("--from-config", "with --only gate.yml: adopt an equal legacy gate with a render stamp, then update from registry config via review PR; refuse hand edits").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
|
|
33649
33698
|
const o = {
|
|
33650
33699
|
class: rawValue("--class", "deployable"),
|
|
33651
33700
|
projectType: rawValue("--project-type", ""),
|
|
33652
33701
|
deployModel: rawValue("--deploy-model", ""),
|
|
33653
33702
|
releaseTrack: rawValue("--release-track", ""),
|
|
33654
33703
|
only: rawValue("--only", ""),
|
|
33704
|
+
fromConfig: rawFlag("--from-config"),
|
|
33655
33705
|
execute: rawFlag("--execute"),
|
|
33656
33706
|
json: rawFlag("--json"),
|
|
33657
33707
|
box: rawValue("--box", ""),
|
|
33658
33708
|
domains: rawValue("--domains", "")
|
|
33659
33709
|
};
|
|
33710
|
+
if (o.fromConfig && (o.only.trim() !== ".github/workflows/gate.yml" && o.only.trim() !== "gate.yml" || o.class !== "deployable" || cmdOpts.var?.length))
|
|
33711
|
+
return fail("bootstrap apply: --from-config requires --only gate.yml, --class deployable, and no --var overrides");
|
|
33660
33712
|
if (o.domains && !o.box) return fail("bootstrap apply: --domains requires --box");
|
|
33661
33713
|
if (o.box && o.only) return fail("bootstrap apply: --box cannot be combined with --only");
|
|
33662
33714
|
let boxDomains;
|
|
@@ -33725,7 +33777,7 @@ function registerBootstrapCommands(program3) {
|
|
|
33725
33777
|
const manifest = loadBootstrapSeeds((0, import_node_fs28.readFileSync)(manifestPath, "utf8"));
|
|
33726
33778
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
33727
33779
|
const slug = parsedRepo.slug;
|
|
33728
|
-
const onlyTarget = o.only.trim();
|
|
33780
|
+
const onlyTarget = o.fromConfig && o.only.trim() === "gate.yml" ? ".github/workflows/gate.yml" : o.only.trim();
|
|
33729
33781
|
const seedsToApply = onlyTarget ? manifest.seeds.filter((s) => s.target === onlyTarget || s.target.replace("{{REPO_SLUG}}", slug) === onlyTarget) : manifest.seeds;
|
|
33730
33782
|
if (onlyTarget && !seedsToApply.length) {
|
|
33731
33783
|
const known = manifest.seeds.map((s) => s.target).join("\n ");
|
|
@@ -33761,6 +33813,8 @@ function registerBootstrapCommands(program3) {
|
|
|
33761
33813
|
registryMeta = meta;
|
|
33762
33814
|
registryRequiredCheckBranches = meta?.requiredCheckBranches;
|
|
33763
33815
|
for (const [k, v] of Object.entries(gateConfigToVars(meta?.gate))) if (rawVars[k] == null) rawVars[k] = v;
|
|
33816
|
+
} else if (o.fromConfig) {
|
|
33817
|
+
return failGraceful(`bootstrap apply: registry META unreadable (${metaRead.error}); --from-config requires the committed registry gate config`);
|
|
33764
33818
|
} else if (o.execute && !onlyTarget) {
|
|
33765
33819
|
return failGraceful(`bootstrap apply: registry META unreadable (${metaRead.error}); refusing the live registry write, which would reset a registered project's releaseTrack and edgeDomains to class defaults (#7138)`);
|
|
33766
33820
|
} else {
|
|
@@ -33867,10 +33921,42 @@ function registerBootstrapCommands(program3) {
|
|
|
33867
33921
|
let scopedPlan;
|
|
33868
33922
|
let scopedContent;
|
|
33869
33923
|
let seededToBranch = 0;
|
|
33924
|
+
let configGateAction;
|
|
33925
|
+
let configGateContent;
|
|
33870
33926
|
if (onlyTarget) {
|
|
33871
|
-
scopedPlan = await targetedSeedPlan(repo, slug, onlyTarget, baseBranch, seedSource.sha, controlClient);
|
|
33927
|
+
scopedPlan = await targetedSeedPlan(repo, slug, onlyTarget, baseBranch, seedSource.sha, controlClient, o.fromConfig);
|
|
33872
33928
|
seedPlan = scopedPlan;
|
|
33873
|
-
if (o.
|
|
33929
|
+
if (o.fromConfig) {
|
|
33930
|
+
const gate = registryMeta?.gate;
|
|
33931
|
+
if (!gate || !Object.keys(gate).length) return failGraceful("bootstrap apply: --from-config requires a non-empty registry gate config; set it with oracle org project set --var gate={...} first");
|
|
33932
|
+
const gateSeed = seedsToApply[0];
|
|
33933
|
+
if (gateSeed.waivers?.[slug]) return failGraceful(`bootstrap apply: gate.yml is waived for ${slug}: ${gateSeed.waivers[slug]}`);
|
|
33934
|
+
if (gateSeed.source !== "seed:gate.template.yml") return failGraceful("bootstrap apply: gate template is not the ci-reconcile seed");
|
|
33935
|
+
try {
|
|
33936
|
+
const gateVars = withDerivedRepoVars(
|
|
33937
|
+
{ ...gateConfigToVars(gate), REPO_SLUG: slug },
|
|
33938
|
+
parsedRepo,
|
|
33939
|
+
"deployable",
|
|
33940
|
+
effectiveTrack,
|
|
33941
|
+
registryMeta?.requiredCheckBranches,
|
|
33942
|
+
registryMeta?.requiredChecks
|
|
33943
|
+
);
|
|
33944
|
+
const rendered = resolveSeedContent(gateSeed, gateVars, readFile9);
|
|
33945
|
+
if (rendered === null || missingPlaceholders(rendered).length) return failGraceful("bootstrap apply: registry gate template is unreadable or has unfilled placeholders");
|
|
33946
|
+
const base = (await readTargetedSeedFiles(repo, scopedPlan, onlyTarget, scopedPlan.baseSha, controlClient)).base?.content ?? null;
|
|
33947
|
+
configGateAction = planGateFromConfig(base, rendered);
|
|
33948
|
+
if (configGateAction.refused && base !== null) {
|
|
33949
|
+
const stamped = parseGateRenderStamp(base);
|
|
33950
|
+
const committedBody = stamped?.body.startsWith(GATE_RENDER_NOTE) ? stamped.body.slice(GATE_RENDER_NOTE.length) : stamped?.body ?? base;
|
|
33951
|
+
console.error(await gateRenderDiff(committedBody, rendered));
|
|
33952
|
+
return failGraceful(`bootstrap apply: ${configGateAction.reason}`);
|
|
33953
|
+
}
|
|
33954
|
+
configGateContent = stampGateRender(rendered);
|
|
33955
|
+
} catch (e) {
|
|
33956
|
+
return failGraceful(`bootstrap apply --from-config: ${e.message}`);
|
|
33957
|
+
}
|
|
33958
|
+
}
|
|
33959
|
+
if (o.execute && scopedPlan.branch && configGateAction?.action !== "skip") {
|
|
33874
33960
|
await prepareTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient, gh2);
|
|
33875
33961
|
}
|
|
33876
33962
|
} else if (o.execute) {
|
|
@@ -33896,7 +33982,7 @@ function registerBootstrapCommands(program3) {
|
|
|
33896
33982
|
}
|
|
33897
33983
|
}
|
|
33898
33984
|
const docsForIndex = [];
|
|
33899
|
-
const seedReadRef = !o.execute
|
|
33985
|
+
const seedReadRef = scopedPlan && (!o.execute || configGateAction?.action === "skip") ? scopedPlan.baseSha : seedPlan.ref;
|
|
33900
33986
|
for (const seed of seedsToApply) {
|
|
33901
33987
|
if (!seed.classes.includes(o.class)) continue;
|
|
33902
33988
|
if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
|
|
@@ -33936,14 +34022,16 @@ function registerBootstrapCommands(program3) {
|
|
|
33936
34022
|
} catch {
|
|
33937
34023
|
exists = false;
|
|
33938
34024
|
}
|
|
33939
|
-
const planned = planSeedAction(resolved, scopedPlan ? baseContent !== null : exists);
|
|
34025
|
+
const planned = configGateAction ?? planSeedAction(resolved, scopedPlan ? baseContent !== null : exists);
|
|
33940
34026
|
if (scopedPlan && planned.action !== "skip") planned.action = exists ? "update" : "create";
|
|
33941
34027
|
const isLegacyBlock = resolved.source === "managed-block";
|
|
33942
34028
|
let content = null;
|
|
33943
34029
|
let isManaged = isLegacyBlock || resolved.managedBlock != null;
|
|
33944
34030
|
const preservedContent = scopedPlan ? baseContent : remoteContent;
|
|
33945
34031
|
if (planned.action === "create" || planned.action === "update") {
|
|
33946
|
-
if (
|
|
34032
|
+
if (o.fromConfig) {
|
|
34033
|
+
content = configGateContent;
|
|
34034
|
+
} else if (isLegacyBlock) {
|
|
33947
34035
|
content = upsertManagedGitignoreBlock(preservedContent).content;
|
|
33948
34036
|
} else {
|
|
33949
34037
|
const rulesetVars = preserveRuleset ? gateSeedVars(
|
|
@@ -34012,8 +34100,8 @@ function registerBootstrapCommands(program3) {
|
|
|
34012
34100
|
}
|
|
34013
34101
|
}
|
|
34014
34102
|
let seedPrUrl;
|
|
34015
|
-
const scopedHead = o.execute && scopedPlan?.branch ? await verifyTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient) : null;
|
|
34016
|
-
if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && (seededToBranch > 0 || scopedContent !== void 0 && scopedHead !== null && scopedHead !== scopedPlan?.baseSha)) {
|
|
34103
|
+
const scopedHead = o.execute && scopedPlan?.branch && configGateAction?.action !== "skip" ? await verifyTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient) : null;
|
|
34104
|
+
if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && configGateAction?.action !== "skip" && (seededToBranch > 0 || scopedContent !== void 0 && scopedHead !== null && scopedHead !== scopedPlan?.baseSha)) {
|
|
34017
34105
|
await gh2(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-F", `delete_branch_on_merge=${MANAGED_DELETE_BRANCH_ON_MERGE}`]).catch(() => {
|
|
34018
34106
|
});
|
|
34019
34107
|
const openPrs = await gh2(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
@@ -34031,9 +34119,9 @@ function registerBootstrapCommands(program3) {
|
|
|
34031
34119
|
"--head",
|
|
34032
34120
|
seedPlan.branch,
|
|
34033
34121
|
"--title",
|
|
34034
|
-
onlyTarget ? `chore: propagate ${onlyManagedBlock ? "Hub-managed block in" : "org-owned"} ${onlyTarget} from MMI-Hub` : `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
|
|
34122
|
+
o.fromConfig ? `chore: render ${onlyTarget} from registry gate config` : onlyTarget ? `chore: propagate ${onlyManagedBlock ? "Hub-managed block in" : "org-owned"} ${onlyTarget} from MMI-Hub` : `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
|
|
34035
34123
|
"--body",
|
|
34036
|
-
onlyTarget ? `Auto-opened by \`mmi-cli devops bootstrap apply ${repo} --only ${onlyTarget} --execute\` (#3818).
|
|
34124
|
+
o.fromConfig ? `Auto-opened by \`mmi-cli devops bootstrap apply ${repo} --only gate.yml --from-config --execute\`. The gate is rendered from the repo's registry gate config through the ci-reconcile seed renderer at MMI-Hub@${seedSource.sha}. This PR carries only \`${onlyTarget}\` for review; no registry META or other controls were changed.` : onlyTarget ? `Auto-opened by \`mmi-cli devops bootstrap apply ${repo} --only ${onlyTarget} --execute\` (#3818).
|
|
34037
34125
|
|
|
34038
34126
|
${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owned \`${onlyTarget}\`` : `The org-owned \`${onlyTarget}\` seed`} is declared by MMI-Hub's \`skills/bootstrap/seeds/manifest.json\` at MMI-Hub@${seedSource.sha}; this PR reconciles that declared scope and preserves everything outside it. It carries that file and nothing else \u2014 no labels, ruleset, merge settings or registry META were touched.
|
|
34039
34127
|
|
|
@@ -34946,18 +35034,30 @@ function formatSuiteStatus(report) {
|
|
|
34946
35034
|
// src/suite-train.ts
|
|
34947
35035
|
var import_node_fs29 = require("node:fs");
|
|
34948
35036
|
var import_node_path29 = require("node:path");
|
|
34949
|
-
var
|
|
35037
|
+
var import_node_crypto13 = require("node:crypto");
|
|
34950
35038
|
var SCHEMA = "mmi-suite-cut/v1";
|
|
34951
35039
|
var SHA2 = /^[a-f0-9]{40}$/i;
|
|
34952
35040
|
var RC_TAG = /^v\d+\.\d+\.\d+-rc\.\d+$/;
|
|
34953
35041
|
var RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
|
|
34954
|
-
function suitePins(record) {
|
|
35042
|
+
function suitePins(record, bundles) {
|
|
34955
35043
|
return record.members.map((m) => m.carried ? { module: m.id, rc: m.rcPin, main: m.mainPin } : {
|
|
34956
35044
|
module: m.id,
|
|
34957
35045
|
rc: `https://${m.id === "katip" ? m.rcHost ?? m.host : m.host}/${m.id === "katip" ? "" : "rc/"}module.js?v=${m.tag}`,
|
|
34958
|
-
main:
|
|
35046
|
+
main: mainPinFor(m.host, bundles[m.id], suiteReleaseTag(m.tag))
|
|
34959
35047
|
});
|
|
34960
35048
|
}
|
|
35049
|
+
function mainPinFor(host, bundles, tag) {
|
|
35050
|
+
const path2 = [bundles?.dev, bundles?.main].map(pathOf).find(Boolean) ?? "/module.js";
|
|
35051
|
+
return `https://${host}${path2}?v=${tag}`;
|
|
35052
|
+
}
|
|
35053
|
+
function pathOf(url) {
|
|
35054
|
+
try {
|
|
35055
|
+
return url ? new URL(url).pathname : void 0;
|
|
35056
|
+
} catch {
|
|
35057
|
+
return void 0;
|
|
35058
|
+
}
|
|
35059
|
+
}
|
|
35060
|
+
var bundlesOf = (members) => Object.fromEntries(members.map((m) => [m.id, m.bundles]));
|
|
34961
35061
|
function suiteReleaseTag(rc) {
|
|
34962
35062
|
if (!RC_TAG.test(rc)) throw new Error(`invalid rc tag ${rc}`);
|
|
34963
35063
|
return rc.replace(/-rc\.\d+$/, "");
|
|
@@ -34982,7 +35082,7 @@ function readSuiteCut(path2) {
|
|
|
34982
35082
|
}
|
|
34983
35083
|
function writeSuiteCut(path2, record) {
|
|
34984
35084
|
(0, import_node_fs29.mkdirSync)((0, import_node_path29.dirname)(path2), { recursive: true });
|
|
34985
|
-
const temp = `${path2}.${(0,
|
|
35085
|
+
const temp = `${path2}.${(0, import_node_crypto13.randomBytes)(8).toString("hex")}.tmp`;
|
|
34986
35086
|
try {
|
|
34987
35087
|
(0, import_node_fs29.writeFileSync)(temp, JSON.stringify(record, null, 2), { flag: "wx" });
|
|
34988
35088
|
(0, import_node_fs29.renameSync)(temp, path2);
|
|
@@ -35003,19 +35103,24 @@ function pinVersion(url) {
|
|
|
35003
35103
|
async function carriedPins(deps, member, bundles) {
|
|
35004
35104
|
const main = bundles.main;
|
|
35005
35105
|
if (!main || !RELEASE_TAG.test(pinVersion(main))) throw new Error(`${member.repo}: nothing new since rc, but its MM-Work main pin ${main ?? "(none)"} has no ?v=vX.Y.Z release; pin it before the suite cut`);
|
|
35106
|
+
if (!await deps.tagSha(member.repo, pinVersion(main))) throw new Error(`${member.repo}: nothing new since rc, but its MM-Work main pin names ${pinVersion(main)}, which was never released; release it or pin its live release before the suite cut`);
|
|
35107
|
+
const mainPin = mainPinFor(new URL(main).host, bundles, pinVersion(main));
|
|
35006
35108
|
for (const url of [bundles.rc, main]) {
|
|
35007
35109
|
const tag = pinVersion(url);
|
|
35008
35110
|
if (!RC_TAG.test(tag) && !RELEASE_TAG.test(tag)) continue;
|
|
35009
35111
|
const sha = await deps.tagSha(member.repo, tag);
|
|
35010
|
-
if (sha && await deps.contains(member.repo, member.development, sha)) return { rcPin: url
|
|
35112
|
+
if (sha && await deps.contains(member.repo, member.development, sha)) return { rcPin: url === main ? mainPin : url, mainPin };
|
|
35011
35113
|
}
|
|
35012
35114
|
throw new Error(`${member.repo}: nothing new since rc, but no pinned MM-Work bundle carries development ${member.development.slice(0, 12)}; pin it before the suite cut`);
|
|
35013
35115
|
}
|
|
35014
|
-
function suiteAbandon(recordPath) {
|
|
35116
|
+
function suiteAbandon(recordPath, ownerGo = false) {
|
|
35015
35117
|
const record = readSuiteCut(recordPath);
|
|
35016
35118
|
if (!record) throw new Error("no suite cut record to abandon");
|
|
35017
|
-
if (record.
|
|
35018
|
-
throw new Error(
|
|
35119
|
+
if (record.leadReleased || record.members.some((m) => m.released)) {
|
|
35120
|
+
throw new Error("the cut already released a member or the lead; finish it with suite release --owner-go, never abandon it");
|
|
35121
|
+
}
|
|
35122
|
+
if (record.pinPr && !ownerGo) {
|
|
35123
|
+
throw new Error(`the cut already opened pin PR ${record.pinPr}; its pins may be visible, so resume it (suite rcand --owner-go), release it (suite release), or retire it with suite abandon --owner-go`);
|
|
35019
35124
|
}
|
|
35020
35125
|
const archive = `${recordPath}.abandoned-${record.leadDevelopment.slice(0, 12)}.json`;
|
|
35021
35126
|
if ((0, import_node_fs29.existsSync)(archive)) throw new Error(`abandoned-cut archive already exists at ${archive}; inspect before abandoning again`);
|
|
@@ -35064,7 +35169,15 @@ async function suiteRcand(deps, opts) {
|
|
|
35064
35169
|
throw new Error("suite roster changed since cut began; refuse to omit or add a member");
|
|
35065
35170
|
const quiet = [];
|
|
35066
35171
|
for (const member of record.members) {
|
|
35067
|
-
if (
|
|
35172
|
+
if (member.tag || member.carried || !await deps.contains(member.repo, member.development, member.previousRc)) continue;
|
|
35173
|
+
const rcTag = pinVersion(modules.find((m) => m.repo === member.repo).bundles.rc);
|
|
35174
|
+
if (RC_TAG.test(rcTag) && (await deps.tagSha(member.repo, rcTag))?.toLowerCase() === member.previousRc.toLowerCase() && !await deps.tagSha(member.repo, suiteReleaseTag(rcTag))) {
|
|
35175
|
+
member.tag = rcTag;
|
|
35176
|
+
member.sha = member.previousRc;
|
|
35177
|
+
writeSuiteCut(deps.recordPath, record);
|
|
35178
|
+
continue;
|
|
35179
|
+
}
|
|
35180
|
+
quiet.push(member);
|
|
35068
35181
|
}
|
|
35069
35182
|
if (record.members.every((m) => m.carried || quiet.includes(m))) {
|
|
35070
35183
|
(0, import_node_fs29.rmSync)(deps.recordPath, { force: true });
|
|
@@ -35076,7 +35189,7 @@ async function suiteRcand(deps, opts) {
|
|
|
35076
35189
|
}
|
|
35077
35190
|
const refusals = [];
|
|
35078
35191
|
for (const member of record.members) {
|
|
35079
|
-
if (member.
|
|
35192
|
+
if (member.published || member.id === "katip") continue;
|
|
35080
35193
|
try {
|
|
35081
35194
|
await deps.preflight(member.repo);
|
|
35082
35195
|
} catch (e) {
|
|
@@ -35093,7 +35206,10 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
|
|
|
35093
35206
|
for (const member of record.members) {
|
|
35094
35207
|
if ((await deps.head(member.repo, "development"))?.toLowerCase() !== member.development.toLowerCase())
|
|
35095
35208
|
throw new Error(`${member.repo}: development moved after the cut snapshot; refuse to add untested code`);
|
|
35096
|
-
if (member.carried)
|
|
35209
|
+
if (member.carried) {
|
|
35210
|
+
if (member.id !== "katip") await deps.publish(member.repo, pinVersion(member.rcPin));
|
|
35211
|
+
continue;
|
|
35212
|
+
}
|
|
35097
35213
|
if (!member.tag) {
|
|
35098
35214
|
const cut = await deps.rcand(member.repo, member.previousRc, member.rcWasAbsent);
|
|
35099
35215
|
if (!RC_TAG.test(cut.tag) || !SHA2.test(cut.sha) || cut.sha.toLowerCase() === member.previousRc.toLowerCase()) throw new Error(`${member.repo}: rcand did not return a new immutable rc tag/SHA`);
|
|
@@ -35117,7 +35233,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
|
|
|
35117
35233
|
if (live.version !== member.backend || devHead?.toLowerCase() !== member.development.toLowerCase())
|
|
35118
35234
|
throw new Error(`${member.repo}: live backend or development changed during the cut; pins unchanged`);
|
|
35119
35235
|
}
|
|
35120
|
-
const pins = suitePins(record);
|
|
35236
|
+
const pins = suitePins(record, bundlesOf(modules));
|
|
35121
35237
|
await deps.verifyLeadCut(record.leadDevelopment);
|
|
35122
35238
|
const pr = await deps.pin({ cut: record.leadDevelopment, pins });
|
|
35123
35239
|
await deps.verifyLeadCut(record.leadDevelopment);
|
|
@@ -35142,7 +35258,7 @@ async function suiteRelease(deps, ownerGo) {
|
|
|
35142
35258
|
if (!record?.pinMerged || !record.leadRcTag) throw new Error("no completed suite rc cut and merged pin PR; release refused");
|
|
35143
35259
|
const members = suiteMembers(await deps.registry()).filter((m) => m.repo !== SUITE_REPO);
|
|
35144
35260
|
if (members.length !== record.members.length || members.some((m) => !record.members.some((r) => r.repo === m.repo && r.id === m.id))) throw new Error("suite roster changed after the cut; release refused");
|
|
35145
|
-
await deps.verifyPins(suitePins(record));
|
|
35261
|
+
await deps.verifyPins(suitePins(record, bundlesOf(members)), record.leadRcTag);
|
|
35146
35262
|
for (const member of record.members) {
|
|
35147
35263
|
if (member.carried) {
|
|
35148
35264
|
const live2 = await deps.backend({ repo: member.repo, id: member.id, bundles: {} });
|
|
@@ -38498,8 +38614,8 @@ async function openSuiteCutPinPr(deps, input) {
|
|
|
38498
38614
|
const ids = input.pins.map((p) => p.module);
|
|
38499
38615
|
if (new Set(ids).size !== ids.length) throw new Error("suite pin PR contains a duplicate module");
|
|
38500
38616
|
const branch = `suite-pin/cut-${input.cut.slice(0, 16)}`;
|
|
38501
|
-
const registry2 = async () => {
|
|
38502
|
-
const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref
|
|
38617
|
+
const registry2 = async (ref = "development") => {
|
|
38618
|
+
const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=${encodeURIComponent(ref)}`]));
|
|
38503
38619
|
if (!get.content) throw new Error("MM-Work seed/modules.json is unreadable");
|
|
38504
38620
|
return JSON.parse(Buffer.from(get.content, "base64").toString("utf8"));
|
|
38505
38621
|
};
|
|
@@ -38523,25 +38639,35 @@ async function openSuiteCutPinPr(deps, input) {
|
|
|
38523
38639
|
})) throw new Error(`${branch} merged but development pins differ from this cut`);
|
|
38524
38640
|
return { status: "merged", pr: prs[0].url };
|
|
38525
38641
|
}
|
|
38526
|
-
|
|
38527
|
-
|
|
38528
|
-
const row = rows.find((r) => r.id === p.module);
|
|
38642
|
+
const matches = (r) => input.pins.every((p) => {
|
|
38643
|
+
const row = r.find((x) => x.id === p.module);
|
|
38529
38644
|
return row?.bundles.rc === p.rc && row?.bundles.main === p.main;
|
|
38530
|
-
})
|
|
38645
|
+
});
|
|
38646
|
+
if (prs.length && matches(await registry2(branch))) return { status: "pending", pr: prs[0].url };
|
|
38647
|
+
if (!prs.length && matches(rows)) throw new Error("pins already match without a merged cut PR \u2014 provenance unverified");
|
|
38531
38648
|
await (0, import_promises5.mkdir)((0, import_node_path34.join)(deps.root, "tmp"), { recursive: true });
|
|
38532
38649
|
scratch = await (0, import_promises5.mkdtemp)((0, import_node_path34.join)(deps.root, "tmp", "suite-pin-"));
|
|
38533
38650
|
const run = (file, args) => deps.run(file, args, scratch);
|
|
38534
|
-
await run("git", ["clone", "--quiet", "--branch", "development", "--single-branch", `https://github.com/${SUITE_REPO}.git`, "."]);
|
|
38535
|
-
if (
|
|
38536
|
-
|
|
38537
|
-
|
|
38651
|
+
await run("git", ["clone", "--quiet", "--branch", prs.length ? branch : "development", "--single-branch", `https://github.com/${SUITE_REPO}.git`, "."]);
|
|
38652
|
+
if (prs.length) {
|
|
38653
|
+
if ((await run("git", ["merge-base", "HEAD", `${input.cut}`]).catch(() => "")).trim().toLowerCase() !== input.cut.toLowerCase())
|
|
38654
|
+
throw new Error(`${branch} no longer builds on the cut ${input.cut.slice(0, 12)}; pin PR left unchanged`);
|
|
38655
|
+
} else {
|
|
38656
|
+
if ((await run("git", ["rev-parse", "HEAD"])).trim().toLowerCase() !== input.cut.toLowerCase())
|
|
38657
|
+
throw new Error("MM-Work development moved after the suite snapshot; no pin PR opened");
|
|
38658
|
+
await run("git", ["switch", "-c", branch]);
|
|
38659
|
+
}
|
|
38538
38660
|
for (const stage of ["rc", "main"]) {
|
|
38539
38661
|
const path2 = (0, import_node_path34.join)(scratch, `${stage}-pins.json`);
|
|
38540
38662
|
await (0, import_promises5.writeFile)(path2, JSON.stringify(input.pins.map((p) => ({ module: p.module, url: p[stage] }))));
|
|
38541
38663
|
await run("node", ["scripts/pin-modules.mjs", "--stage", stage, "--pins", path2]);
|
|
38542
38664
|
}
|
|
38543
38665
|
await run("git", ["add", "seed/modules.json"]);
|
|
38544
|
-
await run("git", ["commit", "-m", `Pin MM-Work suite candidate ${input.cut.slice(0, 12)} (#7262)`]);
|
|
38666
|
+
await run("git", ["commit", "-m", prs.length ? `Correct MM-Work suite candidate pins ${input.cut.slice(0, 12)} (#7450)` : `Pin MM-Work suite candidate ${input.cut.slice(0, 12)} (#7262)`]);
|
|
38667
|
+
if (prs.length) {
|
|
38668
|
+
await run("git", ["push", "origin", branch]);
|
|
38669
|
+
return { status: "pending", pr: prs[0].url };
|
|
38670
|
+
}
|
|
38545
38671
|
await run("git", ["push", "origin", branch]);
|
|
38546
38672
|
const pr = (await run("gh", [
|
|
38547
38673
|
"pr",
|
|
@@ -38761,7 +38887,7 @@ init_github_client();
|
|
|
38761
38887
|
|
|
38762
38888
|
// src/issue-commands.ts
|
|
38763
38889
|
var import_node_fs34 = require("node:fs");
|
|
38764
|
-
var
|
|
38890
|
+
var import_node_crypto14 = require("node:crypto");
|
|
38765
38891
|
init_cli_shared();
|
|
38766
38892
|
init_clean_exit();
|
|
38767
38893
|
init_error_codes();
|
|
@@ -39234,7 +39360,7 @@ function rowIdempotencyKey(batchKey, spec) {
|
|
|
39234
39360
|
const identity = `${spec.type}
|
|
39235
39361
|
${spec.title.trim()}
|
|
39236
39362
|
${spec.body ?? ""}`;
|
|
39237
|
-
const hash = (0,
|
|
39363
|
+
const hash = (0, import_node_crypto14.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
39238
39364
|
return `${batchKey}:${hash}`;
|
|
39239
39365
|
}
|
|
39240
39366
|
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
|
|
@@ -45215,8 +45341,8 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
45215
45341
|
return "";
|
|
45216
45342
|
}
|
|
45217
45343
|
};
|
|
45218
|
-
const registry2 = async () => {
|
|
45219
|
-
const file = await defaultGitHubClient().rest("GET", `repos/${SUITE_REPO}/contents/seed/modules.json?ref
|
|
45344
|
+
const registry2 = async (ref = "development") => {
|
|
45345
|
+
const file = await defaultGitHubClient().rest("GET", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=${encodeURIComponent(ref)}`);
|
|
45220
45346
|
if (!file?.content) throw new Error("MM-Work registry unreadable");
|
|
45221
45347
|
const parsed = JSON.parse(decodeGitHubContents(file.content));
|
|
45222
45348
|
suiteMembers(parsed);
|
|
@@ -45256,12 +45382,12 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
45256
45382
|
if (delta.ahead_by !== 1 || delta.behind_by !== 0 || delta.files?.length !== 1 || delta.files[0] !== "seed/modules.json")
|
|
45257
45383
|
throw new Error("MM-Work development changed outside the one suite pin commit after the cut snapshot; refuse to cut an untested lead");
|
|
45258
45384
|
},
|
|
45259
|
-
verifyPins: async (pins) => {
|
|
45260
|
-
const rows = await registry2();
|
|
45385
|
+
verifyPins: async (pins, ref) => {
|
|
45386
|
+
const rows = await registry2(ref);
|
|
45261
45387
|
for (const pin of pins) {
|
|
45262
45388
|
const row = rows.find((r) => r.id === pin.module);
|
|
45263
45389
|
if (row?.bundles.rc !== pin.rc || row?.bundles.main !== pin.main)
|
|
45264
|
-
throw new Error(`${pin.module}: MM-Work
|
|
45390
|
+
throw new Error(`${pin.module}: MM-Work ${ref} pins do not equal the tested cut (${row?.bundles.main ?? "no main pin"} vs ${pin.main}); release refused`);
|
|
45265
45391
|
}
|
|
45266
45392
|
},
|
|
45267
45393
|
release,
|
|
@@ -45304,19 +45430,19 @@ function registerSuiteCommands(program3) {
|
|
|
45304
45430
|
return fail(`suite rcand: ${e.message}`);
|
|
45305
45431
|
}
|
|
45306
45432
|
});
|
|
45307
|
-
suite.command("abandon").description("retire an unfinished cut
|
|
45433
|
+
suite.command("abandon").description("retire an unfinished cut: archive the record; member rc tags stay (#7396). A cut whose pin PR landed but released nothing needs --owner-go (#7450)").option("--owner-go", "explicit owner approval to retire a cut whose pin PR already landed").option("--json", "structured receipt").action(async (o) => {
|
|
45308
45434
|
try {
|
|
45309
45435
|
const root = await leadRoot();
|
|
45310
45436
|
const unlock = await acquireTrainWriterLock(root, "suite abandon");
|
|
45311
45437
|
let result;
|
|
45312
45438
|
try {
|
|
45313
|
-
result = suiteAbandon(suiteTrainIo(root).recordPath);
|
|
45439
|
+
result = suiteAbandon(suiteTrainIo(root).recordPath, o.ownerGo === true);
|
|
45314
45440
|
} finally {
|
|
45315
45441
|
await unlock();
|
|
45316
45442
|
}
|
|
45317
45443
|
console.log(o.json ? JSON.stringify(result, null, 2) : [
|
|
45318
45444
|
`suite abandon: archived the cut record to ${result.archive}`,
|
|
45319
|
-
...result.orphanTags.length ? [" member rc tags left in place (immutable; the next rc
|
|
45445
|
+
...result.orphanTags.length ? [" member rc tags left in place (immutable; the next cut reuses one still unreleased on its rc head, #7450):", ...result.orphanTags.map((t) => ` - ${t}`)] : [" no member rc tag was minted"],
|
|
45320
45446
|
" the next suite rcand --owner-go re-snapshots every member"
|
|
45321
45447
|
].join("\n"));
|
|
45322
45448
|
} catch (e) {
|
|
@@ -49303,14 +49429,14 @@ vercel preview (${preview.branch}): ${d.state}${d.url ? ` \u2014 ${d.url}` : ""}
|
|
|
49303
49429
|
}
|
|
49304
49430
|
|
|
49305
49431
|
// src/tenant-artifact.ts
|
|
49306
|
-
var
|
|
49432
|
+
var import_node_crypto15 = require("node:crypto");
|
|
49307
49433
|
var import_node_fs48 = require("node:fs");
|
|
49308
49434
|
var import_promises10 = require("node:fs/promises");
|
|
49309
49435
|
var import_node_path46 = require("node:path");
|
|
49310
49436
|
var ARTIFACT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
49311
49437
|
var MAX_BYTES = 5 * 1024 * 1024 * 1024;
|
|
49312
49438
|
async function sha256File(path2) {
|
|
49313
|
-
const hash = (0,
|
|
49439
|
+
const hash = (0, import_node_crypto15.createHash)("sha256");
|
|
49314
49440
|
for await (const chunk of (0, import_node_fs48.createReadStream)(path2)) hash.update(chunk);
|
|
49315
49441
|
return hash.digest("hex");
|
|
49316
49442
|
}
|
|
@@ -49540,13 +49666,13 @@ async function executeWaveLand(plan, deps) {
|
|
|
49540
49666
|
|
|
49541
49667
|
// src/dist-drift.ts
|
|
49542
49668
|
var import_node_child_process18 = require("node:child_process");
|
|
49543
|
-
var
|
|
49669
|
+
var import_node_crypto17 = require("node:crypto");
|
|
49544
49670
|
var import_node_fs50 = require("node:fs");
|
|
49545
49671
|
var import_node_os22 = require("node:os");
|
|
49546
49672
|
var import_node_path48 = require("node:path");
|
|
49547
49673
|
|
|
49548
49674
|
// ../scripts/distribution-digest.mjs
|
|
49549
|
-
var
|
|
49675
|
+
var import_node_crypto16 = require("node:crypto");
|
|
49550
49676
|
var import_node_fs49 = require("node:fs");
|
|
49551
49677
|
var import_node_path47 = require("node:path");
|
|
49552
49678
|
var slash = (value) => value.replaceAll("\\", "/");
|
|
@@ -49559,7 +49685,7 @@ function repoPath(root, declaredPath, label) {
|
|
|
49559
49685
|
return target;
|
|
49560
49686
|
}
|
|
49561
49687
|
function digestFiles(files) {
|
|
49562
|
-
const hash = (0,
|
|
49688
|
+
const hash = (0, import_node_crypto16.createHash)("sha256");
|
|
49563
49689
|
for (const file of [...files].sort((a, b) => a.relative.localeCompare(b.relative))) {
|
|
49564
49690
|
const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs49.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs49.readFileSync)(file.absolute);
|
|
49565
49691
|
hash.update(file.relative, "utf8");
|
|
@@ -49589,7 +49715,7 @@ var DIST_ARTIFACTS = [
|
|
|
49589
49715
|
];
|
|
49590
49716
|
var BOM_DIST_TREE_ID = "mmi-cli-dist";
|
|
49591
49717
|
var ABSENT = "absent";
|
|
49592
|
-
var sha256 = (bytes) => `sha256:${(0,
|
|
49718
|
+
var sha256 = (bytes) => `sha256:${(0, import_node_crypto17.createHash)("sha256").update(bytes).digest("hex")}`;
|
|
49593
49719
|
function artifactDrift(path2, committedBytes, rebuiltBytes) {
|
|
49594
49720
|
const committed = committedBytes ? sha256(committedBytes) : ABSENT;
|
|
49595
49721
|
const rebuiltExpected = rebuiltBytes ? sha256(rebuiltBytes) : ABSENT;
|
package/package.json
CHANGED