@mutmutco/cli 4.5.76 → 4.5.78
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 +388 -47
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -6986,6 +6986,24 @@ function findClaimedButUnregisteredClosings(text, closing, alreadyClosed = []) {
|
|
|
6986
6986
|
}
|
|
6987
6987
|
return [...claimed].filter((n) => !registered.has(n) && !closed.has(n));
|
|
6988
6988
|
}
|
|
6989
|
+
function findLineStartClosings(text, repo) {
|
|
6990
|
+
const found = /* @__PURE__ */ new Set();
|
|
6991
|
+
let inFence = false;
|
|
6992
|
+
for (const line of text.split(/\r?\n/)) {
|
|
6993
|
+
if (FENCE_LINE_RE.test(line)) {
|
|
6994
|
+
inFence = !inFence;
|
|
6995
|
+
continue;
|
|
6996
|
+
}
|
|
6997
|
+
if (inFence || !LINE_START_CLOSING_RE.test(line)) continue;
|
|
6998
|
+
const m = findClosingMentions(line)[0];
|
|
6999
|
+
if (!m || m.negated || !Number.isInteger(m.issue) || m.issue <= 0) continue;
|
|
7000
|
+
const link = /\[#\d+\]\(([^)\s]+)\)/.exec(line);
|
|
7001
|
+
if (link && !(link[1] ?? "").toLowerCase().includes(`github.com/${repo.toLowerCase()}/issues/${m.issue}`)) continue;
|
|
7002
|
+
found.add(m.issue);
|
|
7003
|
+
}
|
|
7004
|
+
const ambiguous = new Set(findCrossRepoAmbiguousClosings(text, [...found], repo));
|
|
7005
|
+
return [...found].filter((n) => !ambiguous.has(n));
|
|
7006
|
+
}
|
|
6989
7007
|
function claimedClosingWarning(unregistered, context = "pr merge") {
|
|
6990
7008
|
const named = unregistered.map((n) => `#${n}`).join(", ");
|
|
6991
7009
|
const first = `#${unregistered[0] ?? "N"}`;
|
|
@@ -7435,6 +7453,9 @@ var DECLARED_ENTRIES = [
|
|
|
7435
7453
|
// archived stage (including a stage whose containers are down) into scratch Postgres and compare
|
|
7436
7454
|
// row counts, and read-verify every storage archive against its listing; per-stage PASS/FAIL.
|
|
7437
7455
|
{ name: "mmi-backup-restore-check (mm-central, mm-work, mmi-oguz)", cadence: "0 3 1 * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup-restore-check \u2192 /opt/mmi-control/nightly-backup-restore-check.sh; monthly restore proof for every archived stage (#7148; source scripts/nightly-backup-restore-check.sh); zuber (46.225.58.31) is gone: deleted 2026-09-18 by the owner-approved ZuberShade decommission (MMC-ZuberShade#1838, snapshot kept); Hetzner has since reassigned that IP to another machine (#7351)" },
|
|
7456
|
+
// #7662 (MM-Work#1520): Vercel Cron's 15-minute central mail call, moved to the box with MM-Work. The app
|
|
7457
|
+
// calls its own endpoint with its own CRON_SECRET via docker exec, so the secret stays in the container.
|
|
7458
|
+
{ name: "mm-work-mail-tick (mm-work)", cadence: "*/15 * * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mm-work-mail-tick \u2192 /opt/mmi-control/mm-work-mail-tick.sh; GET /api/internal/mail per stage listed in MM_WORK_MAIL_STAGES (source scripts/mm-work-mail-tick.sh; log /var/log/mm-work-mail-tick.log)" },
|
|
7438
7459
|
// #3370/#6270: the one runner hygiene layer (runner-hygiene.yml was folded in and deleted). A workflow ran ON the runner and needed checkout +
|
|
7439
7460
|
// setup-node before it can prune, so at 100% it dies at checkout and never prunes — the cleanup is
|
|
7440
7461
|
// unavailable exactly when it is needed. This one is root cron on the box: no checkout, no node, and
|
|
@@ -8560,7 +8581,6 @@ function attributionFields(attr) {
|
|
|
8560
8581
|
// src/secrets-transport.ts
|
|
8561
8582
|
var OWNER = "mutmutco";
|
|
8562
8583
|
var SSM_ROOT = "/mmi-future";
|
|
8563
|
-
var PROJECT_TIER_SEGMENT = "dev";
|
|
8564
8584
|
var ORG_INFRA_SLUG = "_org";
|
|
8565
8585
|
var MMI_VAULT_GUIDANCE = 'Check MMI access with `mmi-cli oracle org access capabilities`; a master can locate credentials with `mmi-cli vault secrets find "<what it is for>"`.';
|
|
8566
8586
|
var KEY_MAX_PATH_SEGMENTS = 4;
|
|
@@ -8573,10 +8593,9 @@ function isValidSecretKey(key) {
|
|
|
8573
8593
|
function stripTrailingCRLF(value) {
|
|
8574
8594
|
return value.replace(/\r\n$|\r$|\n$/, "");
|
|
8575
8595
|
}
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
return key.slice(0, slash2) === PROJECT_TIER_SEGMENT ? "project" : "org";
|
|
8596
|
+
var ORG_WIDE_SLUGS = /* @__PURE__ */ new Set([ORG_INFRA_SLUG, "shared", "cloudflare", "docs", "mmi-hub"]);
|
|
8597
|
+
function classifyTier(slug, _key) {
|
|
8598
|
+
return ORG_WIDE_SLUGS.has(slug) ? "org" : "project";
|
|
8580
8599
|
}
|
|
8581
8600
|
function secretParamName(slug, key) {
|
|
8582
8601
|
return `${SSM_ROOT}/${slug}/${key}`;
|
|
@@ -17230,10 +17249,10 @@ var rollout_plan_default = {
|
|
|
17230
17249
|
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)."
|
|
17231
17250
|
},
|
|
17232
17251
|
baseline: {
|
|
17233
|
-
version: "4.5.
|
|
17234
|
-
tag: "v4.5.
|
|
17235
|
-
commit: "
|
|
17236
|
-
npm: "@mutmutco/cli@4.5.
|
|
17252
|
+
version: "4.5.78",
|
|
17253
|
+
tag: "v4.5.78",
|
|
17254
|
+
commit: "55782d939329",
|
|
17255
|
+
npm: "@mutmutco/cli@4.5.78"
|
|
17237
17256
|
},
|
|
17238
17257
|
exitCriterion: "fleet-n-of-n",
|
|
17239
17258
|
hubOnlyShortcut: "forbidden",
|
|
@@ -17250,14 +17269,14 @@ var rollout_plan_default = {
|
|
|
17250
17269
|
repo: "mutmutco/mmi-hub",
|
|
17251
17270
|
role: "canary",
|
|
17252
17271
|
schedule: "train",
|
|
17253
|
-
v3Target: "v4.5.
|
|
17272
|
+
v3Target: "v4.5.78"
|
|
17254
17273
|
}
|
|
17255
17274
|
],
|
|
17256
17275
|
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.",
|
|
17257
17276
|
rollback: {
|
|
17258
17277
|
independent: true,
|
|
17259
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
17260
|
-
v3Target: "v4.5.
|
|
17278
|
+
mechanism: "npm dist-tag latest -> 4.5.78 and redeploy the Hub Lambda from tag v4.5.78 (55782d939329); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17279
|
+
v3Target: "v4.5.78 (@mutmutco/cli@4.5.78, tag commit 55782d939329 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
17261
17280
|
}
|
|
17262
17281
|
},
|
|
17263
17282
|
{
|
|
@@ -20071,10 +20090,10 @@ async function listRemoteReleaseTags(deps) {
|
|
|
20071
20090
|
}
|
|
20072
20091
|
return [...tags].sort(cmpReleaseTag);
|
|
20073
20092
|
}
|
|
20074
|
-
async function assertComputedReleaseTagStillNext(deps, tag) {
|
|
20093
|
+
async function assertComputedReleaseTagStillNext(deps, tag, preOneZero = false) {
|
|
20075
20094
|
if (!/^v\d+\.\d+\.\d+$/.test(tag)) return;
|
|
20076
20095
|
const remote = await listRemoteReleaseTags(deps);
|
|
20077
|
-
const newer = remote.filter((t) => cmpReleaseTag(t, tag) > 0);
|
|
20096
|
+
const newer = remote.filter((t) => cmpReleaseTag(t, tag) > 0 && !(preOneZero && !t.startsWith("v0.")));
|
|
20078
20097
|
if (newer.length === 0) return;
|
|
20079
20098
|
const latest = newer[newer.length - 1];
|
|
20080
20099
|
throw new Error(
|
|
@@ -21696,6 +21715,17 @@ Resume it: ${resumeCommand}
|
|
|
21696
21715
|
);
|
|
21697
21716
|
}
|
|
21698
21717
|
|
|
21718
|
+
// src/pre-one-zero.ts
|
|
21719
|
+
function preOneZeroInForce(meta) {
|
|
21720
|
+
return !!meta?.suite && meta.preOneZero !== false;
|
|
21721
|
+
}
|
|
21722
|
+
function assertPreOneZero(repo, meta, tag) {
|
|
21723
|
+
if (preOneZeroInForce(meta) && /^v?[1-9]\d*\./.test(tag)) {
|
|
21724
|
+
throw new Error(`${repo} is a ${meta.suite} suite member held on 0.x.y; ${tag} would be major >= 1. Only the owner lifts this: set registry META preOneZero to false. If the tag came from a stale scripts/next-version.mjs, propagate the current seed first (#7699).`);
|
|
21725
|
+
}
|
|
21726
|
+
return tag;
|
|
21727
|
+
}
|
|
21728
|
+
|
|
21699
21729
|
// src/train-doctor.ts
|
|
21700
21730
|
var import_node_child_process9 = require("node:child_process");
|
|
21701
21731
|
var import_node_fs23 = require("node:fs");
|
|
@@ -24832,7 +24862,7 @@ async function runTrainDoctor(input) {
|
|
|
24832
24862
|
let newestRemoteReleaseTag;
|
|
24833
24863
|
try {
|
|
24834
24864
|
remoteTags = tagNames(await runGitRemoteRead(train, ["ls-remote", "--tags", "origin", "refs/tags/v*"]));
|
|
24835
|
-
const releaseTags = await listRemoteReleaseTags(train);
|
|
24865
|
+
const releaseTags = (await listRemoteReleaseTags(train)).filter((t) => !preOneZeroInForce(meta) || t.startsWith("v0."));
|
|
24836
24866
|
const latest = releaseTags[releaseTags.length - 1];
|
|
24837
24867
|
newestRemoteReleaseTag = latest;
|
|
24838
24868
|
if (latest) {
|
|
@@ -26054,7 +26084,7 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
|
|
|
26054
26084
|
let tagLedgerStarted = false;
|
|
26055
26085
|
const recoveryInput = { repo: ctx.repo, tag, stage: "main", deployModel, meta, branchHints };
|
|
26056
26086
|
try {
|
|
26057
|
-
await assertComputedReleaseTagStillNext(deps, tag);
|
|
26087
|
+
await assertComputedReleaseTagStillNext(deps, tag, preOneZeroInForce(meta));
|
|
26058
26088
|
if (deployModel === "hub-serverless") {
|
|
26059
26089
|
await deps.run("node", ["scripts/release-distribution.mjs", "assert-release-lineage", tag.replace(/^v/, ""), "--release-commit", releaseSha]);
|
|
26060
26090
|
}
|
|
@@ -26478,7 +26508,7 @@ Nothing was written. Inspect ${ledger.path} before retrying.`);
|
|
|
26478
26508
|
async function runReleaseResumePipeline(deps, options, prepared) {
|
|
26479
26509
|
const watch = options.watch ?? false;
|
|
26480
26510
|
const { ctx, meta, branchHints, directTrack, deployModel } = prepared;
|
|
26481
|
-
const tags = clean2(await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"])).split("\n").map((t) => t.trim()).filter((t) => /^v\d+\.\d+\.\d
|
|
26511
|
+
const tags = clean2(await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"])).split("\n").map((t) => t.trim()).filter((t) => (preOneZeroInForce(meta) ? /^v0\.\d+\.\d+$/ : /^v\d+\.\d+\.\d+$/).test(t));
|
|
26482
26512
|
const tag = tags[0];
|
|
26483
26513
|
if (!tag) throw new Error("release --resume: no final v* release tag found \u2014 there is no release to resume");
|
|
26484
26514
|
const tagSha = await probeRemoteTag(deps, tag);
|
|
@@ -26838,7 +26868,7 @@ async function runReleaseAbortLocked(deps, options) {
|
|
|
26838
26868
|
throw new Error("release --abort must run from the primary checkout left on main by the failed train; nothing was written");
|
|
26839
26869
|
}
|
|
26840
26870
|
await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
|
|
26841
|
-
const tags = clean2(await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"])).split("\n").map((tag2) => tag2.trim()).filter((tag2) => /^v\d+\.\d+\.\d
|
|
26871
|
+
const tags = clean2(await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"])).split("\n").map((tag2) => tag2.trim()).filter((tag2) => (preOneZeroInForce(meta) ? /^v0\.\d+\.\d+$/ : /^v\d+\.\d+\.\d+$/).test(tag2));
|
|
26842
26872
|
const tag = tags[0];
|
|
26843
26873
|
if (!tag) throw new Error("release --abort found no final v* tag; nothing was written");
|
|
26844
26874
|
const tagSha = await probeRemoteTag(deps, tag);
|
|
@@ -27023,7 +27053,7 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
27023
27053
|
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
27024
27054
|
);
|
|
27025
27055
|
const deployModel2 = await preflight(deps, ctx, "rc", meta);
|
|
27026
|
-
const releaseBase = requireValue(clean2(await deps
|
|
27056
|
+
const releaseBase = requireValue(clean2(await nextVersion(deps, ctx, meta, "cycle")), "release base");
|
|
27027
27057
|
const { tolerated: tolerated2, predicted: predicted2 } = await preflightMergeToMain(
|
|
27028
27058
|
deps,
|
|
27029
27059
|
deployModel2,
|
|
@@ -27048,7 +27078,7 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
27048
27078
|
else await mergeWithToleratedResolution(deps, "development", "development -> rc", "theirs", tolerated2);
|
|
27049
27079
|
rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
|
|
27050
27080
|
resume = await resolveRcResumeTag(deps, releaseBase, rcSha, rcTagProbe);
|
|
27051
|
-
tag2 = resume.tag ?? requireValue(clean2(await deps
|
|
27081
|
+
tag2 = resume.tag ?? requireValue(clean2(await nextVersion(deps, ctx, meta, "rc")), "rc tag");
|
|
27052
27082
|
requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "rc");
|
|
27053
27083
|
assertTagAddressableRequiredContexts(deps, requiredChecks, ctx.repo);
|
|
27054
27084
|
const anchors2 = { repo: ctx.repo, tag: tag2, tagSha: rcSha };
|
|
@@ -27128,7 +27158,7 @@ ${spent.note}`), rcandRecovery(tag2));
|
|
|
27128
27158
|
"nothing to release: origin/development is not ahead of origin/main"
|
|
27129
27159
|
);
|
|
27130
27160
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
27131
|
-
const tag2 = requireValue(clean2(await deps
|
|
27161
|
+
const tag2 = requireValue(clean2(await nextVersion(deps, ctx, meta, "cycle")), "release tag");
|
|
27132
27162
|
const rcShaAtRelease = !directTrack && hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
|
|
27133
27163
|
const { foldPaths: foldPaths2, tolerated: tolerated2, predicted: predicted2 } = await preflightMergeToMain(
|
|
27134
27164
|
deps,
|
|
@@ -27246,7 +27276,7 @@ ${spent.note}`), rcandRecovery(tag2));
|
|
|
27246
27276
|
);
|
|
27247
27277
|
}
|
|
27248
27278
|
const releasedRcSha = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
27249
|
-
const tag = requireValue(clean2(await deps
|
|
27279
|
+
const tag = requireValue(clean2(await nextVersion(deps, ctx, meta, "release")), "release tag");
|
|
27250
27280
|
const tagProbe = { remoteSha: await probeRemoteTag(deps, tag) };
|
|
27251
27281
|
const preFold = {};
|
|
27252
27282
|
const { versionFold, releaseSha } = await runFoldStage(deps, "rc", preFold, "mmi-cli devops release --apply", async () => {
|
|
@@ -27305,19 +27335,23 @@ ${spent.note}`), rcandRecovery(tag2));
|
|
|
27305
27335
|
...ledgerReport ? { cutBy: ledgerReport.cutBy, ledger: ledgerReport } : {}
|
|
27306
27336
|
};
|
|
27307
27337
|
}
|
|
27308
|
-
async function
|
|
27338
|
+
async function nextVersion(deps, ctx, meta, mode) {
|
|
27339
|
+
const held = preOneZeroInForce(meta);
|
|
27340
|
+
return assertPreOneZero(ctx.repo, meta, clean2(await deps.run("node", ["scripts/next-version.mjs", mode, ...held ? ["--pre-one-zero"] : []])));
|
|
27341
|
+
}
|
|
27342
|
+
async function latestLocalReleaseTag(deps, mergedInto, preOneZero = false) {
|
|
27309
27343
|
const tags = clean2(await deps.run("git", ["tag", "--list", "v*", ...mergedInto ? ["--merged", mergedInto] : []])).split("\n");
|
|
27310
27344
|
let latest = { major: 0, minor: 0, patch: 0 };
|
|
27311
27345
|
for (const tag of tags) {
|
|
27312
27346
|
const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag.trim());
|
|
27313
|
-
if (!m) continue;
|
|
27347
|
+
if (!m || preOneZero && m[1] !== "0") continue;
|
|
27314
27348
|
const v = { major: +m[1], minor: +m[2], patch: +m[3] };
|
|
27315
27349
|
const delta = v.major - latest.major || v.minor - latest.minor || v.patch - latest.patch;
|
|
27316
27350
|
if (delta > 0) latest = v;
|
|
27317
27351
|
}
|
|
27318
27352
|
return latest;
|
|
27319
27353
|
}
|
|
27320
|
-
async function resolveTrainBumpIntent(deps, env = process.env, latestRelease) {
|
|
27354
|
+
async function resolveTrainBumpIntent(deps, env = process.env, latestRelease, preOneZero = false) {
|
|
27321
27355
|
const explicitIntent = (env.MMI_BUMP_INTENT ?? "").trim();
|
|
27322
27356
|
if (explicitIntent) {
|
|
27323
27357
|
if (!TRAIN_BUMP_INTENTS.includes(explicitIntent)) {
|
|
@@ -27330,7 +27364,8 @@ async function resolveTrainBumpIntent(deps, env = process.env, latestRelease) {
|
|
|
27330
27364
|
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(explicitVersion);
|
|
27331
27365
|
if (!m) throw new Error(`MMI_RELEASE_VERSION must be X.Y.Z (got ${explicitVersion})`);
|
|
27332
27366
|
const explicit = { major: +m[1], minor: +m[2], patch: +m[3] };
|
|
27333
|
-
|
|
27367
|
+
if (preOneZero && explicit.major >= 1) throw new Error(`MMI_RELEASE_VERSION ${explicitVersion} is major >= 1 but this suite member is held on 0.x.y; only the owner lifts it (registry META preOneZero=false) (#7699)`);
|
|
27368
|
+
const latest = latestRelease ?? await latestLocalReleaseTag(deps, void 0, preOneZero);
|
|
27334
27369
|
const delta = explicit.major - latest.major || explicit.minor - latest.minor || explicit.patch - latest.patch;
|
|
27335
27370
|
if (delta <= 0) {
|
|
27336
27371
|
throw new Error(
|
|
@@ -27341,8 +27376,8 @@ async function resolveTrainBumpIntent(deps, env = process.env, latestRelease) {
|
|
|
27341
27376
|
}
|
|
27342
27377
|
return "patch";
|
|
27343
27378
|
}
|
|
27344
|
-
async function resolveHotfixVersion(deps, env = process.env) {
|
|
27345
|
-
const latest = await latestLocalReleaseTag(deps, "origin/main");
|
|
27379
|
+
async function resolveHotfixVersion(deps, env = process.env, preOneZero = false) {
|
|
27380
|
+
const latest = await latestLocalReleaseTag(deps, "origin/main", preOneZero);
|
|
27346
27381
|
if (latest.major === 0 && latest.minor === 0 && latest.patch === 0) {
|
|
27347
27382
|
throw new Error("no vX.Y.Z release tag found on origin/main \u2014 cannot derive the hotfix PATCH version");
|
|
27348
27383
|
}
|
|
@@ -27354,14 +27389,14 @@ async function resolveHotfixVersion(deps, env = process.env) {
|
|
|
27354
27389
|
declared.push({
|
|
27355
27390
|
name: "MMI_BUMP_INTENT",
|
|
27356
27391
|
value: explicitIntent,
|
|
27357
|
-
intent: await resolveTrainBumpIntent(deps, { MMI_BUMP_INTENT: explicitIntent }, latest)
|
|
27392
|
+
intent: await resolveTrainBumpIntent(deps, { MMI_BUMP_INTENT: explicitIntent }, latest, preOneZero)
|
|
27358
27393
|
});
|
|
27359
27394
|
}
|
|
27360
27395
|
if (explicitVersion) {
|
|
27361
27396
|
declared.push({
|
|
27362
27397
|
name: "MMI_RELEASE_VERSION",
|
|
27363
27398
|
value: explicitVersion,
|
|
27364
|
-
intent: await resolveTrainBumpIntent(deps, { MMI_RELEASE_VERSION: explicitVersion }, latest)
|
|
27399
|
+
intent: await resolveTrainBumpIntent(deps, { MMI_RELEASE_VERSION: explicitVersion }, latest, preOneZero)
|
|
27365
27400
|
});
|
|
27366
27401
|
}
|
|
27367
27402
|
const nonPatch = declared.find((d) => d.intent !== "patch");
|
|
@@ -27385,8 +27420,8 @@ async function runTrainApplyLocked(command, deps, options = {}) {
|
|
|
27385
27420
|
const ctx = await buildTrainApplyContext(deps);
|
|
27386
27421
|
await requireCleanTree(deps);
|
|
27387
27422
|
await runGitRemoteRead(deps, ["fetch", "origin"]);
|
|
27388
|
-
const bumpIntent = await resolveTrainBumpIntent(deps);
|
|
27389
27423
|
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
27424
|
+
const bumpIntent = await resolveTrainBumpIntent(deps, process.env, void 0, preOneZeroInForce(meta));
|
|
27390
27425
|
const branchHints = await loadReleaseTrackBranchHints(deps);
|
|
27391
27426
|
const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
|
|
27392
27427
|
const pipelineInput = { deps, ctx, command, meta, branchHints, watch, options, bumpIntent };
|
|
@@ -30683,7 +30718,7 @@ init_hub_auth();
|
|
|
30683
30718
|
init_github_client();
|
|
30684
30719
|
|
|
30685
30720
|
// src/project-set.ts
|
|
30686
|
-
var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy", "vercelVersionEnv", "suiteHold"];
|
|
30721
|
+
var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy", "vercelVersionEnv", "stageAllowlist", "suiteHold"];
|
|
30687
30722
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
30688
30723
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
30689
30724
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -31063,6 +31098,27 @@ function parseVercelVersionEnvVar(raw) {
|
|
|
31063
31098
|
}
|
|
31064
31099
|
return { project: row.project, key: row.key, ...typeof row.team === "string" ? { team: row.team } : {} };
|
|
31065
31100
|
}
|
|
31101
|
+
function parseStageAllowlistVar(raw) {
|
|
31102
|
+
let parsed;
|
|
31103
|
+
try {
|
|
31104
|
+
parsed = JSON.parse(raw);
|
|
31105
|
+
} catch {
|
|
31106
|
+
throw new Error('org project set: stageAllowlist must be JSON, e.g. {"key":"MMW_STAGE_ALLOWLIST","stages":["dev"]}');
|
|
31107
|
+
}
|
|
31108
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
31109
|
+
throw new Error("org project set: stageAllowlist must be a JSON object { key, stages }");
|
|
31110
|
+
}
|
|
31111
|
+
const row = parsed;
|
|
31112
|
+
const unknown = Object.keys(row).filter((key) => key !== "key" && key !== "stages");
|
|
31113
|
+
if (unknown.length > 0) throw new Error(`org project set: stageAllowlist has unknown field(s): ${unknown.join(", ")}`);
|
|
31114
|
+
if (typeof row.key !== "string" || !VERCEL_VERSION_ENV_NAME_RE.test(row.key)) {
|
|
31115
|
+
throw new Error("org project set: stageAllowlist.key must be an env name (e.g. MMW_STAGE_ALLOWLIST)");
|
|
31116
|
+
}
|
|
31117
|
+
if (!Array.isArray(row.stages) || row.stages.length === 0 || new Set(row.stages).size !== row.stages.length || !row.stages.every((s) => s === "dev" || s === "rc" || s === "main")) {
|
|
31118
|
+
throw new Error("org project set: stageAllowlist.stages must be a non-empty unique array of: dev, rc, main");
|
|
31119
|
+
}
|
|
31120
|
+
return { key: row.key, stages: row.stages };
|
|
31121
|
+
}
|
|
31066
31122
|
function parseCiExemptReasonVar(raw) {
|
|
31067
31123
|
const trimmed = raw.trim();
|
|
31068
31124
|
if (!trimmed) throw new Error("org project set: ciExemptReason must be a non-empty reason (or use --unset ciExemptReason to clear it)");
|
|
@@ -31212,7 +31268,8 @@ var SETTABLE_VAR_KEYS = [
|
|
|
31212
31268
|
"seedCanary",
|
|
31213
31269
|
"installer",
|
|
31214
31270
|
"notifyOnDeploy",
|
|
31215
|
-
"vercelVersionEnv"
|
|
31271
|
+
"vercelVersionEnv",
|
|
31272
|
+
"stageAllowlist"
|
|
31216
31273
|
];
|
|
31217
31274
|
var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
31218
31275
|
var SETTABLE_VAR_HINTS = {
|
|
@@ -31244,6 +31301,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
31244
31301
|
requiredCheckBranches: "non-empty JSON branch-name array; exact product-ruleset scope (#5210)",
|
|
31245
31302
|
notifyOnDeploy: 'JSON stage array, e.g. ["dev"] \u2014 successful tenant-deploy sends <stage>-deployed to the project repo (#7358)',
|
|
31246
31303
|
vercelVersionEnv: "JSON {project,key,team?} \u2014 train sets this Vercel env to the release tag before the main/rc push (#7362)",
|
|
31304
|
+
stageAllowlist: "JSON {key,stages[]} \u2014 `org access sync` writes the developer/admin company-email allowlist to <stage>/<key> for each listed stage (#7678)",
|
|
31247
31305
|
ciExemptReason: "non-empty string \u2014 declares this deployable repo has no CI surface (#4265)",
|
|
31248
31306
|
gate: "JSON {runtime,cmd,affectedCmd,workdir,cacheDepPath,pyVersion}"
|
|
31249
31307
|
};
|
|
@@ -31407,6 +31465,8 @@ function buildProjectSetPatch(input) {
|
|
|
31407
31465
|
patch[key] = parseNotifyOnDeployVar(raw);
|
|
31408
31466
|
} else if (key === "vercelVersionEnv") {
|
|
31409
31467
|
patch[key] = parseVercelVersionEnvVar(raw);
|
|
31468
|
+
} else if (key === "stageAllowlist") {
|
|
31469
|
+
patch[key] = parseStageAllowlistVar(raw);
|
|
31410
31470
|
} else if (key === "ciExemptReason") {
|
|
31411
31471
|
patch[key] = parseCiExemptReasonVar(raw);
|
|
31412
31472
|
} else if (key === "gate") {
|
|
@@ -36017,6 +36077,12 @@ async function runRcDataSwitch(deps, opts) {
|
|
|
36017
36077
|
if (opts.apply && !opts.ownerGo) {
|
|
36018
36078
|
throw new Error("--apply requires --owner-go (it touches the live data key); nothing was written");
|
|
36019
36079
|
}
|
|
36080
|
+
const rcSubstrate = await deps.readLeadRcSubstrate();
|
|
36081
|
+
if (rcSubstrate) {
|
|
36082
|
+
throw new Error(
|
|
36083
|
+
`suite rc-data applies only while rc is served by Vercel (the lead's DEPLOY#rc runs on ${rcSubstrate}); rc data now follows the tenant runtime, not Vercel Preview envs; nothing was written`
|
|
36084
|
+
);
|
|
36085
|
+
}
|
|
36020
36086
|
const secrets = [];
|
|
36021
36087
|
const token = await deps.readToken();
|
|
36022
36088
|
secrets.push(token);
|
|
@@ -36342,9 +36408,49 @@ function parseCherryPickBlockingPaths(message2) {
|
|
|
36342
36408
|
if (!m) return [];
|
|
36343
36409
|
return m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
36344
36410
|
}
|
|
36345
|
-
function
|
|
36411
|
+
async function classifyHotfixPortLanePicks(deps, failures) {
|
|
36412
|
+
const exists = async (ref, path2) => {
|
|
36413
|
+
try {
|
|
36414
|
+
await deps.run("git", ["cat-file", "-e", `${ref}:${path2}`]);
|
|
36415
|
+
return true;
|
|
36416
|
+
} catch {
|
|
36417
|
+
return false;
|
|
36418
|
+
}
|
|
36419
|
+
};
|
|
36420
|
+
const portable = /* @__PURE__ */ new Map();
|
|
36421
|
+
for (const f of failures) {
|
|
36422
|
+
if (f.blocking.length === 0) continue;
|
|
36423
|
+
let fits = false;
|
|
36424
|
+
const bothSides = [];
|
|
36425
|
+
for (const path2 of f.blocking) {
|
|
36426
|
+
const onMain = await exists("origin/main", path2);
|
|
36427
|
+
if (!onMain && await exists("origin/development", path2)) fits = true;
|
|
36428
|
+
else if (onMain) bothSides.push(path2);
|
|
36429
|
+
}
|
|
36430
|
+
if (fits) portable.set(f.sha, bothSides);
|
|
36431
|
+
}
|
|
36432
|
+
return portable;
|
|
36433
|
+
}
|
|
36434
|
+
function bothSidesText(portFailures, portLaneShas) {
|
|
36435
|
+
const rows = portFailures.map((f) => ({ label: f.label, paths: portLaneShas.get(f.sha) ?? [] })).filter((r) => r.paths.length > 0);
|
|
36436
|
+
if (rows.length === 0) return "";
|
|
36437
|
+
return `BEFORE that --ack: ${rows.map((r) => `${r.label} also conflicts on ${r.paths.join(", ")}`).join("; ")}, which exist on both branches \u2014 the port resolves them by hand for main, so land the SAME resolution on development first (the --ack covers the whole commit, and the next release would otherwise overwrite or clash with the main-only content). `;
|
|
36438
|
+
}
|
|
36439
|
+
function formatHotfixBatchPreflightRefusal(failures, fromSpecs, portLaneShas = /* @__PURE__ */ new Map()) {
|
|
36346
36440
|
const lines2 = failures.map((f) => ` - ${f.label}: ${f.blocking.join(", ")}`);
|
|
36441
|
+
const portFailures = failures.filter((f) => portLaneShas.has(f.sha));
|
|
36442
|
+
const synthFailures = failures.filter((f) => !portLaneShas.has(f.sha));
|
|
36443
|
+
const head = `hotfix start batch preflight refused: ${failures.length} pick(s) would hard-stop on origin/main:
|
|
36444
|
+
${lines2.join("\n")}
|
|
36445
|
+
`;
|
|
36347
36446
|
const batchFrom = fromSpecs.join(",");
|
|
36447
|
+
const guide = "https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-pick-refused";
|
|
36448
|
+
if (portFailures.length > 0) {
|
|
36449
|
+
const originals = portFailures.map((f) => f.sha.slice(0, 7)).join(",");
|
|
36450
|
+
const portText = `${portFailures.length === failures.length ? "Every failing pick" : `Pick(s) ${portFailures.map((f) => f.label).join(", ")}`} fail on path(s) ABSENT on origin/main but present on origin/development, so a development-based cite-only synthesis PR cannot carry them (a new development commit cannot add files development already has \u2014 its cherry-pick onto main carries nothing, #7669). Use the main-based PORT-COMMIT lane: cut a branch FROM origin/main, add ONE port commit that brings the listed path(s) and the fix onto main-lineage (e.g. \`git checkout origin/development -- <paths>\` then the fix), push it and keep it pushed until the release \u2014 never merge it into development \u2014 then run \`mmi-cli devops hotfix start --from <portSHA> --ack ${originals}\` (a bare SHA is accepted; the originals are the acked development fixes the port replaces), and record the port SHA in the hotfix PR body. The port commit never reaches development and will not patch-id match, so the NEXT rc\u2192main release must pass \`--ack <the hotfix's main commit>\` (hotfix-coverage). ` + bothSidesText(portFailures, portLaneShas);
|
|
36451
|
+
if (synthFailures.length === 0) return `${head}${portText}Never hand-resolve onto main; see ${guide}`;
|
|
36452
|
+
return `${head}Mixed batch \u2014 two lanes. ${portText}Pick(s) ${synthFailures.map((f) => f.label).join(", ")} fit the development-based cite-only synthesis lane instead: cut the branch FROM origin/development, regenerate the conflict content so it cherry-picks cleanly onto current origin/main, and land that development-parented PR with the cite-only marker (the #5096 flow). Then rerun \`mmi-cli devops hotfix start --from <synthesis merge SHA(s)>,<portSHA> --ack ${originals}\` (the original --from was ${batchFrom}) \u2014 never hand-resolve onto main; never a main-parented branch into development; see ${guide}`;
|
|
36453
|
+
}
|
|
36348
36454
|
return `hotfix start batch preflight refused: ${failures.length} pick(s) would hard-stop on origin/main:
|
|
36349
36455
|
${lines2.join("\n")}
|
|
36350
36456
|
Land ONE development-based cite-only synthesis PR for the whole batch (#4643 supersedes the old cut-from-origin/main wording, which produced main-lineage PRs into development \u2014 the shape portFoldToDevelopment forbids, #4365/#4371): cut the branch FROM origin/development, regenerate the listed conflict content so it cherry-picks cleanly onto current origin/main, and land that development-parented PR with the cite-only marker (the proven #5096 flow), then rerun \`mmi-cli devops hotfix start --from ${batchFrom}\` with the port merge SHA(s) \u2014 never hand-resolve onto main; never a main-parented branch into development; never N serial ports (#3167) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-pick-refused`;
|
|
@@ -36538,7 +36644,7 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
36538
36644
|
await deps.run("git", ["fetch", "origin", "--tags"]);
|
|
36539
36645
|
const { deployModel, meta } = await hotfixPreflight(deps, ctx, "start");
|
|
36540
36646
|
const bumpNotes = [];
|
|
36541
|
-
let resolved = await resolveHotfixVersion(deps);
|
|
36647
|
+
let resolved = await resolveHotfixVersion(deps, process.env, preOneZeroInForce(meta));
|
|
36542
36648
|
for (; ; ) {
|
|
36543
36649
|
const priorPr = await findHotfixPr(deps, ctx, resolved.tag);
|
|
36544
36650
|
if ((priorPr?.state ?? "").toUpperCase() !== "MERGED") break;
|
|
@@ -36608,7 +36714,8 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
36608
36714
|
} else {
|
|
36609
36715
|
const preflight2 = await preflightHotfixBatchPicks(deps, sources, pickTolerated);
|
|
36610
36716
|
if (preflight2.failures.length > 0) {
|
|
36611
|
-
|
|
36717
|
+
const portLaneShas = await classifyHotfixPortLanePicks(deps, preflight2.failures);
|
|
36718
|
+
throw new Error(formatHotfixBatchPreflightRefusal(preflight2.failures, specs, portLaneShas));
|
|
36612
36719
|
}
|
|
36613
36720
|
if (preflight2.skipped.length > 0) {
|
|
36614
36721
|
const skippedShas = new Set(preflight2.skipped.map((s) => s.sha));
|
|
@@ -37720,7 +37827,6 @@ async function advanceClosedIssuesToDone(deps) {
|
|
|
37720
37827
|
error: `expected an array of closing issue references, got ${closing === null ? "null" : typeof closing}`
|
|
37721
37828
|
};
|
|
37722
37829
|
}
|
|
37723
|
-
if (closing.length === 0) return { status: "none", entries: [] };
|
|
37724
37830
|
const refs = [];
|
|
37725
37831
|
for (const [i, candidate] of closing.entries()) {
|
|
37726
37832
|
const repo = repoOf2(candidate);
|
|
@@ -37734,6 +37840,29 @@ async function advanceClosedIssuesToDone(deps) {
|
|
|
37734
37840
|
refs.push({ ref: candidate, repo });
|
|
37735
37841
|
}
|
|
37736
37842
|
const entries = [];
|
|
37843
|
+
const fallbackNumbers = /* @__PURE__ */ new Set();
|
|
37844
|
+
let fallbackError;
|
|
37845
|
+
if (deps.fallback) {
|
|
37846
|
+
try {
|
|
37847
|
+
const registered = new Set(refs.filter((r) => r.repo.toLowerCase() === deps.repo.toLowerCase()).map((r) => r.ref.number));
|
|
37848
|
+
for (const n of [...new Set(await deps.fallback.fetchClaimed())].filter((n2) => !registered.has(n2))) {
|
|
37849
|
+
try {
|
|
37850
|
+
const closed = await deps.fallback.closeIssue(n);
|
|
37851
|
+
if ("error" in closed) entries.push({ issue: `#${n}`, moved: false, status: "failed", error: `GitHub did not register the closing reference and the CLI could not close it (${closed.error})` });
|
|
37852
|
+
else if (closed.state === "closed") {
|
|
37853
|
+
fallbackNumbers.add(n);
|
|
37854
|
+
const [owner, name] = deps.repo.split("/");
|
|
37855
|
+
refs.push({ ref: { number: n, repository: { name: name ?? "", owner: { login: owner ?? "" } } }, repo: deps.repo });
|
|
37856
|
+
}
|
|
37857
|
+
} catch (e) {
|
|
37858
|
+
entries.push({ issue: `#${n}`, moved: false, status: "failed", error: `GitHub did not register the closing reference and the CLI could not close it (${e.message})` });
|
|
37859
|
+
}
|
|
37860
|
+
}
|
|
37861
|
+
} catch (e) {
|
|
37862
|
+
fallbackError = e.message;
|
|
37863
|
+
}
|
|
37864
|
+
}
|
|
37865
|
+
if (refs.length === 0 && entries.length === 0) return { status: "none", entries: [], ...fallbackError ? { fallbackError } : {} };
|
|
37737
37866
|
for (const { ref, repo } of refs) {
|
|
37738
37867
|
if (repo.toLowerCase() !== deps.repo.toLowerCase()) {
|
|
37739
37868
|
entries.push({
|
|
@@ -37774,14 +37903,15 @@ async function advanceClosedIssuesToDone(deps) {
|
|
|
37774
37903
|
entries.push({ issue, moved: false, status: "failed", error: e.message });
|
|
37775
37904
|
}
|
|
37776
37905
|
}
|
|
37777
|
-
|
|
37906
|
+
for (const e of entries) if (fallbackNumbers.has(Number(e.issue.slice(1)))) e.closedByFallback = true;
|
|
37907
|
+
return { status: entries.every((e) => e.moved) && !fallbackError ? "ok" : "partial", entries, ...fallbackError ? { fallbackError } : {} };
|
|
37778
37908
|
}
|
|
37779
37909
|
function boardAdvanceFailures(result) {
|
|
37780
37910
|
return (result?.entries ?? []).filter((e) => !e.moved);
|
|
37781
37911
|
}
|
|
37782
37912
|
function boardAdvanceExitCode(result) {
|
|
37783
37913
|
if (!result) return void 0;
|
|
37784
|
-
if (result.status === "fetch-failed") return 1;
|
|
37914
|
+
if (result.status === "fetch-failed" || result.fallbackError) return 1;
|
|
37785
37915
|
return boardAdvanceFailures(result).length > 0 ? 1 : void 0;
|
|
37786
37916
|
}
|
|
37787
37917
|
function boardAdvanceFailureMessage(result) {
|
|
@@ -37790,6 +37920,7 @@ function boardAdvanceFailureMessage(result) {
|
|
|
37790
37920
|
return `pr merge: could not read the PR's closing issues (${result.error ?? "unknown error"}) \u2014 the PR MERGED, but no board item was advanced. Check the board by hand.`;
|
|
37791
37921
|
}
|
|
37792
37922
|
const failures = boardAdvanceFailures(result);
|
|
37923
|
+
if (result.fallbackError) return `pr merge: could not read the PR body to close unregistered closing lines (${result.fallbackError}) \u2014 the PR MERGED; check by hand whether its "Closes #N" issues are still open.`;
|
|
37793
37924
|
if (!failures.length) return void 0;
|
|
37794
37925
|
const named = failures.map((f) => f.error ? `${f.issue} (${f.error})` : f.issue).join(", ");
|
|
37795
37926
|
return `pr merge: board advance incomplete for ${named} \u2014 the PR MERGED, but ${failures.length === 1 ? "this issue" : "these issues"} did not reach Done on the board.`;
|
|
@@ -38882,6 +39013,41 @@ async function resolveBoardAdvanceForPr(prNumber, repoOption) {
|
|
|
38882
39013
|
}
|
|
38883
39014
|
return advanceClosedIssuesToDone({
|
|
38884
39015
|
repo,
|
|
39016
|
+
// #7691: GitHub sometimes fails to register a body `Closes #N`, so the merge closes nothing. Close the
|
|
39017
|
+
// still-OPEN issues the body claims on a deliberate line-start closing line (same repo only). GitHub closes
|
|
39018
|
+
// only for PRs into the default branch, so a PR into rc/main (a train or hotfix PR) never falls back.
|
|
39019
|
+
fallback: {
|
|
39020
|
+
fetchClaimed: async () => {
|
|
39021
|
+
const { stdout } = await execFileP("gh", ["pr", "view", prNumber, ...repoArgs, "--json", "body,baseRefName"], { timeout: GC_GH_TIMEOUT_MS });
|
|
39022
|
+
const { body, baseRefName } = JSON.parse(stdout || "null") ?? {};
|
|
39023
|
+
if (typeof body !== "string" || typeof baseRefName !== "string") throw new Error("gh returned no PR body or base branch");
|
|
39024
|
+
const repoView = await execFileP("gh", ["repo", "view", repo, "--json", "defaultBranchRef"], { timeout: GC_GH_TIMEOUT_MS });
|
|
39025
|
+
const defaultBranch = JSON.parse(repoView.stdout || "null")?.defaultBranchRef?.name;
|
|
39026
|
+
if (typeof defaultBranch !== "string" || !defaultBranch) throw new Error("gh returned no default branch");
|
|
39027
|
+
if (baseRefName !== defaultBranch) return [];
|
|
39028
|
+
return findLineStartClosings(body, repo);
|
|
39029
|
+
},
|
|
39030
|
+
closeIssue: async (n) => {
|
|
39031
|
+
try {
|
|
39032
|
+
const view = await execFileP("gh", ["issue", "view", String(n), "--repo", repo, "--json", "state"], { timeout: GC_GH_TIMEOUT_MS });
|
|
39033
|
+
if (JSON.parse(view.stdout || "null")?.state !== "OPEN") return { state: "already-closed" };
|
|
39034
|
+
await execFileP("gh", [
|
|
39035
|
+
"issue",
|
|
39036
|
+
"close",
|
|
39037
|
+
String(n),
|
|
39038
|
+
"--repo",
|
|
39039
|
+
repo,
|
|
39040
|
+
"--reason",
|
|
39041
|
+
"completed",
|
|
39042
|
+
"--comment",
|
|
39043
|
+
`Closed by the CLI after PR #${prNumber} merged: its body says it closes this issue, but GitHub did not register the closing reference (#7691).`
|
|
39044
|
+
], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
39045
|
+
return { state: "closed" };
|
|
39046
|
+
} catch (e) {
|
|
39047
|
+
return { error: e.message };
|
|
39048
|
+
}
|
|
39049
|
+
}
|
|
39050
|
+
},
|
|
38885
39051
|
fetchClosingIssues: async () => {
|
|
38886
39052
|
const { stdout } = await execFileP("gh", ["pr", "view", prNumber, ...repoArgs, "--json", "closingIssuesReferences"], { timeout: GC_GH_TIMEOUT_MS });
|
|
38887
39053
|
const parsed = JSON.parse(stdout || "null");
|
|
@@ -41689,6 +41855,8 @@ function findEnvFiles(root) {
|
|
|
41689
41855
|
// src/stage-runtime-env.ts
|
|
41690
41856
|
var SERVICE_NAME_RE = /^[A-Za-z0-9_.-]+$/;
|
|
41691
41857
|
var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
41858
|
+
var EXCLUDE_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
41859
|
+
var EXCLUDE_LABEL = "mmi.runtime-env.exclude";
|
|
41692
41860
|
var RUNTIME_ENV_OVERRIDE_RELPATH = "tmp/stage/runtime-env.override.yml";
|
|
41693
41861
|
function labelsMarkRuntimeEnv(labels) {
|
|
41694
41862
|
if (Array.isArray(labels)) {
|
|
@@ -41699,6 +41867,50 @@ function labelsMarkRuntimeEnv(labels) {
|
|
|
41699
41867
|
}
|
|
41700
41868
|
return false;
|
|
41701
41869
|
}
|
|
41870
|
+
function labelValue(labels, name) {
|
|
41871
|
+
if (Array.isArray(labels)) {
|
|
41872
|
+
for (const l of labels) {
|
|
41873
|
+
if (typeof l !== "string") continue;
|
|
41874
|
+
const eq = l.indexOf("=");
|
|
41875
|
+
if (eq < 0 || l.slice(0, eq).trim() !== name) continue;
|
|
41876
|
+
const value = l.slice(eq + 1).trim();
|
|
41877
|
+
const quote = value[0];
|
|
41878
|
+
const quoted = value.length >= 2 && (quote === '"' || quote === "'") && value.endsWith(quote);
|
|
41879
|
+
return quoted ? value.slice(1, -1) : value;
|
|
41880
|
+
}
|
|
41881
|
+
return "";
|
|
41882
|
+
}
|
|
41883
|
+
if (labels && typeof labels === "object") return String(labels[name] ?? "");
|
|
41884
|
+
return "";
|
|
41885
|
+
}
|
|
41886
|
+
function parseRuntimeEnvExclude(value, service = "?") {
|
|
41887
|
+
if (!value.trim()) return [];
|
|
41888
|
+
return value.split(",").map((raw) => {
|
|
41889
|
+
const key = raw.trim();
|
|
41890
|
+
if (!EXCLUDE_KEY_RE.test(key)) {
|
|
41891
|
+
throw new Error(`invalid ${EXCLUDE_LABEL} entry on service ${service}: '${key}' (comma-separated env-var names matching ^[A-Z_][A-Z0-9_]*$)`);
|
|
41892
|
+
}
|
|
41893
|
+
return key;
|
|
41894
|
+
});
|
|
41895
|
+
}
|
|
41896
|
+
function runtimeEnvExcludes(composeConfigJson) {
|
|
41897
|
+
let parsed;
|
|
41898
|
+
try {
|
|
41899
|
+
parsed = JSON.parse(composeConfigJson);
|
|
41900
|
+
} catch {
|
|
41901
|
+
return {};
|
|
41902
|
+
}
|
|
41903
|
+
const services = parsed?.services;
|
|
41904
|
+
if (!services || typeof services !== "object" || Array.isArray(services)) return {};
|
|
41905
|
+
const out = {};
|
|
41906
|
+
for (const [name, svc] of Object.entries(services)) {
|
|
41907
|
+
const labels = svc?.labels;
|
|
41908
|
+
if (!SERVICE_NAME_RE.test(name) || !labelsMarkRuntimeEnv(labels)) continue;
|
|
41909
|
+
const keys = parseRuntimeEnvExclude(labelValue(labels, EXCLUDE_LABEL), name);
|
|
41910
|
+
if (keys.length) out[name] = keys;
|
|
41911
|
+
}
|
|
41912
|
+
return out;
|
|
41913
|
+
}
|
|
41702
41914
|
function runtimeEnvMarkedServices(composeConfigJson) {
|
|
41703
41915
|
let parsed;
|
|
41704
41916
|
try {
|
|
@@ -41710,19 +41922,24 @@ function runtimeEnvMarkedServices(composeConfigJson) {
|
|
|
41710
41922
|
if (!services || typeof services !== "object" || Array.isArray(services)) return [];
|
|
41711
41923
|
return Object.entries(services).filter(([name, svc]) => SERVICE_NAME_RE.test(name) && labelsMarkRuntimeEnv(svc?.labels)).map(([name]) => name).sort();
|
|
41712
41924
|
}
|
|
41713
|
-
function renderRuntimeEnvOverride(services, keys) {
|
|
41925
|
+
function renderRuntimeEnvOverride(services, keys, excludes = {}) {
|
|
41714
41926
|
const safeServices = services.filter((s) => SERVICE_NAME_RE.test(s)).sort();
|
|
41715
41927
|
const safeKeys = [...new Set(keys.filter((k) => ENV_KEY_RE.test(k)))].sort();
|
|
41716
|
-
|
|
41928
|
+
const keysFor = (service) => {
|
|
41929
|
+
const skip = new Set(excludes[service] ?? []);
|
|
41930
|
+
return safeKeys.filter((k) => !skip.has(k));
|
|
41931
|
+
};
|
|
41932
|
+
const withKeys = safeServices.filter((s) => keysFor(s).length);
|
|
41933
|
+
if (!withKeys.length) return null;
|
|
41717
41934
|
const lines2 = [
|
|
41718
41935
|
"# Generated by `mmi-cli stage` (#6345) \u2014 do not edit, do not commit.",
|
|
41719
41936
|
'# Passes the vault-resolved runtime env through to every service labelled mmi.runtime-env: "true",',
|
|
41720
41937
|
"# exactly as the box deploy does. KEY NAMES ONLY: the values live in the compose process env (#2655).",
|
|
41721
41938
|
"services:"
|
|
41722
41939
|
];
|
|
41723
|
-
for (const service of
|
|
41940
|
+
for (const service of withKeys) {
|
|
41724
41941
|
lines2.push(` ${service}:`, " environment:");
|
|
41725
|
-
for (const key of
|
|
41942
|
+
for (const key of keysFor(service)) lines2.push(` ${key}: \${${key}:?}`);
|
|
41726
41943
|
}
|
|
41727
41944
|
return `${lines2.join("\n")}
|
|
41728
41945
|
`;
|
|
@@ -42124,7 +42341,7 @@ async function prepareRuntimeEnvPassthrough(cwd, composeEnv, vaultEnvMerge) {
|
|
|
42124
42341
|
} catch {
|
|
42125
42342
|
return {};
|
|
42126
42343
|
}
|
|
42127
|
-
const body = renderRuntimeEnvOverride(runtimeEnvMarkedServices(configJson), keys);
|
|
42344
|
+
const body = renderRuntimeEnvOverride(runtimeEnvMarkedServices(configJson), keys, runtimeEnvExcludes(configJson));
|
|
42128
42345
|
if (!body) return {};
|
|
42129
42346
|
const overridePath = (0, import_node_path38.join)(cwd, RUNTIME_ENV_OVERRIDE_RELPATH);
|
|
42130
42347
|
(0, import_node_fs38.mkdirSync)((0, import_node_path38.join)(cwd, "tmp", "stage"), { recursive: true });
|
|
@@ -44100,6 +44317,11 @@ ${list}`);
|
|
|
44100
44317
|
`
|
|
44101
44318
|
);
|
|
44102
44319
|
}
|
|
44320
|
+
const safeTitle = rewriteUnregisteredClosingKeywords(title, findClosingMentions(body).map((m) => m.issue));
|
|
44321
|
+
if (safeTitle !== title) {
|
|
44322
|
+
title = safeTitle;
|
|
44323
|
+
process.stderr.write('pr create: WARNING \u2014 rewrote close-keyword text in the title to "part of #N": the squash commit subject would close it (#7688). Put closing keywords on a "Closes #N" body line.\n');
|
|
44324
|
+
}
|
|
44103
44325
|
const docsCheck = await checkDocsIndexAtHead({ repo: o.repo, head: o.head }, createPrCreateDocsIndexDeps());
|
|
44104
44326
|
if (docsCheck && !docsCheck.ok) {
|
|
44105
44327
|
return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
|
|
@@ -45047,7 +45269,7 @@ async function requireFreshTrainCli(commandName) {
|
|
|
45047
45269
|
process.exit(converge.reexecCode);
|
|
45048
45270
|
}
|
|
45049
45271
|
var GH_TRAIN_TIMEOUT_MS = 3e4;
|
|
45050
|
-
var GH_RUN_WATCH_TIMEOUT_MS =
|
|
45272
|
+
var GH_RUN_WATCH_TIMEOUT_MS = 90 * 6e4;
|
|
45051
45273
|
var NODE_PREPARE_TIMEOUT_MS = 10 * 6e4;
|
|
45052
45274
|
var NODE_VERIFY_TIMEOUT_MS = 5 * 6e4;
|
|
45053
45275
|
var NPM_TRAIN_TIMEOUT_MS = 6e4;
|
|
@@ -46311,6 +46533,12 @@ function suiteRcDataProdDeps() {
|
|
|
46311
46533
|
},
|
|
46312
46534
|
readToken: readOrgVercelApiToken,
|
|
46313
46535
|
readVault: async (vaultKey) => fetchSecretValue(makeSecretsDeps(await loadConfig()), vaultKey, { repo: RC_DATA_VAULT_REPO }),
|
|
46536
|
+
readLeadRcSubstrate: async () => {
|
|
46537
|
+
const facts = await fetchDeployFactsBySlug("mm-work", registryClientDeps(await loadConfig()));
|
|
46538
|
+
if (!facts) throw new Error("MM-Work deploy facts unreadable; stop");
|
|
46539
|
+
const rc = facts.stages.rc;
|
|
46540
|
+
return rc?.present ? rc.substrate ?? "an unnamed substrate" : null;
|
|
46541
|
+
},
|
|
46314
46542
|
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))
|
|
46315
46543
|
};
|
|
46316
46544
|
}
|
|
@@ -49665,7 +49893,7 @@ function registerSchedulesCommands(program3) {
|
|
|
49665
49893
|
await failGraceful(e.message);
|
|
49666
49894
|
}
|
|
49667
49895
|
});
|
|
49668
|
-
schedules.command("park <schedule-id>").description("record a deliberate hold on a SCHEDULE# row \u2014 the reconciler skips it and `org schedules` renders `parked (<reason>)` instead of DRIFT (#4257)").requiredOption("--reason <ref>", "why the lane is held, e.g. an issue ref (#1774)").action(async (scheduleId, o) => {
|
|
49896
|
+
schedules.command("park <schedule-id>").description("record a deliberate hold on a SCHEDULE# row \u2014 the reconciler skips it and `org schedules` renders `parked (<reason>)` instead of DRIFT (#4257). It does NOT stop an armed clock from dispatching: to stop a lane firing, disable its workflow (`gh workflow disable`) or remove its header and re-register (#7685)").requiredOption("--reason <ref>", "why the lane is held, e.g. an issue ref (#1774)").action(async (scheduleId, o) => {
|
|
49669
49897
|
try {
|
|
49670
49898
|
const res = await postSchedulesPark(scheduleId, o.reason, registryClientDeps(await loadConfig()));
|
|
49671
49899
|
if (!res.ok) {
|
|
@@ -49673,6 +49901,7 @@ function registerSchedulesCommands(program3) {
|
|
|
49673
49901
|
return;
|
|
49674
49902
|
}
|
|
49675
49903
|
console.log(JSON.stringify(res.body, null, 2));
|
|
49904
|
+
console.error(`schedules park: ${scheduleId} is parked for drift reporting only \u2014 an armed clock still dispatches it. To stop it firing: gh workflow disable <workflow> -R <owner/repo>, or remove its schedule header and re-run \`mmi-cli harbour org schedules register\`.`);
|
|
49676
49905
|
} catch (e) {
|
|
49677
49906
|
await failGraceful(e.message);
|
|
49678
49907
|
}
|
|
@@ -49717,6 +49946,96 @@ function registerSchedulesCommands(program3) {
|
|
|
49717
49946
|
});
|
|
49718
49947
|
}
|
|
49719
49948
|
|
|
49949
|
+
// ../company-emails.json
|
|
49950
|
+
var company_emails_default = {
|
|
49951
|
+
_comment: "Org-level, NON-secret GitHub login -> company email map (#7678). `mmi-cli oracle org access sync <repo>` maps a repo's current developer/admin collaborators through it to rebuild the stage allowlist a project opts into with the registry field `stageAllowlist` (e.g. MM-Work's MMW_STAGE_ALLOWLIST). Bundled into mmi-cli at build time (cli/src/stage-allowlist.ts) \u2014 one source of truth, edited here in a PR. Keys are lowercase GitHub logins; a collaborator missing here is reported loudly by the sync, never silently dropped.",
|
|
49952
|
+
emails: {
|
|
49953
|
+
jervaise: "suphi@mutatismutandis.co",
|
|
49954
|
+
sezenlerinadil: "adil@mutatismutandis.co"
|
|
49955
|
+
}
|
|
49956
|
+
};
|
|
49957
|
+
|
|
49958
|
+
// src/stage-allowlist.ts
|
|
49959
|
+
var ALLOWLIST_STAGES = ["dev", "rc", "main"];
|
|
49960
|
+
var ALLOWLIST_ROLES = /* @__PURE__ */ new Set(["write", "maintain", "admin"]);
|
|
49961
|
+
function bundledCompanyEmails() {
|
|
49962
|
+
return normalizeEmailMap(company_emails_default.emails ?? {});
|
|
49963
|
+
}
|
|
49964
|
+
function normalizeEmailMap(raw) {
|
|
49965
|
+
const out = {};
|
|
49966
|
+
for (const [login, email] of Object.entries(raw)) {
|
|
49967
|
+
if (typeof email === "string" && email.trim()) out[login.toLowerCase()] = email.trim().toLowerCase();
|
|
49968
|
+
}
|
|
49969
|
+
return out;
|
|
49970
|
+
}
|
|
49971
|
+
function stageAllowlistConfig(project2) {
|
|
49972
|
+
const cfg = project2?.stageAllowlist;
|
|
49973
|
+
if (!cfg || typeof cfg.key !== "string" || !Array.isArray(cfg.stages)) return null;
|
|
49974
|
+
const stages = cfg.stages.filter((s) => ALLOWLIST_STAGES.includes(s));
|
|
49975
|
+
return stages.length > 0 ? { key: cfg.key, stages } : null;
|
|
49976
|
+
}
|
|
49977
|
+
function developerAdminLogins(collaborators) {
|
|
49978
|
+
const logins = /* @__PURE__ */ new Set();
|
|
49979
|
+
for (const c of collaborators) {
|
|
49980
|
+
const role = c.role_name ?? (c.permissions?.admin ? "admin" : c.permissions?.maintain ? "maintain" : c.permissions?.push ? "write" : "read");
|
|
49981
|
+
if (ALLOWLIST_ROLES.has(role)) logins.add(c.login.toLowerCase());
|
|
49982
|
+
}
|
|
49983
|
+
return [...logins].sort();
|
|
49984
|
+
}
|
|
49985
|
+
function computeAllowlist(logins, emailMap) {
|
|
49986
|
+
const emails = /* @__PURE__ */ new Set();
|
|
49987
|
+
const unmapped = [];
|
|
49988
|
+
for (const login of logins) {
|
|
49989
|
+
const email = emailMap[login.toLowerCase()];
|
|
49990
|
+
if (email) emails.add(email);
|
|
49991
|
+
else unmapped.push(login);
|
|
49992
|
+
}
|
|
49993
|
+
return { emails: [...emails].sort(), unmapped };
|
|
49994
|
+
}
|
|
49995
|
+
function diffAllowlist(current, next) {
|
|
49996
|
+
const before = new Set((current ?? "").split(",").map((e) => e.trim().toLowerCase()).filter(Boolean));
|
|
49997
|
+
const after = new Set(next);
|
|
49998
|
+
const added = [...after].filter((e) => !before.has(e)).sort();
|
|
49999
|
+
const removed = [...before].filter((e) => !after.has(e)).sort();
|
|
50000
|
+
return { added, removed, changed: added.length > 0 || removed.length > 0 };
|
|
50001
|
+
}
|
|
50002
|
+
async function syncStageAllowlist(deps, repo, config, emailMap, opts = {}) {
|
|
50003
|
+
const logins = developerAdminLogins(await deps.listCollaborators(repo));
|
|
50004
|
+
const { emails, unmapped } = computeAllowlist(logins, emailMap);
|
|
50005
|
+
const stages = [];
|
|
50006
|
+
for (const stage of config.stages) {
|
|
50007
|
+
const coordinate = `${stage}/${config.key}`;
|
|
50008
|
+
const diff = diffAllowlist(await deps.readValue(repo, coordinate), emails);
|
|
50009
|
+
if (!diff.changed || opts.dryRun) {
|
|
50010
|
+
stages.push({ stage, ...diff, written: false });
|
|
50011
|
+
continue;
|
|
50012
|
+
}
|
|
50013
|
+
if (emails.length === 0) {
|
|
50014
|
+
stages.push({ stage, ...diff, written: false, error: "no developer/admin collaborator has a mapped email \u2014 refusing to write an empty allowlist" });
|
|
50015
|
+
continue;
|
|
50016
|
+
}
|
|
50017
|
+
const written = await deps.writeValue(repo, coordinate, emails.join(","));
|
|
50018
|
+
stages.push({ stage, ...diff, written, ...written ? {} : { error: `vault write of ${coordinate} was refused` } });
|
|
50019
|
+
}
|
|
50020
|
+
return { repo, key: config.key, logins, unmapped, emails, stages, ok: unmapped.length === 0 && stages.every((s) => !s.error) };
|
|
50021
|
+
}
|
|
50022
|
+
function renderStageAllowlist(r, dryRun = false) {
|
|
50023
|
+
const lines2 = [`${r.repo}: ${r.key} from ${r.logins.length} developer/admin collaborator(s): ${r.logins.map((l) => `@${l}`).join(", ") || "(none)"}`];
|
|
50024
|
+
for (const s of r.stages) {
|
|
50025
|
+
const change = !s.changed ? "unchanged" : `${s.added.length ? `+${s.added.join(" +")}` : ""}${s.added.length && s.removed.length ? " " : ""}${s.removed.length ? `-${s.removed.join(" -")}` : ""}`;
|
|
50026
|
+
const verb = s.error ? "FAILED" : s.written ? "written" : s.changed && dryRun ? "would write" : "no write needed";
|
|
50027
|
+
lines2.push(` ${s.stage}/${r.key}: ${change} (${verb})${s.error ? ` \u2014 ${s.error}` : ""}`);
|
|
50028
|
+
}
|
|
50029
|
+
for (const login of r.unmapped) {
|
|
50030
|
+
lines2.push(` UNMAPPED: @${login} holds developer/admin on ${r.repo} but has NO company email in company-emails.json \u2014 they are NOT on the allowlist. Add "${login.toLowerCase()}" there, then re-run.`);
|
|
50031
|
+
}
|
|
50032
|
+
if (r.stages.some((s) => s.written)) lines2.push(" redeploy the stage for the runtime to pick the new value up: `mmi-cli devops runtime tenant redeploy <owner/repo> <stage>`");
|
|
50033
|
+
return lines2.join("\n");
|
|
50034
|
+
}
|
|
50035
|
+
async function listRepoCollaborators(client, repo) {
|
|
50036
|
+
return client.restPaginate(`repos/${repo}/collaborators`);
|
|
50037
|
+
}
|
|
50038
|
+
|
|
49720
50039
|
// src/session-report.ts
|
|
49721
50040
|
init_cli_shared();
|
|
49722
50041
|
var GC_GH_TIMEOUT_MS4 = 2e4;
|
|
@@ -52474,7 +52793,7 @@ ${filelessTransitionGuide(target, o.stage)}`
|
|
|
52474
52793
|
return reportWrite("org project set-deploy", res);
|
|
52475
52794
|
});
|
|
52476
52795
|
var projectTenantTasks = project.command("tenant-tasks").description("project-admin self-declare of tenant tasks for every stage of your own repo \u2014 new-task-only; editing an existing task stays master-only (`org project set --var tenantTasks=...`)");
|
|
52477
|
-
projectTenantTasks.command("declare <name>").description("register a NEW tenant task \u2014 becomes runnable via `runtime tenant control \u2026 run-task` on its declared dev/rc/main stages. Body shape {service,command[],stages[],timeoutSeconds?,artifact?}; required artifact tasks must carry {artifact} in the command argv.").option("--service <name>", "service in the tenant runtime the task runs in (required unless --var carries it)").option("--command <argv...>",
|
|
52796
|
+
projectTenantTasks.command("declare <name>").description("register a NEW tenant task \u2014 becomes runnable via `runtime tenant control \u2026 run-task` on its declared dev/rc/main stages. Body shape {service,command[],stages[],timeoutSeconds?,artifact?}; required artifact tasks must carry {artifact} in the command argv.").option("--service <name>", "service in the tenant runtime the task runs in (required unless --var carries it)").option("--command <argv...>", `task command as argv (repeatable; required unless --var carries it). An argv token that starts with "-" (python -m mod) is read as an option and the whole call fails to parse: declare such a command with --var '{"service":"<svc>","command":["python","-m","mod"],"stages":["dev"]}' instead (#7668)`).option("--stages <list>", "comma-separated dev,rc,main stages this task may run on (required unless --var carries it)").option("--timeout-seconds <n>", "optional wall-clock ceiling 1..3600").option("--artifact <none|required>", "artifact contract (default none; required tasks must contain {artifact} in argv)").option("--var <json>", "full task object {service,command[],stages[],timeoutSeconds?,artifact?} \u2014 use instead of the individual flags. A task carrying secretOutput:{path,vault:{repo,key},overwrite?} is master-only; declare that via `org project set --var tenantTasks=...`").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (name, o) => {
|
|
52478
52797
|
const cfg = await loadConfig();
|
|
52479
52798
|
let target;
|
|
52480
52799
|
try {
|
|
@@ -52722,6 +53041,28 @@ access.command("role [repo]").description("D14 train authority for a repo (serve
|
|
|
52722
53041
|
console.log(`${a.repo}: @${a.login} is ${a.role} \u2014 train ${a.train ? "AUTHORIZED" : "not authorized"}${a.hubTrainMasterOnly ? " (Hub train is master-only)" : ""}`);
|
|
52723
53042
|
if (!a.train) process.exitCode = 1;
|
|
52724
53043
|
});
|
|
53044
|
+
access.command("sync [repo]").description("rebuild the stage allowlist (e.g. MM-Work MMW_STAGE_ALLOWLIST) from the repo's developer/admin collaborators via company-emails.json and write it to the vault for each stage the project opted into (registry stageAllowlist); run it after every developer/admin change (#7678)").option("--dry-run", "print what would change without writing").option("--json", "machine-readable output").action(async (repoArg) => {
|
|
53045
|
+
const o = { json: rawFlag("--json"), dryRun: rawFlag("--dry-run") };
|
|
53046
|
+
const repo = repoArg ?? await resolveRepo();
|
|
53047
|
+
if (!repo) return fail("org access sync: pass <owner/repo> or run inside a repo checkout");
|
|
53048
|
+
const cfg = await loadConfig();
|
|
53049
|
+
if (!cfg.sagaApiUrl) return fail("org access sync: Hub API URL not configured");
|
|
53050
|
+
const projects = await fetchProjectsList(registryClientDeps(cfg));
|
|
53051
|
+
if (!projects) return failGraceful("org access sync: could not read the org project registry \u2014 nothing written");
|
|
53052
|
+
const project2 = projects.find((p) => (p.repos ?? []).some((r) => r.toLowerCase() === repo.toLowerCase())) ?? null;
|
|
53053
|
+
const config = stageAllowlistConfig(project2);
|
|
53054
|
+
if (!config) return failGraceful(`org access sync: ${repo} has no stageAllowlist opt-in \u2014 set it with \`mmi-cli oracle org project set ${repo} --var 'stageAllowlist={"key":"<KEY>","stages":["dev"]}'\``);
|
|
53055
|
+
const secretsDeps = makeSecretsDeps(cfg);
|
|
53056
|
+
const client = defaultGitHubClient();
|
|
53057
|
+
const result = await syncStageAllowlist({
|
|
53058
|
+
listCollaborators: (r) => listRepoCollaborators(client, r),
|
|
53059
|
+
readValue: (r, coordinate) => fetchSecretValue(secretsDeps, coordinate, { repo: r }),
|
|
53060
|
+
// The value is written in-process from here; putSecret confirms by NAME only.
|
|
53061
|
+
writeValue: (r, coordinate, value) => putSecret(secretsDeps, coordinate, value, { repo: r })
|
|
53062
|
+
}, repo, config, bundledCompanyEmails(), { dryRun: o.dryRun });
|
|
53063
|
+
console.log(o.json ? JSON.stringify(result, null, 2) : renderStageAllowlist(result, o.dryRun));
|
|
53064
|
+
if (!result.ok) process.exitCode = 1;
|
|
53065
|
+
});
|
|
52725
53066
|
access.command("audit").description("audit collaborator roles + train-branch push allowlists vs the locked state; read-only, emits gh remediation, never applies").option("--json", "machine-readable output").option("--repo <owner/repo>", "audit a single repo instead of the whole org").option("--class <class>", "repo class for --repo (deployable | content)", "deployable").action(async () => {
|
|
52726
53067
|
const o = { json: rawFlag("--json"), repo: rawValue("--repo", ""), class: rawValue("--class", "deployable") };
|
|
52727
53068
|
const deps = { client: defaultGitHubClient() };
|
package/package.json
CHANGED