@mutmutco/cli 3.100.0 → 3.101.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +120 -19
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -12168,6 +12168,16 @@ async function auditRepoCi(repo, deps) {
|
|
|
12168
12168
|
});
|
|
12169
12169
|
}
|
|
12170
12170
|
}
|
|
12171
|
+
if (repoClass === "deployable") {
|
|
12172
|
+
const exemptReason = meta?.ciExemptReason?.trim();
|
|
12173
|
+
const declaredExempt = statusChecks.size === 0 && !!exemptReason;
|
|
12174
|
+
checks.push({
|
|
12175
|
+
ok: statusChecks.size > 0 || declaredExempt,
|
|
12176
|
+
label: declaredExempt ? "deployable repo declared CI-exempt, not required to gate (#4265)" : "deployable repo requires at least one status check (#4260)",
|
|
12177
|
+
detail: declaredExempt ? `declared-exempt: ${exemptReason}` : statusChecks.size === 0 ? "zero required contexts on an active branch ruleset \u2014 any PR, including an automation seed PR, merges on an empty gate" : void 0,
|
|
12178
|
+
remediation: declaredExempt || statusChecks.size > 0 ? void 0 : `Determine this repo's correct gate context from its own PR-triggered workflow, set it via mmi-cli org project set ${repo} --var requiredChecks=[...], then mmi-cli ci reconcile --repo ${repo} --apply, or, if this repo genuinely has no CI surface, declare it via mmi-cli org project set ${repo} --var ciExemptReason="<why>" (#4265)`
|
|
12179
|
+
});
|
|
12180
|
+
}
|
|
12171
12181
|
if (deployableGated || repoClass === "hub") {
|
|
12172
12182
|
const gate = await readDefaultBranchGate(repo, deps.client, baseBranch, Date.now(), gateWorkflowFiles(prWorkflowPaths));
|
|
12173
12183
|
checks.push({
|
|
@@ -13865,6 +13875,12 @@ async function postSchedulesMode(slug, mode, deps) {
|
|
|
13865
13875
|
async function postSchedulesRun(id, deps) {
|
|
13866
13876
|
return postJson("/schedules/run", { id }, deps, "POST", { noRetry: true });
|
|
13867
13877
|
}
|
|
13878
|
+
async function postSchedulesPark(id, reason, deps) {
|
|
13879
|
+
return postJson("/schedules/park", { id, reason }, deps);
|
|
13880
|
+
}
|
|
13881
|
+
async function postSchedulesUnpark(id, deps) {
|
|
13882
|
+
return postJson("/schedules/unpark", { id }, deps);
|
|
13883
|
+
}
|
|
13868
13884
|
async function tenantControl(payload, deps) {
|
|
13869
13885
|
return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
|
|
13870
13886
|
}
|
|
@@ -20923,7 +20939,7 @@ function authorizeBodyHasMismatch(body) {
|
|
|
20923
20939
|
}
|
|
20924
20940
|
|
|
20925
20941
|
// src/project-set.ts
|
|
20926
|
-
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate", "seedCanary"];
|
|
20942
|
+
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "ciExemptReason", "gate", "seedCanary"];
|
|
20927
20943
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
20928
20944
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
20929
20945
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -21218,6 +21234,11 @@ function parseRequiredChecksVar(raw) {
|
|
|
21218
21234
|
}
|
|
21219
21235
|
return parsed.map((c) => c.trim());
|
|
21220
21236
|
}
|
|
21237
|
+
function parseCiExemptReasonVar(raw) {
|
|
21238
|
+
const trimmed = raw.trim();
|
|
21239
|
+
if (!trimmed) throw new Error("org project set: ciExemptReason must be a non-empty reason (or use --unset ciExemptReason to clear it)");
|
|
21240
|
+
return trimmed;
|
|
21241
|
+
}
|
|
21221
21242
|
function parseGateVar(raw) {
|
|
21222
21243
|
let parsed;
|
|
21223
21244
|
try {
|
|
@@ -21270,6 +21291,7 @@ var SETTABLE_VAR_KEYS = [
|
|
|
21270
21291
|
"portRange",
|
|
21271
21292
|
"ci",
|
|
21272
21293
|
"requiredChecks",
|
|
21294
|
+
"ciExemptReason",
|
|
21273
21295
|
"gate",
|
|
21274
21296
|
"secrets",
|
|
21275
21297
|
"seedCanary"
|
|
@@ -21296,6 +21318,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
21296
21318
|
portRange: "JSON {start,end} or [start,end]",
|
|
21297
21319
|
ci: "none \u2014 declare intentional no-ci",
|
|
21298
21320
|
requiredChecks: 'JSON array, e.g. ["gate"] or [] for no-ci',
|
|
21321
|
+
ciExemptReason: "non-empty string \u2014 declares this deployable repo has no CI surface (#4265)",
|
|
21299
21322
|
gate: "JSON {runtime,cmd,workdir,cacheDepPath,pyVersion}"
|
|
21300
21323
|
};
|
|
21301
21324
|
function settableVarHelp() {
|
|
@@ -21393,6 +21416,8 @@ function buildProjectSetPatch(input) {
|
|
|
21393
21416
|
patch[key] = raw;
|
|
21394
21417
|
} else if (key === "requiredChecks") {
|
|
21395
21418
|
patch[key] = parseRequiredChecksVar(raw);
|
|
21419
|
+
} else if (key === "ciExemptReason") {
|
|
21420
|
+
patch[key] = parseCiExemptReasonVar(raw);
|
|
21396
21421
|
} else if (key === "gate") {
|
|
21397
21422
|
patch[key] = parseGateVar(raw);
|
|
21398
21423
|
} else {
|
|
@@ -22730,6 +22755,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
22730
22755
|
const liveByName = new Map(liveGithub.map((e) => [e.name, e]));
|
|
22731
22756
|
const registryById = new Map(registryGithub.map((r) => [r.id, r]));
|
|
22732
22757
|
const drifts = [];
|
|
22758
|
+
const parked = [];
|
|
22733
22759
|
for (const e of liveGithub) {
|
|
22734
22760
|
if (!registryById.has(e.name)) {
|
|
22735
22761
|
drifts.push({
|
|
@@ -22742,6 +22768,10 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
22742
22768
|
}
|
|
22743
22769
|
}
|
|
22744
22770
|
for (const r of registryGithub) {
|
|
22771
|
+
if (r.parkedReason) {
|
|
22772
|
+
parked.push({ id: r.id, executor: r.executor, repo: r.repo, reason: r.parkedReason });
|
|
22773
|
+
continue;
|
|
22774
|
+
}
|
|
22745
22775
|
const live = liveByName.get(r.id);
|
|
22746
22776
|
if (live) {
|
|
22747
22777
|
if (cadenceStale(r.cadence, live.cadence)) {
|
|
@@ -22763,7 +22793,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
22763
22793
|
name: r.id,
|
|
22764
22794
|
executor: r.executor || "github-actions",
|
|
22765
22795
|
detail: "workflow file present but DISABLED in GitHub Actions \u2014 the dispatcher cannot run it",
|
|
22766
|
-
remedy: `\`gh workflow enable\` it in ${r.repo} to re-arm
|
|
22796
|
+
remedy: `\`gh workflow enable\` it in ${r.repo} to re-arm, or if this is a deliberate hold run \`mmi-cli org schedules park ${r.id} --reason <ref>\` (#4257) so it stops reading as drift \u2014 never prune a lane parked on purpose`
|
|
22767
22797
|
});
|
|
22768
22798
|
}
|
|
22769
22799
|
continue;
|
|
@@ -22778,7 +22808,10 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
22778
22808
|
});
|
|
22779
22809
|
}
|
|
22780
22810
|
}
|
|
22781
|
-
return
|
|
22811
|
+
return {
|
|
22812
|
+
drifts: drifts.sort((a, b) => a.class.localeCompare(b.class) || a.name.localeCompare(b.name)),
|
|
22813
|
+
parked: parked.sort((a, b) => a.id.localeCompare(b.id))
|
|
22814
|
+
};
|
|
22782
22815
|
}
|
|
22783
22816
|
function suppressSelfManagedDrifts(drifts, selfManagedRepos) {
|
|
22784
22817
|
return drifts.filter((d) => {
|
|
@@ -22789,6 +22822,9 @@ function suppressSelfManagedDrifts(drifts, selfManagedRepos) {
|
|
|
22789
22822
|
function renderDrift(d) {
|
|
22790
22823
|
return `${d.class}: ${d.name} (${d.executor}) \u2014 ${d.detail}; remedy: ${d.remedy}`;
|
|
22791
22824
|
}
|
|
22825
|
+
function renderParked(p) {
|
|
22826
|
+
return `parked (${p.reason}): ${p.id}`;
|
|
22827
|
+
}
|
|
22792
22828
|
function strayCronDrift(name) {
|
|
22793
22829
|
return {
|
|
22794
22830
|
class: "stray-cron",
|
|
@@ -22824,12 +22860,17 @@ function unlauncheredLlmDrifts(entries) {
|
|
|
22824
22860
|
}
|
|
22825
22861
|
var HARBOUR_ARM_GRACE_MS = 30 * 60 * 1e3;
|
|
22826
22862
|
function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
|
|
22827
|
-
if (!opts.schedulerRead) return [];
|
|
22863
|
+
if (!opts.schedulerRead) return { drifts: [], parked: [] };
|
|
22828
22864
|
const now = opts.now ?? Date.now();
|
|
22829
22865
|
const armed = new Set(liveEntries.map((e) => e.scheduleId).filter((id) => Boolean(id)));
|
|
22830
22866
|
const drifts = [];
|
|
22867
|
+
const parked = [];
|
|
22831
22868
|
for (const row of registry2) {
|
|
22832
22869
|
if (row.executor === "github-actions") continue;
|
|
22870
|
+
if (row.parkedReason) {
|
|
22871
|
+
parked.push({ id: row.id, executor: row.executor, repo: row.repo, reason: row.parkedReason });
|
|
22872
|
+
continue;
|
|
22873
|
+
}
|
|
22833
22874
|
if (armed.has(row.id)) continue;
|
|
22834
22875
|
const stamped = row.updatedAt ? Date.parse(row.updatedAt) : NaN;
|
|
22835
22876
|
if (Number.isFinite(stamped) && now - stamped < HARBOUR_ARM_GRACE_MS) continue;
|
|
@@ -22841,25 +22882,36 @@ function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
|
|
|
22841
22882
|
remedy: "check the fleet-clock reconciler tick; re-run `mmi-cli org schedules register` for the repo and verify with `mmi-cli org schedules`"
|
|
22842
22883
|
});
|
|
22843
22884
|
}
|
|
22844
|
-
return
|
|
22885
|
+
return {
|
|
22886
|
+
drifts: drifts.sort((a, b) => a.name.localeCompare(b.name)),
|
|
22887
|
+
parked: parked.sort((a, b) => a.id.localeCompare(b.id))
|
|
22888
|
+
};
|
|
22845
22889
|
}
|
|
22846
22890
|
function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}, disabledWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
22847
22891
|
if (registry2 === null) {
|
|
22848
22892
|
return {
|
|
22849
22893
|
reconciliation: [],
|
|
22850
22894
|
driftLines: [],
|
|
22895
|
+
parked: [],
|
|
22896
|
+
parkedLines: [],
|
|
22851
22897
|
incomplete: ["registry: could not read GET /schedules/list \u2014 reconciliation skipped (Hub API unreachable or not authenticated)"]
|
|
22852
22898
|
};
|
|
22853
22899
|
}
|
|
22854
22900
|
const live = githubEntries2.filter((e) => e.executor === "github-actions");
|
|
22855
|
-
const
|
|
22856
|
-
|
|
22857
|
-
|
|
22858
|
-
|
|
22859
|
-
|
|
22860
|
-
|
|
22861
|
-
];
|
|
22862
|
-
return {
|
|
22901
|
+
const githubResult = reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames, disabledWorkflowNames);
|
|
22902
|
+
const harbourResult = registeredButUnarmedDrifts(registry2, harbour.awsEntries ?? [], {
|
|
22903
|
+
schedulerRead: Boolean(harbour.schedulerRead),
|
|
22904
|
+
now: harbour.now
|
|
22905
|
+
});
|
|
22906
|
+
const drifts = [...githubResult.drifts, ...harbourResult.drifts];
|
|
22907
|
+
const parked = [...githubResult.parked, ...harbourResult.parked].sort((a, b) => a.id.localeCompare(b.id));
|
|
22908
|
+
return {
|
|
22909
|
+
reconciliation: drifts,
|
|
22910
|
+
driftLines: drifts.map(renderDrift),
|
|
22911
|
+
parked,
|
|
22912
|
+
parkedLines: parked.map(renderParked),
|
|
22913
|
+
incomplete: []
|
|
22914
|
+
};
|
|
22863
22915
|
}
|
|
22864
22916
|
|
|
22865
22917
|
// src/schedules-commands.ts
|
|
@@ -23023,7 +23075,9 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
|
|
|
23023
23075
|
entries: sortEntries([...gh.entries, ...aws.entries, ...DECLARED_ENTRIES]),
|
|
23024
23076
|
incomplete: [...gh.incomplete, ...aws.incomplete, ...recon.incomplete],
|
|
23025
23077
|
drift: driftLines,
|
|
23026
|
-
reconciliation
|
|
23078
|
+
reconciliation,
|
|
23079
|
+
parked: recon.parked,
|
|
23080
|
+
parkedLines: recon.parkedLines
|
|
23027
23081
|
};
|
|
23028
23082
|
}
|
|
23029
23083
|
async function defaultProjectsRead() {
|
|
@@ -23036,10 +23090,13 @@ function warnIncomplete2(incomplete) {
|
|
|
23036
23090
|
function reportDrift(drift) {
|
|
23037
23091
|
for (const d of drift) console.error(`org schedules: DRIFT \u2014 ${d}`);
|
|
23038
23092
|
}
|
|
23093
|
+
function reportParked(parked) {
|
|
23094
|
+
for (const p of parked) console.log(`org schedules: ${p}`);
|
|
23095
|
+
}
|
|
23039
23096
|
function registerSchedulesCommands(program3) {
|
|
23040
23097
|
const schedules = program3.command("schedules").description("the org schedules notebook \u2014 every armed cron, timer, and scheduled LLM lane, resolved live (jerv side lives in jerv-cli schedules)").option("--json", "machine-readable output (consumed by jerv-cli schedules --all)").option("--doc <path>", "optional snapshot only \u2014 splice inventory between schedules:inventory markers; live `org schedules` / --json remains SSOT (Hub#4120)").action(async (o) => {
|
|
23041
23098
|
try {
|
|
23042
|
-
const { entries, incomplete, drift, reconciliation } = await fetchNotebook();
|
|
23099
|
+
const { entries, incomplete, drift, reconciliation, parked, parkedLines } = await fetchNotebook();
|
|
23043
23100
|
if (o.doc) {
|
|
23044
23101
|
if (incomplete.length) {
|
|
23045
23102
|
warnIncomplete2(incomplete);
|
|
@@ -23055,9 +23112,10 @@ function registerSchedulesCommands(program3) {
|
|
|
23055
23112
|
}
|
|
23056
23113
|
if (o.json) {
|
|
23057
23114
|
const now = /* @__PURE__ */ new Date();
|
|
23058
|
-
console.log(JSON.stringify({ org: entries.map((e) => withTickState(e, now)), incomplete, drift, reconciliation }, null, 2));
|
|
23115
|
+
console.log(JSON.stringify({ org: entries.map((e) => withTickState(e, now)), incomplete, drift, reconciliation, parked }, null, 2));
|
|
23059
23116
|
} else {
|
|
23060
23117
|
console.log(formatSchedulesTable(entries));
|
|
23118
|
+
reportParked(parkedLines);
|
|
23061
23119
|
reportDrift(drift);
|
|
23062
23120
|
warnIncomplete2(incomplete);
|
|
23063
23121
|
}
|
|
@@ -23078,6 +23136,30 @@ function registerSchedulesCommands(program3) {
|
|
|
23078
23136
|
await failGraceful(e.message);
|
|
23079
23137
|
}
|
|
23080
23138
|
});
|
|
23139
|
+
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) => {
|
|
23140
|
+
try {
|
|
23141
|
+
const res = await postSchedulesPark(scheduleId, o.reason, registryClientDeps(await loadConfig()));
|
|
23142
|
+
if (!res.ok) {
|
|
23143
|
+
await failGraceful(`schedules park: HTTP ${res.status} \u2014 ${JSON.stringify(res.body ?? res.error ?? "failed")}`);
|
|
23144
|
+
return;
|
|
23145
|
+
}
|
|
23146
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
23147
|
+
} catch (e) {
|
|
23148
|
+
await failGraceful(e.message);
|
|
23149
|
+
}
|
|
23150
|
+
});
|
|
23151
|
+
schedules.command("unpark <schedule-id>").description("clear a deliberate hold on a SCHEDULE# row \u2014 reconciliation resumes normally (#4257)").action(async (scheduleId) => {
|
|
23152
|
+
try {
|
|
23153
|
+
const res = await postSchedulesUnpark(scheduleId, registryClientDeps(await loadConfig()));
|
|
23154
|
+
if (!res.ok) {
|
|
23155
|
+
await failGraceful(`schedules unpark: HTTP ${res.status} \u2014 ${JSON.stringify(res.body ?? res.error ?? "failed")}`);
|
|
23156
|
+
return;
|
|
23157
|
+
}
|
|
23158
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
23159
|
+
} catch (e) {
|
|
23160
|
+
await failGraceful(e.message);
|
|
23161
|
+
}
|
|
23162
|
+
});
|
|
23081
23163
|
schedules.command("mode <mode>").description("fleet schedules doctrine for a repo: 'managed' (default \u2014 Hub registry + drift enforcement) or 'self-managed' (opt out; the repo owns its crons). Project-admin self-serve (#3228).").option("--repo <name>", "bare repo name (defaults to the origin remote basename)").action(async (mode, o) => {
|
|
23082
23164
|
try {
|
|
23083
23165
|
if (mode !== "managed" && mode !== "self-managed") {
|
|
@@ -25210,9 +25292,28 @@ LIVE apply to ${repo}:
|
|
|
25210
25292
|
}
|
|
25211
25293
|
const bySlugMeta = new Map(projects.flatMap((p) => (p.repos ?? []).map((r) => [(r.includes("/") ? r : `mutmutco/${r}`).toLowerCase(), p])));
|
|
25212
25294
|
const classOf = (repo) => bySlugMeta.get(repo.toLowerCase())?.class ?? "deployable";
|
|
25213
|
-
const
|
|
25214
|
-
const
|
|
25215
|
-
|
|
25295
|
+
const inClassRepos = rosterRepos2.filter((repo) => seed.classes.includes(classOf(repo)));
|
|
25296
|
+
const classPriority = [.../* @__PURE__ */ new Set(["deployable", ...seed.classes])].filter((c) => seed.classes.includes(c));
|
|
25297
|
+
let canaryProject = null;
|
|
25298
|
+
for (const cls of classPriority) {
|
|
25299
|
+
const inClass = projects.filter((p) => p.seedCanary === true && (p.class ?? "deployable") === cls);
|
|
25300
|
+
if (inClass.length === 0) continue;
|
|
25301
|
+
if (inClass.length > 1) {
|
|
25302
|
+
const slugs = inClass.map((p) => (p.repos ?? [])[0]?.split("/").pop() ?? "?").join(", ");
|
|
25303
|
+
return fail(
|
|
25304
|
+
`bootstrap propagate: target '${seed.target}' (classes [${seed.classes.join(", ")}]) matches ${inClass.length} seedCanary:true repos in class '${cls}' (${slugs}) \u2014 exactly one seedCanary:true repo per class is required`
|
|
25305
|
+
);
|
|
25306
|
+
}
|
|
25307
|
+
canaryProject = inClass[0];
|
|
25308
|
+
break;
|
|
25309
|
+
}
|
|
25310
|
+
if (!canaryProject) {
|
|
25311
|
+
return fail(
|
|
25312
|
+
`bootstrap propagate: target '${seed.target}' needs classes [${seed.classes.join(", ")}] but no registry repo with seedCanary:true carries a matching class \u2014 set seedCanary:true on an in-class repo (mmi-cli org project set <slug> --var seedCanary=true)`
|
|
25313
|
+
);
|
|
25314
|
+
}
|
|
25315
|
+
const canarySlug = (canaryProject.repos ?? [])[0]?.split("/").pop()?.toLowerCase() ?? null;
|
|
25316
|
+
const repos = inClassRepos.map((repo) => {
|
|
25216
25317
|
const slug = repo.split("/").pop().toLowerCase();
|
|
25217
25318
|
const waiver = seed.waivers?.[slug];
|
|
25218
25319
|
return { repo, slug, waiver };
|
package/package.json
CHANGED