@mutmutco/cli 3.119.0 → 3.121.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 +238 -21
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -7411,6 +7411,7 @@ async function removeWorktreeWithRecovery(wtPath, deps) {
|
|
|
7411
7411
|
const backoff = deps.backoffMs ?? [250, 1e3];
|
|
7412
7412
|
let attempts = 0;
|
|
7413
7413
|
let lastError;
|
|
7414
|
+
const stillOnDisk = () => deps.pathExists?.(wtPath) ?? false;
|
|
7414
7415
|
let safeForGitPrimary = true;
|
|
7415
7416
|
if (deps.detachReparsePoints) {
|
|
7416
7417
|
try {
|
|
@@ -7424,7 +7425,9 @@ async function removeWorktreeWithRecovery(wtPath, deps) {
|
|
|
7424
7425
|
attempts++;
|
|
7425
7426
|
try {
|
|
7426
7427
|
await deps.git(["worktree", "remove", "--force", wtPath]);
|
|
7427
|
-
return { status: "removed", attempts, recovery: i > 0 ? "retry" : void 0 };
|
|
7428
|
+
if (!stillOnDisk()) return { status: "removed", attempts, recovery: i > 0 ? "retry" : void 0 };
|
|
7429
|
+
lastError = new Error(`worktree directory still present after 'git worktree remove --force': ${wtPath}`);
|
|
7430
|
+
break;
|
|
7428
7431
|
} catch (e) {
|
|
7429
7432
|
lastError = e;
|
|
7430
7433
|
const retriesLeft = i < maxAttempts - 1;
|
|
@@ -7439,12 +7442,18 @@ async function removeWorktreeWithRecovery(wtPath, deps) {
|
|
|
7439
7442
|
try {
|
|
7440
7443
|
await deps.removeWorktreeDir(wtPath);
|
|
7441
7444
|
await deps.git(["worktree", "prune"]).catch(() => "");
|
|
7442
|
-
return { status: "removed", attempts, recovery: "fallback" };
|
|
7445
|
+
if (!stillOnDisk()) return { status: "removed", attempts, recovery: "fallback" };
|
|
7446
|
+
lastError = new Error(`worktree directory still present after on-disk removal: ${wtPath}`);
|
|
7443
7447
|
} catch (fallbackError) {
|
|
7444
7448
|
lastError = fallbackError;
|
|
7445
7449
|
}
|
|
7446
7450
|
}
|
|
7447
|
-
return {
|
|
7451
|
+
return {
|
|
7452
|
+
status: "failed",
|
|
7453
|
+
attempts,
|
|
7454
|
+
error: errorMessage(lastError),
|
|
7455
|
+
...stillOnDisk() ? { remainsOnDisk: true } : {}
|
|
7456
|
+
};
|
|
7448
7457
|
}
|
|
7449
7458
|
var NO_NESTED_LINKS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
7450
7459
|
function baseName(p) {
|
|
@@ -8282,7 +8291,9 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
8282
8291
|
sleep: options.sleep ?? defaultSleep,
|
|
8283
8292
|
detachReparsePoints: options.detachReparsePoints,
|
|
8284
8293
|
// #3064: junction-safe deferred sweep too
|
|
8285
|
-
removeWorktreeDir: options.removeWorktreeDir
|
|
8294
|
+
removeWorktreeDir: options.removeWorktreeDir,
|
|
8295
|
+
pathExists: options.pathExists
|
|
8296
|
+
// #4850: a swept entry is only cleared once its directory is gone
|
|
8286
8297
|
};
|
|
8287
8298
|
await sweepDeferredWorktrees(options.deferredStore, removeDeps, options.removalContext).catch(() => void 0);
|
|
8288
8299
|
let stageTeardown;
|
|
@@ -8326,7 +8337,9 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
8326
8337
|
sleep: options.sleep ?? defaultSleep,
|
|
8327
8338
|
detachReparsePoints: options.detachReparsePoints,
|
|
8328
8339
|
// #3064: reach the actual pr-merge teardown path
|
|
8329
|
-
removeWorktreeDir: options.removeWorktreeDir
|
|
8340
|
+
removeWorktreeDir: options.removeWorktreeDir,
|
|
8341
|
+
pathExists: options.pathExists
|
|
8342
|
+
// #4850: never report `removed` while the directory survives
|
|
8330
8343
|
});
|
|
8331
8344
|
if (options.removalContext) {
|
|
8332
8345
|
recordWorktreeRemoval(options.removalContext.primaryRoot, {
|
|
@@ -9263,6 +9276,9 @@ function resolveWorktreeBase(from, remote) {
|
|
|
9263
9276
|
if (SHA_LIKE_RE.test(from)) return { base: from };
|
|
9264
9277
|
return { base: from, fetchBranch: from, preferRemote: `${remotePrefix}${from}` };
|
|
9265
9278
|
}
|
|
9279
|
+
function worktreeAddGitPrefix() {
|
|
9280
|
+
return process.platform === "win32" ? ["-c", "core.longpaths=true"] : [];
|
|
9281
|
+
}
|
|
9266
9282
|
var GIT_CONFIG_LOCK_RE = /could not lock config file|unable to write upstream branch configuration|unable to access ['"]?\.git\/config['"]?: Permission denied|unknown error occurred while reading the configuration files/i;
|
|
9267
9283
|
function isGitConfigLockError(error) {
|
|
9268
9284
|
return GIT_CONFIG_LOCK_RE.test(error instanceof Error ? error.message : String(error));
|
|
@@ -9304,6 +9320,8 @@ async function addWorktreeRobust(wtPath, branch, base, deps) {
|
|
|
9304
9320
|
await deps.git(["worktree", "add", wtPath, "-b", branch, base]);
|
|
9305
9321
|
return;
|
|
9306
9322
|
} catch (e) {
|
|
9323
|
+
await deps.cleanupPartial?.().catch(() => {
|
|
9324
|
+
});
|
|
9307
9325
|
await cleanupOrphanBranch(branch, base, preExistingOid, deps).catch(() => {
|
|
9308
9326
|
});
|
|
9309
9327
|
const retriesLeft = i < maxAttempts - 1;
|
|
@@ -9967,6 +9985,13 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
9967
9985
|
"docs/superpowers/",
|
|
9968
9986
|
".playwright-mcp/",
|
|
9969
9987
|
".claude/worktrees/",
|
|
9988
|
+
// `.mmi/` is org session scratch (session id, throttle traces, saga queue — AGENTS.md "No committed
|
|
9989
|
+
// `.mmi`"). #2319 retired this rule believing repo-local `.mmi` state was gone; #4833 proved repos
|
|
9990
|
+
// still get `.mmi/` written (older tooling on the machine, product-repo scripts), and dropping the
|
|
9991
|
+
// rule left that scratch untracked-visible one `git add .` away from a commit. Ignore the dir's
|
|
9992
|
+
// contents at ANY depth; an already-tracked `.mmi/config.json` keeps tracking regardless (git never
|
|
9993
|
+
// untracks a tracked file), so no re-inclusion line is needed.
|
|
9994
|
+
"**/.mmi/*",
|
|
9970
9995
|
// #3425: local agent config the org push wall (`mmi-no-agent-files-org`) forbids committing, at any
|
|
9971
9996
|
// depth — leaving it untracked is the ONLY legal state, so it is pure `git status` noise. Only the two
|
|
9972
9997
|
// dirs no repo has cause to track: `.claude/` is excluded because MMI-Hub tracks `.claude/settings.json`,
|
|
@@ -9979,6 +10004,12 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
9979
10004
|
// thing blocking the cleanup that gate's own worktree needs. Org-universal: every repo runs the same
|
|
9980
10005
|
// gate, so this belongs in the managed block rather than in one repo's own ignores.
|
|
9981
10006
|
".jerv/",
|
|
10007
|
+
// #4856: the JervCode (pi) host writes its loop journal to `.pi/loops/` in whatever repo it runs in —
|
|
10008
|
+
// the same runtime-state class as `.jerv/` and `.codex/` above, and the one active surface missing from
|
|
10009
|
+
// this list. Left un-ignored it is permanent `?? .pi/` noise that refuses a release clean-tree gate
|
|
10010
|
+
// (measured on the v3.120.0 Hub train), with no legal resolution: committing another host's runtime
|
|
10011
|
+
// state is not an option, so the operator's only move was to park the directory outside the repo.
|
|
10012
|
+
".pi/",
|
|
9982
10013
|
// #2321 doctrine: the canonical worktree home is the SIBLING `../mmi-worktrees/<RepoName>/<branch>` (outside the tree,
|
|
9983
10014
|
// via `mmi-cli worktree create`), so a repo-local `.worktrees/` is NOT un-ignored org-wide — `mmi-cli
|
|
9984
10015
|
// doctor` flags one explicitly instead (buildRepoLocalWorktreeCheck) rather than baking the fallback path
|
|
@@ -15820,10 +15851,10 @@ var rollout_plan_default = {
|
|
|
15820
15851
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
15821
15852
|
},
|
|
15822
15853
|
baseline: {
|
|
15823
|
-
version: "3.
|
|
15824
|
-
tag: "v3.
|
|
15825
|
-
commit: "
|
|
15826
|
-
npm: "@mutmutco/cli@3.
|
|
15854
|
+
version: "3.121.0",
|
|
15855
|
+
tag: "v3.121.0",
|
|
15856
|
+
commit: "3fc99b89d353",
|
|
15857
|
+
npm: "@mutmutco/cli@3.121.0"
|
|
15827
15858
|
},
|
|
15828
15859
|
exitCriterion: "fleet-n-of-n",
|
|
15829
15860
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15840,14 +15871,14 @@ var rollout_plan_default = {
|
|
|
15840
15871
|
repo: "mutmutco/mmi-hub",
|
|
15841
15872
|
role: "canary",
|
|
15842
15873
|
schedule: "train",
|
|
15843
|
-
v3Target: "v3.
|
|
15874
|
+
v3Target: "v3.121.0"
|
|
15844
15875
|
}
|
|
15845
15876
|
],
|
|
15846
15877
|
rollbackTrigger: "Any red inside the post-cut soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a v3 client refused while the compat window must still admit it (SUPPORTED_MINOR_WINDOW=2, MIN_CLIENT_VERSION 0.0.0 \u2014 D6a), or npm consumer install/doctor failure on the v4 dist.",
|
|
15847
15878
|
rollback: {
|
|
15848
15879
|
independent: true,
|
|
15849
|
-
mechanism: "npm dist-tag latest -> 3.
|
|
15850
|
-
v3Target: "v3.
|
|
15880
|
+
mechanism: "npm dist-tag latest -> 3.121.0 and redeploy the Hub Lambda from tag v3.121.0 (3fc99b89d353); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15881
|
+
v3Target: "v3.121.0 (@mutmutco/cli@3.121.0, tag commit 3fc99b89d353 \u2014 the preserved latest-v3 distribution, D6b)"
|
|
15851
15882
|
}
|
|
15852
15883
|
},
|
|
15853
15884
|
{
|
|
@@ -24696,7 +24727,7 @@ function collectLeaves(node, acc) {
|
|
|
24696
24727
|
function buildHouses(tree) {
|
|
24697
24728
|
const collect = (node, parentHouse, root, out) => {
|
|
24698
24729
|
if (node.house === root && parentHouse !== root) {
|
|
24699
|
-
const entry = { path: node.path };
|
|
24730
|
+
const entry = { path: node.path, canonical: node.canonical };
|
|
24700
24731
|
if (node.description) entry.description = node.description;
|
|
24701
24732
|
out.push(entry);
|
|
24702
24733
|
}
|
|
@@ -33256,7 +33287,9 @@ function worktreeRemoveDeps(execGit) {
|
|
|
33256
33287
|
// #3064: unlink any reparse point (esp. a `node_modules` junction to base) before the git-native
|
|
33257
33288
|
// `worktree remove --force`, which would otherwise recurse through it and empty the base checkout.
|
|
33258
33289
|
detachReparsePoints: (worktreePath) => detachReparsePoints(worktreePath, realWorktreeDirRemover),
|
|
33259
|
-
removeWorktreeDir: async (worktreePath) => removeWorktreeTree(worktreePath, await resolvePrimaryCheckout(execGit), realWorktreeDirRemover)
|
|
33290
|
+
removeWorktreeDir: async (worktreePath) => removeWorktreeTree(worktreePath, await resolvePrimaryCheckout(execGit), realWorktreeDirRemover),
|
|
33291
|
+
// #4850: verify the directory is actually gone before any caller reports completion.
|
|
33292
|
+
pathExists: (worktreePath) => (0, import_node_fs39.existsSync)(worktreePath)
|
|
33260
33293
|
};
|
|
33261
33294
|
}
|
|
33262
33295
|
async function worktreeHasStageState(worktreePath) {
|
|
@@ -34393,7 +34426,7 @@ function registerWorktreeCommands(program3) {
|
|
|
34393
34426
|
);
|
|
34394
34427
|
report.push({
|
|
34395
34428
|
step: "remove worktree",
|
|
34396
|
-
status: removeOutcome.status === "removed" ? removeOutcome.recovery ? `done (${removeOutcome.recovery})` : "done" : `failed: ${removeOutcome.error ?? "lock held"} \u2014 run: git -C "${primaryCheckout}" worktree remove --force "${wtPath}"`
|
|
34429
|
+
status: removeOutcome.status === "removed" ? removeOutcome.recovery ? `done (${removeOutcome.recovery})` : "done" : removeOutcome.remainsOnDisk ? `failed: ${removeOutcome.error ?? "lock held"} \u2014 the directory "${wtPath}" is still on disk; cd every shell out of it (a cwd inside holds it open on Windows), then delete that directory` : `failed: ${removeOutcome.error ?? "lock held"} \u2014 run: git -C "${primaryCheckout}" worktree remove --force "${wtPath}"`
|
|
34397
34430
|
});
|
|
34398
34431
|
if (!shouldContinueLandCleanup(removeOutcome.status)) {
|
|
34399
34432
|
const result2 = {
|
|
@@ -35865,6 +35898,31 @@ function markerCommentBody(body, markerId) {
|
|
|
35865
35898
|
return `${prCommentMarker(markerId)}
|
|
35866
35899
|
${body}`;
|
|
35867
35900
|
}
|
|
35901
|
+
var CHECK_LOG_TAIL_LINES = 60;
|
|
35902
|
+
var CHECK_LOG_MAX_BUFFER = 32 * 1024 * 1024;
|
|
35903
|
+
function parseCheckJobId(details) {
|
|
35904
|
+
const match = /\/job\/(\d+)\b/.exec(details ?? "");
|
|
35905
|
+
return match ? match[1] : null;
|
|
35906
|
+
}
|
|
35907
|
+
function failingCheckRows(rows) {
|
|
35908
|
+
return rows.filter((row) => row.status === "fail" || row.status === "failure");
|
|
35909
|
+
}
|
|
35910
|
+
function tailLines(text, max = CHECK_LOG_TAIL_LINES) {
|
|
35911
|
+
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
|
35912
|
+
return { tail: lines.slice(-max).join("\n"), truncated: lines.length > max };
|
|
35913
|
+
}
|
|
35914
|
+
async function attachFailureLogs(rows, fetchLog) {
|
|
35915
|
+
for (const row of failingCheckRows(rows)) {
|
|
35916
|
+
const jobId = parseCheckJobId(row.details);
|
|
35917
|
+
if (!jobId) continue;
|
|
35918
|
+
try {
|
|
35919
|
+
const { tail, truncated } = tailLines(await fetchLog(jobId));
|
|
35920
|
+
if (tail) row.log = { jobId, tail, truncated };
|
|
35921
|
+
} catch {
|
|
35922
|
+
}
|
|
35923
|
+
}
|
|
35924
|
+
return rows;
|
|
35925
|
+
}
|
|
35868
35926
|
function parsePrChecksTable(stdout) {
|
|
35869
35927
|
const lines = stdout.trim().split(/\r?\n/).filter((l) => l.trim());
|
|
35870
35928
|
if (!lines.length) return [];
|
|
@@ -36190,7 +36248,7 @@ function registerPrLifecycleCommands(program3) {
|
|
|
36190
36248
|
return failGraceful(`pr update-branch: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
36191
36249
|
}
|
|
36192
36250
|
});
|
|
36193
|
-
pr2.command("checks <number>").description("one-shot non-blocking check snapshot; --watch streams state with per-check failure log pointers").option("--json", "machine-readable output").option("--watch", "poll until success/failure, streaming one JSON line per poll").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
|
|
36251
|
+
pr2.command("checks <number>").description("one-shot non-blocking check snapshot; --watch streams state with per-check failure log pointers").option("--json", "machine-readable output").option("--watch", "poll until success/failure, streaming one JSON line per poll").option("--logs", `fold the last ${CHECK_LOG_TAIL_LINES} log lines of every FAILING check into its row, so reading why CI is red needs no second tool (#4853)`).option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
|
|
36194
36252
|
const n = assertPositiveInt("pr checks", number);
|
|
36195
36253
|
let repo;
|
|
36196
36254
|
try {
|
|
@@ -36199,6 +36257,15 @@ function registerPrLifecycleCommands(program3) {
|
|
|
36199
36257
|
return fail(`pr checks: ${e.message}`);
|
|
36200
36258
|
}
|
|
36201
36259
|
const repoArgs = ["--repo", repo];
|
|
36260
|
+
const fetchLog = async (jobId) => {
|
|
36261
|
+
const { stdout } = await execFileP2(
|
|
36262
|
+
"gh",
|
|
36263
|
+
["run", "view", ...repoArgs, "--job", jobId, "--log-failed"],
|
|
36264
|
+
{ timeout: GC_GH_TIMEOUT_MS4, maxBuffer: CHECK_LOG_MAX_BUFFER }
|
|
36265
|
+
);
|
|
36266
|
+
return stdout;
|
|
36267
|
+
};
|
|
36268
|
+
const withLogs = async (rows) => o.logs ? attachFailureLogs(rows, fetchLog) : rows;
|
|
36202
36269
|
const snap = async () => {
|
|
36203
36270
|
let stdout = "";
|
|
36204
36271
|
let stderr = "";
|
|
@@ -36218,7 +36285,7 @@ function registerPrLifecycleCommands(program3) {
|
|
|
36218
36285
|
try {
|
|
36219
36286
|
if (!o.watch) {
|
|
36220
36287
|
const { state, rows } = await snap();
|
|
36221
|
-
const output = { state, checks: rows };
|
|
36288
|
+
const output = { state, checks: await withLogs(rows) };
|
|
36222
36289
|
console.log(JSON.stringify(output));
|
|
36223
36290
|
if (state === "failure" || state === "failing") process.exitCode = 1;
|
|
36224
36291
|
return;
|
|
@@ -36227,7 +36294,8 @@ function registerPrLifecycleCommands(program3) {
|
|
|
36227
36294
|
let lastState = "";
|
|
36228
36295
|
while (Date.now() < deadline) {
|
|
36229
36296
|
const { state, rows } = await snap();
|
|
36230
|
-
const
|
|
36297
|
+
const checks = state === "failure" ? await withLogs(rows) : rows;
|
|
36298
|
+
const payload = { state, timestamp: (/* @__PURE__ */ new Date()).toISOString(), checks };
|
|
36231
36299
|
console.log(JSON.stringify(payload));
|
|
36232
36300
|
if (state === "success") return;
|
|
36233
36301
|
if (state === "failure") {
|
|
@@ -39227,6 +39295,93 @@ function hasRepoLocalWorktrees() {
|
|
|
39227
39295
|
return root !== null && (0, import_node_fs46.existsSync)((0, import_node_path43.join)(root, ".worktrees"));
|
|
39228
39296
|
}
|
|
39229
39297
|
|
|
39298
|
+
// src/cross-repo-filing-issue.ts
|
|
39299
|
+
async function discoverSameOrgIssueNumber(client, currentRepo, number) {
|
|
39300
|
+
const [owner] = currentRepo.split("/");
|
|
39301
|
+
if (!owner) throw new Error(`cannot derive an organization from ${currentRepo}`);
|
|
39302
|
+
const repositories = await client.restPaginate(`orgs/${owner}/repos?type=all`);
|
|
39303
|
+
const hits = [];
|
|
39304
|
+
let next = 0;
|
|
39305
|
+
const worker = async () => {
|
|
39306
|
+
while (next < repositories.length) {
|
|
39307
|
+
const repo = repositories[next++]?.full_name;
|
|
39308
|
+
if (!repo || repo.toLowerCase() === currentRepo.toLowerCase()) continue;
|
|
39309
|
+
const issue2 = await readExactIssue(client, repo, number);
|
|
39310
|
+
if (issue2) hits.push(issue2);
|
|
39311
|
+
}
|
|
39312
|
+
};
|
|
39313
|
+
await Promise.all(Array.from({ length: Math.min(8, repositories.length) }, () => worker()));
|
|
39314
|
+
return hits.sort((left, right) => left.repo.localeCompare(right.repo));
|
|
39315
|
+
}
|
|
39316
|
+
async function readExactIssue(client, repo, number) {
|
|
39317
|
+
try {
|
|
39318
|
+
const issue2 = await client.rest("GET", `repos/${repo}/issues/${number}`);
|
|
39319
|
+
if (issue2.pull_request || issue2.number !== number || !issue2.title || !issue2.html_url) return void 0;
|
|
39320
|
+
const state = issue2.state?.toUpperCase();
|
|
39321
|
+
if (state !== "OPEN" && state !== "CLOSED") return void 0;
|
|
39322
|
+
return { repo, number, title: issue2.title, url: issue2.html_url, state };
|
|
39323
|
+
} catch (e) {
|
|
39324
|
+
if (e instanceof GitHubApiError && e.status === 404) return void 0;
|
|
39325
|
+
throw e;
|
|
39326
|
+
}
|
|
39327
|
+
}
|
|
39328
|
+
function explicitSameOrgIssueUrls(text, prRepo, closingNumbers) {
|
|
39329
|
+
const [owner] = prRepo.split("/");
|
|
39330
|
+
const found = /* @__PURE__ */ new Map();
|
|
39331
|
+
for (const match of text.matchAll(/https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)\b/gi)) {
|
|
39332
|
+
const repo = match[1];
|
|
39333
|
+
const number = Number(match[2]);
|
|
39334
|
+
if (!repo || !Number.isInteger(number) || !closingNumbers.has(number)) continue;
|
|
39335
|
+
if (repo.split("/")[0]?.toLowerCase() !== owner.toLowerCase() || repo.toLowerCase() === prRepo.toLowerCase()) continue;
|
|
39336
|
+
found.set(`${repo.toLowerCase()}#${number}`, { repo, number });
|
|
39337
|
+
}
|
|
39338
|
+
return [...found.values()];
|
|
39339
|
+
}
|
|
39340
|
+
async function reconcileCrossRepoFilingIssue(client, prRepo, prNumber) {
|
|
39341
|
+
try {
|
|
39342
|
+
const pr2 = await client.rest("GET", `repos/${prRepo}/pulls/${prNumber}`);
|
|
39343
|
+
if (!pr2.merged_at) return { status: "not-applicable" };
|
|
39344
|
+
const text = `${pr2.title ?? ""}
|
|
39345
|
+
${pr2.body ?? ""}`;
|
|
39346
|
+
const closingNumbers = new Set(findClosingMentions(text).filter((mention) => !mention.negated).map((mention) => mention.issue));
|
|
39347
|
+
if (closingNumbers.size === 0) return { status: "not-applicable" };
|
|
39348
|
+
const explicit = explicitSameOrgIssueUrls(text, prRepo, closingNumbers);
|
|
39349
|
+
let target;
|
|
39350
|
+
if (explicit.length > 1) {
|
|
39351
|
+
return { status: "failed", error: `multiple same-org issue URLs match bare closing references (${explicit.map((x) => `${x.repo}#${x.number}`).join(", ")})` };
|
|
39352
|
+
}
|
|
39353
|
+
if (explicit.length === 1) {
|
|
39354
|
+
target = await readExactIssue(client, explicit[0].repo, explicit[0].number);
|
|
39355
|
+
if (!target) return { status: "failed", issue: `${explicit[0].repo}#${explicit[0].number}`, error: "the explicit foreign issue URL did not resolve to an issue" };
|
|
39356
|
+
} else {
|
|
39357
|
+
const branchNumber = Number(/^#?(\d+)(?:-|$)/.exec(pr2.head?.ref ?? "")?.[1]);
|
|
39358
|
+
if (!Number.isInteger(branchNumber) || !closingNumbers.has(branchNumber)) return { status: "not-applicable" };
|
|
39359
|
+
if (await readExactIssue(client, prRepo, branchNumber)) return { status: "not-applicable" };
|
|
39360
|
+
const matches = await discoverSameOrgIssueNumber(client, prRepo, branchNumber);
|
|
39361
|
+
if (matches.length !== 1) {
|
|
39362
|
+
return {
|
|
39363
|
+
status: "failed",
|
|
39364
|
+
error: matches.length === 0 ? `branch #${branchNumber} matches a bare closing reference, but no same-org filing issue was found` : `branch #${branchNumber} is ambiguous across ${matches.map((match) => match.repo).join(", ")}`
|
|
39365
|
+
};
|
|
39366
|
+
}
|
|
39367
|
+
target = matches[0];
|
|
39368
|
+
}
|
|
39369
|
+
const issue2 = `${target.repo}#${target.number}`;
|
|
39370
|
+
if (target.state === "CLOSED") return { status: "already-closed", issue: issue2, url: target.url };
|
|
39371
|
+
const prUrl = pr2.html_url ?? `https://github.com/${prRepo}/pull/${prNumber}`;
|
|
39372
|
+
const comment = await client.rest("POST", `repos/${target.repo}/issues/${target.number}/comments`, {
|
|
39373
|
+
body: { body: `Resolved by merged PR ${prUrl}.
|
|
39374
|
+
|
|
39375
|
+
<!-- mmi-cross-repo-close:${prRepo}#${prNumber} -->` }
|
|
39376
|
+
});
|
|
39377
|
+
if (!comment.html_url) throw new Error(`GitHub created no evidence URL on ${issue2}`);
|
|
39378
|
+
await closeIssue(client, { ref: issue2, reason: "completed", evidence: comment.html_url });
|
|
39379
|
+
return { status: "closed", issue: issue2, url: target.url, evidence: comment.html_url };
|
|
39380
|
+
} catch (e) {
|
|
39381
|
+
return { status: "failed", error: e.message };
|
|
39382
|
+
}
|
|
39383
|
+
}
|
|
39384
|
+
|
|
39230
39385
|
// src/index.ts
|
|
39231
39386
|
var execFileGitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
39232
39387
|
async function githubRepoReachProbe() {
|
|
@@ -40220,7 +40375,31 @@ withExamples(mutating(
|
|
|
40220
40375
|
`worktree create: issue ${selector.repo}#${selector.number} belongs to ${selector.repo}, but this checkout is ${resolvedRepo} \u2014 re-run from ${selector.repo}'s primary checkout. Refusing to attach a foreign issue branch here (MMI-Hub#4351).`
|
|
40221
40376
|
);
|
|
40222
40377
|
}
|
|
40223
|
-
const
|
|
40378
|
+
const issueTitle = await fetchIssueTitle(selector.repo, selector.number);
|
|
40379
|
+
if (!o.slug && !issueTitle && o.claim && /^#?\d+$/.test(target)) {
|
|
40380
|
+
try {
|
|
40381
|
+
const client = defaultGitHubClient();
|
|
40382
|
+
const local = await readExactIssue(client, selector.repo, selector.number);
|
|
40383
|
+
if (!local) {
|
|
40384
|
+
const matches = await discoverSameOrgIssueNumber(client, selector.repo, selector.number);
|
|
40385
|
+
if (matches.length === 1) {
|
|
40386
|
+
const foreign = matches[0];
|
|
40387
|
+
let board = "its registered board";
|
|
40388
|
+
try {
|
|
40389
|
+
const cfg = await loadConfigForBoardSelector2(`${foreign.repo}#${foreign.number}`, foreign.repo);
|
|
40390
|
+
if (cfg.projectOwner && cfg.projectNumber) board = `${cfg.projectOwner} project #${cfg.projectNumber}`;
|
|
40391
|
+
} catch {
|
|
40392
|
+
}
|
|
40393
|
+
return fail(
|
|
40394
|
+
`worktree create: issue #${selector.number} is not in ${selector.repo}; it exists as ${foreign.repo}#${foreign.number} on ${board}. Re-run from ${foreign.repo}'s primary checkout with \`mmi-cli worktree create ${foreign.repo}#${foreign.number} --claim\`.`
|
|
40395
|
+
);
|
|
40396
|
+
}
|
|
40397
|
+
}
|
|
40398
|
+
} catch (e) {
|
|
40399
|
+
console.warn(` warning: could not discover whether #${selector.number} belongs to another same-org repo (${e.message})`);
|
|
40400
|
+
}
|
|
40401
|
+
}
|
|
40402
|
+
const slug = o.slug ?? (issueTitle ? slugifyIssueTitle(issueTitle) : "");
|
|
40224
40403
|
branch = buildNewBranchName(selector.number, slug ?? "");
|
|
40225
40404
|
if (PROTECTED_BRANCHES2.has(branch)) {
|
|
40226
40405
|
return fail(`worktree create: generated branch name '${branch}' collides with a protected train branch \u2014 pass a branch name instead`);
|
|
@@ -40281,8 +40460,13 @@ withExamples(mutating(
|
|
|
40281
40460
|
}
|
|
40282
40461
|
if (!resumed) {
|
|
40283
40462
|
step = `git worktree add ${wtPath}`;
|
|
40463
|
+
const wtPathPreExisted = (0, import_node_fs47.existsSync)(wtPath);
|
|
40464
|
+
const partialRemove = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
|
|
40284
40465
|
await withWorktreeAddLock(repoRoot2, () => addWorktreeRobust(wtPath, branch, base, {
|
|
40285
|
-
|
|
40466
|
+
// #4834: `-c core.longpaths=true` rides the add command itself — a Windows worktree path
|
|
40467
|
+
// pushes long repos past MAX_PATH while the main checkout still fits. Command-scoped, so
|
|
40468
|
+
// the shared `.git/config` this lock serializes is never touched.
|
|
40469
|
+
git: async (args) => (await execFileP2("git", [...worktreeAddGitPrefix(), ...args], { timeout: GH_MUTATION_TIMEOUT_MS })).stdout,
|
|
40286
40470
|
revParse: async (ref) => {
|
|
40287
40471
|
try {
|
|
40288
40472
|
return (await execFileP2("git", ["rev-parse", "--verify", ref], { timeout: GIT_TIMEOUT_MS })).stdout.trim() || void 0;
|
|
@@ -40291,6 +40475,13 @@ withExamples(mutating(
|
|
|
40291
40475
|
}
|
|
40292
40476
|
},
|
|
40293
40477
|
deleteBranch: (b) => execFileP2("git", ["branch", "-D", b], { timeout: GIT_TIMEOUT_MS }).then(() => void 0),
|
|
40478
|
+
cleanupPartial: async () => {
|
|
40479
|
+
if (wtPathPreExisted || !(0, import_node_fs47.existsSync)(wtPath)) return;
|
|
40480
|
+
partialRemove.detachReparsePoints(wtPath);
|
|
40481
|
+
await execFileP2("git", ["worktree", "remove", "--force", wtPath], { timeout: GIT_TIMEOUT_MS }).catch(() => partialRemove.removeWorktreeDir(wtPath).then(() => void 0));
|
|
40482
|
+
await execFileP2("git", ["worktree", "prune"], { timeout: GIT_TIMEOUT_MS }).catch(() => {
|
|
40483
|
+
});
|
|
40484
|
+
},
|
|
40294
40485
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
40295
40486
|
log: (m) => {
|
|
40296
40487
|
if (!o.json) console.error(` ${m}`);
|
|
@@ -42318,6 +42509,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
42318
42509
|
};
|
|
42319
42510
|
}
|
|
42320
42511
|
const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
|
|
42512
|
+
const crossRepoFilingIssue = repoForPostCleanup ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), repoForPostCleanup, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
|
|
42321
42513
|
invalidateStatuslineBoardCache();
|
|
42322
42514
|
console.log(JSON.stringify({
|
|
42323
42515
|
...buildPrMergeResultPayload({
|
|
@@ -42331,11 +42523,16 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
42331
42523
|
// `boardAdvance` keeps its published array shape; `boardAdvanceStatus` is the field a caller reads to
|
|
42332
42524
|
// tell "merged clean" from "merged, but the board did not follow" without guessing from the exit code.
|
|
42333
42525
|
...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
|
|
42334
|
-
boardAdvanceStatus: boardAdvance.status
|
|
42526
|
+
boardAdvanceStatus: boardAdvance.status,
|
|
42527
|
+
...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {}
|
|
42335
42528
|
}));
|
|
42336
42529
|
const boardAdvanceMessage = boardAdvanceFailureMessage(boardAdvance);
|
|
42337
42530
|
if (boardAdvanceMessage) console.error(boardAdvanceMessage);
|
|
42531
|
+
if (crossRepoFilingIssue.status === "failed") {
|
|
42532
|
+
console.error(`pr merge: cross-repo filing-issue reconciliation failed (${crossRepoFilingIssue.error}) \u2014 the PR MERGED, but a foreign filing issue may remain open.`);
|
|
42533
|
+
}
|
|
42338
42534
|
process.exitCode = boardAdvanceExitCode(boardAdvance) ?? process.exitCode;
|
|
42535
|
+
if (crossRepoFilingIssue.status === "failed") process.exitCode = 1;
|
|
42339
42536
|
});
|
|
42340
42537
|
registerQueryCommands(program2);
|
|
42341
42538
|
registerWorktreeCommands(program2);
|
|
@@ -42475,6 +42672,21 @@ function renderAlignment(label, alignment) {
|
|
|
42475
42672
|
}
|
|
42476
42673
|
return `${label}: ALIGNMENT PR PENDING \u2014 land it with \`mmi-cli devops pr merge ${alignment.prNumber ?? "<number>"} --auto --merge\`${alignment.prUrl ? ` (${alignment.prUrl})` : ""}`;
|
|
42477
42674
|
}
|
|
42675
|
+
var JERV_POWERTOOLS_REPO = "mutmutco/Jerv-PowerTools";
|
|
42676
|
+
async function runPostReleaseJervDoctor(repo) {
|
|
42677
|
+
if (repo.toLowerCase() !== JERV_POWERTOOLS_REPO.toLowerCase()) return void 0;
|
|
42678
|
+
try {
|
|
42679
|
+
await execJervCli(["doctor"], { timeout: 10 * 6e4 });
|
|
42680
|
+
return { status: "completed", note: "jerv-cli doctor completed on this owner machine" };
|
|
42681
|
+
} catch (e) {
|
|
42682
|
+
const err = e;
|
|
42683
|
+
const detail = (err.stderr?.trim() || err.message || "unknown doctor failure").split(/\r?\n/)[0];
|
|
42684
|
+
return {
|
|
42685
|
+
status: "reported-issues",
|
|
42686
|
+
note: `jerv-cli doctor ran but reported issues (${detail})`
|
|
42687
|
+
};
|
|
42688
|
+
}
|
|
42689
|
+
}
|
|
42478
42690
|
function renderTrainApply(commandName, r) {
|
|
42479
42691
|
let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2014 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
42480
42692
|
if (r.versionFold) base = `${base}; ${r.versionFold}`;
|
|
@@ -42493,6 +42705,9 @@ function renderTrainApply(commandName, r) {
|
|
|
42493
42705
|
if (r.projectInfoSync) {
|
|
42494
42706
|
base = `${base}; project info: ${r.projectInfoSync.note}`;
|
|
42495
42707
|
}
|
|
42708
|
+
if (r.postReleaseJervDoctor) {
|
|
42709
|
+
base = `${base}; Jerv convergence: ${r.postReleaseJervDoctor.note}`;
|
|
42710
|
+
}
|
|
42496
42711
|
if (r.announceNote) base = `${base}; announce: ${r.announceNote}`;
|
|
42497
42712
|
if (r.releaseVerdict) {
|
|
42498
42713
|
const v = r.releaseVerdict;
|
|
@@ -42646,6 +42861,7 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
42646
42861
|
const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
42647
42862
|
const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev });
|
|
42648
42863
|
let projectInfoSync;
|
|
42864
|
+
const postReleaseJervDoctor = commandName === "release" ? await runPostReleaseJervDoctor(result.repo) : void 0;
|
|
42649
42865
|
if (commandName === "release") {
|
|
42650
42866
|
try {
|
|
42651
42867
|
projectInfoSync = await runProjectInfoSync(result.repo, true);
|
|
@@ -42672,6 +42888,7 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
42672
42888
|
const reported = {
|
|
42673
42889
|
...result,
|
|
42674
42890
|
...projectInfoSync ? { projectInfoSync } : {},
|
|
42891
|
+
...postReleaseJervDoctor ? { postReleaseJervDoctor } : {},
|
|
42675
42892
|
...releaseVerdict ? { releaseVerdict } : {}
|
|
42676
42893
|
};
|
|
42677
42894
|
printLine(o.json ? JSON.stringify(reported, null, 2) : renderTrainApply(commandName, reported));
|
package/package.json
CHANGED