@mutmutco/cli 3.75.0 → 3.77.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 +1290 -283
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3416,7 +3416,7 @@ var program = new Command();
|
|
|
3416
3416
|
|
|
3417
3417
|
// src/index.ts
|
|
3418
3418
|
var import_promises10 = require("node:fs/promises");
|
|
3419
|
-
var
|
|
3419
|
+
var import_node_fs34 = require("node:fs");
|
|
3420
3420
|
var import_node_child_process15 = require("node:child_process");
|
|
3421
3421
|
|
|
3422
3422
|
// src/cli-shared.ts
|
|
@@ -3579,7 +3579,12 @@ var ERROR_CODES = {
|
|
|
3579
3579
|
/** Missing / rejected credentials on a path that needs auth. */
|
|
3580
3580
|
ERR_NO_AUTH: "ERR_NO_AUTH",
|
|
3581
3581
|
/** The operation partially succeeded (some units done, some failed). */
|
|
3582
|
-
ERR_PARTIAL: "ERR_PARTIAL"
|
|
3582
|
+
ERR_PARTIAL: "ERR_PARTIAL",
|
|
3583
|
+
/** The request was well-formed and every referent resolved, but the target's CURRENT STATE forbids the
|
|
3584
|
+
* mutation (HTTP 409 semantics) — e.g. reparenting an issue that already has a parent. Deliberately not
|
|
3585
|
+
* one code per API constraint: retry is futile until the named state is changed, and that is the fact a
|
|
3586
|
+
* caller has to act on, whichever rule produced it. */
|
|
3587
|
+
ERR_STATE_CONFLICT: "ERR_STATE_CONFLICT"
|
|
3583
3588
|
};
|
|
3584
3589
|
var ERROR_CODE_REFERENCE = [
|
|
3585
3590
|
{
|
|
@@ -3621,6 +3626,11 @@ var ERROR_CODE_REFERENCE = [
|
|
|
3621
3626
|
code: ERROR_CODES.ERR_PARTIAL,
|
|
3622
3627
|
meaning: "A batch operation completed some units and failed others.",
|
|
3623
3628
|
typical_fix: "Read the per-unit results, fix the failed inputs, and rerun only the failed units."
|
|
3629
|
+
},
|
|
3630
|
+
{
|
|
3631
|
+
code: ERROR_CODES.ERR_STATE_CONFLICT,
|
|
3632
|
+
meaning: "The request was valid and every referent resolved, but the target resource's current state forbids it.",
|
|
3633
|
+
typical_fix: "A plain retry fails identically. Read the state field the envelope names (e.g. `current_parent`), change that state deliberately with the command that owns it, then retry."
|
|
3624
3634
|
}
|
|
3625
3635
|
];
|
|
3626
3636
|
function buildErrorEnvelope(message, payload) {
|
|
@@ -3629,6 +3639,7 @@ function buildErrorEnvelope(message, payload) {
|
|
|
3629
3639
|
if (payload.expected !== void 0) env.expected = payload.expected;
|
|
3630
3640
|
if (payload.did_you_mean !== void 0) env.did_you_mean = payload.did_you_mean;
|
|
3631
3641
|
if (payload.corrected_command !== void 0) env.corrected_command = payload.corrected_command;
|
|
3642
|
+
if (payload.current_parent !== void 0) env.current_parent = payload.current_parent;
|
|
3632
3643
|
return env;
|
|
3633
3644
|
}
|
|
3634
3645
|
function formatErrorEnvelope(message, payload) {
|
|
@@ -4067,11 +4078,18 @@ function fail(msg, payload) {
|
|
|
4067
4078
|
}
|
|
4068
4079
|
hardExit(1);
|
|
4069
4080
|
}
|
|
4070
|
-
function
|
|
4081
|
+
async function failGracefulEnvelope(msg, payload) {
|
|
4082
|
+
if (payload && argvWantsMachineFailure()) {
|
|
4083
|
+
console.error(formatErrorEnvelope(msg, payload));
|
|
4084
|
+
return cleanExit(1);
|
|
4085
|
+
}
|
|
4086
|
+
return failGraceful(msg);
|
|
4087
|
+
}
|
|
4088
|
+
async function planMutation(opts, args, commandName, planFn) {
|
|
4071
4089
|
const validateOnly = opts.validateOnly === true;
|
|
4072
4090
|
const dryRun = opts.dryRun === true;
|
|
4073
4091
|
if (!validateOnly && !dryRun) return null;
|
|
4074
|
-
const planned = planFn ? planFn(opts, args) : { command: commandName, args };
|
|
4092
|
+
const planned = planFn ? await planFn(opts, args) : { command: commandName, args };
|
|
4075
4093
|
return validateOnly ? { ok: true, planned } : { dry_run: true, planned };
|
|
4076
4094
|
}
|
|
4077
4095
|
function commandPath(cmd) {
|
|
@@ -4097,11 +4115,11 @@ function mutating(cmd, planFn) {
|
|
|
4097
4115
|
cmd.option("--dry-run", "resolve + validate, print the planned action as JSON, and exit without writing");
|
|
4098
4116
|
cmd.option("--validate-only", "validate flags/enums/refs, print {ok,planned} or the C2 error envelope, and exit without writing");
|
|
4099
4117
|
const registerAction = cmd.action.bind(cmd);
|
|
4100
|
-
cmd.action = ((handler) => registerAction((...actionArgs) => {
|
|
4118
|
+
cmd.action = ((handler) => registerAction(async (...actionArgs) => {
|
|
4101
4119
|
const command = actionArgs[actionArgs.length - 1];
|
|
4102
4120
|
const opts = command.opts();
|
|
4103
4121
|
const positionals = actionArgs.slice(0, -2);
|
|
4104
|
-
const out = planMutation(opts, positionals, commandPath(command), planFn);
|
|
4122
|
+
const out = await planMutation(opts, positionals, commandPath(command), planFn);
|
|
4105
4123
|
if (out) {
|
|
4106
4124
|
console.log(JSON.stringify(out));
|
|
4107
4125
|
return;
|
|
@@ -4128,6 +4146,97 @@ function printLine(value) {
|
|
|
4128
4146
|
`);
|
|
4129
4147
|
}
|
|
4130
4148
|
|
|
4149
|
+
// src/issue-surface.ts
|
|
4150
|
+
var SURFACE_PREFIX = "surface:";
|
|
4151
|
+
var SURFACE_READ_TIMEOUT_MS = 1e4;
|
|
4152
|
+
function labelsCarrySurface(labels) {
|
|
4153
|
+
return (labels ?? []).some((l) => l.trim().toLowerCase().startsWith(SURFACE_PREFIX));
|
|
4154
|
+
}
|
|
4155
|
+
function surfaceLabel(value) {
|
|
4156
|
+
const v = value.trim();
|
|
4157
|
+
return v.toLowerCase().startsWith(SURFACE_PREFIX) ? v : `${SURFACE_PREFIX}${v}`;
|
|
4158
|
+
}
|
|
4159
|
+
async function readRepoSurfaceLabels(repo, deps = {}) {
|
|
4160
|
+
const run = deps.run ?? execFileP2;
|
|
4161
|
+
try {
|
|
4162
|
+
const { stdout } = await run(
|
|
4163
|
+
"gh",
|
|
4164
|
+
["label", "list", "--repo", repo, "--limit", "1000", "--json", "name"],
|
|
4165
|
+
{ timeout: SURFACE_READ_TIMEOUT_MS }
|
|
4166
|
+
);
|
|
4167
|
+
const parsed = JSON.parse(stdout);
|
|
4168
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
4169
|
+
return parsed.map((l) => l.name).filter((n) => typeof n === "string" && n.toLowerCase().startsWith(SURFACE_PREFIX));
|
|
4170
|
+
} catch {
|
|
4171
|
+
return void 0;
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
async function checkSurfaceRequirement(input, deps = {}) {
|
|
4175
|
+
if (input.waiver) {
|
|
4176
|
+
const reason = input.waiver.reason.trim();
|
|
4177
|
+
if (!reason) {
|
|
4178
|
+
return {
|
|
4179
|
+
enforcing: false,
|
|
4180
|
+
refusal: {
|
|
4181
|
+
message: `${input.command ?? "issue create"}: a surface waiver without a reason is not a ruling \u2014 declare WHY this filing is genuinely surface-less, or drop the waiver and let the preflight run`,
|
|
4182
|
+
payload: {
|
|
4183
|
+
code: ERROR_CODES.ERR_MISSING_FLAG,
|
|
4184
|
+
offending_flag: "waiver.reason",
|
|
4185
|
+
expected: ["a non-empty waiver reason"]
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
return {
|
|
4191
|
+
enforcing: false,
|
|
4192
|
+
waiver: { applied: true, reason }
|
|
4193
|
+
};
|
|
4194
|
+
}
|
|
4195
|
+
const read = deps.read ?? readRepoSurfaceLabels;
|
|
4196
|
+
const known = await read(input.repo, { run: deps.run });
|
|
4197
|
+
if (known === void 0) {
|
|
4198
|
+
return {
|
|
4199
|
+
enforcing: false,
|
|
4200
|
+
warn: `warning: could not read ${input.repo}'s labels, so the surface-label requirement was not checked; if that board enforces one surface:* label per open issue, add one with \`mmi-cli issue edit <n> --add-label surface:<value>\``
|
|
4201
|
+
};
|
|
4202
|
+
}
|
|
4203
|
+
if (known.length === 0) return { enforcing: false };
|
|
4204
|
+
if (labelsCarrySurface(input.labels)) return { enforcing: true };
|
|
4205
|
+
const command = input.command ?? "issue create";
|
|
4206
|
+
const where = input.rowLabel ? `${input.rowLabel}: ` : "";
|
|
4207
|
+
return {
|
|
4208
|
+
enforcing: true,
|
|
4209
|
+
refusal: {
|
|
4210
|
+
message: `${command}: ${where}${input.repo} requires every open issue to carry exactly one surface:* label, and this one sets none \u2014 filing it unlabeled reds the board check on every open PR in that repo. Pass --surface <value>, or --no-surface if this filing is genuinely exempt`,
|
|
4211
|
+
payload: {
|
|
4212
|
+
code: ERROR_CODES.ERR_MISSING_FLAG,
|
|
4213
|
+
offending_flag: "--surface",
|
|
4214
|
+
// Advisory, not a closed enum: the board rule is "exactly one surface:* label", whatever its value.
|
|
4215
|
+
expected: [...known].sort()
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
};
|
|
4219
|
+
}
|
|
4220
|
+
function conflictingSurfaceInputs(surfaceFlag, labels) {
|
|
4221
|
+
if (!surfaceFlag) return void 0;
|
|
4222
|
+
const wanted = surfaceLabel(surfaceFlag).toLowerCase();
|
|
4223
|
+
const fromLabels = (labels ?? []).map((l) => l.trim()).filter((l) => l.toLowerCase().startsWith(SURFACE_PREFIX));
|
|
4224
|
+
const disagreeing = fromLabels.filter((l) => l.toLowerCase() !== wanted);
|
|
4225
|
+
if (!disagreeing.length) return void 0;
|
|
4226
|
+
return {
|
|
4227
|
+
message: `issue create: --surface ${surfaceLabel(surfaceFlag)} contradicts --label ${disagreeing.join(", ")} \u2014 an issue carries exactly one surface, so name it once`,
|
|
4228
|
+
payload: {
|
|
4229
|
+
code: ERROR_CODES.ERR_CONFLICTING_FLAGS,
|
|
4230
|
+
offending_flag: "--surface",
|
|
4231
|
+
expected: [surfaceLabel(surfaceFlag), ...disagreeing]
|
|
4232
|
+
}
|
|
4233
|
+
};
|
|
4234
|
+
}
|
|
4235
|
+
async function surfaceLabelApplies(repo, deps = {}) {
|
|
4236
|
+
const known = await (deps.read ?? readRepoSurfaceLabels)(repo, { run: deps.run });
|
|
4237
|
+
return (known?.length ?? 0) > 0;
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4131
4240
|
// src/session-start.ts
|
|
4132
4241
|
var import_node_fs8 = require("node:fs");
|
|
4133
4242
|
var import_node_path6 = require("node:path");
|
|
@@ -6378,10 +6487,12 @@ async function primaryCheckoutRootOf(git2) {
|
|
|
6378
6487
|
return void 0;
|
|
6379
6488
|
}
|
|
6380
6489
|
}
|
|
6490
|
+
var SHA_LIKE_RE = /^[0-9a-f]{7,40}$/i;
|
|
6381
6491
|
function resolveWorktreeBase(from, remote) {
|
|
6382
6492
|
const remotePrefix = `${remote}/`;
|
|
6383
|
-
|
|
6384
|
-
return { base: from
|
|
6493
|
+
if (from.startsWith(remotePrefix)) return { base: from, fetchBranch: from.slice(remotePrefix.length) };
|
|
6494
|
+
if (SHA_LIKE_RE.test(from)) return { base: from };
|
|
6495
|
+
return { base: from, fetchBranch: from, preferRemote: `${remotePrefix}${from}` };
|
|
6385
6496
|
}
|
|
6386
6497
|
var GIT_CONFIG_LOCK_RE = /could not lock config file|unable to write upstream branch configuration/i;
|
|
6387
6498
|
function isGitConfigLockError(error) {
|
|
@@ -6491,7 +6602,7 @@ function commandLadderHint() {
|
|
|
6491
6602
|
}
|
|
6492
6603
|
|
|
6493
6604
|
// src/index.ts
|
|
6494
|
-
var
|
|
6605
|
+
var import_node_path33 = require("node:path");
|
|
6495
6606
|
|
|
6496
6607
|
// src/merge-ci-policy.ts
|
|
6497
6608
|
function resolveMergeCiPolicy(input) {
|
|
@@ -6776,22 +6887,80 @@ function patchRulesetRequiredContexts(body, contexts) {
|
|
|
6776
6887
|
function findProductRuleset(rulesets) {
|
|
6777
6888
|
return rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
|
|
6778
6889
|
}
|
|
6779
|
-
async function
|
|
6780
|
-
const
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6890
|
+
async function completedGateRuns(repo, client, query, perPage = 1, gateFiles = DEFAULT_GATE_FILES) {
|
|
6891
|
+
const merged = [];
|
|
6892
|
+
let lastError;
|
|
6893
|
+
let anyRead = false;
|
|
6894
|
+
for (const file of gateFiles) {
|
|
6895
|
+
try {
|
|
6896
|
+
const res = await client.rest(
|
|
6897
|
+
"GET",
|
|
6898
|
+
`repos/${repo}/actions/workflows/${encodeURIComponent(file)}/runs?status=completed&per_page=${perPage}${query}`,
|
|
6899
|
+
{ timeoutMs: 2e4 }
|
|
6900
|
+
);
|
|
6901
|
+
anyRead = true;
|
|
6902
|
+
merged.push(...res?.workflow_runs ?? []);
|
|
6903
|
+
} catch (e) {
|
|
6904
|
+
lastError = e;
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6907
|
+
if (!anyRead && lastError) throw lastError;
|
|
6908
|
+
return merged.sort((a, b) => Date.parse(b.updated_at ?? "") - Date.parse(a.updated_at ?? ""));
|
|
6786
6909
|
}
|
|
6787
|
-
|
|
6910
|
+
var GATE_STREAK_PAGE = 30;
|
|
6911
|
+
var DEFAULT_GATE_FILES = ["gate.yml"];
|
|
6912
|
+
async function readDefaultBranchGate(repo, client, baseBranch, now = Date.now(), gateFiles = DEFAULT_GATE_FILES) {
|
|
6913
|
+
const named = gateFiles.length === 1 ? gateFiles[0] : `[${gateFiles.join(", ")}]`;
|
|
6914
|
+
let runs;
|
|
6915
|
+
let onBaseBranch = true;
|
|
6788
6916
|
try {
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6917
|
+
runs = await completedGateRuns(repo, client, `&branch=${encodeURIComponent(baseBranch)}`, GATE_STREAK_PAGE, gateFiles);
|
|
6918
|
+
if (!runs.length) {
|
|
6919
|
+
runs = await completedGateRuns(repo, client, "", GATE_STREAK_PAGE, gateFiles);
|
|
6920
|
+
onBaseBranch = false;
|
|
6921
|
+
}
|
|
6922
|
+
} catch (e) {
|
|
6923
|
+
return { state: "unknown", detail: `${named} run history unreadable \u2014 ${e.message}` };
|
|
6924
|
+
}
|
|
6925
|
+
const latest = runs[0];
|
|
6926
|
+
if (!latest) {
|
|
6927
|
+
return {
|
|
6928
|
+
state: "unknown",
|
|
6929
|
+
detail: `no completed ${named} run on ${baseBranch} or any branch \u2014 a freshly seeded gate and a gate that cannot pass look the same from here`
|
|
6930
|
+
};
|
|
6931
|
+
}
|
|
6932
|
+
const where = onBaseBranch ? baseBranch : `${latest.head_branch ?? "any branch"} (no completed run on ${baseBranch})`;
|
|
6933
|
+
const common = {
|
|
6934
|
+
conclusion: latest.conclusion ?? void 0,
|
|
6935
|
+
branch: latest.head_branch ?? void 0,
|
|
6936
|
+
runUrl: latest.html_url ?? void 0
|
|
6937
|
+
};
|
|
6938
|
+
if (latest.conclusion === "success") {
|
|
6939
|
+
return { ...common, state: "green", detail: `latest completed ${named} run on ${where} succeeded at ${latest.updated_at ?? "unknown time"}` };
|
|
6794
6940
|
}
|
|
6941
|
+
let since = latest;
|
|
6942
|
+
let bounded = runs.length >= GATE_STREAK_PAGE;
|
|
6943
|
+
for (const run of runs) {
|
|
6944
|
+
if (run.conclusion === "success") {
|
|
6945
|
+
bounded = false;
|
|
6946
|
+
break;
|
|
6947
|
+
}
|
|
6948
|
+
since = run;
|
|
6949
|
+
}
|
|
6950
|
+
const sinceAt = since.updated_at ?? latest.updated_at ?? void 0;
|
|
6951
|
+
const redDays = sinceAt ? Math.max(0, Math.floor((now - Date.parse(sinceAt)) / 864e5)) : void 0;
|
|
6952
|
+
const age = redDays == null ? "unknown duration" : `${redDays} day${redDays === 1 ? "" : "s"}${bounded ? " or longer" : ""}`;
|
|
6953
|
+
return {
|
|
6954
|
+
...common,
|
|
6955
|
+
state: "red",
|
|
6956
|
+
redSince: sinceAt,
|
|
6957
|
+
redDays,
|
|
6958
|
+
redSinceBounded: bounded,
|
|
6959
|
+
detail: `gate.yml is failing on ${where} \u2014 ${latest.conclusion ?? "not successful"} since ${sinceAt ?? "an unknown time"} (${age})`
|
|
6960
|
+
};
|
|
6961
|
+
}
|
|
6962
|
+
async function gateIsProvenGreen(repo, client, baseBranch, gateFiles = DEFAULT_GATE_FILES) {
|
|
6963
|
+
return (await readDefaultBranchGate(repo, client, baseBranch, Date.now(), gateFiles)).state === "green";
|
|
6795
6964
|
}
|
|
6796
6965
|
async function activateProductRuleset(repo, rulesetBody, client, enforcement = "active") {
|
|
6797
6966
|
const body = { ...rulesetBody, enforcement };
|
|
@@ -6919,6 +7088,16 @@ function collectPullRequestWorkflowContexts(workflows) {
|
|
|
6919
7088
|
}
|
|
6920
7089
|
return [...contexts].sort((a, b) => a.localeCompare(b));
|
|
6921
7090
|
}
|
|
7091
|
+
function pathFilteredPullRequestWorkflows(workflows) {
|
|
7092
|
+
const filtered = [];
|
|
7093
|
+
for (const wf of workflows) {
|
|
7094
|
+
if (!workflowTriggersPullRequest(wf.body)) continue;
|
|
7095
|
+
const onBlock = extractOnBlock(wf.body);
|
|
7096
|
+
if (!onBlock) continue;
|
|
7097
|
+
if (/^\s*paths(-ignore)?\s*:/m.test(onBlock)) filtered.push(wf.path);
|
|
7098
|
+
}
|
|
7099
|
+
return filtered.sort((a, b) => a.localeCompare(b));
|
|
7100
|
+
}
|
|
6922
7101
|
function contextsMatchRuleset(required, emitted) {
|
|
6923
7102
|
if (required.size !== emitted.size) return false;
|
|
6924
7103
|
for (const c of required) if (!emitted.has(c)) return false;
|
|
@@ -7732,29 +7911,32 @@ function decideSeedDelivery(branchRules) {
|
|
|
7732
7911
|
if (gating.length) return { mode: "pr", reason: `base branch is protected (${gating.join(", ")}) \u2014 seed via a branch + PR` };
|
|
7733
7912
|
return { mode: "direct", reason: "base branch is unprotected \u2014 direct PUT is safe" };
|
|
7734
7913
|
}
|
|
7735
|
-
function planSeedDelivery(branchRules, slug, baseBranch) {
|
|
7914
|
+
function planSeedDelivery(branchRules, slug, baseBranch, branchPrefix = "bootstrap-seed") {
|
|
7736
7915
|
const decision = decideSeedDelivery(branchRules);
|
|
7737
7916
|
if (decision.mode === "pr") {
|
|
7738
|
-
const branch =
|
|
7917
|
+
const branch = `${branchPrefix}-${slug}`;
|
|
7739
7918
|
return { mode: "pr", ref: branch, branch, reason: decision.reason };
|
|
7740
7919
|
}
|
|
7741
7920
|
return { mode: "direct", ref: baseBranch, reason: decision.reason };
|
|
7742
7921
|
}
|
|
7743
|
-
function
|
|
7744
|
-
const
|
|
7922
|
+
function contentPutBody(path2, content, branch, sha) {
|
|
7923
|
+
const body = {
|
|
7924
|
+
message: `bootstrap: seed ${path2}`,
|
|
7925
|
+
content: Buffer.from(content, "utf8").toString("base64"),
|
|
7926
|
+
branch
|
|
7927
|
+
};
|
|
7928
|
+
if (sha) body.sha = sha;
|
|
7929
|
+
return body;
|
|
7930
|
+
}
|
|
7931
|
+
function contentPutInputArgs(repo, path2, inputFile) {
|
|
7932
|
+
return [
|
|
7745
7933
|
"api",
|
|
7746
7934
|
"-X",
|
|
7747
7935
|
"PUT",
|
|
7748
7936
|
`repos/${repo}/contents/${path2.split("/").map(encodeURIComponent).join("/")}`,
|
|
7749
|
-
"
|
|
7750
|
-
|
|
7751
|
-
"-f",
|
|
7752
|
-
`content=${Buffer.from(content, "utf8").toString("base64")}`,
|
|
7753
|
-
"-f",
|
|
7754
|
-
`branch=${branch}`
|
|
7937
|
+
"--input",
|
|
7938
|
+
inputFile
|
|
7755
7939
|
];
|
|
7756
|
-
if (sha) args.push("-f", `sha=${sha}`);
|
|
7757
|
-
return args;
|
|
7758
7940
|
}
|
|
7759
7941
|
|
|
7760
7942
|
// src/ci-audit.ts
|
|
@@ -7764,6 +7946,9 @@ var PRODUCT_GATE_PATH = ".github/workflows/gate.yml";
|
|
|
7764
7946
|
var PRODUCT_RULESET_REF = ".github/rulesets/mmi-product-required-checks.json";
|
|
7765
7947
|
var GATE_TEMPLATE_SEED = "seed:gate.template.yml";
|
|
7766
7948
|
var RULESET_TEMPLATE_SEED = "seed:mmi-product-required-checks.template.json";
|
|
7949
|
+
var RULESET_REFERENCE_MATCH_LABEL = "committed ruleset reference matches the live ruleset (#3816)";
|
|
7950
|
+
var GATE_GREEN_ON_BASE_LABEL = "gate is green on the default branch (#3819)";
|
|
7951
|
+
var RECONCILE_SEED_BRANCH_PREFIX = "ci-reconcile-ruleset-ref";
|
|
7767
7952
|
function slugFromRepo(repo) {
|
|
7768
7953
|
return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
7769
7954
|
}
|
|
@@ -7777,6 +7962,9 @@ function classifyRepo(repo, meta) {
|
|
|
7777
7962
|
function rulesetStatusChecks(rulesets) {
|
|
7778
7963
|
return new Set(rulesets.flatMap((ruleset) => (ruleset.rules || []).filter((rule) => rule.type === "required_status_checks").flatMap((rule) => rule.parameters?.required_status_checks || []).map((check) => check.context).filter((context) => Boolean(context))));
|
|
7779
7964
|
}
|
|
7965
|
+
function sortedUnique(values) {
|
|
7966
|
+
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
7967
|
+
}
|
|
7780
7968
|
async function restJson(deps, path2, fallback) {
|
|
7781
7969
|
try {
|
|
7782
7970
|
return await deps.client.rest("GET", path2) ?? fallback;
|
|
@@ -7820,6 +8008,11 @@ async function listWorkflowPaths(deps, repo, branch) {
|
|
|
7820
8008
|
return e?.status === 404 ? [] : void 0;
|
|
7821
8009
|
}
|
|
7822
8010
|
}
|
|
8011
|
+
var AGENT_PR_BOOKKEEPING_CONTEXTS = /* @__PURE__ */ new Set(["guard", "verdict"]);
|
|
8012
|
+
function gateWorkflowFiles(prWorkflowPaths) {
|
|
8013
|
+
const files = prWorkflowPaths.map((p) => p.slice(p.lastIndexOf("/") + 1)).filter((f) => f !== "agent-pr.yml");
|
|
8014
|
+
return files.length ? files : DEFAULT_GATE_FILES;
|
|
8015
|
+
}
|
|
7823
8016
|
async function resolveEmittedPrContexts(deps, repo, branch) {
|
|
7824
8017
|
const paths = await listWorkflowPaths(deps, repo, branch) ?? [];
|
|
7825
8018
|
const workflows = [];
|
|
@@ -7946,7 +8139,7 @@ async function auditRepoCi(repo, deps) {
|
|
|
7946
8139
|
return emittedPrContexts;
|
|
7947
8140
|
};
|
|
7948
8141
|
if (explicitNoCi) {
|
|
7949
|
-
const emitted = await getEmittedPrContexts();
|
|
8142
|
+
const emitted = (await getEmittedPrContexts()).filter((c) => !AGENT_PR_BOOKKEEPING_CONTEXTS.has(c));
|
|
7950
8143
|
if (emitted.length > 0) {
|
|
7951
8144
|
checks.push({
|
|
7952
8145
|
ok: false,
|
|
@@ -8001,6 +8194,34 @@ async function auditRepoCi(repo, deps) {
|
|
|
8001
8194
|
});
|
|
8002
8195
|
}
|
|
8003
8196
|
}
|
|
8197
|
+
const productRuleset = rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
|
|
8198
|
+
const committedReferenceRaw = await fetchFileContent(deps, repo, baseBranch, PRODUCT_RULESET_REF);
|
|
8199
|
+
if (productRuleset && committedReferenceRaw != null) {
|
|
8200
|
+
let committedContexts = null;
|
|
8201
|
+
try {
|
|
8202
|
+
committedContexts = rulesetRequiredContexts(stripRulesetComment(committedReferenceRaw));
|
|
8203
|
+
} catch {
|
|
8204
|
+
committedContexts = null;
|
|
8205
|
+
}
|
|
8206
|
+
const live = sortedUnique(rulesetRequiredContexts(productRuleset));
|
|
8207
|
+
const declared = committedContexts == null ? null : sortedUnique(committedContexts);
|
|
8208
|
+
const aligned = declared != null && declared.length === live.length && declared.every((c, i) => c === live[i]);
|
|
8209
|
+
checks.push({
|
|
8210
|
+
ok: aligned,
|
|
8211
|
+
label: RULESET_REFERENCE_MATCH_LABEL,
|
|
8212
|
+
detail: aligned ? `[${live.join(", ")}] on both sides` : declared == null ? `${PRODUCT_RULESET_REF} on ${baseBranch} is not parseable JSON \u2014 it cannot be compared to the live ruleset` : `${PRODUCT_RULESET_REF} declares [${declared.join(", ")}] but the live ${PRODUCT_RULESET_NAME} ruleset requires [${live.join(", ")}]`,
|
|
8213
|
+
remediation: aligned ? void 0 : `mmi-cli ci reconcile --repo ${repo} --apply`
|
|
8214
|
+
});
|
|
8215
|
+
}
|
|
8216
|
+
}
|
|
8217
|
+
if (deployableGated || repoClass === "hub") {
|
|
8218
|
+
const gate = await readDefaultBranchGate(repo, deps.client, baseBranch, Date.now(), gateWorkflowFiles(prWorkflowPaths));
|
|
8219
|
+
checks.push({
|
|
8220
|
+
ok: gate.state !== "red",
|
|
8221
|
+
label: GATE_GREEN_ON_BASE_LABEL,
|
|
8222
|
+
detail: gate.state === "green" ? void 0 : gate.detail,
|
|
8223
|
+
remediation: gate.state === "red" ? `Fix the gate in ${repo} \u2014 it blocks every PR there, and ${PRODUCT_RULESET_NAME} activation is held back by the #3694 guard until it is green${gate.runUrl ? ` (${gate.runUrl})` : ""}` : void 0
|
|
8224
|
+
});
|
|
8004
8225
|
}
|
|
8005
8226
|
const workflowPaths = repoClass === "deployable" ? prWorkflowPaths : [];
|
|
8006
8227
|
const { policy, reason } = resolveMergeCiPolicy({
|
|
@@ -8022,16 +8243,21 @@ async function auditRepoCi(repo, deps) {
|
|
|
8022
8243
|
detail: `${policy} (${reason})`
|
|
8023
8244
|
});
|
|
8024
8245
|
}
|
|
8246
|
+
const NO_CANCEL_WAIVER = /ci-audit:\s*allow-no-cancel-in-progress\b[^\n]*#\d+/i;
|
|
8025
8247
|
if (deployableGated || repoClass === "hub") {
|
|
8026
8248
|
const allPaths = await listWorkflowPaths(deps, repo, baseBranch) ?? [];
|
|
8027
8249
|
const prPaths = await filterPullRequestTriggered(deps, repo, baseBranch, allPaths);
|
|
8028
8250
|
const missing = [];
|
|
8251
|
+
const waived = [];
|
|
8029
8252
|
const gateBodies = [];
|
|
8030
8253
|
for (const path2 of prPaths) {
|
|
8031
8254
|
const body = await fetchFileContent(deps, repo, baseBranch, path2);
|
|
8032
8255
|
if (body == null) continue;
|
|
8033
8256
|
if (isGateWorkflowPath(path2)) gateBodies.push({ path: path2, body });
|
|
8034
|
-
if (!/^\s*concurrency:/m.test(body) || !/cancel-in-progress:\s*true/.test(body))
|
|
8257
|
+
if (!/^\s*concurrency:/m.test(body) || !/cancel-in-progress:\s*true/.test(body)) {
|
|
8258
|
+
if (NO_CANCEL_WAIVER.test(body)) waived.push(path2);
|
|
8259
|
+
else missing.push(path2);
|
|
8260
|
+
}
|
|
8035
8261
|
}
|
|
8036
8262
|
if (gateBodies.length > 0) {
|
|
8037
8263
|
const budget = checkGateBudget(gateBodies, { hubLocalAllowed: repoClass === "hub" });
|
|
@@ -8043,10 +8269,14 @@ async function auditRepoCi(repo, deps) {
|
|
|
8043
8269
|
});
|
|
8044
8270
|
}
|
|
8045
8271
|
if (prPaths.length > 0) {
|
|
8272
|
+
const waivedNote = waived.length ? `${waived.length} waived by declared marker: ${waived.join(", ")}` : void 0;
|
|
8046
8273
|
checks.push({
|
|
8047
8274
|
ok: missing.length === 0,
|
|
8048
8275
|
label: "PR workflows cancel superseded runs (#3001)",
|
|
8049
|
-
detail:
|
|
8276
|
+
detail: [
|
|
8277
|
+
missing.length ? `missing per-PR concurrency cancel-in-progress: ${missing.join(", ")}` : void 0,
|
|
8278
|
+
waivedNote
|
|
8279
|
+
].filter(Boolean).join("; ") || void 0,
|
|
8050
8280
|
remediation: missing.length ? `Add the per-PR concurrency block from skills/bootstrap/seeds/gate.template.yml (group: <workflow>-PR-or-ref, cancel-in-progress: true) to: ${missing.join(", ")}` : void 0
|
|
8051
8281
|
});
|
|
8052
8282
|
}
|
|
@@ -8070,24 +8300,38 @@ async function auditRepoCi(repo, deps) {
|
|
|
8070
8300
|
const ok = checks.every((c) => c.ok);
|
|
8071
8301
|
return { repo, class: repoClass, mergePolicy: policy, ok, checks, explicitNoCi };
|
|
8072
8302
|
}
|
|
8303
|
+
var REGISTRY_UNREADABLE_DETAIL = `the registry roster read returned nothing \u2014 the fleet was NOT audited. 0 repos read from the registry; the number left unaudited is unknown because the roster is what names them. This is "cannot audit the fleet", never "the fleet is green". Under GitHub Actions the CLI needs a CI Hub session (MMI_HUB_TOKEN, minted from the job's OIDC claim via POST /auth/session-ci) \u2014 an App installation token has no /user identity and can never pass /auth/session (#3215).`;
|
|
8073
8304
|
async function auditOrgCi(deps, repoFilter) {
|
|
8305
|
+
if (repoFilter) {
|
|
8306
|
+
const report = await auditRepoCi(repoFilter, deps);
|
|
8307
|
+
return { ok: report.ok, scope: "single-repo", repos: [report] };
|
|
8308
|
+
}
|
|
8074
8309
|
const projects = await deps.listProjects();
|
|
8075
|
-
if (!projects) {
|
|
8076
|
-
|
|
8077
|
-
const report = await auditRepoCi(single, deps);
|
|
8078
|
-
return { ok: report.ok, repos: [report] };
|
|
8310
|
+
if (!projects || projects.length === 0) {
|
|
8311
|
+
return { ok: false, scope: "registry-unreadable", repos: [], scopeDetail: REGISTRY_UNREADABLE_DETAIL };
|
|
8079
8312
|
}
|
|
8080
|
-
const targets =
|
|
8313
|
+
const targets = collectRegistryRepos(projects);
|
|
8081
8314
|
const repos = [];
|
|
8082
8315
|
for (const repo of targets) {
|
|
8083
8316
|
repos.push(await auditRepoCi(repo, deps));
|
|
8084
8317
|
}
|
|
8085
|
-
return { ok: repos.every((r) => r.ok), repos };
|
|
8318
|
+
return { ok: repos.every((r) => r.ok), scope: "fleet", repos };
|
|
8319
|
+
}
|
|
8320
|
+
function renderCiAuditScopeLine(report) {
|
|
8321
|
+
if (report.scope === "registry-unreadable") {
|
|
8322
|
+
return `scope: REGISTRY UNREADABLE \u2014 repos audited: 0 \u2014 ${report.scopeDetail ?? REGISTRY_UNREADABLE_DETAIL}`;
|
|
8323
|
+
}
|
|
8324
|
+
if (report.scope === "single-repo") {
|
|
8325
|
+
return `scope: single-repo (explicit --repo) \u2014 repos audited: ${report.repos.length} \u2014 this is NOT a fleet verdict`;
|
|
8326
|
+
}
|
|
8327
|
+
return `scope: fleet (registry roster) \u2014 repos audited: ${report.repos.length}`;
|
|
8086
8328
|
}
|
|
8087
8329
|
function renderCiAuditMarkdown(report) {
|
|
8088
8330
|
const lines = [
|
|
8089
8331
|
`# CI merge-readiness audit`,
|
|
8090
8332
|
"",
|
|
8333
|
+
renderCiAuditScopeLine(report),
|
|
8334
|
+
"",
|
|
8091
8335
|
`Fleet: ${report.ok ? "OK" : "GAPS"} (${report.repos.filter((r) => r.ok).length}/${report.repos.length} repos ready)`,
|
|
8092
8336
|
"",
|
|
8093
8337
|
"| Repo | Class | Policy | OK | Top gap |",
|
|
@@ -8100,7 +8344,10 @@ function renderCiAuditMarkdown(report) {
|
|
|
8100
8344
|
return lines.join("\n");
|
|
8101
8345
|
}
|
|
8102
8346
|
function renderCiAuditText(report) {
|
|
8103
|
-
const lines = [
|
|
8347
|
+
const lines = [
|
|
8348
|
+
`mmi-cli ci audit: ${report.ok ? "OK" : "GAPS"} (${report.repos.length} repos)`,
|
|
8349
|
+
renderCiAuditScopeLine(report)
|
|
8350
|
+
];
|
|
8104
8351
|
for (const r of report.repos) {
|
|
8105
8352
|
lines.push(`
|
|
8106
8353
|
${r.repo} (${r.class}, policy=${r.mergePolicy}) ${r.ok ? "OK" : "GAP"}`);
|
|
@@ -8148,6 +8395,17 @@ async function fetchRulesetSeedBody(deps, repo) {
|
|
|
8148
8395
|
return null;
|
|
8149
8396
|
}
|
|
8150
8397
|
}
|
|
8398
|
+
async function readLiveProductContexts(deps, repo) {
|
|
8399
|
+
try {
|
|
8400
|
+
const list = await deps.client.rest("GET", `repos/${repo}/rulesets`, { timeoutMs: 2e4 });
|
|
8401
|
+
const existing = findProductRuleset(list ?? []);
|
|
8402
|
+
if (existing?.id == null) return null;
|
|
8403
|
+
const detail = await deps.client.rest("GET", `repos/${repo}/rulesets/${existing.id}`, { timeoutMs: 2e4 });
|
|
8404
|
+
return sortedUnique(rulesetRequiredContexts(detail));
|
|
8405
|
+
} catch {
|
|
8406
|
+
return null;
|
|
8407
|
+
}
|
|
8408
|
+
}
|
|
8151
8409
|
async function contentSha(deps, repo, branch, path2) {
|
|
8152
8410
|
try {
|
|
8153
8411
|
const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
|
|
@@ -8173,48 +8431,101 @@ async function putSeedFile(deps, repo, path2, content, branch) {
|
|
|
8173
8431
|
if (sha) body.sha = sha;
|
|
8174
8432
|
await deps.client.rest("PUT", `repos/${repo}/contents/${encodedPath}`, { body });
|
|
8175
8433
|
}
|
|
8434
|
+
async function deliverSeedFile(deps, repo, path2, content, baseBranch) {
|
|
8435
|
+
let branchRules = [];
|
|
8436
|
+
try {
|
|
8437
|
+
branchRules = await deps.client.rest("GET", `repos/${repo}/rules/branches/${encodeURIComponent(baseBranch)}`);
|
|
8438
|
+
} catch {
|
|
8439
|
+
branchRules = [];
|
|
8440
|
+
}
|
|
8441
|
+
const plan = planSeedDelivery(branchRules, slugFromRepo(repo), baseBranch, RECONCILE_SEED_BRANCH_PREFIX);
|
|
8442
|
+
if (plan.mode === "direct" || !plan.branch) {
|
|
8443
|
+
await putSeedFile(deps, repo, path2, content, baseBranch);
|
|
8444
|
+
return { plan };
|
|
8445
|
+
}
|
|
8446
|
+
const head = await deps.client.rest("GET", `repos/${repo}/git/ref/heads/${encodeURIComponent(baseBranch)}`);
|
|
8447
|
+
const sha = head?.object?.sha;
|
|
8448
|
+
if (!sha) throw new Error(`cannot resolve ${baseBranch} head on ${repo} \u2014 seed branch not cut`);
|
|
8449
|
+
try {
|
|
8450
|
+
await deps.client.rest("POST", `repos/${repo}/git/refs`, { body: { ref: `refs/heads/${plan.branch}`, sha } });
|
|
8451
|
+
} catch (e) {
|
|
8452
|
+
if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
|
|
8453
|
+
}
|
|
8454
|
+
await putSeedFile(deps, repo, path2, content, plan.branch);
|
|
8455
|
+
const owner = repo.includes("/") ? repo.split("/")[0] : "mutmutco";
|
|
8456
|
+
const open2 = await deps.client.rest(
|
|
8457
|
+
"GET",
|
|
8458
|
+
`repos/${repo}/pulls?state=open&base=${encodeURIComponent(baseBranch)}&head=${encodeURIComponent(`${owner}:${plan.branch}`)}`
|
|
8459
|
+
).catch(() => []);
|
|
8460
|
+
const decision = decideSeedPrAction((open2 ?? []).map((pr2) => ({ url: pr2?.html_url })));
|
|
8461
|
+
if (decision.action === "reuse") return { plan, prUrl: decision.url };
|
|
8462
|
+
const created = await deps.client.rest("POST", `repos/${repo}/pulls`, {
|
|
8463
|
+
body: {
|
|
8464
|
+
title: `chore: reconcile ${path2} with the live ${PRODUCT_RULESET_NAME} ruleset (#3816)`,
|
|
8465
|
+
head: plan.branch,
|
|
8466
|
+
base: baseBranch,
|
|
8467
|
+
body: `\`mmi-cli ci reconcile --apply\` brings this repo's committed ruleset reference into agreement with its LIVE \`${PRODUCT_RULESET_NAME}\` ruleset.
|
|
8468
|
+
|
|
8469
|
+
The base branch is protected, so the reference cannot be written directly \u2014 the same branch+PR seed delivery \`bootstrap apply\` uses (#2286.1). Refs mutmutco/MMI-Hub#3816.
|
|
8470
|
+
`
|
|
8471
|
+
}
|
|
8472
|
+
});
|
|
8473
|
+
return { plan, prUrl: created?.html_url };
|
|
8474
|
+
}
|
|
8176
8475
|
async function seedGateYml(repo, deps, meta, result) {
|
|
8177
8476
|
const baseBranch = "development";
|
|
8178
8477
|
const releaseTrack = isReleaseTrack(meta?.releaseTrack) ? meta?.releaseTrack : void 0;
|
|
8179
8478
|
const parsed = parseOwnerRepo(repo);
|
|
8479
|
+
const refOnlyVars = () => withDerivedRepoVars({ REPO_SLUG: parsed.slug }, parsed, "deployable", releaseTrack);
|
|
8180
8480
|
if (await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH)) {
|
|
8181
|
-
await seedRulesetRefIfMissing(repo, deps,
|
|
8182
|
-
return;
|
|
8481
|
+
return await seedRulesetRefIfMissing(repo, deps, refOnlyVars(), baseBranch, result);
|
|
8183
8482
|
}
|
|
8184
8483
|
if (!deps.readSeedFile && !deps.renderSeed) {
|
|
8185
8484
|
result.skipped.push(`gate.yml missing but no seed-template source wired \u2014 run bootstrap apply`);
|
|
8186
|
-
return;
|
|
8485
|
+
return "none";
|
|
8486
|
+
}
|
|
8487
|
+
const prTriggered = await prTriggeredWorkflowsOnRef(deps, repo, baseBranch, "deployable");
|
|
8488
|
+
if (prTriggered === void 0) {
|
|
8489
|
+
result.skipped.push("gate.yml missing and the workflows listing is unreadable \u2014 never seeding a gate on UNKNOWN");
|
|
8490
|
+
return "none";
|
|
8491
|
+
}
|
|
8492
|
+
if (prTriggered.length > 0) {
|
|
8493
|
+
return await seedRulesetRefIfMissing(repo, deps, refOnlyVars(), baseBranch, result);
|
|
8187
8494
|
}
|
|
8188
8495
|
const gate = meta?.gate;
|
|
8189
8496
|
if (!gate || typeof gate === "object" && Object.keys(gate).length === 0) {
|
|
8190
8497
|
result.skipped.push(`gate.yml missing \u2014 no registry gate config; set \`mmi-cli org project set ${repo} --var gate={...}\` first, then re-run reconcile`);
|
|
8191
|
-
return;
|
|
8498
|
+
return "none";
|
|
8192
8499
|
}
|
|
8193
8500
|
const derivedVars = withDerivedRepoVars({ ...gateConfigToVars(gate), REPO_SLUG: parsed.slug }, parsed, "deployable", releaseTrack);
|
|
8194
8501
|
const rendered = renderSeedBody(deps, GATE_TEMPLATE_SEED, PRODUCT_GATE_PATH, derivedVars);
|
|
8195
8502
|
if (rendered == null) {
|
|
8196
8503
|
result.errors.push(`gate.yml re-seed: could not render ${GATE_TEMPLATE_SEED} (template unreadable or unfilled)`);
|
|
8197
|
-
return;
|
|
8504
|
+
return "none";
|
|
8198
8505
|
}
|
|
8199
8506
|
try {
|
|
8200
8507
|
await putSeedFile(deps, repo, PRODUCT_GATE_PATH, rendered, baseBranch);
|
|
8201
8508
|
result.applied.push(`seeded ${PRODUCT_GATE_PATH}`);
|
|
8202
8509
|
} catch (e) {
|
|
8203
8510
|
result.errors.push(`gate.yml re-seed failed: ${e.message}`);
|
|
8204
|
-
return;
|
|
8511
|
+
return "none";
|
|
8205
8512
|
}
|
|
8206
|
-
await seedRulesetRefIfMissing(repo, deps, derivedVars, baseBranch, result);
|
|
8513
|
+
return await seedRulesetRefIfMissing(repo, deps, derivedVars, baseBranch, result);
|
|
8207
8514
|
}
|
|
8208
8515
|
async function seedRulesetRefIfMissing(repo, deps, derivedVars, baseBranch, result) {
|
|
8209
|
-
if (await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF)) return;
|
|
8210
|
-
if (!deps.readSeedFile && !deps.renderSeed) return;
|
|
8516
|
+
if (await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF)) return "committed";
|
|
8517
|
+
if (!deps.readSeedFile && !deps.renderSeed) return "none";
|
|
8211
8518
|
const rulesetBody = renderSeedBody(deps, RULESET_TEMPLATE_SEED, PRODUCT_RULESET_REF, derivedVars);
|
|
8212
|
-
if (rulesetBody == null) return;
|
|
8519
|
+
if (rulesetBody == null) return "none";
|
|
8213
8520
|
try {
|
|
8214
|
-
await
|
|
8215
|
-
result.applied.push(
|
|
8521
|
+
const { plan, prUrl } = await deliverSeedFile(deps, repo, PRODUCT_RULESET_REF, rulesetBody, baseBranch);
|
|
8522
|
+
result.applied.push(
|
|
8523
|
+
`seeded ${PRODUCT_RULESET_REF}` + (plan.mode === "direct" ? ` (committed to ${baseBranch})` : ` (${plan.reason}${prUrl ? ` \u2014 ${prUrl}` : ""})`)
|
|
8524
|
+
);
|
|
8525
|
+
return plan.mode === "direct" ? "committed" : "pending";
|
|
8216
8526
|
} catch (e) {
|
|
8217
8527
|
result.errors.push(`ruleset reference seed failed: ${e.message}`);
|
|
8528
|
+
return "none";
|
|
8218
8529
|
}
|
|
8219
8530
|
}
|
|
8220
8531
|
async function parkProductRuleset(repo, deps) {
|
|
@@ -8248,19 +8559,47 @@ async function applyCiReconcileRepo(repo, deps) {
|
|
|
8248
8559
|
const meta = await deps.getProjectMeta(slugFromRepo(repo));
|
|
8249
8560
|
const report = await auditRepoCi(repo, deps);
|
|
8250
8561
|
if (report.class !== "deployable" || report.explicitNoCi) return merge;
|
|
8251
|
-
await seedGateYml(repo, deps, meta, merge);
|
|
8562
|
+
const refDelivery = await seedGateYml(repo, deps, meta, merge);
|
|
8563
|
+
const baseBranch = "development";
|
|
8252
8564
|
const driftCheck = report.checks.find((c) => c.label === "required check contexts match PR workflows");
|
|
8253
8565
|
const requiredCheck = report.checks.find((c) => c.label === "product required status checks active");
|
|
8254
|
-
|
|
8255
|
-
|
|
8566
|
+
const referenceCheck = report.checks.find((c) => c.label === RULESET_REFERENCE_MATCH_LABEL);
|
|
8567
|
+
if (requiredCheck?.ok && (driftCheck?.ok ?? true) && (referenceCheck?.ok ?? true)) {
|
|
8568
|
+
merge.skipped.push("product ruleset already active, aligned, and matching its committed reference");
|
|
8256
8569
|
return merge;
|
|
8257
8570
|
}
|
|
8258
8571
|
const raw = await fetchRulesetSeedBody(deps, repo);
|
|
8259
8572
|
if (!raw) {
|
|
8260
|
-
|
|
8573
|
+
if (refDelivery === "pending") {
|
|
8574
|
+
merge.skipped.push(
|
|
8575
|
+
`${PRODUCT_RULESET_REF} was delivered on a seed branch this run \u2014 the base branch is protected, so it lands when that PR merges; re-run the reconcile then to activate the ruleset`
|
|
8576
|
+
);
|
|
8577
|
+
} else {
|
|
8578
|
+
merge.errors.push(`missing ${PRODUCT_RULESET_REF} on development \u2014 run bootstrap apply first`);
|
|
8579
|
+
}
|
|
8261
8580
|
return merge;
|
|
8262
8581
|
}
|
|
8263
|
-
|
|
8582
|
+
const activationNeeded = !requiredCheck?.ok || !(driftCheck?.ok ?? true);
|
|
8583
|
+
const prWorkflows = await prTriggeredWorkflowsOnRef(deps, repo, baseBranch, "deployable") ?? [];
|
|
8584
|
+
const gateFiles = gateWorkflowFiles(prWorkflows);
|
|
8585
|
+
const allPrWorkflows = await listWorkflowPaths(deps, repo, baseBranch) ?? [];
|
|
8586
|
+
if (activationNeeded) {
|
|
8587
|
+
const bodies = [];
|
|
8588
|
+
for (const path2 of allPrWorkflows) {
|
|
8589
|
+
const body = await fetchFileContent(deps, repo, baseBranch, path2);
|
|
8590
|
+
if (body) bodies.push({ path: path2, body });
|
|
8591
|
+
}
|
|
8592
|
+
const filteredPaths = new Set(pathFilteredPullRequestWorkflows(bodies).filter((p) => !p.endsWith("/agent-pr.yml")));
|
|
8593
|
+
const safeContexts = new Set(collectPullRequestWorkflowContexts(bodies.filter((b) => !filteredPaths.has(b.path))));
|
|
8594
|
+
const unsafe = collectPullRequestWorkflowContexts(bodies.filter((b) => filteredPaths.has(b.path))).filter((c) => !safeContexts.has(c));
|
|
8595
|
+
if (unsafe.length) {
|
|
8596
|
+
merge.skipped.push(
|
|
8597
|
+
`product ruleset left non-enforcing \u2014 [${unsafe.join(", ")}] ${unsafe.length === 1 ? "is" : "are"} emitted ONLY by path-filtered workflow(s) (${[...filteredPaths].join(", ")}), so the context is not reported on a PR outside those paths and requiring it would block that PR forever (#3836). Give the gate a companion job that runs unconditionally, then re-run this command`
|
|
8598
|
+
);
|
|
8599
|
+
return merge;
|
|
8600
|
+
}
|
|
8601
|
+
}
|
|
8602
|
+
if (activationNeeded && !await gateIsProvenGreen(repo, deps.client, baseBranch, gateFiles)) {
|
|
8264
8603
|
merge.skipped.push(
|
|
8265
8604
|
"product ruleset left non-enforcing \u2014 the gate is not green on development; activating it would block every PR (#3694)"
|
|
8266
8605
|
);
|
|
@@ -8268,29 +8607,55 @@ async function applyCiReconcileRepo(repo, deps) {
|
|
|
8268
8607
|
}
|
|
8269
8608
|
try {
|
|
8270
8609
|
let body = stripRulesetComment(raw);
|
|
8271
|
-
|
|
8272
|
-
|
|
8610
|
+
let targetContexts;
|
|
8611
|
+
if (!(driftCheck?.ok ?? true)) {
|
|
8273
8612
|
const emitted = registryRequiredContexts(meta) ?? await resolveEmittedPrContexts(deps, repo, baseBranch);
|
|
8274
8613
|
if (!emitted.length) {
|
|
8275
8614
|
merge.errors.push("cannot reconcile ruleset contexts \u2014 no PR workflow job ids found");
|
|
8276
8615
|
return merge;
|
|
8277
8616
|
}
|
|
8617
|
+
targetContexts = emitted;
|
|
8278
8618
|
body = patchRulesetRequiredContexts(body, emitted);
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8619
|
+
} else if (!(referenceCheck?.ok ?? true)) {
|
|
8620
|
+
const live = await readLiveProductContexts(deps, repo);
|
|
8621
|
+
if (live == null) {
|
|
8622
|
+
merge.errors.push(`cannot read the live ${PRODUCT_RULESET_NAME} ruleset \u2014 ${PRODUCT_RULESET_REF} left untouched`);
|
|
8623
|
+
return merge;
|
|
8624
|
+
}
|
|
8625
|
+
targetContexts = live;
|
|
8626
|
+
body = patchRulesetRequiredContexts(body, live);
|
|
8627
|
+
} else {
|
|
8628
|
+
targetContexts = rulesetRequiredContexts(body);
|
|
8629
|
+
}
|
|
8630
|
+
if (!(referenceCheck?.ok ?? true) || !(driftCheck?.ok ?? true)) {
|
|
8631
|
+
let comment;
|
|
8282
8632
|
try {
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8633
|
+
comment = JSON.parse(raw)._comment;
|
|
8634
|
+
} catch {
|
|
8635
|
+
comment = void 0;
|
|
8636
|
+
}
|
|
8637
|
+
const fileBody = comment === void 0 ? body : { _comment: comment, ...body };
|
|
8638
|
+
try {
|
|
8639
|
+
const { plan, prUrl } = await deliverSeedFile(
|
|
8640
|
+
deps,
|
|
8641
|
+
repo,
|
|
8642
|
+
PRODUCT_RULESET_REF,
|
|
8643
|
+
`${JSON.stringify(fileBody, null, 2)}
|
|
8644
|
+
`,
|
|
8645
|
+
baseBranch
|
|
8646
|
+
);
|
|
8647
|
+
merge.applied.push(
|
|
8648
|
+
plan.mode === "pr" ? `${PRODUCT_RULESET_REF} contexts \u2192 [${targetContexts.join(", ")}] delivered on ${plan.branch} (${plan.reason})${prUrl ? `: ${prUrl}` : ""}` : `reconciled ${PRODUCT_RULESET_REF} contexts \u2192 [${targetContexts.join(", ")}]`
|
|
8649
|
+
);
|
|
8286
8650
|
} catch (e) {
|
|
8287
|
-
merge.errors.push(`ruleset reference
|
|
8651
|
+
merge.errors.push(`ruleset reference delivery failed: ${e.message}`);
|
|
8288
8652
|
}
|
|
8289
|
-
return merge;
|
|
8290
8653
|
}
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8654
|
+
if (activationNeeded) {
|
|
8655
|
+
const activation = await activateProductRuleset(repo, body, deps.client);
|
|
8656
|
+
if (activation.action === "skipped") merge.skipped.push(activation.detail ?? "product ruleset");
|
|
8657
|
+
else merge.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
|
|
8658
|
+
}
|
|
8294
8659
|
} catch (e) {
|
|
8295
8660
|
merge.errors.push(e.message);
|
|
8296
8661
|
}
|
|
@@ -9147,7 +9512,7 @@ async function executeWaveLand(plan, deps, opts = { preserveWorktree: true }) {
|
|
|
9147
9512
|
}
|
|
9148
9513
|
|
|
9149
9514
|
// src/index.ts
|
|
9150
|
-
var
|
|
9515
|
+
var import_node_os13 = require("node:os");
|
|
9151
9516
|
|
|
9152
9517
|
// src/board.ts
|
|
9153
9518
|
var import_node_child_process8 = require("node:child_process");
|
|
@@ -11079,6 +11444,16 @@ async function fetchSecretForUse(deps, { repo, key, slug }) {
|
|
|
11079
11444
|
|
|
11080
11445
|
// src/report.ts
|
|
11081
11446
|
var HUB_REPO2 = "mutmutco/MMI-Hub";
|
|
11447
|
+
var REPORT_LABEL = "report";
|
|
11448
|
+
var REPORT_SURFACE_WAIVER_REASON = "Hub-only friction filing; attribution repo is footer-only; no honest single surface";
|
|
11449
|
+
function preflightReportSurface() {
|
|
11450
|
+
return checkSurfaceRequirement({
|
|
11451
|
+
repo: HUB_REPO2,
|
|
11452
|
+
labels: [REPORT_LABEL],
|
|
11453
|
+
command: "report",
|
|
11454
|
+
waiver: { reason: REPORT_SURFACE_WAIVER_REASON }
|
|
11455
|
+
});
|
|
11456
|
+
}
|
|
11082
11457
|
function findDuplicateReport(source, openReports, threshold = 0.6) {
|
|
11083
11458
|
const normalizedTitle = normalizeTitle(source.title);
|
|
11084
11459
|
let best;
|
|
@@ -11561,6 +11936,49 @@ function resolveParentField(payload) {
|
|
|
11561
11936
|
const parsed = typeof raw === "string" ? parseParentIssueUrl(raw) : null;
|
|
11562
11937
|
return parsed ? { parent: parsed } : { parentReadError: `unrecognised parent_issue_url: ${JSON.stringify(raw)}` };
|
|
11563
11938
|
}
|
|
11939
|
+
async function readIssueParent(runGh, repo, number) {
|
|
11940
|
+
try {
|
|
11941
|
+
const stdout = await runGh(["api", `repos/${repo}/issues/${number}`], RESOLVE_ID_TIMEOUT_MS);
|
|
11942
|
+
const resolved = resolveParentField(JSON.parse(stdout));
|
|
11943
|
+
return "parent" in resolved ? resolved.parent : void 0;
|
|
11944
|
+
} catch {
|
|
11945
|
+
return void 0;
|
|
11946
|
+
}
|
|
11947
|
+
}
|
|
11948
|
+
function isParentConflictMessage(text) {
|
|
11949
|
+
return typeof text === "string" && /sub[- ]?issue may only have one parent/i.test(text);
|
|
11950
|
+
}
|
|
11951
|
+
function buildReparentConflictPayload(input) {
|
|
11952
|
+
const { childRepo, childNumber, requestedParent, currentParent, offendingFlag } = input;
|
|
11953
|
+
const child2 = `${childRepo}#${childNumber}`;
|
|
11954
|
+
const payload = { code: ERROR_CODES.ERR_STATE_CONFLICT };
|
|
11955
|
+
if (offendingFlag) payload.offending_flag = offendingFlag;
|
|
11956
|
+
if (!currentParent) {
|
|
11957
|
+
return {
|
|
11958
|
+
message: currentParent === null ? `${child2} cannot be reparented under ${requestedParent} \u2014 GitHub refused it under the one-parent rule, but reading the issue back showed no parent at all. Something changed it concurrently; re-read with \`mmi-cli issue view ${childNumber} --repo ${childRepo}\` and retry` : `${child2} cannot be reparented under ${requestedParent} \u2014 GitHub allows a sub-issue exactly one parent and it already has one. Its current parent could not be read; check with \`mmi-cli issue view ${childNumber} --repo ${childRepo}\``,
|
|
11959
|
+
payload
|
|
11960
|
+
};
|
|
11961
|
+
}
|
|
11962
|
+
const current = `${currentParent.repo}#${currentParent.number}`;
|
|
11963
|
+
const sameRepo2 = currentParent.repo.toLowerCase() === childRepo.toLowerCase();
|
|
11964
|
+
const repoFlag = ` --repo ${childRepo}`;
|
|
11965
|
+
payload.current_parent = current;
|
|
11966
|
+
const requested = parseIssueRef(requestedParent);
|
|
11967
|
+
const alreadyThere = currentParent.number === requested.number && (requested.repo ?? childRepo).toLowerCase() === currentParent.repo.toLowerCase();
|
|
11968
|
+
if (!alreadyThere) {
|
|
11969
|
+
payload.corrected_command = `mmi-cli issue unlink-child ${sameRepo2 ? String(currentParent.number) : current} ${childNumber}${repoFlag} && mmi-cli issue edit ${childNumber}${repoFlag} --parent ${requestedParent}`;
|
|
11970
|
+
}
|
|
11971
|
+
return {
|
|
11972
|
+
message: alreadyThere ? `${child2} is already a sub-issue of ${current} \u2014 GitHub allows a sub-issue exactly one parent, so this reparent is a no-op it refuses rather than performs; nothing to do` : `${child2} already has a parent (${current}) \u2014 GitHub allows a sub-issue exactly one parent. Reparenting it under ${requestedParent} would REMOVE it from ${current}; unlink it there first if that is what you want`,
|
|
11973
|
+
payload
|
|
11974
|
+
};
|
|
11975
|
+
}
|
|
11976
|
+
async function classifyReparentFailure(e, runGh, childRepo, childNumber, requestedParent, offendingFlag) {
|
|
11977
|
+
const err = e;
|
|
11978
|
+
if (!isParentConflictMessage((err.stderr || err.message || String(e)).trim())) return void 0;
|
|
11979
|
+
const currentParent = await readIssueParent(runGh, childRepo, childNumber);
|
|
11980
|
+
return buildReparentConflictPayload({ childRepo, childNumber, requestedParent, currentParent, offendingFlag });
|
|
11981
|
+
}
|
|
11564
11982
|
function parentLinkFields(result, error) {
|
|
11565
11983
|
if (result) return { parent: result };
|
|
11566
11984
|
if (error) return { parentLinkError: error };
|
|
@@ -14448,7 +14866,8 @@ function trainPlan(command, options = {}) {
|
|
|
14448
14866
|
{ label: "fold the version bump into the release commit (Hub: full distribution set; app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
|
|
14449
14867
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
14450
14868
|
{ label: "trigger the repo deploy path from the release event", command: "hub-serverless: deploy.yml + publish.yml auto-fire on the release; registry-publish: own publish.yml auto-fires, watched on that repo, never a central dispatch (#2428); other models deploy via their own workflow", gated: true },
|
|
14451
|
-
{ label: "roll development forward", gated: true }
|
|
14869
|
+
{ label: "roll development forward", gated: true },
|
|
14870
|
+
{ label: "synchronize the owning Project short description + thin README from the repo docs and current member repos", command: "mmi-cli org project sync-info --apply", gated: true }
|
|
14452
14871
|
];
|
|
14453
14872
|
}
|
|
14454
14873
|
if (options.dev) {
|
|
@@ -14463,7 +14882,8 @@ function trainPlan(command, options = {}) {
|
|
|
14463
14882
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
14464
14883
|
{ label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch", gated: true },
|
|
14465
14884
|
{ label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli runtime tenant control <owner/repo> rc retire", gated: true },
|
|
14466
|
-
{ label: "roll development forward and align rc to the released main", gated: true }
|
|
14885
|
+
{ label: "roll development forward and align rc to the released main", gated: true },
|
|
14886
|
+
{ label: "synchronize the owning Project short description + thin README from the repo docs and current member repos", command: "mmi-cli org project sync-info --apply", gated: true }
|
|
14467
14887
|
];
|
|
14468
14888
|
}
|
|
14469
14889
|
return [
|
|
@@ -14476,7 +14896,8 @@ function trainPlan(command, options = {}) {
|
|
|
14476
14896
|
{ label: "fold the version bump into the release commit (app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
|
|
14477
14897
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
14478
14898
|
{ label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml + publish.yml auto-fire on the release, correlate/watch those runs; registry-publish: no manual dispatch, own publish.yml auto-fires on the release, correlate/watch that run on the product repo (#2428)", gated: true },
|
|
14479
|
-
{ label: "roll development forward", gated: true }
|
|
14899
|
+
{ label: "roll development forward", gated: true },
|
|
14900
|
+
{ label: "synchronize the owning Project short description + thin README from the repo docs and current member repos", command: "mmi-cli org project sync-info --apply", gated: true }
|
|
14480
14901
|
];
|
|
14481
14902
|
}
|
|
14482
14903
|
return [
|
|
@@ -15164,17 +15585,23 @@ function partialTrainRecoveryError(cause, input) {
|
|
|
15164
15585
|
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
15165
15586
|
const branch = input.stage;
|
|
15166
15587
|
const releaseState = input.stage === "rc" ? "GitHub Release n/a" : "GitHub Release not created";
|
|
15167
|
-
const
|
|
15168
|
-
|
|
15169
|
-
const deployStep = input.stage === "main" ? "3" : "2";
|
|
15588
|
+
const tenant2 = input.deployModel === "tenant-container";
|
|
15589
|
+
const deployLine = tenant2 ? ` - deploy: dispatched by the train after the branch push; recover with \`mmi-cli runtime tenant redeploy ${input.repo} ${input.stage} --watch\` once the branch and release exist.` : ` - deploy: this repo is \`${input.deployModel ?? "not tenant-container"}\` \u2014 it has NO tenant runtime. Its own release/main-push workflows publish and deploy once the branch and Release exist. Do NOT run the tenant redeployer.`;
|
|
15170
15590
|
return new Error(
|
|
15171
15591
|
`${causeMessage}
|
|
15172
15592
|
|
|
15173
|
-
partial train state
|
|
15174
|
-
|
|
15175
|
-
|
|
15176
|
-
|
|
15177
|
-
|
|
15593
|
+
partial train state for ${input.tag}:
|
|
15594
|
+
- tag ${input.tag}: PUSHED to origin
|
|
15595
|
+
- origin/${branch}: NOT pushed
|
|
15596
|
+
- ${releaseState}
|
|
15597
|
+
` + deployLine + `
|
|
15598
|
+
|
|
15599
|
+
The tag is public and correct \u2014 do not delete or force-move it.
|
|
15600
|
+
|
|
15601
|
+
Resume it: mmi-cli release --resume${input.stage === "main" ? "" : " (main-stage partials only)"}
|
|
15602
|
+
|
|
15603
|
+
That completes THIS release at ${input.tag} \u2014 branch push, GitHub Release, the deploy path named above, then the development roll-forward. It preserves the version and refuses unless the partial state is proven (tag on origin, not on origin/main, no Release).
|
|
15604
|
+
Do NOT rerun the train instead: with ${input.tag} on origin the cycle resolver returns the NEXT version, so a rerun cuts a second release rather than finishing this one \u2014 and MMI_RELEASE_VERSION cannot pin it back, because it refuses a version that is not ahead of the latest tag, which ${input.tag} now is.`
|
|
15178
15605
|
);
|
|
15179
15606
|
}
|
|
15180
15607
|
async function probeRemoteTag(deps, tag) {
|
|
@@ -15349,6 +15776,10 @@ async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha, enumera
|
|
|
15349
15776
|
workflowRuns.push(...await discoverShaWorkflowRuns(deps, repo, headSha, seen));
|
|
15350
15777
|
return workflowRuns;
|
|
15351
15778
|
}
|
|
15779
|
+
async function enumerateOwnWorkflowRuns(deps, repo, headSha) {
|
|
15780
|
+
const runs = await discoverShaWorkflowRuns(deps, repo, headSha, /* @__PURE__ */ new Set());
|
|
15781
|
+
return runs.length ? runs : [{ workflow: `sha-enumeration(${headSha.slice(0, 7)}) no runs yet`, conclusion: "pending" }];
|
|
15782
|
+
}
|
|
15352
15783
|
var NON_DEPLOY_EVENTS = /* @__PURE__ */ new Set([
|
|
15353
15784
|
"pull_request",
|
|
15354
15785
|
"pull_request_target",
|
|
@@ -15425,7 +15856,11 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
|
|
|
15425
15856
|
}
|
|
15426
15857
|
if (model === "registry-publish") {
|
|
15427
15858
|
const note = ref === "rc" ? "no dispatch on rc: registry-publish repos have no rc-stage Release to publish from" : "no central dispatch: this repo's own publish.yml auto-fires on the published Release (#2428)";
|
|
15428
|
-
if (ref === "rc" || !
|
|
15859
|
+
if (ref === "rc" || !autoRunHeadSha) return { note, deployStatus: "pending" };
|
|
15860
|
+
if (!watch) {
|
|
15861
|
+
const listed = await enumerateOwnWorkflowRuns(deps, ctx.repo, autoRunHeadSha);
|
|
15862
|
+
return { note, workflowRuns: listed, deployStatus: aggregateWorkflowRuns(listed) };
|
|
15863
|
+
}
|
|
15429
15864
|
const since = autoRunSince ?? (deps.now ?? Date.now)();
|
|
15430
15865
|
const workflowRuns = await watchOwnWorkflowRuns(
|
|
15431
15866
|
deps,
|
|
@@ -15440,8 +15875,12 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
|
|
|
15440
15875
|
}
|
|
15441
15876
|
if (model === "hub-serverless") {
|
|
15442
15877
|
const note = ref === "rc" ? "no manual dispatch: deploy.yml auto-fires on the rc push (rc stage)" : "no manual dispatch: deploy.yml + publish.yml auto-fire on the published Release (prod)";
|
|
15443
|
-
if (!watch) return { note, deployStatus: "pending" };
|
|
15444
15878
|
if (!autoRunHeadSha) return { note, deployStatus: "pending" };
|
|
15879
|
+
if (!watch) {
|
|
15880
|
+
if (ref === "rc") return { note, deployStatus: "pending" };
|
|
15881
|
+
const listed = await enumerateOwnWorkflowRuns(deps, HUB_REPO3, autoRunHeadSha);
|
|
15882
|
+
return { note, workflowRuns: listed, deployStatus: aggregateWorkflowRuns(listed) };
|
|
15883
|
+
}
|
|
15445
15884
|
const since = autoRunSince ?? (deps.now ?? Date.now)();
|
|
15446
15885
|
const targets = ref === "rc" ? [{ workflow: "deploy.yml", event: "push", branch: "rc" }] : [
|
|
15447
15886
|
{ workflow: "deploy.yml", event: "release" },
|
|
@@ -15747,7 +16186,7 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
|
|
|
15747
16186
|
try {
|
|
15748
16187
|
checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
|
|
15749
16188
|
} catch (e) {
|
|
15750
|
-
throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main" });
|
|
16189
|
+
throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main", deployModel });
|
|
15751
16190
|
}
|
|
15752
16191
|
if (trueMergeGateNote) checks = `${trueMergeGateNote}; ${checks}`;
|
|
15753
16192
|
await runGitPush(deps, ["push", "origin", "main"]);
|
|
@@ -15767,6 +16206,61 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
|
|
|
15767
16206
|
}
|
|
15768
16207
|
return { checks, releaseUrl, announceNote, dispatch };
|
|
15769
16208
|
}
|
|
16209
|
+
async function runReleaseResume(deps, options = {}) {
|
|
16210
|
+
const watch = options.watch ?? false;
|
|
16211
|
+
const ctx = await buildTrainApplyContext(deps);
|
|
16212
|
+
await requireCleanTree(deps);
|
|
16213
|
+
await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
|
|
16214
|
+
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
16215
|
+
const deployModel = await preflight(deps, ctx, "main", meta);
|
|
16216
|
+
const tags = clean2(await deps.run("git", ["tag", "--list", "v*", "--sort=-v:refname"])).split("\n").map((t) => t.trim()).filter(Boolean);
|
|
16217
|
+
const tag = tags[0];
|
|
16218
|
+
if (!tag) throw new Error("release --resume: no v* tag found \u2014 there is no partial release to resume");
|
|
16219
|
+
const tagSha = await probeRemoteTag(deps, tag);
|
|
16220
|
+
if (!tagSha) {
|
|
16221
|
+
throw new Error(
|
|
16222
|
+
`release --resume: ${tag} is not on origin \u2014 nothing was partially released. A local-only tag is not a partial release; run the train normally.`
|
|
16223
|
+
);
|
|
16224
|
+
}
|
|
16225
|
+
const base = { command: "release-resume", repo: ctx.repo, tag, tagSha, deployModel };
|
|
16226
|
+
if (!await isStrayUnreleasedTag(deps, tag, tagSha, ctx.repo)) {
|
|
16227
|
+
throw new Error(
|
|
16228
|
+
`release --resume: ${tag} is NOT a resumable partial \u2014 it is either already reachable from origin/main or it already has a GitHub Release, which means that release completed. (This probe also answers "not resumable" when it cannot prove otherwise \u2014 an unreadable Release or a failed ancestry check is never treated as safe to resume.) Nothing was written.`
|
|
16229
|
+
);
|
|
16230
|
+
}
|
|
16231
|
+
try {
|
|
16232
|
+
await deps.run("git", ["merge-base", "--is-ancestor", "origin/main", tagSha]);
|
|
16233
|
+
} catch {
|
|
16234
|
+
throw new Error(
|
|
16235
|
+
`release --resume: origin/main is not an ancestor of ${tag} (${tagSha.slice(0, 12)}) \u2014 main has moved on since the partial release, so finishing it here would not be a fast-forward. Resolve by hand; nothing was written.`
|
|
16236
|
+
);
|
|
16237
|
+
}
|
|
16238
|
+
const steps = [];
|
|
16239
|
+
await runGitPush(deps, ["push", "origin", `${tagSha}:refs/heads/main`]);
|
|
16240
|
+
steps.push(`pushed ${tagSha.slice(0, 12)} to origin/main`);
|
|
16241
|
+
const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
16242
|
+
steps.push(`created the GitHub Release for ${tag}`);
|
|
16243
|
+
await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
|
|
16244
|
+
steps.push("verified the Release published against the tagged commit");
|
|
16245
|
+
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
16246
|
+
const autoRunSince = (deps.now ?? Date.now)();
|
|
16247
|
+
const deployDispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir);
|
|
16248
|
+
const publishDispatch = deployDispatch.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report") : null;
|
|
16249
|
+
const dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
16250
|
+
steps.push(`dispatched the ${deployModel} deploy path`);
|
|
16251
|
+
const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
16252
|
+
steps.push(`development roll-forward: ${devRollForward.status}`);
|
|
16253
|
+
return {
|
|
16254
|
+
...base,
|
|
16255
|
+
resumed: true,
|
|
16256
|
+
steps,
|
|
16257
|
+
releaseUrl,
|
|
16258
|
+
announceNote,
|
|
16259
|
+
dispatch,
|
|
16260
|
+
devRollForward,
|
|
16261
|
+
note: `resumed and completed ${tag} \u2014 the original version was preserved, not re-cut`
|
|
16262
|
+
};
|
|
16263
|
+
}
|
|
15770
16264
|
async function pushRcAlignment(deps) {
|
|
15771
16265
|
try {
|
|
15772
16266
|
await deps.run("git", ["push", "origin", "main:rc"]);
|
|
@@ -15815,7 +16309,7 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
15815
16309
|
try {
|
|
15816
16310
|
checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
|
|
15817
16311
|
} catch (e) {
|
|
15818
|
-
throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc" });
|
|
16312
|
+
throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc", deployModel: deployModel2 });
|
|
15819
16313
|
}
|
|
15820
16314
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
15821
16315
|
await runGitPush(deps, ["push", "origin", "rc"]);
|
|
@@ -16132,14 +16626,14 @@ async function runTenantReconcile(deps, options) {
|
|
|
16132
16626
|
};
|
|
16133
16627
|
}
|
|
16134
16628
|
function tenantControlWatches(action) {
|
|
16135
|
-
return action === "status" || action === "retire" || action === "verify-secrets" || action === "verify-broker";
|
|
16629
|
+
return action === "status" || action === "retire" || action === "verify-secrets" || action === "verify-broker" || action === "logs";
|
|
16136
16630
|
}
|
|
16137
16631
|
async function runTenantControl(deps, options) {
|
|
16138
16632
|
const { repo, stage, action } = options;
|
|
16139
16633
|
const watch = options.watch ?? tenantControlWatches(action);
|
|
16140
16634
|
const base = { command: "tenant-control", repo, stage, action };
|
|
16141
16635
|
const since = (deps.now ?? Date.now)();
|
|
16142
|
-
const d = await deps.dispatchTenantControl({ repo, stage, action });
|
|
16636
|
+
const d = await deps.dispatchTenantControl({ repo, stage, action, lines: options.lines });
|
|
16143
16637
|
if (!d.ok) {
|
|
16144
16638
|
const transport = d.category === "transport-failed";
|
|
16145
16639
|
return {
|
|
@@ -16156,10 +16650,12 @@ async function runTenantControl(deps, options) {
|
|
|
16156
16650
|
if (action === "retire") {
|
|
16157
16651
|
result.category = conclusion === "success" ? "retired" : conclusion === "failure" ? "control-run-failed" : "wait-timeout";
|
|
16158
16652
|
}
|
|
16159
|
-
if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets" || action === "verify-broker")) {
|
|
16653
|
+
if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets" || action === "verify-broker" || action === "logs")) {
|
|
16160
16654
|
const output = extractControlOutputFromLog(await fetchControlRunLog(deps, runId));
|
|
16161
16655
|
if (action === "status") {
|
|
16162
16656
|
result.serviceState = parseStatusSnippet(output).serviceState;
|
|
16657
|
+
} else if (action === "logs") {
|
|
16658
|
+
result.logs = output;
|
|
16163
16659
|
} else if (action === "verify-secrets") {
|
|
16164
16660
|
result.secrets = parseVerifySecrets(output);
|
|
16165
16661
|
result.secretsRaw = output;
|
|
@@ -18130,6 +18626,101 @@ function docsAuditStatus(fetch2, opts) {
|
|
|
18130
18626
|
return { ok: true, state: "clean", line: `docs audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
|
|
18131
18627
|
}
|
|
18132
18628
|
|
|
18629
|
+
// src/project-info-sync.ts
|
|
18630
|
+
var import_node_fs22 = require("node:fs");
|
|
18631
|
+
var import_node_path20 = require("node:path");
|
|
18632
|
+
var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!, $readme: String!) {
|
|
18633
|
+
updateProjectV2(input: { projectId: $projectId, shortDescription: $shortDescription, readme: $readme }) {
|
|
18634
|
+
projectV2 { id }
|
|
18635
|
+
}
|
|
18636
|
+
}`;
|
|
18637
|
+
function shortDescriptionFromReadme(markdown) {
|
|
18638
|
+
const lines = markdown.replace(/\r/g, "").split("\n");
|
|
18639
|
+
const h1 = lines.findIndex((line) => /^#\s+\S/.test(line.trim()));
|
|
18640
|
+
const paragraph = [];
|
|
18641
|
+
for (const raw of lines.slice(h1 >= 0 ? h1 + 1 : 0)) {
|
|
18642
|
+
const line = raw.trim();
|
|
18643
|
+
if (!line) {
|
|
18644
|
+
if (paragraph.length) break;
|
|
18645
|
+
continue;
|
|
18646
|
+
}
|
|
18647
|
+
if (/^(?:#|<!--|!\[|\[!\[|<img\b)/i.test(line)) {
|
|
18648
|
+
if (paragraph.length) break;
|
|
18649
|
+
continue;
|
|
18650
|
+
}
|
|
18651
|
+
paragraph.push(line);
|
|
18652
|
+
}
|
|
18653
|
+
const text = paragraph.join(" ").replace(/!\[[^\]]*]\([^)]*\)/g, "").replace(/\[([^\]]+)]\([^)]*\)/g, "$1").replace(/[*_`~]|<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
18654
|
+
if (!text) throw new Error("org project sync-info: README.md has no reader-facing description below its H1");
|
|
18655
|
+
const sentence = text.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? text;
|
|
18656
|
+
return sentence.length <= 240 ? sentence : `${sentence.slice(0, 237).trimEnd()}...`;
|
|
18657
|
+
}
|
|
18658
|
+
function entriesFor(project2, projects) {
|
|
18659
|
+
return project2.projectId ? projects.filter((entry) => entry.projectId === project2.projectId) : [project2];
|
|
18660
|
+
}
|
|
18661
|
+
function branchFor(repo, projects) {
|
|
18662
|
+
const entry = projects.find((p) => (p.repos ?? []).some((r) => r.toLowerCase() === repo.toLowerCase()));
|
|
18663
|
+
return typeof entry?.branch === "string" && entry.branch.trim() ? entry.branch.trim() : entry?.releaseTrack === "trunk" || entry?.class === "content" ? "main" : "development";
|
|
18664
|
+
}
|
|
18665
|
+
function sharedName(entries, fallback) {
|
|
18666
|
+
const names = entries.map((entry) => entry.name?.trim()).filter((name) => Boolean(name));
|
|
18667
|
+
if (names.length <= 1) return names[0] ?? fallback;
|
|
18668
|
+
let prefix = names[0];
|
|
18669
|
+
for (const name of names.slice(1)) {
|
|
18670
|
+
while (prefix && !name.toLowerCase().startsWith(prefix.toLowerCase())) prefix = prefix.slice(0, -1);
|
|
18671
|
+
}
|
|
18672
|
+
return prefix.replace(/[-_\s]+$/, "") || fallback;
|
|
18673
|
+
}
|
|
18674
|
+
function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
|
|
18675
|
+
if (!project2.projectId) throw new Error(`org project sync-info: ${targetRepo2} registry META has no projectId`);
|
|
18676
|
+
const readmePath = (0, import_node_path20.join)(repoRoot2, "README.md");
|
|
18677
|
+
if (!(0, import_node_fs22.existsSync)(readmePath)) throw new Error(`org project sync-info: ${targetRepo2} has no README.md`);
|
|
18678
|
+
const entries = entriesFor(project2, projects);
|
|
18679
|
+
const memberRepos = [...new Set(entries.flatMap((entry) => entry.repos ?? []))].filter((repo) => /^[^/]+\/[^/]+$/.test(repo)).sort((a, b) => a.localeCompare(b));
|
|
18680
|
+
const projectName = sharedName(entries, project2.name?.trim() || targetRepo2.split("/").pop() || targetRepo2);
|
|
18681
|
+
if (!memberRepos.length) throw new Error(`org project sync-info: project ${projectName} has no registered member repos`);
|
|
18682
|
+
const entryNames = entries.map((entry) => entry.name?.trim()).filter((name) => Boolean(name));
|
|
18683
|
+
const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0, import_node_fs22.readFileSync)(readmePath, "utf8")) : `Shared work across ${new Intl.ListFormat("en", { type: "conjunction" }).format(entryNames)}.`;
|
|
18684
|
+
const lines = [
|
|
18685
|
+
`# ${projectName}`,
|
|
18686
|
+
"",
|
|
18687
|
+
shortDescription,
|
|
18688
|
+
"",
|
|
18689
|
+
"## Member repos",
|
|
18690
|
+
"",
|
|
18691
|
+
...memberRepos.map((repo) => {
|
|
18692
|
+
const entry = projects.find((p) => (p.repos ?? []).some((r) => r.toLowerCase() === repo.toLowerCase()));
|
|
18693
|
+
const name = entry?.name?.trim() || repo.split("/")[1];
|
|
18694
|
+
const base = `https://github.com/${repo}`;
|
|
18695
|
+
const branch = branchFor(repo, projects);
|
|
18696
|
+
return `- [${name}](${base}) \u2014 [README](${base}/blob/${branch}/README.md) \xB7 [architecture](${base}/blob/${branch}/architecture.md)`;
|
|
18697
|
+
})
|
|
18698
|
+
];
|
|
18699
|
+
const targetBase = `https://github.com/${targetRepo2}`;
|
|
18700
|
+
const targetBranch = branchFor(targetRepo2, projects);
|
|
18701
|
+
const orgDocs = [
|
|
18702
|
+
(0, import_node_fs22.existsSync)((0, import_node_path20.join)(repoRoot2, "docs", "org-readme.md")) ? `- [Org identity](${targetBase}/blob/${targetBranch}/docs/org-readme.md)` : "",
|
|
18703
|
+
(0, import_node_fs22.existsSync)((0, import_node_path20.join)(repoRoot2, "docs", "org-architecture.md")) ? `- [Org architecture](${targetBase}/blob/${targetBranch}/docs/org-architecture.md)` : ""
|
|
18704
|
+
].filter(Boolean);
|
|
18705
|
+
if (orgDocs.length) lines.push("", "## Organisation docs", "", ...orgDocs);
|
|
18706
|
+
return { projectId: project2.projectId, projectName, targetRepo: targetRepo2, memberRepos, shortDescription, readme: `${lines.join("\n")}
|
|
18707
|
+
` };
|
|
18708
|
+
}
|
|
18709
|
+
async function syncProjectInfo(plan, client, apply) {
|
|
18710
|
+
if (apply) {
|
|
18711
|
+
await client.graphql(UPDATE_PROJECT_INFO, {
|
|
18712
|
+
projectId: plan.projectId,
|
|
18713
|
+
shortDescription: plan.shortDescription,
|
|
18714
|
+
readme: plan.readme
|
|
18715
|
+
});
|
|
18716
|
+
}
|
|
18717
|
+
return {
|
|
18718
|
+
...plan,
|
|
18719
|
+
applied: apply,
|
|
18720
|
+
note: apply ? `Project ${plan.projectName} information synchronized` : `Project ${plan.projectName} information would be synchronized (dry-run; pass --apply)`
|
|
18721
|
+
};
|
|
18722
|
+
}
|
|
18723
|
+
|
|
18133
18724
|
// src/oauth.ts
|
|
18134
18725
|
var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
|
|
18135
18726
|
var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
|
|
@@ -18968,8 +19559,8 @@ function writeError(res) {
|
|
|
18968
19559
|
}
|
|
18969
19560
|
|
|
18970
19561
|
// src/secrets-commands.ts
|
|
18971
|
-
var
|
|
18972
|
-
var
|
|
19562
|
+
var import_node_fs23 = require("node:fs");
|
|
19563
|
+
var import_node_path21 = require("node:path");
|
|
18973
19564
|
var import_node_os8 = require("node:os");
|
|
18974
19565
|
|
|
18975
19566
|
// src/project-runtime.ts
|
|
@@ -19093,18 +19684,18 @@ function collectMap(value, previous = []) {
|
|
|
19093
19684
|
return [...previous, value];
|
|
19094
19685
|
}
|
|
19095
19686
|
async function decryptRailsCredentials(input) {
|
|
19096
|
-
const appDir = (0,
|
|
19687
|
+
const appDir = (0, import_node_path21.resolve)(input.appDir ?? process.cwd());
|
|
19097
19688
|
const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
|
|
19098
19689
|
const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
|
|
19099
|
-
const credentialsPath = (0,
|
|
19100
|
-
const masterKeyPath = (0,
|
|
19690
|
+
const credentialsPath = (0, import_node_path21.resolve)(appDir, credentialsFile);
|
|
19691
|
+
const masterKeyPath = (0, import_node_path21.resolve)(appDir, masterKeyFile);
|
|
19101
19692
|
const env = {
|
|
19102
19693
|
...process.env,
|
|
19103
19694
|
MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
|
|
19104
19695
|
MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
|
|
19105
19696
|
};
|
|
19106
|
-
if ((0,
|
|
19107
|
-
env.RAILS_MASTER_KEY = (0,
|
|
19697
|
+
if ((0, import_node_fs23.existsSync)(masterKeyPath)) {
|
|
19698
|
+
env.RAILS_MASTER_KEY = (0, import_node_fs23.readFileSync)(masterKeyPath, "utf8").trim();
|
|
19108
19699
|
}
|
|
19109
19700
|
const script = [
|
|
19110
19701
|
'require "json"',
|
|
@@ -19114,9 +19705,9 @@ async function decryptRailsCredentials(input) {
|
|
|
19114
19705
|
'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
|
|
19115
19706
|
"puts JSON.generate(config.config)"
|
|
19116
19707
|
].join("\n");
|
|
19117
|
-
const scriptDir = (0,
|
|
19118
|
-
const scriptPath = (0,
|
|
19119
|
-
(0,
|
|
19708
|
+
const scriptDir = (0, import_node_fs23.mkdtempSync)((0, import_node_path21.join)((0, import_node_os8.tmpdir)(), "mmi-rails-decrypt-"));
|
|
19709
|
+
const scriptPath = (0, import_node_path21.join)(scriptDir, "decrypt.rb");
|
|
19710
|
+
(0, import_node_fs23.writeFileSync)(scriptPath, script, "utf8");
|
|
19120
19711
|
try {
|
|
19121
19712
|
const args = ["exec", "ruby", scriptPath];
|
|
19122
19713
|
const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
|
|
@@ -19128,7 +19719,7 @@ async function decryptRailsCredentials(input) {
|
|
|
19128
19719
|
});
|
|
19129
19720
|
return JSON.parse(stdout);
|
|
19130
19721
|
} finally {
|
|
19131
|
-
(0,
|
|
19722
|
+
(0, import_node_fs23.rmSync)(scriptDir, { recursive: true, force: true });
|
|
19132
19723
|
}
|
|
19133
19724
|
}
|
|
19134
19725
|
async function readSecretStdin() {
|
|
@@ -19218,7 +19809,7 @@ function registerSecretsCommands(program3) {
|
|
|
19218
19809
|
let body;
|
|
19219
19810
|
if (o.file) {
|
|
19220
19811
|
try {
|
|
19221
|
-
body = (0,
|
|
19812
|
+
body = (0, import_node_fs23.readFileSync)((0, import_node_path21.resolve)(o.file), "utf8");
|
|
19222
19813
|
} catch (e) {
|
|
19223
19814
|
return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
|
|
19224
19815
|
}
|
|
@@ -19323,7 +19914,7 @@ function registerSecretsCommands(program3) {
|
|
|
19323
19914
|
{
|
|
19324
19915
|
...d,
|
|
19325
19916
|
decryptRailsCredentials,
|
|
19326
|
-
removeFile: (path2) => (0,
|
|
19917
|
+
removeFile: (path2) => (0, import_node_fs23.unlinkSync)((0, import_node_path21.resolve)(o.appDir ?? process.cwd(), path2))
|
|
19327
19918
|
},
|
|
19328
19919
|
{
|
|
19329
19920
|
repo: o.repo,
|
|
@@ -19517,7 +20108,7 @@ function checkGithubPools(probe) {
|
|
|
19517
20108
|
}
|
|
19518
20109
|
|
|
19519
20110
|
// src/box-commands.ts
|
|
19520
|
-
var
|
|
20111
|
+
var import_node_fs24 = require("node:fs");
|
|
19521
20112
|
|
|
19522
20113
|
// src/box.ts
|
|
19523
20114
|
var BOX_KEYS = {
|
|
@@ -19720,7 +20311,7 @@ function registerBoxCommands(program3) {
|
|
|
19720
20311
|
}
|
|
19721
20312
|
if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
|
|
19722
20313
|
else if (o.ssh && o.script) {
|
|
19723
|
-
(0,
|
|
20314
|
+
(0, import_node_fs24.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
|
|
19724
20315
|
console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
|
|
19725
20316
|
} else if (o.ssh) console.log(`${formatSshRecipe(found)}
|
|
19726
20317
|
${SSH_RECIPE_AGENT_NOTE}`);
|
|
@@ -20412,7 +21003,7 @@ function registerSchedulesCommands(program3) {
|
|
|
20412
21003
|
|
|
20413
21004
|
// src/file-lock.ts
|
|
20414
21005
|
var import_promises4 = require("node:fs/promises");
|
|
20415
|
-
var
|
|
21006
|
+
var import_node_path22 = require("node:path");
|
|
20416
21007
|
var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
20417
21008
|
var IMMEDIATE_RETRY_BUDGET = 3;
|
|
20418
21009
|
var FileLockBusyError = class extends Error {
|
|
@@ -20497,7 +21088,7 @@ async function releaseFileLock(lockPath, guard) {
|
|
|
20497
21088
|
}
|
|
20498
21089
|
async function withFileLock(lockPath, opts, fn) {
|
|
20499
21090
|
const resolved = resolveFileLockOpts(opts);
|
|
20500
|
-
await (0, import_promises4.mkdir)((0,
|
|
21091
|
+
await (0, import_promises4.mkdir)((0, import_node_path22.dirname)(lockPath), { recursive: true }).catch(() => void 0);
|
|
20501
21092
|
const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
|
|
20502
21093
|
try {
|
|
20503
21094
|
return await fn();
|
|
@@ -20508,7 +21099,7 @@ async function withFileLock(lockPath, opts, fn) {
|
|
|
20508
21099
|
|
|
20509
21100
|
// src/schedules-lift-command.ts
|
|
20510
21101
|
var import_promises5 = require("node:fs/promises");
|
|
20511
|
-
var
|
|
21102
|
+
var import_node_path23 = require("node:path");
|
|
20512
21103
|
|
|
20513
21104
|
// src/schedules-lift.ts
|
|
20514
21105
|
var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor", "llm", "output", "breaks", "kill"];
|
|
@@ -20614,7 +21205,7 @@ async function readWorkflowFiles(dir) {
|
|
|
20614
21205
|
const files = [];
|
|
20615
21206
|
for (const name of names.sort()) {
|
|
20616
21207
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
20617
|
-
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises5.readFile)((0,
|
|
21208
|
+
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises5.readFile)((0, import_node_path23.join)(dir, name), "utf8") });
|
|
20618
21209
|
}
|
|
20619
21210
|
return files;
|
|
20620
21211
|
}
|
|
@@ -21156,7 +21747,63 @@ function registerQueryCommands(program3) {
|
|
|
21156
21747
|
}
|
|
21157
21748
|
|
|
21158
21749
|
// src/bootstrap-commands.ts
|
|
21159
|
-
var
|
|
21750
|
+
var import_node_fs25 = require("node:fs");
|
|
21751
|
+
var import_node_os9 = require("node:os");
|
|
21752
|
+
var import_node_path24 = require("node:path");
|
|
21753
|
+
|
|
21754
|
+
// src/bootstrap-drift.ts
|
|
21755
|
+
function byteComparableSeeds(manifest, cls) {
|
|
21756
|
+
return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
|
|
21757
|
+
}
|
|
21758
|
+
function compareSeedBytes(hubContent, repoContent) {
|
|
21759
|
+
if (repoContent === null) return "absent";
|
|
21760
|
+
const normalize = (s) => s.replace(/\r\n/g, "\n");
|
|
21761
|
+
return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
|
|
21762
|
+
}
|
|
21763
|
+
function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
|
|
21764
|
+
const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
|
|
21765
|
+
const slug = repo.includes("/") ? repo.slice(repo.indexOf("/") + 1).toLowerCase() : repo.toLowerCase();
|
|
21766
|
+
const findings = [];
|
|
21767
|
+
for (const seed of seeds) {
|
|
21768
|
+
const hub = hubContents.get(seed.target);
|
|
21769
|
+
if (hub == null) {
|
|
21770
|
+
findings.push({
|
|
21771
|
+
repo,
|
|
21772
|
+
target: seed.target,
|
|
21773
|
+
state: "drift",
|
|
21774
|
+
detail: `the Hub's own copy could not be read \u2014 the manifest declares a file MMI-Hub does not have`
|
|
21775
|
+
});
|
|
21776
|
+
continue;
|
|
21777
|
+
}
|
|
21778
|
+
const state = compareSeedBytes(hub, byTarget.get(seed.target) ?? null);
|
|
21779
|
+
if (state === "match") continue;
|
|
21780
|
+
const why = seed.waivers?.[slug];
|
|
21781
|
+
if (why) {
|
|
21782
|
+
findings.push({ repo, target: seed.target, state: "waived", detail: `${state} \u2014 waived: ${why}` });
|
|
21783
|
+
continue;
|
|
21784
|
+
}
|
|
21785
|
+
findings.push({
|
|
21786
|
+
repo,
|
|
21787
|
+
target: seed.target,
|
|
21788
|
+
state,
|
|
21789
|
+
detail: state === "absent" ? "declared org-owned in the manifest but not present on the base branch" : "differs from MMI-Hub's copy \u2014 propagate with `mmi-cli bootstrap apply <repo> --only <target> --execute`, or, if this repo is RIGHT to differ, declare a waiver for it on the seed in the manifest (#3842)"
|
|
21790
|
+
});
|
|
21791
|
+
}
|
|
21792
|
+
return findings;
|
|
21793
|
+
}
|
|
21794
|
+
function renderSeedDriftReport(findings, reposAudited, seedsPerRepo) {
|
|
21795
|
+
const lines = [`org-seed drift: ${reposAudited} repo(s) audited, ${seedsPerRepo} byte-comparable seed(s) each`];
|
|
21796
|
+
const waived = findings.filter((f) => f.state === "waived");
|
|
21797
|
+
const real = findings.filter((f) => f.state !== "waived");
|
|
21798
|
+
for (const f of waived) lines.push(` WAIVED ${f.repo} ${f.target} \u2014 ${f.detail}`);
|
|
21799
|
+
if (!real.length) {
|
|
21800
|
+
lines.push(" clean \u2014 every repo carries the Hub's bytes for every org-owned whole-file seed" + (waived.length ? ` (${waived.length} declared waiver(s) above)` : ""));
|
|
21801
|
+
return lines.join("\n");
|
|
21802
|
+
}
|
|
21803
|
+
for (const f of real) lines.push(` ${f.state.toUpperCase().padEnd(6)} ${f.repo} ${f.target} \u2014 ${f.detail}`);
|
|
21804
|
+
lines.push(` \u2014 ${real.length} finding(s)${waived.length ? `, ${waived.length} waived` : ""}`);
|
|
21805
|
+
return lines.join("\n");
|
|
21806
|
+
}
|
|
21160
21807
|
|
|
21161
21808
|
// src/bootstrap-verify.ts
|
|
21162
21809
|
var TRAIN_BRANCHES2 = ["development", "rc", "main"];
|
|
@@ -21716,13 +22363,13 @@ function registerBootstrapCommands(program3) {
|
|
|
21716
22363
|
client: defaultGitHubClient(),
|
|
21717
22364
|
projectMeta: meta,
|
|
21718
22365
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
21719
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
22366
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs25.existsSync)(path2) ? (0, import_node_fs25.readFileSync)(path2, "utf8") : null,
|
|
21720
22367
|
// requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
|
|
21721
22368
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
21722
22369
|
// #3689: the same committed map the org access audit reads (#3664), so a sanctioned admin is not a
|
|
21723
22370
|
// permanent bootstrap failure on one surface and an intended state on the other. Absent file → no
|
|
21724
22371
|
// sanction, which is the pre-#3664 behaviour.
|
|
21725
|
-
sanctionedAdmins: (0,
|
|
22372
|
+
sanctionedAdmins: (0, import_node_fs25.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs25.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
|
|
21726
22373
|
requiredGcpApis: (() => {
|
|
21727
22374
|
const v = meta?.requiredGcpApis;
|
|
21728
22375
|
if (Array.isArray(v)) return v;
|
|
@@ -21757,12 +22404,76 @@ function registerBootstrapCommands(program3) {
|
|
|
21757
22404
|
else console.log(renderOrgRulesetDriftReport(plan));
|
|
21758
22405
|
if (plan.action !== "noop") process.exitCode = 1;
|
|
21759
22406
|
});
|
|
21760
|
-
bootstrap.command("
|
|
22407
|
+
bootstrap.command("drift").description("#3818: compare every org-owned whole-file seed against MMI-Hub's copy across the registry roster; read-only").option("--repo <owner/repo>", "audit one repo instead of the roster (never a fleet verdict)").option("--json", "machine-readable output").action(async () => {
|
|
22408
|
+
const o = { repo: rawValue("--repo", ""), json: rawFlag("--json") };
|
|
22409
|
+
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
22410
|
+
if (!(0, import_node_fs25.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
|
|
22411
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs25.readFileSync)(manifestPath, "utf8"));
|
|
22412
|
+
const hubContents = /* @__PURE__ */ new Map();
|
|
22413
|
+
for (const s of manifest.seeds) {
|
|
22414
|
+
if (s.ownership !== "org" || s.source !== "self") continue;
|
|
22415
|
+
hubContents.set(s.target, (0, import_node_fs25.existsSync)(s.target) ? (0, import_node_fs25.readFileSync)(s.target, "utf8") : null);
|
|
22416
|
+
}
|
|
22417
|
+
let targets;
|
|
22418
|
+
let classOf = (_repo) => "deployable";
|
|
22419
|
+
if (o.repo) {
|
|
22420
|
+
targets = [o.repo];
|
|
22421
|
+
} else {
|
|
22422
|
+
const projects = await fetchProjectsList(registryClientDeps(await loadConfig()));
|
|
22423
|
+
if (!projects || projects.length === 0) {
|
|
22424
|
+
return failGraceful("bootstrap drift: the registry roster is unreadable or empty \u2014 refusing to report a fleet verdict from a scope this command could not establish (#3808)");
|
|
22425
|
+
}
|
|
22426
|
+
targets = collectRegistryRepos(projects);
|
|
22427
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
22428
|
+
for (const p of projects) for (const r of p.repos ?? []) byRepo.set((r.includes("/") ? r : `mutmutco/${r}`).toLowerCase(), p.class ?? "deployable");
|
|
22429
|
+
classOf = (repo) => byRepo.get(repo.toLowerCase()) ?? "deployable";
|
|
22430
|
+
}
|
|
22431
|
+
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
22432
|
+
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
22433
|
+
const findings = [];
|
|
22434
|
+
let seedsPerRepo = 0;
|
|
22435
|
+
for (const repo of targets) {
|
|
22436
|
+
const cls = classOf(repo);
|
|
22437
|
+
const seeds = byteComparableSeeds(manifest, cls);
|
|
22438
|
+
seedsPerRepo = Math.max(seedsPerRepo, seeds.length);
|
|
22439
|
+
const baseBranch = cls === "content" ? "main" : "development";
|
|
22440
|
+
const reads = [];
|
|
22441
|
+
for (const seed of seeds) {
|
|
22442
|
+
let content = null;
|
|
22443
|
+
try {
|
|
22444
|
+
const r = await gh(["api", `repos/${repo}/contents/${enc(seed.target)}?ref=${baseBranch}`]);
|
|
22445
|
+
const parsed = JSON.parse(r.stdout);
|
|
22446
|
+
content = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : null;
|
|
22447
|
+
} catch {
|
|
22448
|
+
content = null;
|
|
22449
|
+
}
|
|
22450
|
+
reads.push({ target: seed.target, content });
|
|
22451
|
+
}
|
|
22452
|
+
findings.push(...auditRepoSeedDrift(repo, seeds, hubContents, reads));
|
|
22453
|
+
}
|
|
22454
|
+
if (o.json) {
|
|
22455
|
+
console.log(JSON.stringify({
|
|
22456
|
+
// #3842: `ok` reflects real findings; waivers ride the payload so a consumer can see every
|
|
22457
|
+
// standing exception without them counting as drift.
|
|
22458
|
+
ok: findings.every((f) => f.state === "waived"),
|
|
22459
|
+
scope: o.repo ? "single-repo" : "fleet",
|
|
22460
|
+
reposAudited: targets.length,
|
|
22461
|
+
seedsPerRepo,
|
|
22462
|
+
waived: findings.filter((f) => f.state === "waived").length,
|
|
22463
|
+
findings
|
|
22464
|
+
}, null, 2));
|
|
22465
|
+
} else {
|
|
22466
|
+
console.log(renderSeedDriftReport(findings, targets.length, seedsPerRepo));
|
|
22467
|
+
}
|
|
22468
|
+
if (findings.some((f) => f.state !== "waived")) process.exitCode = 1;
|
|
22469
|
+
});
|
|
22470
|
+
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("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
|
|
21761
22471
|
const o = {
|
|
21762
22472
|
class: rawValue("--class", "deployable"),
|
|
21763
22473
|
projectType: rawValue("--project-type", ""),
|
|
21764
22474
|
deployModel: rawValue("--deploy-model", ""),
|
|
21765
22475
|
releaseTrack: rawValue("--release-track", ""),
|
|
22476
|
+
only: rawValue("--only", ""),
|
|
21766
22477
|
execute: rawFlag("--execute"),
|
|
21767
22478
|
json: rawFlag("--json")
|
|
21768
22479
|
};
|
|
@@ -21778,13 +22489,32 @@ function registerBootstrapCommands(program3) {
|
|
|
21778
22489
|
return fail(`bootstrap apply: ${e.message}`);
|
|
21779
22490
|
}
|
|
21780
22491
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
21781
|
-
if (!(0,
|
|
21782
|
-
const manifest = loadBootstrapSeeds((0,
|
|
22492
|
+
if (!(0, import_node_fs25.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
|
|
22493
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs25.readFileSync)(manifestPath, "utf8"));
|
|
21783
22494
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
21784
22495
|
const slug = parsedRepo.slug;
|
|
22496
|
+
const onlyTarget = o.only.trim();
|
|
22497
|
+
const seedsToApply = onlyTarget ? manifest.seeds.filter((s) => s.target === onlyTarget || s.target.replace("{{REPO_SLUG}}", slug) === onlyTarget) : manifest.seeds;
|
|
22498
|
+
if (onlyTarget && !seedsToApply.length) {
|
|
22499
|
+
const known = manifest.seeds.map((s) => s.target).join("\n ");
|
|
22500
|
+
return fail(`bootstrap apply: --only '${onlyTarget}' names no seed in ${manifestPath}. Declared targets:
|
|
22501
|
+
${known}`);
|
|
22502
|
+
}
|
|
21785
22503
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
21786
|
-
const readFile9 = (p) => (0,
|
|
22504
|
+
const readFile9 = (p) => (0, import_node_fs25.existsSync)(p) ? (0, import_node_fs25.readFileSync)(p, "utf8") : null;
|
|
21787
22505
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
22506
|
+
const putSeed = async (target, content, ref, sha) => {
|
|
22507
|
+
const tmp = (0, import_node_path24.join)((0, import_node_os9.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
22508
|
+
(0, import_node_fs25.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
|
|
22509
|
+
try {
|
|
22510
|
+
await gh(contentPutInputArgs(repo, target, tmp));
|
|
22511
|
+
} finally {
|
|
22512
|
+
try {
|
|
22513
|
+
(0, import_node_fs25.unlinkSync)(tmp);
|
|
22514
|
+
} catch {
|
|
22515
|
+
}
|
|
22516
|
+
}
|
|
22517
|
+
};
|
|
21788
22518
|
const rawVars = {};
|
|
21789
22519
|
for (const value of cmdOpts.var ?? []) {
|
|
21790
22520
|
const eq = value.indexOf("=");
|
|
@@ -21849,7 +22579,7 @@ function registerBootstrapCommands(program3) {
|
|
|
21849
22579
|
}
|
|
21850
22580
|
}
|
|
21851
22581
|
const docsForIndex = [];
|
|
21852
|
-
for (const seed of
|
|
22582
|
+
for (const seed of seedsToApply) {
|
|
21853
22583
|
if (!seed.classes.includes(o.class)) continue;
|
|
21854
22584
|
if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
|
|
21855
22585
|
if (!seedMatchesProjectType(seed, applyProjectType)) continue;
|
|
@@ -21881,12 +22611,12 @@ function registerBootstrapCommands(program3) {
|
|
|
21881
22611
|
docsForIndex.push({ path: resolved.target, content: docBody });
|
|
21882
22612
|
}
|
|
21883
22613
|
if (o.execute && (action.action === "create" || action.action === "update")) {
|
|
21884
|
-
await
|
|
22614
|
+
await putSeed(resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0);
|
|
21885
22615
|
applied.push(`${action.action} ${resolved.target}`);
|
|
21886
22616
|
if (seedPlan.mode === "pr") seededToBranch++;
|
|
21887
22617
|
}
|
|
21888
22618
|
}
|
|
21889
|
-
const indexContent = seededDocsIndex(docsForIndex);
|
|
22619
|
+
const indexContent = onlyTarget ? null : seededDocsIndex(docsForIndex);
|
|
21890
22620
|
if (indexContent) {
|
|
21891
22621
|
let indexCurrent = null;
|
|
21892
22622
|
try {
|
|
@@ -21903,7 +22633,7 @@ function registerBootstrapCommands(program3) {
|
|
|
21903
22633
|
reason: indexCurrent === null ? "generated routing index (#3545)" : "routing index present \u2014 regenerate with `mmi-cli docs index --write`, which sees the whole tree"
|
|
21904
22634
|
});
|
|
21905
22635
|
if (o.execute && indexAction === "create") {
|
|
21906
|
-
await
|
|
22636
|
+
await putSeed(DOCS_INDEX_PATH, indexContent, seedPlan.ref, void 0);
|
|
21907
22637
|
applied.push(`${indexAction} ${DOCS_INDEX_PATH}`);
|
|
21908
22638
|
if (seedPlan.mode === "pr") seededToBranch++;
|
|
21909
22639
|
}
|
|
@@ -21927,9 +22657,13 @@ function registerBootstrapCommands(program3) {
|
|
|
21927
22657
|
"--head",
|
|
21928
22658
|
seedPlan.branch,
|
|
21929
22659
|
"--title",
|
|
21930
|
-
`bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
|
|
22660
|
+
onlyTarget ? `chore: propagate org-owned ${onlyTarget} from MMI-Hub` : `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
|
|
21931
22661
|
"--body",
|
|
21932
|
-
`Auto-opened by \`mmi-cli bootstrap apply
|
|
22662
|
+
onlyTarget ? `Auto-opened by \`mmi-cli bootstrap apply ${repo} --only ${onlyTarget} --execute\` (#3818).
|
|
22663
|
+
|
|
22664
|
+
\`${onlyTarget}\` is an org-owned seed declared in MMI-Hub's \`skills/bootstrap/seeds/manifest.json\`; this PR brings this repo's copy to the Hub's. It carries that file and nothing else \u2014 no labels, ruleset, merge settings or registry META were touched.
|
|
22665
|
+
|
|
22666
|
+
\`${baseBranch}\` is protected (${seedPlan.reason}), so delivery goes via this branch + PR \u2014 a direct contents PUT 409s on a protected base.` : `Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#2286): \`${baseBranch}\` is protected (${seedPlan.reason}), so the org-managed seed files are delivered via this branch + PR \u2014 a direct contents PUT 409s on a protected base ("N of N required status checks are expected").`
|
|
21933
22667
|
]);
|
|
21934
22668
|
seedPrUrl = created.url;
|
|
21935
22669
|
}
|
|
@@ -21945,7 +22679,7 @@ function registerBootstrapCommands(program3) {
|
|
|
21945
22679
|
});
|
|
21946
22680
|
applied.push(autoMergeEnabled ? `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)` : `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge refused \u2014 PR is already clean. Land it: mmi-cli pr land <n> --repo ${repo})`);
|
|
21947
22681
|
}
|
|
21948
|
-
if (o.execute && o.class === "deployable") {
|
|
22682
|
+
if (o.execute && !onlyTarget && o.class === "deployable") {
|
|
21949
22683
|
try {
|
|
21950
22684
|
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]);
|
|
21951
22685
|
applied.push("merge settings: allow_auto_merge, squash, delete-branch-on-merge");
|
|
@@ -21974,7 +22708,7 @@ function registerBootstrapCommands(program3) {
|
|
|
21974
22708
|
}
|
|
21975
22709
|
}
|
|
21976
22710
|
}
|
|
21977
|
-
if (o.execute) {
|
|
22711
|
+
if (o.execute && !onlyTarget) {
|
|
21978
22712
|
for (const l of manifest.labels) {
|
|
21979
22713
|
try {
|
|
21980
22714
|
await gh(["label", "create", l.name, "--color", l.color, "--description", l.description, "--force", "-R", repo]);
|
|
@@ -21994,17 +22728,19 @@ function registerBootstrapCommands(program3) {
|
|
|
21994
22728
|
}
|
|
21995
22729
|
}
|
|
21996
22730
|
const ddbWrites = [];
|
|
21997
|
-
let registerPayload;
|
|
21998
|
-
|
|
21999
|
-
|
|
22000
|
-
|
|
22001
|
-
|
|
22002
|
-
|
|
22003
|
-
|
|
22004
|
-
|
|
22005
|
-
|
|
22731
|
+
let registerPayload = {};
|
|
22732
|
+
if (!onlyTarget) {
|
|
22733
|
+
try {
|
|
22734
|
+
registerPayload = buildRegisterPayload(repo, o.class, vars, {
|
|
22735
|
+
projectType: o.projectType || void 0,
|
|
22736
|
+
deployModel: o.deployModel || void 0,
|
|
22737
|
+
releaseTrack: bootstrapReleaseTrack
|
|
22738
|
+
});
|
|
22739
|
+
} catch (e) {
|
|
22740
|
+
return fail(`bootstrap apply: ${e.message}`);
|
|
22741
|
+
}
|
|
22006
22742
|
}
|
|
22007
|
-
if (o.execute) {
|
|
22743
|
+
if (o.execute && !onlyTarget) {
|
|
22008
22744
|
const cfg = await loadConfig();
|
|
22009
22745
|
const res = await registerProject(registerPayload, registryClientDeps(cfg));
|
|
22010
22746
|
if (res.ok) {
|
|
@@ -22015,7 +22751,7 @@ function registerBootstrapCommands(program3) {
|
|
|
22015
22751
|
applied.push(`ddb register ${registerPayload.slug} (failed: ${why})`);
|
|
22016
22752
|
}
|
|
22017
22753
|
}
|
|
22018
|
-
if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites }, null, 2));
|
|
22754
|
+
if (o.json) console.log(JSON.stringify({ repo, class: o.class, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites }, null, 2));
|
|
22019
22755
|
else {
|
|
22020
22756
|
console.log(renderSeedPlan(actions));
|
|
22021
22757
|
if (o.execute) console.log(`
|
|
@@ -22026,12 +22762,12 @@ LIVE apply to ${repo}:
|
|
|
22026
22762
|
}
|
|
22027
22763
|
|
|
22028
22764
|
// src/stage-commands.ts
|
|
22029
|
-
var
|
|
22030
|
-
var
|
|
22765
|
+
var import_node_fs27 = require("node:fs");
|
|
22766
|
+
var import_node_path26 = require("node:path");
|
|
22031
22767
|
|
|
22032
22768
|
// src/port-registry.ts
|
|
22033
|
-
var
|
|
22034
|
-
var
|
|
22769
|
+
var import_node_fs26 = require("node:fs");
|
|
22770
|
+
var import_node_path25 = require("node:path");
|
|
22035
22771
|
|
|
22036
22772
|
// ../infra/port-geometry.mjs
|
|
22037
22773
|
var PORT_BLOCK = 100;
|
|
@@ -22045,8 +22781,8 @@ function nextPortBlock(registry2) {
|
|
|
22045
22781
|
return [base, base + PORT_SPAN];
|
|
22046
22782
|
}
|
|
22047
22783
|
function loadPortRegistry(path2) {
|
|
22048
|
-
if (!(0,
|
|
22049
|
-
const raw = JSON.parse((0,
|
|
22784
|
+
if (!(0, import_node_fs26.existsSync)(path2)) return {};
|
|
22785
|
+
const raw = JSON.parse((0, import_node_fs26.readFileSync)(path2, "utf8"));
|
|
22050
22786
|
const out = {};
|
|
22051
22787
|
for (const [key, value] of Object.entries(raw)) {
|
|
22052
22788
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -22060,9 +22796,9 @@ function ensurePortRange(repo, path2) {
|
|
|
22060
22796
|
const existing = registry2[repo];
|
|
22061
22797
|
if (existing) return existing;
|
|
22062
22798
|
const range = nextPortBlock(registry2);
|
|
22063
|
-
const raw = (0,
|
|
22799
|
+
const raw = (0, import_node_fs26.existsSync)(path2) ? JSON.parse((0, import_node_fs26.readFileSync)(path2, "utf8")) : {};
|
|
22064
22800
|
raw[repo] = range;
|
|
22065
|
-
(0,
|
|
22801
|
+
(0, import_node_fs26.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
22066
22802
|
return range;
|
|
22067
22803
|
}
|
|
22068
22804
|
function portCursorSeed(registry2) {
|
|
@@ -22084,22 +22820,22 @@ function existingPortRange(repo, registry2) {
|
|
|
22084
22820
|
return registry2[repo] ?? null;
|
|
22085
22821
|
}
|
|
22086
22822
|
function portRangeInfraAt(root, source) {
|
|
22087
|
-
const registryPath = (0,
|
|
22088
|
-
const ddbScriptPath = (0,
|
|
22089
|
-
if (!(0,
|
|
22823
|
+
const registryPath = (0, import_node_path25.join)(root, "infra", "port-ranges.json");
|
|
22824
|
+
const ddbScriptPath = (0, import_node_path25.join)(root, "infra", "port-ddb.mjs");
|
|
22825
|
+
if (!(0, import_node_fs26.existsSync)(registryPath) || !(0, import_node_fs26.existsSync)(ddbScriptPath)) return null;
|
|
22090
22826
|
return { root, source, registryPath, ddbScriptPath };
|
|
22091
22827
|
}
|
|
22092
22828
|
function resolvePortRangeInfra(cwd, packageDir) {
|
|
22093
22829
|
const direct = portRangeInfraAt(cwd, "cwd");
|
|
22094
22830
|
if (direct) return direct;
|
|
22095
|
-
for (let dir = cwd; ; dir = (0,
|
|
22096
|
-
const sibling = portRangeInfraAt((0,
|
|
22831
|
+
for (let dir = cwd; ; dir = (0, import_node_path25.dirname)(dir)) {
|
|
22832
|
+
const sibling = portRangeInfraAt((0, import_node_path25.join)(dir, "MMI-Hub"), "sibling-hub");
|
|
22097
22833
|
if (sibling) return sibling;
|
|
22098
|
-
const parent = (0,
|
|
22834
|
+
const parent = (0, import_node_path25.dirname)(dir);
|
|
22099
22835
|
if (parent === dir) break;
|
|
22100
22836
|
}
|
|
22101
22837
|
if (packageDir) {
|
|
22102
|
-
const pkgRoot = (0,
|
|
22838
|
+
const pkgRoot = (0, import_node_path25.join)(packageDir, "..", "..");
|
|
22103
22839
|
const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
|
|
22104
22840
|
if (pkgFrom) return pkgFrom;
|
|
22105
22841
|
}
|
|
@@ -22275,8 +23011,8 @@ function registerStageCommands(program3) {
|
|
|
22275
23011
|
const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
|
|
22276
23012
|
return decideStage({
|
|
22277
23013
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
22278
|
-
hasCompose: (0,
|
|
22279
|
-
hasEnvExample: (0,
|
|
23014
|
+
hasCompose: (0, import_node_fs27.existsSync)((0, import_node_path26.join)(process.cwd(), "docker-compose.yml")),
|
|
23015
|
+
hasEnvExample: (0, import_node_fs27.existsSync)((0, import_node_path26.join)(process.cwd(), ".env.example"))
|
|
22280
23016
|
});
|
|
22281
23017
|
}
|
|
22282
23018
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -22714,10 +23450,10 @@ function registerBoardCommands(program3) {
|
|
|
22714
23450
|
}
|
|
22715
23451
|
|
|
22716
23452
|
// src/merge-cleanup.ts
|
|
22717
|
-
var
|
|
23453
|
+
var import_node_fs28 = require("node:fs");
|
|
22718
23454
|
var import_promises7 = require("node:fs/promises");
|
|
22719
|
-
var
|
|
22720
|
-
var
|
|
23455
|
+
var import_node_path28 = require("node:path");
|
|
23456
|
+
var import_node_os10 = require("node:os");
|
|
22721
23457
|
var import_node_child_process13 = require("node:child_process");
|
|
22722
23458
|
|
|
22723
23459
|
// src/board-advance.ts
|
|
@@ -22804,7 +23540,7 @@ function boardAdvanceFailureMessage(result) {
|
|
|
22804
23540
|
|
|
22805
23541
|
// src/deferred-registry-store.ts
|
|
22806
23542
|
var import_promises6 = require("node:fs/promises");
|
|
22807
|
-
var
|
|
23543
|
+
var import_node_path27 = require("node:path");
|
|
22808
23544
|
var sleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
22809
23545
|
async function atomicWrite(target, contents) {
|
|
22810
23546
|
const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
@@ -22855,12 +23591,12 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
|
|
|
22855
23591
|
},
|
|
22856
23592
|
// Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
|
|
22857
23593
|
write: async (entries) => {
|
|
22858
|
-
await (0, import_promises6.mkdir)((0,
|
|
23594
|
+
await (0, import_promises6.mkdir)((0, import_node_path27.dirname)(registryPath), { recursive: true });
|
|
22859
23595
|
await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
|
|
22860
23596
|
},
|
|
22861
23597
|
// Serialized read-modify-write under the repo-wide lock (#2846).
|
|
22862
23598
|
update: async (mutate) => {
|
|
22863
|
-
await (0, import_promises6.mkdir)((0,
|
|
23599
|
+
await (0, import_promises6.mkdir)((0, import_node_path27.dirname)(registryPath), { recursive: true });
|
|
22864
23600
|
const deadline = Date.now() + opts.maxWaitMs;
|
|
22865
23601
|
for (; ; ) {
|
|
22866
23602
|
const guard = await acquireLock(lockPath, opts, deadline);
|
|
@@ -23021,7 +23757,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
23021
23757
|
);
|
|
23022
23758
|
const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
23023
23759
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
23024
|
-
const primaryRepoRoot = worktreeGitRoot ? (0,
|
|
23760
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path28.dirname)((0, import_node_path28.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
23025
23761
|
const gcActor = describeActor({ env: process.env, surface: detectSurface(process.env), cwd: process.cwd() });
|
|
23026
23762
|
const owners = readWorktreeOwners(primaryRepoRoot);
|
|
23027
23763
|
const removalNow = Date.now();
|
|
@@ -23052,7 +23788,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
23052
23788
|
const cleanup = await cleanupPrMergeLocalBranch(branch.branch, {
|
|
23053
23789
|
beforeWorktrees,
|
|
23054
23790
|
startingPath: branch.worktreePath,
|
|
23055
|
-
pathExists: (p) => (0,
|
|
23791
|
+
pathExists: (p) => (0, import_node_fs28.existsSync)(p),
|
|
23056
23792
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
23057
23793
|
teardownWorktreeStage,
|
|
23058
23794
|
deferredStore,
|
|
@@ -23080,7 +23816,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
23080
23816
|
for (const wt of worktreeDirsToRemove) {
|
|
23081
23817
|
try {
|
|
23082
23818
|
const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
|
|
23083
|
-
realpath: (path2) => (0,
|
|
23819
|
+
realpath: (path2) => (0, import_node_fs28.realpathSync)(path2)
|
|
23084
23820
|
});
|
|
23085
23821
|
if (!cleanupTarget.ok) {
|
|
23086
23822
|
result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
|
|
@@ -23145,13 +23881,13 @@ async function composeOverrideBodyFile(prNumber, repoArgs, gh) {
|
|
|
23145
23881
|
const commits = JSON.parse(raw).commits ?? [];
|
|
23146
23882
|
const body = squashBodyWithOverride(commits.map((c) => ({ headline: c.messageHeadline ?? "", body: c.messageBody ?? "" })), process.cwd());
|
|
23147
23883
|
if (!body) return void 0;
|
|
23148
|
-
const dir = (0,
|
|
23149
|
-
const path2 = (0,
|
|
23150
|
-
(0,
|
|
23884
|
+
const dir = (0, import_node_fs28.mkdtempSync)((0, import_node_path28.join)((0, import_node_os10.tmpdir)(), "mmi-squash-body-"));
|
|
23885
|
+
const path2 = (0, import_node_path28.join)(dir, "body.txt");
|
|
23886
|
+
(0, import_node_fs28.writeFileSync)(path2, `${body}
|
|
23151
23887
|
`, "utf8");
|
|
23152
23888
|
return { path: path2, cleanup: () => {
|
|
23153
23889
|
try {
|
|
23154
|
-
(0,
|
|
23890
|
+
(0, import_node_fs28.rmSync)(dir, { recursive: true, force: true });
|
|
23155
23891
|
} catch {
|
|
23156
23892
|
}
|
|
23157
23893
|
} };
|
|
@@ -23273,13 +24009,13 @@ var realWorktreeDirRemover = {
|
|
|
23273
24009
|
probe: (p) => {
|
|
23274
24010
|
let st;
|
|
23275
24011
|
try {
|
|
23276
|
-
st = (0,
|
|
24012
|
+
st = (0, import_node_fs28.lstatSync)(p);
|
|
23277
24013
|
} catch {
|
|
23278
24014
|
return null;
|
|
23279
24015
|
}
|
|
23280
24016
|
if (st.isSymbolicLink()) return "link";
|
|
23281
24017
|
try {
|
|
23282
|
-
(0,
|
|
24018
|
+
(0, import_node_fs28.readlinkSync)(p);
|
|
23283
24019
|
return "link";
|
|
23284
24020
|
} catch {
|
|
23285
24021
|
}
|
|
@@ -23287,7 +24023,7 @@ var realWorktreeDirRemover = {
|
|
|
23287
24023
|
},
|
|
23288
24024
|
readdir: (p) => {
|
|
23289
24025
|
try {
|
|
23290
|
-
return (0,
|
|
24026
|
+
return (0, import_node_fs28.readdirSync)(p);
|
|
23291
24027
|
} catch {
|
|
23292
24028
|
return [];
|
|
23293
24029
|
}
|
|
@@ -23296,9 +24032,9 @@ var realWorktreeDirRemover = {
|
|
|
23296
24032
|
// leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
|
|
23297
24033
|
detachLink: (p) => {
|
|
23298
24034
|
try {
|
|
23299
|
-
(0,
|
|
24035
|
+
(0, import_node_fs28.rmdirSync)(p);
|
|
23300
24036
|
} catch {
|
|
23301
|
-
(0,
|
|
24037
|
+
(0, import_node_fs28.unlinkSync)(p);
|
|
23302
24038
|
}
|
|
23303
24039
|
},
|
|
23304
24040
|
removeTree: (p) => (0, import_promises7.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
@@ -23331,9 +24067,9 @@ async function worktreeHasStageState(worktreePath) {
|
|
|
23331
24067
|
}
|
|
23332
24068
|
}
|
|
23333
24069
|
function stageStateFileBelongsToWorktree(statePath, worktreePath) {
|
|
23334
|
-
if (!(0,
|
|
24070
|
+
if (!(0, import_node_fs28.existsSync)(statePath)) return false;
|
|
23335
24071
|
try {
|
|
23336
|
-
const state = JSON.parse((0,
|
|
24072
|
+
const state = JSON.parse((0, import_node_fs28.readFileSync)(statePath, "utf8"));
|
|
23337
24073
|
const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
|
|
23338
24074
|
return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
|
|
23339
24075
|
} catch {
|
|
@@ -23540,9 +24276,9 @@ async function fetchRestCorePool(gh = defaultGhApi) {
|
|
|
23540
24276
|
}
|
|
23541
24277
|
|
|
23542
24278
|
// src/worktree-lifecycle-commands.ts
|
|
23543
|
-
var
|
|
24279
|
+
var import_node_fs29 = require("node:fs");
|
|
23544
24280
|
var import_promises8 = require("node:fs/promises");
|
|
23545
|
-
var
|
|
24281
|
+
var import_node_path29 = require("node:path");
|
|
23546
24282
|
var GH_TIMEOUT_MS = 2e4;
|
|
23547
24283
|
var DEFAULT_BASE = "origin/development";
|
|
23548
24284
|
var DEFAULT_REMOTE = "origin";
|
|
@@ -23678,7 +24414,7 @@ function classifyStaleLeaks(input) {
|
|
|
23678
24414
|
var defaultOrphanDirScanDeps = {
|
|
23679
24415
|
listDirs: (root) => {
|
|
23680
24416
|
try {
|
|
23681
|
-
return (0,
|
|
24417
|
+
return (0, import_node_fs29.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path29.join)(root, e.name));
|
|
23682
24418
|
} catch {
|
|
23683
24419
|
return [];
|
|
23684
24420
|
}
|
|
@@ -23825,13 +24561,13 @@ function registerWorktreeCommands(program3) {
|
|
|
23825
24561
|
const headBorn = await execFileP2("git", ["-C", wtPath || ".", "rev-parse", "--verify", "--quiet", "HEAD"], { timeout: GIT_TIMEOUT_MS }).then(() => true).catch(() => false);
|
|
23826
24562
|
const branch = headBorn ? (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS })).stdout.trim() : (await execFileP2("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
23827
24563
|
if (!wtPath || !branch) return fail("worktree land: not inside a git worktree");
|
|
23828
|
-
const gitFile = (0,
|
|
23829
|
-
const isLinked = (0,
|
|
24564
|
+
const gitFile = (0, import_node_path29.join)(wtPath, ".git");
|
|
24565
|
+
const isLinked = (0, import_node_fs29.existsSync)(gitFile) && (0, import_node_fs29.statSync)(gitFile).isFile();
|
|
23830
24566
|
if (apply && !isLinked) {
|
|
23831
24567
|
return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
|
|
23832
24568
|
}
|
|
23833
24569
|
const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
23834
|
-
const primaryCheckout = commonDir ? (0,
|
|
24570
|
+
const primaryCheckout = commonDir ? (0, import_node_path29.dirname)(commonDir) : wtPath;
|
|
23835
24571
|
const localBranchNames = await execFileP2("git", ["-C", primaryCheckout, "for-each-ref", "--format=%(refname:short)", "refs/heads"], { timeout: GIT_TIMEOUT_MS }).then(({ stdout }) => new Set((stdout || "").split("\n").map((l) => l.trim()).filter(Boolean))).catch(() => void 0);
|
|
23836
24572
|
const orphan = classifyOrphanedWorktree({
|
|
23837
24573
|
branch,
|
|
@@ -24018,10 +24754,10 @@ async function gatherWorktreeContext() {
|
|
|
24018
24754
|
if (s) stages.push({ path: wt.path, port: s.port });
|
|
24019
24755
|
}
|
|
24020
24756
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
24021
|
-
const primaryRepoRoot = worktreeGitRoot ? (0,
|
|
24757
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path29.dirname)((0, import_node_path29.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
24022
24758
|
const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
|
|
24023
24759
|
let orphanDirs = [];
|
|
24024
|
-
if ((0,
|
|
24760
|
+
if ((0, import_node_fs29.existsSync)(wtRoot)) {
|
|
24025
24761
|
orphanDirs = scanOrphanDirs(wtRoot, worktreeGitRoot, {
|
|
24026
24762
|
...defaultOrphanDirScanDeps,
|
|
24027
24763
|
listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
|
|
@@ -24044,9 +24780,17 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
|
|
|
24044
24780
|
}
|
|
24045
24781
|
|
|
24046
24782
|
// src/issue-commands.ts
|
|
24047
|
-
var
|
|
24783
|
+
var import_node_fs30 = require("node:fs");
|
|
24048
24784
|
var import_node_crypto5 = require("node:crypto");
|
|
24049
24785
|
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
24786
|
+
var ReparentConflictError = class extends Error {
|
|
24787
|
+
constructor(message, payload) {
|
|
24788
|
+
super(message);
|
|
24789
|
+
this.payload = payload;
|
|
24790
|
+
this.name = "ReparentConflictError";
|
|
24791
|
+
}
|
|
24792
|
+
payload;
|
|
24793
|
+
};
|
|
24050
24794
|
async function editIssue(client, options, deps = {}) {
|
|
24051
24795
|
const parsed = parseIssueRef(options.ref);
|
|
24052
24796
|
const repo = parsed.repo ?? options.defaultRepo;
|
|
@@ -24054,7 +24798,7 @@ async function editIssue(client, options, deps = {}) {
|
|
|
24054
24798
|
const url = `https://github.com/${repo}/issues/${parsed.number}`;
|
|
24055
24799
|
const patch = {};
|
|
24056
24800
|
let bodyChanged = false;
|
|
24057
|
-
const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0,
|
|
24801
|
+
const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs30.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
|
|
24058
24802
|
if (options.titleFile !== void 0) {
|
|
24059
24803
|
patch.title = await resolveIssueTitle({ title: options.title, titleFile: options.titleFile }, textDeps());
|
|
24060
24804
|
} else if (options.title !== void 0) {
|
|
@@ -24095,7 +24839,12 @@ async function editIssue(client, options, deps = {}) {
|
|
|
24095
24839
|
let parentResult;
|
|
24096
24840
|
if (options.parent) {
|
|
24097
24841
|
const run = deps.runGh ?? ghRunner;
|
|
24098
|
-
|
|
24842
|
+
try {
|
|
24843
|
+
parentResult = await linkSubIssue(run, options.parent, options.ref, repo);
|
|
24844
|
+
} catch (e) {
|
|
24845
|
+
const conflict = await classifyReparentFailure(e, run, repo, parsed.number, options.parent, "--parent");
|
|
24846
|
+
throw conflict ? new ReparentConflictError(conflict.message, conflict.payload) : e;
|
|
24847
|
+
}
|
|
24099
24848
|
}
|
|
24100
24849
|
return {
|
|
24101
24850
|
number: parsed.number,
|
|
@@ -24285,7 +25034,7 @@ ${spec.body ?? ""}`;
|
|
|
24285
25034
|
const hash = (0, import_node_crypto5.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
24286
25035
|
return `${batchKey}:${hash}`;
|
|
24287
25036
|
}
|
|
24288
|
-
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo"]);
|
|
25037
|
+
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
|
|
24289
25038
|
function validateBatchSpecs(specs) {
|
|
24290
25039
|
const errors = [];
|
|
24291
25040
|
const validated = [];
|
|
@@ -24326,10 +25075,47 @@ function validateBatchSpecs(specs) {
|
|
|
24326
25075
|
errors.push({ row, error: e.message });
|
|
24327
25076
|
continue;
|
|
24328
25077
|
}
|
|
25078
|
+
if (spec.surface !== void 0) {
|
|
25079
|
+
if (typeof spec.surface !== "string" || !spec.surface.trim()) {
|
|
25080
|
+
errors.push({ row, error: "surface must be a non-empty string" });
|
|
25081
|
+
continue;
|
|
25082
|
+
}
|
|
25083
|
+
if (!labelsCarrySurface(spec.labels)) spec.labels = [...spec.labels ?? [], surfaceLabel(spec.surface)];
|
|
25084
|
+
delete spec.surface;
|
|
25085
|
+
}
|
|
24329
25086
|
validated.push({ row, spec, priority, type: spec.type });
|
|
24330
25087
|
}
|
|
24331
25088
|
return { ok: errors.length === 0, errors, validated };
|
|
24332
25089
|
}
|
|
25090
|
+
async function preflightBatchSurfaces(validated, rowRepo, options) {
|
|
25091
|
+
const applies = /* @__PURE__ */ new Map();
|
|
25092
|
+
const appliesTo = async (repo) => {
|
|
25093
|
+
if (!applies.has(repo)) applies.set(repo, await surfaceLabelApplies(repo));
|
|
25094
|
+
return applies.get(repo);
|
|
25095
|
+
};
|
|
25096
|
+
if (options.surface) {
|
|
25097
|
+
for (const { spec } of validated) {
|
|
25098
|
+
if (labelsCarrySurface(spec.labels)) continue;
|
|
25099
|
+
if (await appliesTo(rowRepo(spec))) spec.labels = [...spec.labels ?? [], surfaceLabel(options.surface)];
|
|
25100
|
+
}
|
|
25101
|
+
}
|
|
25102
|
+
if (options.noSurface) return [];
|
|
25103
|
+
const errors = [];
|
|
25104
|
+
const cache = /* @__PURE__ */ new Map();
|
|
25105
|
+
for (const { row, spec } of validated) {
|
|
25106
|
+
const repo = rowRepo(spec);
|
|
25107
|
+
const key = [repo, labelsCarrySurface(spec.labels) ? "has" : "none"].join("::");
|
|
25108
|
+
let verdict = cache.get(key);
|
|
25109
|
+
if (!verdict) {
|
|
25110
|
+
verdict = await checkSurfaceRequirement({ repo, labels: spec.labels, command: "issue create --batch" });
|
|
25111
|
+
cache.set(key, verdict);
|
|
25112
|
+
if (verdict.warn) process.stderr.write(`${verdict.warn}
|
|
25113
|
+
`);
|
|
25114
|
+
}
|
|
25115
|
+
if (verdict.refusal) errors.push({ row, error: verdict.refusal.message });
|
|
25116
|
+
}
|
|
25117
|
+
return errors;
|
|
25118
|
+
}
|
|
24333
25119
|
async function createIssuesBatch(specs, options, deps = {}) {
|
|
24334
25120
|
const ensureLabels = deps.ensureLabels ?? ensureLabelsExist;
|
|
24335
25121
|
const client = deps.client ?? defaultGitHubClient();
|
|
@@ -24345,6 +25131,12 @@ ${lines}`);
|
|
|
24345
25131
|
throw new Error("could not resolve repo \u2014 pass --repo owner/repo or set repo per row");
|
|
24346
25132
|
}
|
|
24347
25133
|
const rowRepo = (spec) => spec.repo ?? defaultRepo;
|
|
25134
|
+
const surfaceErrors = await preflightBatchSurfaces(validation.validated, rowRepo, options);
|
|
25135
|
+
if (surfaceErrors.length) {
|
|
25136
|
+
const lines = surfaceErrors.map((e) => ` row ${e.row}: ${e.error}`).join("\n");
|
|
25137
|
+
throw new Error(`batch validation failed (${surfaceErrors.length} error(s)):
|
|
25138
|
+
${lines}`);
|
|
25139
|
+
}
|
|
24348
25140
|
const labelsByRepo = /* @__PURE__ */ new Map();
|
|
24349
25141
|
for (const { spec } of validation.validated) {
|
|
24350
25142
|
if (!spec.labels?.length) continue;
|
|
@@ -24451,7 +25243,10 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
24451
25243
|
});
|
|
24452
25244
|
console.log(JSON.stringify(result));
|
|
24453
25245
|
} catch (e) {
|
|
24454
|
-
return
|
|
25246
|
+
return failGracefulEnvelope(
|
|
25247
|
+
`issue edit failed: ${e.message}`,
|
|
25248
|
+
e instanceof ReparentConflictError ? e.payload : void 0
|
|
25249
|
+
);
|
|
24455
25250
|
}
|
|
24456
25251
|
});
|
|
24457
25252
|
mutating(
|
|
@@ -24566,7 +25361,7 @@ function extendCreateCommand(issue2, batchAttach) {
|
|
|
24566
25361
|
if (opts.batch) {
|
|
24567
25362
|
let specs;
|
|
24568
25363
|
try {
|
|
24569
|
-
const raw = (0,
|
|
25364
|
+
const raw = (0, import_node_fs30.readFileSync)(opts.batch, "utf8");
|
|
24570
25365
|
specs = JSON.parse(raw);
|
|
24571
25366
|
if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
|
|
24572
25367
|
} catch (e) {
|
|
@@ -24580,14 +25375,28 @@ ${lines}`);
|
|
|
24580
25375
|
}
|
|
24581
25376
|
const batchParent = opts.parent;
|
|
24582
25377
|
const batchPriority = opts.priority ? normalizePriority(opts.priority) : void 0;
|
|
25378
|
+
const batchSurface = typeof opts.surface === "string" && opts.surface.trim() ? opts.surface : void 0;
|
|
25379
|
+
const batchNoSurface = rawFlag("--no-surface");
|
|
24583
25380
|
if (opts.dryRun || opts.validateOnly) {
|
|
25381
|
+
const defaultRepo = await resolveRepo(opts.repo);
|
|
25382
|
+
const planRowRepo = (spec) => spec.repo ?? defaultRepo;
|
|
25383
|
+
const surfaceErrors = defaultRepo || validation.validated.every((v) => v.spec.repo) ? await preflightBatchSurfaces(validation.validated, planRowRepo, { surface: batchSurface, noSurface: batchNoSurface }) : [];
|
|
25384
|
+
if (surfaceErrors.length) {
|
|
25385
|
+
const lines = surfaceErrors.map((e) => ` row ${e.row}: ${e.error}`).join("\n");
|
|
25386
|
+
return fail(`issue create --batch: validation failed (${surfaceErrors.length} error(s)):
|
|
25387
|
+
${lines}`, {
|
|
25388
|
+
code: ERROR_CODES.ERR_MISSING_FLAG,
|
|
25389
|
+
offending_flag: "--surface"
|
|
25390
|
+
});
|
|
25391
|
+
}
|
|
24584
25392
|
const planned = validation.validated.map((v) => ({
|
|
24585
25393
|
row: v.row,
|
|
24586
25394
|
type: v.type,
|
|
24587
25395
|
title: v.spec.title,
|
|
24588
25396
|
priority: v.spec.priority ? v.priority : batchPriority ?? v.priority,
|
|
24589
25397
|
repo: v.spec.repo,
|
|
24590
|
-
...v.spec.parent ?? batchParent ? { parent: v.spec.parent ?? batchParent } : {}
|
|
25398
|
+
...v.spec.parent ?? batchParent ? { parent: v.spec.parent ?? batchParent } : {},
|
|
25399
|
+
...v.spec.labels?.length ? { labels: v.spec.labels } : {}
|
|
24591
25400
|
}));
|
|
24592
25401
|
console.log(JSON.stringify(opts.validateOnly ? { ok: true, planned } : { dry_run: true, planned }));
|
|
24593
25402
|
return;
|
|
@@ -24597,7 +25406,9 @@ ${lines}`);
|
|
|
24597
25406
|
repo: opts.repo,
|
|
24598
25407
|
idempotencyKey: opts.idempotencyKey,
|
|
24599
25408
|
parent: batchParent,
|
|
24600
|
-
priority: batchPriority
|
|
25409
|
+
priority: batchPriority,
|
|
25410
|
+
surface: batchSurface,
|
|
25411
|
+
noSurface: batchNoSurface
|
|
24601
25412
|
}, { attach: batchAttach });
|
|
24602
25413
|
console.log(JSON.stringify(result));
|
|
24603
25414
|
if (result.failures.length) process.exitCode = 1;
|
|
@@ -24624,8 +25435,8 @@ ${lines}`);
|
|
|
24624
25435
|
}
|
|
24625
25436
|
|
|
24626
25437
|
// src/train-commands.ts
|
|
24627
|
-
var
|
|
24628
|
-
var
|
|
25438
|
+
var import_node_fs31 = require("node:fs");
|
|
25439
|
+
var import_node_path30 = require("node:path");
|
|
24629
25440
|
|
|
24630
25441
|
// src/train-status.ts
|
|
24631
25442
|
function buildTrainStatusReport(input) {
|
|
@@ -24665,7 +25476,7 @@ function formatTrainStatus(r) {
|
|
|
24665
25476
|
// src/train-commands.ts
|
|
24666
25477
|
function readRepoVersion() {
|
|
24667
25478
|
try {
|
|
24668
|
-
return JSON.parse((0,
|
|
25479
|
+
return JSON.parse((0, import_node_fs31.readFileSync)((0, import_node_path30.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
24669
25480
|
} catch {
|
|
24670
25481
|
return void 0;
|
|
24671
25482
|
}
|
|
@@ -24811,9 +25622,9 @@ function registerDeployCommands(program3) {
|
|
|
24811
25622
|
}
|
|
24812
25623
|
|
|
24813
25624
|
// src/discovery-commands.ts
|
|
24814
|
-
var
|
|
24815
|
-
var
|
|
24816
|
-
var
|
|
25625
|
+
var import_node_fs32 = require("node:fs");
|
|
25626
|
+
var import_node_os11 = require("node:os");
|
|
25627
|
+
var import_node_path31 = require("node:path");
|
|
24817
25628
|
var GC_GH_TIMEOUT_MS3 = 2e4;
|
|
24818
25629
|
async function collectStatus() {
|
|
24819
25630
|
let branch = "";
|
|
@@ -24988,10 +25799,10 @@ async function collectOnboardStatus() {
|
|
|
24988
25799
|
else if (top) nextCommand = `mmi-cli board claim ${top.number} # ${top.title}`;
|
|
24989
25800
|
else nextCommand = "mmi-cli board read \u2014 no claimable items found";
|
|
24990
25801
|
}
|
|
24991
|
-
const home = (0,
|
|
25802
|
+
const home = (0, import_node_os11.homedir)();
|
|
24992
25803
|
const plugin = onboardPluginGate({
|
|
24993
|
-
readKnown: () => readFileSyncSafe((0,
|
|
24994
|
-
readSettings: () => readFileSyncSafe((0,
|
|
25804
|
+
readKnown: () => readFileSyncSafe((0, import_node_path31.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs32.readFileSync),
|
|
25805
|
+
readSettings: () => readFileSyncSafe((0, import_node_path31.join)(home, ".claude", "settings.json"), import_node_fs32.readFileSync)
|
|
24995
25806
|
});
|
|
24996
25807
|
return { track, board, registry: registry2, secrets, plugin, nextCommand };
|
|
24997
25808
|
}
|
|
@@ -26503,17 +27314,17 @@ function parseOriginRepo(remoteUrl) {
|
|
|
26503
27314
|
}
|
|
26504
27315
|
function ghHostsConfigPath(env, platform2) {
|
|
26505
27316
|
const sep2 = platform2 === "win32" ? "\\" : "/";
|
|
26506
|
-
const
|
|
27317
|
+
const join28 = (...parts) => parts.join(sep2);
|
|
26507
27318
|
const explicit = env.GH_CONFIG_DIR?.trim();
|
|
26508
|
-
if (explicit) return
|
|
27319
|
+
if (explicit) return join28(explicit, "hosts.yml");
|
|
26509
27320
|
if (platform2 === "win32") {
|
|
26510
27321
|
const appData = (env.AppData ?? env.APPDATA)?.trim();
|
|
26511
|
-
return appData ?
|
|
27322
|
+
return appData ? join28(appData, "GitHub CLI", "hosts.yml") : void 0;
|
|
26512
27323
|
}
|
|
26513
27324
|
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
26514
|
-
if (xdg) return
|
|
27325
|
+
if (xdg) return join28(xdg, "gh", "hosts.yml");
|
|
26515
27326
|
const home = env.HOME?.trim();
|
|
26516
|
-
return home ?
|
|
27327
|
+
return home ? join28(home, ".config", "gh", "hosts.yml") : void 0;
|
|
26517
27328
|
}
|
|
26518
27329
|
function parseGhHostsAccounts(yaml, host = "github.com") {
|
|
26519
27330
|
let hostIndent = null;
|
|
@@ -26563,9 +27374,9 @@ function ghAccountCaveat(announcedLogin, accounts) {
|
|
|
26563
27374
|
}
|
|
26564
27375
|
|
|
26565
27376
|
// src/doctor-io.ts
|
|
26566
|
-
var
|
|
26567
|
-
var
|
|
26568
|
-
var
|
|
27377
|
+
var import_node_fs33 = require("node:fs");
|
|
27378
|
+
var import_node_os12 = require("node:os");
|
|
27379
|
+
var import_node_path32 = require("node:path");
|
|
26569
27380
|
var import_node_child_process14 = require("node:child_process");
|
|
26570
27381
|
var import_node_util8 = require("node:util");
|
|
26571
27382
|
var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process14.execFile);
|
|
@@ -26573,7 +27384,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
|
|
|
26573
27384
|
function installedClaudePluginVersion() {
|
|
26574
27385
|
try {
|
|
26575
27386
|
const file = JSON.parse(
|
|
26576
|
-
(0,
|
|
27387
|
+
(0, import_node_fs33.readFileSync)((0, import_node_path32.join)((0, import_node_os12.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
|
|
26577
27388
|
);
|
|
26578
27389
|
const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
|
|
26579
27390
|
if (versions.length === 0) return void 0;
|
|
@@ -26613,13 +27424,13 @@ function worktreeRootSync() {
|
|
|
26613
27424
|
}
|
|
26614
27425
|
var gitignorePath = () => {
|
|
26615
27426
|
const root = worktreeRootSync();
|
|
26616
|
-
return root === null ? null : (0,
|
|
27427
|
+
return root === null ? null : (0, import_node_path32.join)(root, ".gitignore");
|
|
26617
27428
|
};
|
|
26618
27429
|
function readGitignore() {
|
|
26619
27430
|
const path2 = gitignorePath();
|
|
26620
27431
|
if (path2 === null) return null;
|
|
26621
27432
|
try {
|
|
26622
|
-
return (0,
|
|
27433
|
+
return (0, import_node_fs33.readFileSync)(path2, "utf8");
|
|
26623
27434
|
} catch {
|
|
26624
27435
|
return null;
|
|
26625
27436
|
}
|
|
@@ -26628,7 +27439,7 @@ function writeGitignore(content) {
|
|
|
26628
27439
|
const path2 = gitignorePath();
|
|
26629
27440
|
if (path2 === null) return false;
|
|
26630
27441
|
try {
|
|
26631
|
-
(0,
|
|
27442
|
+
(0, import_node_fs33.writeFileSync)(path2, content, "utf8");
|
|
26632
27443
|
return true;
|
|
26633
27444
|
} catch {
|
|
26634
27445
|
return false;
|
|
@@ -26652,7 +27463,7 @@ async function repoRoot() {
|
|
|
26652
27463
|
}
|
|
26653
27464
|
function hasRepoLocalWorktrees() {
|
|
26654
27465
|
const root = worktreeRootSync();
|
|
26655
|
-
return root !== null && (0,
|
|
27466
|
+
return root !== null && (0, import_node_fs33.existsSync)((0, import_node_path32.join)(root, ".worktrees"));
|
|
26656
27467
|
}
|
|
26657
27468
|
|
|
26658
27469
|
// src/index.ts
|
|
@@ -26667,7 +27478,8 @@ async function readDocsAuditFetch(repo) {
|
|
|
26667
27478
|
const list = await fetchDocsAuditList(registryClientDeps(await loadConfig()));
|
|
26668
27479
|
if ("notArmed" in list) return { notArmed: true };
|
|
26669
27480
|
if (!list.ok) return { ok: false, error: list.error };
|
|
26670
|
-
const
|
|
27481
|
+
const wanted = repo.toLowerCase();
|
|
27482
|
+
const row = list.rows.find((r) => String(r.repo ?? "").toLowerCase() === wanted);
|
|
26671
27483
|
return {
|
|
26672
27484
|
ok: true,
|
|
26673
27485
|
verdict: row ? { repo: row.repo, date: row.date, shaRange: row.shaRange, outcome: row.outcome, checkerVendor: row.checkerVendor } : null
|
|
@@ -26687,8 +27499,8 @@ ${r.stderr ?? ""}`).catch(() => "");
|
|
|
26687
27499
|
function ghMultiAccountCaveat(announcedLogin) {
|
|
26688
27500
|
try {
|
|
26689
27501
|
const hostsPath = ghHostsConfigPath(process.env, process.platform);
|
|
26690
|
-
if (!hostsPath || !(0,
|
|
26691
|
-
return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0,
|
|
27502
|
+
if (!hostsPath || !(0, import_node_fs34.existsSync)(hostsPath)) return void 0;
|
|
27503
|
+
return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs34.readFileSync)(hostsPath, "utf8")));
|
|
26692
27504
|
} catch {
|
|
26693
27505
|
return void 0;
|
|
26694
27506
|
}
|
|
@@ -26696,12 +27508,12 @@ function ghMultiAccountCaveat(announcedLogin) {
|
|
|
26696
27508
|
var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
|
|
26697
27509
|
var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
|
|
26698
27510
|
function envHealLockPath(home) {
|
|
26699
|
-
return (0,
|
|
27511
|
+
return (0, import_node_path33.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
|
|
26700
27512
|
}
|
|
26701
27513
|
async function withEnvHealLock(what, run) {
|
|
26702
27514
|
try {
|
|
26703
27515
|
return await withFileLock(
|
|
26704
|
-
envHealLockPath((0,
|
|
27516
|
+
envHealLockPath((0, import_node_os13.homedir)()),
|
|
26705
27517
|
{ staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
|
|
26706
27518
|
run
|
|
26707
27519
|
);
|
|
@@ -26804,7 +27616,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
26804
27616
|
const configRoot = surfaceConfigRoot(surface);
|
|
26805
27617
|
const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
|
|
26806
27618
|
const plan = buildPluginCachePlan(
|
|
26807
|
-
(0,
|
|
27619
|
+
(0, import_node_os13.homedir)(),
|
|
26808
27620
|
running,
|
|
26809
27621
|
pluginCacheFsDeps(configRoot, () => 0),
|
|
26810
27622
|
{ configRoot, includeStaging: surface !== "codex" }
|
|
@@ -26857,11 +27669,11 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
26857
27669
|
marketplaceRows: () => {
|
|
26858
27670
|
try {
|
|
26859
27671
|
if (detectSurface(process.env) === "codex") return [];
|
|
26860
|
-
const home = (0,
|
|
27672
|
+
const home = (0, import_node_os13.homedir)();
|
|
26861
27673
|
return marketplaceRows(
|
|
26862
27674
|
MMI_MARKETPLACE_NAME,
|
|
26863
|
-
readFileSyncSafe((0,
|
|
26864
|
-
readFileSyncSafe((0,
|
|
27675
|
+
readFileSyncSafe((0, import_node_path33.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs34.readFileSync),
|
|
27676
|
+
readFileSyncSafe((0, import_node_path33.join)(home, ".claude", "settings.json"), import_node_fs34.readFileSync)
|
|
26865
27677
|
);
|
|
26866
27678
|
} catch {
|
|
26867
27679
|
return [];
|
|
@@ -27067,19 +27879,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
|
27067
27879
|
});
|
|
27068
27880
|
var rules = program2.command("rules").description("org-managed .gitignore delivery");
|
|
27069
27881
|
rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
|
|
27070
|
-
const path2 = (0,
|
|
27071
|
-
const current = (0,
|
|
27882
|
+
const path2 = (0, import_node_path33.join)(process.cwd(), ".gitignore");
|
|
27883
|
+
const current = (0, import_node_fs34.existsSync)(path2) ? (0, import_node_fs34.readFileSync)(path2, "utf8") : null;
|
|
27072
27884
|
const plan = planManagedGitignore(current);
|
|
27073
27885
|
const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
27074
27886
|
if (opts.json) {
|
|
27075
|
-
if (opts.write && plan.changed) (0,
|
|
27887
|
+
if (opts.write && plan.changed) (0, import_node_fs34.writeFileSync)(path2, plan.content, "utf8");
|
|
27076
27888
|
console.log(JSON.stringify(plan, null, 2));
|
|
27077
27889
|
if (!opts.write && plan.changed) process.exitCode = 1;
|
|
27078
27890
|
return;
|
|
27079
27891
|
}
|
|
27080
27892
|
if (opts.write) {
|
|
27081
27893
|
if (plan.changed) {
|
|
27082
|
-
(0,
|
|
27894
|
+
(0, import_node_fs34.writeFileSync)(path2, plan.content, "utf8");
|
|
27083
27895
|
console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
|
|
27084
27896
|
} else {
|
|
27085
27897
|
console.log("mmi-cli org rules gitignore: up to date");
|
|
@@ -27234,8 +28046,8 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
27234
28046
|
if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
|
|
27235
28047
|
let root;
|
|
27236
28048
|
if (o.root !== void 0) {
|
|
27237
|
-
root = (0,
|
|
27238
|
-
if (!(0,
|
|
28049
|
+
root = (0, import_node_path33.resolve)(o.root);
|
|
28050
|
+
if (!(0, import_node_fs34.existsSync)(root) || !(0, import_node_fs34.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
|
|
27239
28051
|
const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
27240
28052
|
if (isPathUnderDirectory(gcRepoRoot, root)) {
|
|
27241
28053
|
return fail(`worktree gc: --root ${root} contains this checkout \u2014 name a worktrees root, not the repo or an ancestor of it`);
|
|
@@ -27300,7 +28112,7 @@ async function primaryCheckoutRoot(from) {
|
|
|
27300
28112
|
return primaryCheckoutRootOf(async (args) => (await execFileP2("git", ["-C", from, ...args], { timeout: GIT_TIMEOUT_MS })).stdout);
|
|
27301
28113
|
}
|
|
27302
28114
|
async function unprovenWorktreeReason(wtPath, repoRoot2) {
|
|
27303
|
-
if (!(0,
|
|
28115
|
+
if (!(0, import_node_fs34.existsSync)(wtPath)) return `${wtPath} does not exist on disk`;
|
|
27304
28116
|
const porcelain = (await execFileP2("git", ["-C", repoRoot2, "worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
|
|
27305
28117
|
const registered = parseWorktreePorcelainEntries(porcelain);
|
|
27306
28118
|
if (!registered.length) {
|
|
@@ -27321,26 +28133,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
|
27321
28133
|
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
27322
28134
|
const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
|
|
27323
28135
|
const take = () => {
|
|
27324
|
-
const fd = (0,
|
|
28136
|
+
const fd = (0, import_node_fs34.openSync)(lockPath, "wx");
|
|
27325
28137
|
try {
|
|
27326
|
-
(0,
|
|
28138
|
+
(0, import_node_fs34.writeSync)(fd, String(Date.now()));
|
|
27327
28139
|
} finally {
|
|
27328
|
-
(0,
|
|
28140
|
+
(0, import_node_fs34.closeSync)(fd);
|
|
27329
28141
|
}
|
|
27330
28142
|
return () => {
|
|
27331
28143
|
try {
|
|
27332
|
-
(0,
|
|
28144
|
+
(0, import_node_fs34.rmSync)(lockPath, { force: true });
|
|
27333
28145
|
} catch {
|
|
27334
28146
|
}
|
|
27335
28147
|
};
|
|
27336
28148
|
};
|
|
27337
28149
|
try {
|
|
27338
|
-
(0,
|
|
28150
|
+
(0, import_node_fs34.mkdirSync)((0, import_node_path33.dirname)(lockPath), { recursive: true });
|
|
27339
28151
|
return take();
|
|
27340
28152
|
} catch {
|
|
27341
28153
|
try {
|
|
27342
|
-
if (Date.now() - (0,
|
|
27343
|
-
(0,
|
|
28154
|
+
if (Date.now() - (0, import_node_fs34.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
28155
|
+
(0, import_node_fs34.rmSync)(lockPath, { force: true });
|
|
27344
28156
|
return take();
|
|
27345
28157
|
}
|
|
27346
28158
|
} catch {
|
|
@@ -27376,11 +28188,25 @@ withExamples(mutating(
|
|
|
27376
28188
|
}
|
|
27377
28189
|
const repoRoot2 = await primaryCheckoutRoot(process.cwd()) ?? process.cwd();
|
|
27378
28190
|
const wtPath = o.path ?? defaultWorktreePath(repoRoot2, branch);
|
|
27379
|
-
const { base, fetchBranch } = resolveWorktreeBase(fromRef, o.remote);
|
|
28191
|
+
const { base: fallbackBase, fetchBranch, preferRemote } = resolveWorktreeBase(fromRef, o.remote);
|
|
28192
|
+
let base = fallbackBase;
|
|
27380
28193
|
step = `fetch the base ref ${fromRef}`;
|
|
27381
28194
|
if (fetchBranch) {
|
|
27382
28195
|
const fetchErr = await execFileP2("git", ["fetch", o.remote, fetchBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).then(() => void 0).catch((e) => (e instanceof Error ? e.message : String(e)).split("\n")[0]);
|
|
27383
|
-
if (fetchErr) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
|
|
28196
|
+
if (fetchErr && !preferRemote) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
|
|
28197
|
+
}
|
|
28198
|
+
const revParseRef = async (ref) => {
|
|
28199
|
+
try {
|
|
28200
|
+
return (await execFileP2("git", ["rev-parse", "--verify", ref], { timeout: GIT_TIMEOUT_MS })).stdout.trim() || void 0;
|
|
28201
|
+
} catch {
|
|
28202
|
+
return void 0;
|
|
28203
|
+
}
|
|
28204
|
+
};
|
|
28205
|
+
if (preferRemote && await revParseRef(preferRemote)) base = preferRemote;
|
|
28206
|
+
if (!o.json) {
|
|
28207
|
+
const baseSha = (await revParseRef(base))?.slice(0, 12) ?? "unresolved";
|
|
28208
|
+
const localOnly = preferRemote && base !== preferRemote ? ` (no ${preferRemote} \u2014 local ref)` : "";
|
|
28209
|
+
console.error(` base ${base} ${baseSha}${localOnly}`);
|
|
27384
28210
|
}
|
|
27385
28211
|
step = `git worktree add ${wtPath}`;
|
|
27386
28212
|
await addWorktreeRobust(wtPath, branch, base, {
|
|
@@ -27642,12 +28468,27 @@ docsAudit.command("record").description("write a dated janitor verdict for one r
|
|
|
27642
28468
|
await failGraceful(e.message);
|
|
27643
28469
|
}
|
|
27644
28470
|
});
|
|
27645
|
-
docsAudit.command("status").description("read the janitor dead-man verdict back for one repo \u2014 missing/stale/failed is RED; a not-yet-armed registry route is an informational exit 0").option("--repo <owner/name>", "the repo to check (default: the current repo)").action(async (o) => {
|
|
28471
|
+
docsAudit.command("status").description("read the janitor dead-man verdict back for one repo \u2014 missing/stale/failed is RED; a not-yet-armed registry route is an informational exit 0").option("--repo <owner/name>", "the repo to check (default: the current repo)").option("--json", "machine-readable output \u2014 the discrete state rather than the sentence").action(async (o) => {
|
|
27646
28472
|
try {
|
|
27647
28473
|
const repo = o.repo ?? await currentRepoFullName();
|
|
27648
28474
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
27649
|
-
const
|
|
27650
|
-
|
|
28475
|
+
const fetched = await readDocsAuditFetch(repo);
|
|
28476
|
+
const result = docsAuditStatus(fetched, { repo, today });
|
|
28477
|
+
if (o.json) {
|
|
28478
|
+
const verdict = "ok" in fetched && fetched.ok ? fetched.verdict : null;
|
|
28479
|
+
console.log(JSON.stringify({
|
|
28480
|
+
repo,
|
|
28481
|
+
armed: !("notArmed" in fetched),
|
|
28482
|
+
ok: result.ok,
|
|
28483
|
+
state: result.state,
|
|
28484
|
+
date: verdict?.date ?? null,
|
|
28485
|
+
outcome: verdict?.outcome ?? null,
|
|
28486
|
+
checkerVendor: verdict?.checkerVendor ?? null,
|
|
28487
|
+
line: result.line
|
|
28488
|
+
}, null, 2));
|
|
28489
|
+
} else {
|
|
28490
|
+
console.log(result.line);
|
|
28491
|
+
}
|
|
27651
28492
|
if (!result.ok) process.exitCode = 1;
|
|
27652
28493
|
} catch (e) {
|
|
27653
28494
|
await failGraceful(e.message);
|
|
@@ -27663,17 +28504,25 @@ async function reportWrite(label, res) {
|
|
|
27663
28504
|
return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
27664
28505
|
}
|
|
27665
28506
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
27666
|
-
tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; project-admin own dev/rc, master main").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker watch by default)").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
|
|
28507
|
+
tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; project-admin own dev/rc, master main").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker/logs watch by default)").option("--lines <n>", "logs only: trailing lines of the tenant service to return (1-2000, default 200)").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
|
|
27667
28508
|
try {
|
|
27668
|
-
|
|
28509
|
+
let lines;
|
|
28510
|
+
if (o.lines !== void 0) {
|
|
28511
|
+
if (action !== "logs") return fail("runtime tenant control: --lines is valid only for the logs action");
|
|
28512
|
+
lines = Number(o.lines);
|
|
28513
|
+
if (!Number.isInteger(lines) || lines < 1 || lines > 2e3) return fail("runtime tenant control: --lines must be an integer between 1 and 2000");
|
|
28514
|
+
}
|
|
28515
|
+
const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch, lines });
|
|
27669
28516
|
if (!o.json && action === "verify-secrets" && result.secrets) {
|
|
27670
28517
|
const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
|
|
27671
|
-
const { lines, failure } = renderVerifySecrets(body);
|
|
27672
|
-
for (const line of
|
|
28518
|
+
const { lines: lines2, failure } = renderVerifySecrets(body);
|
|
28519
|
+
for (const line of lines2) printLine(line);
|
|
27673
28520
|
if (failure) return failGraceful(`runtime tenant control ${stage} verify-secrets: ${failure}`);
|
|
28521
|
+
} else if (!o.json && action === "logs" && result.logs) {
|
|
28522
|
+
printLine(result.logs);
|
|
27674
28523
|
} else if (!o.json && action === "verify-broker" && result.broker) {
|
|
27675
|
-
const { lines, failure } = renderVerifyBroker({ broker: result.broker, raw: result.brokerRaw });
|
|
27676
|
-
for (const line of
|
|
28524
|
+
const { lines: lines2, failure } = renderVerifyBroker({ broker: result.broker, raw: result.brokerRaw });
|
|
28525
|
+
for (const line of lines2) printLine(line);
|
|
27677
28526
|
if (failure) return failGraceful(`runtime tenant control ${stage} verify-broker: ${failure}`);
|
|
27678
28527
|
} else {
|
|
27679
28528
|
printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
|
|
@@ -27769,6 +28618,34 @@ var project = program2.command("project").description("the DDB org registry \u20
|
|
|
27769
28618
|
async function projectTarget(commandName, explicitTarget) {
|
|
27770
28619
|
return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
|
|
27771
28620
|
}
|
|
28621
|
+
async function runProjectInfoSync(target, apply) {
|
|
28622
|
+
const currentRepo = await resolveRepo();
|
|
28623
|
+
if (!currentRepo) throw new Error("org project sync-info: run from the repository whose Project information is being synchronized");
|
|
28624
|
+
if (target.toLowerCase() !== currentRepo.toLowerCase() && slugOf(target) !== slugOf(currentRepo)) {
|
|
28625
|
+
throw new Error(`org project sync-info: ${target} is not the current checkout (${currentRepo}); run from the target repository`);
|
|
28626
|
+
}
|
|
28627
|
+
const targetRepo2 = currentRepo;
|
|
28628
|
+
const cfg = await loadConfig();
|
|
28629
|
+
const registry2 = registryClientDeps(cfg);
|
|
28630
|
+
const [read, projects] = await Promise.all([
|
|
28631
|
+
fetchProjectBySlugChecked(slugOf(targetRepo2), registry2),
|
|
28632
|
+
fetchProjectsList(registry2)
|
|
28633
|
+
]);
|
|
28634
|
+
if (!read.ok) throw new Error(`org project sync-info: Hub registry read failed (${read.error})`);
|
|
28635
|
+
if (!read.project) throw new Error(`org project sync-info: no registry META for ${targetRepo2}`);
|
|
28636
|
+
if (!projects) throw new Error("org project sync-info: Hub project list unavailable");
|
|
28637
|
+
if (apply) {
|
|
28638
|
+
const authority = await fetchTrainAuthority(targetRepo2, registry2);
|
|
28639
|
+
if (!authority.ok) throw new Error(`org project sync-info: train authority unverified (${authority.error})`);
|
|
28640
|
+
if (!authority.authority.train) {
|
|
28641
|
+
throw new Error(`org project sync-info: ${authority.authority.login} has no train authority for ${targetRepo2}`);
|
|
28642
|
+
}
|
|
28643
|
+
}
|
|
28644
|
+
const repoRoot2 = await gitOut(["rev-parse", "--show-toplevel"]);
|
|
28645
|
+
if (!repoRoot2) throw new Error("org project sync-info: cannot resolve the current repository root");
|
|
28646
|
+
const plan = buildProjectInfoSyncPlan(targetRepo2, read.project, projects, repoRoot2);
|
|
28647
|
+
return syncProjectInfo(plan, defaultGitHubClient(), apply);
|
|
28648
|
+
}
|
|
27772
28649
|
project.command("list").description("list all projects (identity + board, never deploy coords)").option("--json", "machine-readable output").action(async (o) => {
|
|
27773
28650
|
const cfg = await loadConfig();
|
|
27774
28651
|
const projects = await fetchProjectsList(registryClientDeps(cfg));
|
|
@@ -27809,6 +28686,18 @@ deploys run centrally (tenant-deploy.yml); product repos carry no deploy files.
|
|
|
27809
28686
|
);
|
|
27810
28687
|
}
|
|
27811
28688
|
});
|
|
28689
|
+
project.command("sync-info [owner/repo]").description("synchronize the owning GitHub Project's short description + thin README from this repo's README and the registry's current member repos; dry-run by default").option("--apply", "write the synchronized Project information (train-authority gated)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
28690
|
+
let target;
|
|
28691
|
+
try {
|
|
28692
|
+
target = await projectTarget("org project sync-info", repoOrSlug);
|
|
28693
|
+
const result = await runProjectInfoSync(target, Boolean(o.apply));
|
|
28694
|
+
if (o.json) return printLine(JSON.stringify(result, null, 2));
|
|
28695
|
+
printLine(`org project sync-info: ${result.note}`);
|
|
28696
|
+
printLine(` members: ${result.memberRepos.join(", ")}`);
|
|
28697
|
+
} catch (e) {
|
|
28698
|
+
return failGraceful(e.message);
|
|
28699
|
+
}
|
|
28700
|
+
});
|
|
27812
28701
|
var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)");
|
|
27813
28702
|
projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
27814
28703
|
const cfg = await loadConfig();
|
|
@@ -27841,7 +28730,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
27841
28730
|
if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
|
|
27842
28731
|
if (o.secretsFile) {
|
|
27843
28732
|
try {
|
|
27844
|
-
vars.push(`secrets=${(0,
|
|
28733
|
+
vars.push(`secrets=${(0, import_node_fs34.readFileSync)(o.secretsFile, "utf8")}`);
|
|
27845
28734
|
} catch (e) {
|
|
27846
28735
|
return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
|
|
27847
28736
|
}
|
|
@@ -28138,16 +29027,43 @@ function resolveCreateType(raw, command, labels) {
|
|
|
28138
29027
|
}
|
|
28139
29028
|
return raw;
|
|
28140
29029
|
}
|
|
29030
|
+
function resolveCreateSurface(opts) {
|
|
29031
|
+
return typeof opts.surface === "string" && opts.surface.trim() ? surfaceLabel(opts.surface) : void 0;
|
|
29032
|
+
}
|
|
29033
|
+
function surfaceWaived() {
|
|
29034
|
+
return rawFlag("--no-surface");
|
|
29035
|
+
}
|
|
28141
29036
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
28142
29037
|
withExamples(mutating(
|
|
28143
|
-
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks or newlines needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
|
|
29038
|
+
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks or newlines needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--surface <surface>", "issue surface, with or without the surface: prefix (#3789). Required when the target repo runs the one-surface-label board rule; any value satisfies it, so this is not a closed enum").option("--no-surface", "file without a surface label on a repo that requires one \u2014 for a genuinely exempt filing (e.g. a coop proof issue that spans every surface)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
|
|
28144
29039
|
// --dry-run/--validate-only plan: validate --type + --priority (mirrors the action — a bad enum fails
|
|
28145
29040
|
// ERR_BAD_ENUM, a missing priority defaults to medium) then echo the resolved create intent. Refs
|
|
28146
29041
|
// (`--parent`) and title-source are validated by the action on a real run.
|
|
28147
|
-
(opts) => {
|
|
29042
|
+
async (opts) => {
|
|
28148
29043
|
const type = resolveCreateType(opts.type, "issue create", opts.label);
|
|
28149
29044
|
const priority = resolveCreatePriority(opts.priority, "issue create");
|
|
28150
|
-
|
|
29045
|
+
const planLabels = opts.label ?? [];
|
|
29046
|
+
const clash = conflictingSurfaceInputs(opts.surface, planLabels);
|
|
29047
|
+
if (clash) fail(clash.message, clash.payload);
|
|
29048
|
+
const planRepo = opts.batch || surfaceWaived() ? void 0 : await resolveRepo(opts.repo);
|
|
29049
|
+
const surface = resolveCreateSurface(opts);
|
|
29050
|
+
if (planRepo) {
|
|
29051
|
+
const { refusal, warn } = await checkSurfaceRequirement({
|
|
29052
|
+
repo: planRepo,
|
|
29053
|
+
labels: [...planLabels, ...surface ? [surface] : []]
|
|
29054
|
+
});
|
|
29055
|
+
if (warn) process.stderr.write(`${warn}
|
|
29056
|
+
`);
|
|
29057
|
+
if (refusal) fail(refusal.message, refusal.payload);
|
|
29058
|
+
}
|
|
29059
|
+
return {
|
|
29060
|
+
command: "issue create",
|
|
29061
|
+
type,
|
|
29062
|
+
title: opts.title ?? opts.titleFile,
|
|
29063
|
+
priority,
|
|
29064
|
+
repo: opts.repo,
|
|
29065
|
+
...surface ? { surface } : {}
|
|
29066
|
+
};
|
|
28151
29067
|
}
|
|
28152
29068
|
).action(async (o) => {
|
|
28153
29069
|
let args;
|
|
@@ -28157,6 +29073,7 @@ withExamples(mutating(
|
|
|
28157
29073
|
let issueType;
|
|
28158
29074
|
let extraLabels = [];
|
|
28159
29075
|
let targetRepo2;
|
|
29076
|
+
let surfaceFlagLabel;
|
|
28160
29077
|
try {
|
|
28161
29078
|
issueType = resolveCreateType(o.type, "issue create", o.label);
|
|
28162
29079
|
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises10.readFile, readStdin });
|
|
@@ -28164,6 +29081,13 @@ withExamples(mutating(
|
|
|
28164
29081
|
if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
|
|
28165
29082
|
priority = resolveCreatePriority(o.priority, "issue create");
|
|
28166
29083
|
extraLabels = [...o.label ?? []];
|
|
29084
|
+
const clash = conflictingSurfaceInputs(typeof o.surface === "string" ? o.surface : void 0, extraLabels);
|
|
29085
|
+
if (clash) return fail(clash.message, clash.payload);
|
|
29086
|
+
const surfaceFromFlag = resolveCreateSurface(o);
|
|
29087
|
+
if (surfaceFromFlag && !labelsCarrySurface(extraLabels)) {
|
|
29088
|
+
extraLabels.push(surfaceFromFlag);
|
|
29089
|
+
surfaceFlagLabel = surfaceFromFlag;
|
|
29090
|
+
}
|
|
28167
29091
|
targetRepo2 = await resolveRepo(o.repo);
|
|
28168
29092
|
if (!targetRepo2) {
|
|
28169
29093
|
return fail("issue create: could not resolve the target repo \u2014 run inside a git checkout or pass --repo <owner/repo>");
|
|
@@ -28180,6 +29104,27 @@ withExamples(mutating(
|
|
|
28180
29104
|
} catch (e) {
|
|
28181
29105
|
return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
|
|
28182
29106
|
}
|
|
29107
|
+
{
|
|
29108
|
+
const { refusal, warn, enforcing } = await checkSurfaceRequirement({ repo: targetRepo2, labels: extraLabels });
|
|
29109
|
+
if (warn) process.stderr.write(`${warn}
|
|
29110
|
+
`);
|
|
29111
|
+
if (!enforcing && surfaceFlagLabel) {
|
|
29112
|
+
extraLabels = extraLabels.filter((l) => l !== surfaceFlagLabel);
|
|
29113
|
+
args = buildIssueArgs({
|
|
29114
|
+
type: issueType,
|
|
29115
|
+
title,
|
|
29116
|
+
body,
|
|
29117
|
+
priority,
|
|
29118
|
+
repo: targetRepo2,
|
|
29119
|
+
labels: extraLabels.length ? extraLabels : void 0
|
|
29120
|
+
});
|
|
29121
|
+
process.stderr.write(
|
|
29122
|
+
`warning: --surface ${surfaceFlagLabel} was dropped \u2014 ${targetRepo2} defines no surface:* labels, and creating one here would switch the one-surface-label rule on for every later filing in it. Use --label ${surfaceFlagLabel} if you really mean to start that taxonomy.
|
|
29123
|
+
`
|
|
29124
|
+
);
|
|
29125
|
+
}
|
|
29126
|
+
if (refusal && !surfaceWaived()) return fail(refusal.message, refusal.payload);
|
|
29127
|
+
}
|
|
28183
29128
|
await ensureLabelsExist(extraLabels, targetRepo2);
|
|
28184
29129
|
const created = await ghCreate(args);
|
|
28185
29130
|
const { projectItemId, onBoard } = await attachToProject(created.number, targetRepo2, priority);
|
|
@@ -28289,6 +29234,15 @@ jsonParity(issue.command("link-child <parent> <child>").description("link an exi
|
|
|
28289
29234
|
const result = await linkSubIssue(ghRunner2, parentRef, childRef, defaultRepo);
|
|
28290
29235
|
console.log(JSON.stringify(result));
|
|
28291
29236
|
} catch (e) {
|
|
29237
|
+
let conflict;
|
|
29238
|
+
try {
|
|
29239
|
+
const child2 = parseIssueRef(childRef);
|
|
29240
|
+
const childRepo = child2.repo ?? defaultRepo;
|
|
29241
|
+
if (childRepo) conflict = await classifyReparentFailure(e, ghRunner2, childRepo, child2.number, parentRef);
|
|
29242
|
+
} catch {
|
|
29243
|
+
conflict = void 0;
|
|
29244
|
+
}
|
|
29245
|
+
if (conflict) return fail(`issue link-child: ${conflict.message}`, conflict.payload);
|
|
28292
29246
|
const err = e;
|
|
28293
29247
|
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
28294
29248
|
return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
@@ -28380,6 +29334,9 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
28380
29334
|
}
|
|
28381
29335
|
const cfg = await loadConfig();
|
|
28382
29336
|
if (!cfg.sagaApiUrl) return fail("report: Hub API URL not configured");
|
|
29337
|
+
const { warn: surfaceWarn } = await preflightReportSurface();
|
|
29338
|
+
if (surfaceWarn) process.stderr.write(`${surfaceWarn}
|
|
29339
|
+
`);
|
|
28383
29340
|
const result = await fileReport(
|
|
28384
29341
|
{
|
|
28385
29342
|
apiUrl: cfg.sagaApiUrl,
|
|
@@ -28452,6 +29409,14 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
28452
29409
|
return console.log(JSON.stringify({ deduped: true, number: dup.number, url: dup.url, score: dup.score }));
|
|
28453
29410
|
}
|
|
28454
29411
|
}
|
|
29412
|
+
const { warn: surfaceWarn } = await checkSurfaceRequirement({
|
|
29413
|
+
repo: targetRepo2,
|
|
29414
|
+
labels: [SKILL_LESSON_LABEL],
|
|
29415
|
+
command: "skill-lesson",
|
|
29416
|
+
waiver: { reason: "tooling lesson spans product surfaces \u2014 coop-proof class (#3789)" }
|
|
29417
|
+
});
|
|
29418
|
+
if (surfaceWarn) process.stderr.write(`${surfaceWarn}
|
|
29419
|
+
`);
|
|
28455
29420
|
try {
|
|
28456
29421
|
await execFileP2("gh", ["label", "create", SKILL_LESSON_LABEL, "--color", "c2e0c6", "--repo", targetRepo2], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
28457
29422
|
} catch {
|
|
@@ -28501,11 +29466,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
|
|
|
28501
29466
|
}
|
|
28502
29467
|
});
|
|
28503
29468
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
28504
|
-
const wfDir = (0,
|
|
28505
|
-
if (!(0,
|
|
28506
|
-
return (0,
|
|
29469
|
+
const wfDir = (0, import_node_path33.join)(cwd, ".github", "workflows");
|
|
29470
|
+
if (!(0, import_node_fs34.existsSync)(wfDir)) return [];
|
|
29471
|
+
return (0, import_node_fs34.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
|
|
28507
29472
|
try {
|
|
28508
|
-
return workflowReportsPrChecks((0,
|
|
29473
|
+
return workflowReportsPrChecks((0, import_node_fs34.readFileSync)((0, import_node_path33.join)(wfDir, name), "utf8"));
|
|
28509
29474
|
} catch {
|
|
28510
29475
|
return true;
|
|
28511
29476
|
}
|
|
@@ -28537,16 +29502,16 @@ function ciAuditDeps() {
|
|
|
28537
29502
|
// gate re-seed step is skipped gracefully rather than failing mid-run.
|
|
28538
29503
|
readSeedFile: (path2) => {
|
|
28539
29504
|
if (!root) return null;
|
|
28540
|
-
const fullPath = (0,
|
|
28541
|
-
return (0,
|
|
29505
|
+
const fullPath = (0, import_node_path33.join)(root, path2);
|
|
29506
|
+
return (0, import_node_fs34.existsSync)(fullPath) ? (0, import_node_fs34.readFileSync)(fullPath, "utf8") : null;
|
|
28542
29507
|
}
|
|
28543
29508
|
};
|
|
28544
29509
|
}
|
|
28545
29510
|
function hubRoot() {
|
|
28546
|
-
const fromPkg = (0,
|
|
29511
|
+
const fromPkg = (0, import_node_path33.join)(__dirname, "..", "..");
|
|
28547
29512
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
28548
|
-
if ((0,
|
|
28549
|
-
if ((0,
|
|
29513
|
+
if ((0, import_node_fs34.existsSync)((0, import_node_path33.join)(fromPkg, marker))) return fromPkg;
|
|
29514
|
+
if ((0, import_node_fs34.existsSync)((0, import_node_path33.join)(process.cwd(), marker))) return process.cwd();
|
|
28550
29515
|
return null;
|
|
28551
29516
|
}
|
|
28552
29517
|
pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
|
|
@@ -28849,7 +29814,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
28849
29814
|
localCleanup = await cleanupPrMergeLocalBranch(headRef, {
|
|
28850
29815
|
beforeWorktrees,
|
|
28851
29816
|
startingPath,
|
|
28852
|
-
pathExists: (p) => (0,
|
|
29817
|
+
pathExists: (p) => (0, import_node_fs34.existsSync)(p),
|
|
28853
29818
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
28854
29819
|
teardownWorktreeStage,
|
|
28855
29820
|
deferredStore,
|
|
@@ -28934,8 +29899,8 @@ function trainApplyDeps() {
|
|
|
28934
29899
|
// Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
|
|
28935
29900
|
// workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
|
|
28936
29901
|
// outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
|
|
28937
|
-
dispatchTenantControl: async ({ repo, stage, action }) => {
|
|
28938
|
-
const res = await tenantControl({ repo, stage, action }, registryClientDeps(await loadConfig()));
|
|
29902
|
+
dispatchTenantControl: async ({ repo, stage, action, lines }) => {
|
|
29903
|
+
const res = await tenantControl({ repo, stage, action, ...lines != null ? { lines } : {} }, registryClientDeps(await loadConfig()));
|
|
28939
29904
|
if (res.ok) return { ok: true };
|
|
28940
29905
|
const body = res.body;
|
|
28941
29906
|
return { ok: false, category: body?.category, error: body?.error ?? res.error };
|
|
@@ -28983,9 +29948,19 @@ function renderDeployLine(d) {
|
|
|
28983
29948
|
if (d.deployStatus === "success") parts.push("deploy: SUCCEEDED");
|
|
28984
29949
|
else if (d.deployStatus === "failure") parts.push("deploy: FAILED (promotion stands; retry the deploy, do not re-tag)");
|
|
28985
29950
|
else if (d.runId != null) parts.push(`deploy: UNVERIFIED \u2014 dispatched, not resolved (watch: gh run watch ${d.runId} --repo mutmutco/MMI-Hub --exit-status)`);
|
|
29951
|
+
else if (d.workflowRuns?.length) parts.push("deploy: UNVERIFIED \u2014 the runs above are enumerated, not resolved; watch each to conclusion before calling this release healthy (#3322)");
|
|
28986
29952
|
else parts.push("deploy: UNVERIFIED \u2014 no run correlated or watched; resolve every workflow run on the release SHA before calling this release healthy (#3322)");
|
|
28987
29953
|
return parts.join("; ");
|
|
28988
29954
|
}
|
|
29955
|
+
function renderReleaseResume(r) {
|
|
29956
|
+
const lines = [`mmi-cli release --resume: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}] \u2014 ${r.note}`];
|
|
29957
|
+
for (const step of r.steps) lines.push(` - ${step}`);
|
|
29958
|
+
if (r.releaseUrl) lines.push(` release: ${r.releaseUrl}`);
|
|
29959
|
+
if (r.announceNote) lines.push(` announce: ${r.announceNote}`);
|
|
29960
|
+
if (r.dispatch) lines.push(` deploy: ${r.dispatch.note}`);
|
|
29961
|
+
if (r.devRollForward) lines.push(` development: ${r.devRollForward.note}`);
|
|
29962
|
+
return lines.join("\n");
|
|
29963
|
+
}
|
|
28989
29964
|
function renderTrainApply(commandName, r) {
|
|
28990
29965
|
let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2192 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
28991
29966
|
if (r.versionFold) base = `${base}; ${r.versionFold}`;
|
|
@@ -29005,6 +29980,9 @@ function renderTrainApply(commandName, r) {
|
|
|
29005
29980
|
if (r.checkout) {
|
|
29006
29981
|
base = `${base}; checkout: ${r.checkout.note}`;
|
|
29007
29982
|
}
|
|
29983
|
+
if (r.projectInfoSync) {
|
|
29984
|
+
base = `${base}; project info: ${r.projectInfoSync.note}`;
|
|
29985
|
+
}
|
|
29008
29986
|
return r.announceNote ? `${base}; announce: ${r.announceNote}` : base;
|
|
29009
29987
|
}
|
|
29010
29988
|
function renderTenantRedeploy(r) {
|
|
@@ -29023,7 +30001,12 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
29023
30001
|
const RELEASE_ONLY_FLAGS = [
|
|
29024
30002
|
{ flags: "--announce-summary-file <path>", description: "agent-curated summary lines for the Hub Slack announcement (#883)" },
|
|
29025
30003
|
{ flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
|
|
29026
|
-
{ flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" }
|
|
30004
|
+
{ flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" },
|
|
30005
|
+
// #3851: finish a release that pushed its tag and then stopped. NOT a rerun — a rerun would cut the
|
|
30006
|
+
// NEXT version, because the cycle resolver advances past the pushed tag and MMI_RELEASE_VERSION
|
|
30007
|
+
// refuses a version that is not ahead of the latest. This is the only path allowed to target an
|
|
30008
|
+
// existing tag, and it proves the release is genuinely partial before writing anything.
|
|
30009
|
+
{ flags: "--resume", description: "finish a release whose tag was pushed but whose main push / Release / deploy never happened \u2014 preserves the original version, refuses unless the partial state is proven (#3851)" }
|
|
29027
30010
|
];
|
|
29028
30011
|
for (const f of RELEASE_ONLY_FLAGS) {
|
|
29029
30012
|
if (commandName === "release") {
|
|
@@ -29047,6 +30030,18 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
29047
30030
|
if (o.announceSummaryFile && commandName !== "release") {
|
|
29048
30031
|
return fail(`${commandName}: --announce-summary-file applies only to release \u2014 rcand posts no Hub Slack announcement. Run: mmi-cli release --announce-summary-file <path>`);
|
|
29049
30032
|
}
|
|
30033
|
+
if (o.resume && commandName !== "release") {
|
|
30034
|
+
return fail(`${commandName}: --resume applies only to release \u2014 it finishes a partially-released main. Run: mmi-cli release --resume`);
|
|
30035
|
+
}
|
|
30036
|
+
if (o.resume) {
|
|
30037
|
+
if (o.apply) return fail("release: --resume and --apply are mutually exclusive \u2014 --apply cuts the NEXT version, --resume finishes the one whose tag is already pushed");
|
|
30038
|
+
try {
|
|
30039
|
+
const result = await runReleaseResume(trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile });
|
|
30040
|
+
return printLine(o.json ? JSON.stringify(result, null, 2) : renderReleaseResume(result));
|
|
30041
|
+
} catch (e) {
|
|
30042
|
+
return failGraceful(`release --resume: ${e.message}`);
|
|
30043
|
+
}
|
|
30044
|
+
}
|
|
29050
30045
|
if (o.apply && o.repo) {
|
|
29051
30046
|
const rerun = `mmi-cli ${commandName} --apply${o.watch ? " --watch" : ""}${o.dev ? " --dev" : ""}${o.json ? " --json" : ""}`;
|
|
29052
30047
|
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun);
|
|
@@ -29056,7 +30051,19 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
29056
30051
|
try {
|
|
29057
30052
|
const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
29058
30053
|
const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev });
|
|
29059
|
-
|
|
30054
|
+
let projectInfoSync;
|
|
30055
|
+
if (commandName === "release") {
|
|
30056
|
+
try {
|
|
30057
|
+
projectInfoSync = await runProjectInfoSync(result.repo, true);
|
|
30058
|
+
} catch (e) {
|
|
30059
|
+
const error = e.message;
|
|
30060
|
+
projectInfoSync = { applied: false, note: `FAILED \u2014 ${error}`, error };
|
|
30061
|
+
}
|
|
30062
|
+
}
|
|
30063
|
+
const reported = { ...result, ...projectInfoSync ? { projectInfoSync } : {} };
|
|
30064
|
+
printLine(o.json ? JSON.stringify(reported, null, 2) : renderTrainApply(commandName, reported));
|
|
30065
|
+
if (projectInfoSync && "error" in projectInfoSync) process.exitCode = 1;
|
|
30066
|
+
return;
|
|
29060
30067
|
} catch (e) {
|
|
29061
30068
|
return failGraceful(`${commandName}: ${e.message}`);
|
|
29062
30069
|
}
|
|
@@ -29209,12 +30216,12 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
29209
30216
|
targets = resolution.targets;
|
|
29210
30217
|
}
|
|
29211
30218
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
29212
|
-
const fileMatrix = (0,
|
|
30219
|
+
const fileMatrix = (0, import_node_fs34.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs34.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
29213
30220
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
29214
30221
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
29215
|
-
const fileContracts = (0,
|
|
30222
|
+
const fileContracts = (0, import_node_fs34.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs34.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
29216
30223
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
29217
|
-
const sanctioned = (0,
|
|
30224
|
+
const sanctioned = (0, import_node_fs34.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs34.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
29218
30225
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
|
|
29219
30226
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
29220
30227
|
if (!report.ok) process.exitCode = 1;
|
|
@@ -29247,16 +30254,16 @@ function directoryBytes(path2) {
|
|
|
29247
30254
|
let total = 0;
|
|
29248
30255
|
let entries;
|
|
29249
30256
|
try {
|
|
29250
|
-
entries = (0,
|
|
30257
|
+
entries = (0, import_node_fs34.readdirSync)(path2, { withFileTypes: true });
|
|
29251
30258
|
} catch {
|
|
29252
30259
|
return 0;
|
|
29253
30260
|
}
|
|
29254
30261
|
for (const entry of entries) {
|
|
29255
|
-
const child2 = (0,
|
|
30262
|
+
const child2 = (0, import_node_path33.join)(path2, entry.name);
|
|
29256
30263
|
if (entry.isDirectory()) total += directoryBytes(child2);
|
|
29257
30264
|
else {
|
|
29258
30265
|
try {
|
|
29259
|
-
total += (0,
|
|
30266
|
+
total += (0, import_node_fs34.statSync)(child2).size;
|
|
29260
30267
|
} catch {
|
|
29261
30268
|
}
|
|
29262
30269
|
}
|
|
@@ -29264,25 +30271,25 @@ function directoryBytes(path2) {
|
|
|
29264
30271
|
return total;
|
|
29265
30272
|
}
|
|
29266
30273
|
function listDirEntries(dir) {
|
|
29267
|
-
return (0,
|
|
30274
|
+
return (0, import_node_fs34.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
|
|
29268
30275
|
}
|
|
29269
30276
|
function readInstalledPluginRefs(configRoot) {
|
|
29270
30277
|
const p = installedPluginsPathForConfig(configRoot);
|
|
29271
|
-
if (!(0,
|
|
30278
|
+
if (!(0, import_node_fs34.existsSync)(p)) return [];
|
|
29272
30279
|
try {
|
|
29273
|
-
return installedPluginPaths((0,
|
|
30280
|
+
return installedPluginPaths((0, import_node_fs34.readFileSync)(p, "utf8"));
|
|
29274
30281
|
} catch {
|
|
29275
30282
|
return null;
|
|
29276
30283
|
}
|
|
29277
30284
|
}
|
|
29278
30285
|
function pluginCacheFsDeps(configRoot, dirBytes) {
|
|
29279
30286
|
return {
|
|
29280
|
-
exists: (p) => (0,
|
|
29281
|
-
listVersionDirs: (root) => (0,
|
|
30287
|
+
exists: (p) => (0, import_node_fs34.existsSync)(p),
|
|
30288
|
+
listVersionDirs: (root) => (0, import_node_fs34.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
|
|
29282
30289
|
dirBytes,
|
|
29283
|
-
listStagingDirs: (root) => (0,
|
|
30290
|
+
listStagingDirs: (root) => (0, import_node_fs34.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
29284
30291
|
try {
|
|
29285
|
-
return { name: d.name, mtimeMs: newestMtimeMs((0,
|
|
30292
|
+
return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path33.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs34.statSync)(p).mtimeMs) };
|
|
29286
30293
|
} catch {
|
|
29287
30294
|
return { name: d.name, mtimeMs: Date.now() };
|
|
29288
30295
|
}
|
|
@@ -29296,10 +30303,10 @@ function stagingApplyFsGuard(configRoot) {
|
|
|
29296
30303
|
return {
|
|
29297
30304
|
referencedPaths: () => readInstalledPluginRefs(configRoot),
|
|
29298
30305
|
mtimeMs: (name) => {
|
|
29299
|
-
const p = (0,
|
|
29300
|
-
if (!(0,
|
|
30306
|
+
const p = (0, import_node_path33.join)(stagingRoot, name);
|
|
30307
|
+
if (!(0, import_node_fs34.existsSync)(p)) return null;
|
|
29301
30308
|
try {
|
|
29302
|
-
return newestMtimeMs(p, listDirEntries, (q) => (0,
|
|
30309
|
+
return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs34.statSync)(q).mtimeMs);
|
|
29303
30310
|
} catch {
|
|
29304
30311
|
return null;
|
|
29305
30312
|
}
|
|
@@ -29319,13 +30326,13 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
|
|
|
29319
30326
|
return;
|
|
29320
30327
|
}
|
|
29321
30328
|
const plan = buildPluginCachePlan(
|
|
29322
|
-
(0,
|
|
30329
|
+
(0, import_node_os13.homedir)(),
|
|
29323
30330
|
running,
|
|
29324
30331
|
pluginCacheFsDeps(configRoot, directoryBytes),
|
|
29325
30332
|
{ withBytes: true, configRoot, includeStaging: surface !== "codex" }
|
|
29326
30333
|
);
|
|
29327
30334
|
const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
|
|
29328
|
-
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0,
|
|
30335
|
+
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs34.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
|
|
29329
30336
|
const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
|
|
29330
30337
|
if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
|
|
29331
30338
|
else console.log(renderPluginCachePlan(plan, result));
|