@mutmutco/cli 4.3.1 → 4.3.3
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 +2016 -570
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3416,7 +3416,7 @@ function useColor() {
|
|
|
3416
3416
|
var program = new Command();
|
|
3417
3417
|
|
|
3418
3418
|
// src/command-composition.ts
|
|
3419
|
-
var
|
|
3419
|
+
var import_node_fs48 = require("node:fs");
|
|
3420
3420
|
|
|
3421
3421
|
// src/clean-exit.ts
|
|
3422
3422
|
var UNDICI_GLOBAL_DISPATCHER_SYMBOL = Object.getOwnPropertySymbols(globalThis).find(
|
|
@@ -3465,19 +3465,19 @@ function hardExit(code) {
|
|
|
3465
3465
|
}
|
|
3466
3466
|
var STDIO_FLUSH_TIMEOUT_MS = 2e3;
|
|
3467
3467
|
function flushStream(stream) {
|
|
3468
|
-
return new Promise((
|
|
3468
|
+
return new Promise((resolve7) => {
|
|
3469
3469
|
try {
|
|
3470
|
-
stream.write("", () =>
|
|
3470
|
+
stream.write("", () => resolve7());
|
|
3471
3471
|
} catch {
|
|
3472
|
-
|
|
3472
|
+
resolve7();
|
|
3473
3473
|
}
|
|
3474
3474
|
});
|
|
3475
3475
|
}
|
|
3476
3476
|
async function flushStdio(timeoutMs = STDIO_FLUSH_TIMEOUT_MS) {
|
|
3477
3477
|
await Promise.race([
|
|
3478
3478
|
Promise.all([flushStream(process.stdout), flushStream(process.stderr)]),
|
|
3479
|
-
new Promise((
|
|
3480
|
-
setTimeout(
|
|
3479
|
+
new Promise((resolve7) => {
|
|
3480
|
+
setTimeout(resolve7, timeoutMs).unref?.();
|
|
3481
3481
|
})
|
|
3482
3482
|
]);
|
|
3483
3483
|
}
|
|
@@ -3485,7 +3485,7 @@ async function cleanExit(code) {
|
|
|
3485
3485
|
process.exitCode = code;
|
|
3486
3486
|
await closeHttpPool();
|
|
3487
3487
|
await flushStdio();
|
|
3488
|
-
await new Promise((
|
|
3488
|
+
await new Promise((resolve7) => setImmediate(resolve7));
|
|
3489
3489
|
return void 0;
|
|
3490
3490
|
}
|
|
3491
3491
|
var CLI_EXIT_WATCHDOG_MS = (() => {
|
|
@@ -3930,7 +3930,7 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
|
|
|
3930
3930
|
const attempts = opts.attempts ?? 3;
|
|
3931
3931
|
const baseDelayMs = opts.baseDelayMs ?? 250;
|
|
3932
3932
|
const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
|
|
3933
|
-
const sleep2 = opts.sleep ?? ((ms) => new Promise((
|
|
3933
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
3934
3934
|
let lastErr;
|
|
3935
3935
|
for (let i = 0; i < attempts; i++) {
|
|
3936
3936
|
const isLast = i === attempts - 1;
|
|
@@ -4198,8 +4198,8 @@ async function readStdin(opts = {}) {
|
|
|
4198
4198
|
})().catch(() => {
|
|
4199
4199
|
});
|
|
4200
4200
|
let timer;
|
|
4201
|
-
const timeout = new Promise((
|
|
4202
|
-
timer = setTimeout(
|
|
4201
|
+
const timeout = new Promise((resolve7) => {
|
|
4202
|
+
timer = setTimeout(resolve7, timeoutMs);
|
|
4203
4203
|
});
|
|
4204
4204
|
try {
|
|
4205
4205
|
await Promise.race([drain, timeout]);
|
|
@@ -4253,12 +4253,12 @@ function killProcessTree(pid) {
|
|
|
4253
4253
|
function execFileHard(file, args, options) {
|
|
4254
4254
|
const { timeout, step, ...rest } = options;
|
|
4255
4255
|
const started = Date.now();
|
|
4256
|
-
return new Promise((
|
|
4256
|
+
return new Promise((resolve7, reject) => {
|
|
4257
4257
|
const child2 = (0, import_node_child_process2.execFile)(file, args, { encoding: "utf8", windowsHide: true, ...rest, timeout: 0 }, (error, stdout, stderr) => {
|
|
4258
4258
|
clearTimeout(timer);
|
|
4259
4259
|
if (expired) return;
|
|
4260
4260
|
if (error) reject(error);
|
|
4261
|
-
else
|
|
4261
|
+
else resolve7({ stdout: String(stdout), stderr: String(stderr) });
|
|
4262
4262
|
});
|
|
4263
4263
|
let expired = false;
|
|
4264
4264
|
const timer = setTimeout(() => {
|
|
@@ -5219,8 +5219,8 @@ function unknownCommandDomainGuide(parentPath, token) {
|
|
|
5219
5219
|
}
|
|
5220
5220
|
|
|
5221
5221
|
// src/command-composition.ts
|
|
5222
|
-
var
|
|
5223
|
-
var
|
|
5222
|
+
var import_node_os20 = require("node:os");
|
|
5223
|
+
var import_node_path45 = require("node:path");
|
|
5224
5224
|
|
|
5225
5225
|
// src/board-read.ts
|
|
5226
5226
|
var import_node_fs13 = require("node:fs");
|
|
@@ -5330,7 +5330,7 @@ function rateLimitedReceipt(opts) {
|
|
|
5330
5330
|
};
|
|
5331
5331
|
}
|
|
5332
5332
|
async function runWithRateLimitBackoff(operation, opts) {
|
|
5333
|
-
const sleep2 = opts.sleep ?? ((ms) => new Promise((
|
|
5333
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
5334
5334
|
const now = opts.now ?? Date.now;
|
|
5335
5335
|
const log = opts.log ?? ((message) => console.warn(message));
|
|
5336
5336
|
const capMs = opts.capMs ?? RATE_LIMIT_WAIT_CAP_MS;
|
|
@@ -5555,6 +5555,31 @@ function rewriteNegatedClosingPhrases(text) {
|
|
|
5555
5555
|
(seg, i) => i % 2 === 1 ? seg : seg.replace(NEGATED_CLOSING_PHRASE_RE, (_m, plain, linked) => `leaves #${plain ?? linked} open`)
|
|
5556
5556
|
).join("");
|
|
5557
5557
|
}
|
|
5558
|
+
var LINE_START_CLOSING_RE = /^\s*(?:[-*+]\s+)?(?:\*\*|__)?\s*(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:#\d+|\[#\d+\]\()/i;
|
|
5559
|
+
var FENCE_LINE_RE = /^\s*(?:```|~~~)/;
|
|
5560
|
+
function rewriteCloseKeywordProse(text) {
|
|
5561
|
+
if (!text.includes("#")) return { text, rewritten: [] };
|
|
5562
|
+
const rewritten = /* @__PURE__ */ new Set();
|
|
5563
|
+
let inFence = false;
|
|
5564
|
+
const lines = text.split("\n").map((line) => {
|
|
5565
|
+
if (FENCE_LINE_RE.test(line)) {
|
|
5566
|
+
inFence = !inFence;
|
|
5567
|
+
return line;
|
|
5568
|
+
}
|
|
5569
|
+
if (inFence || LINE_START_CLOSING_RE.test(line)) return line;
|
|
5570
|
+
CLOSING_MENTION_RE.lastIndex = 0;
|
|
5571
|
+
return line.split(/(`[^`\n]*`)/g).map((seg, i) => {
|
|
5572
|
+
if (i % 2 === 1) return seg;
|
|
5573
|
+
return seg.replace(CLOSING_MENTION_RE, (match, _keyword, plain, linked) => {
|
|
5574
|
+
const n = Number(plain ?? linked);
|
|
5575
|
+
if (!Number.isInteger(n) || n <= 0) return match;
|
|
5576
|
+
rewritten.add(n);
|
|
5577
|
+
return `part of #${n}`;
|
|
5578
|
+
});
|
|
5579
|
+
}).join("");
|
|
5580
|
+
});
|
|
5581
|
+
return rewritten.size ? { text: lines.join("\n"), rewritten: [...rewritten] } : { text, rewritten: [] };
|
|
5582
|
+
}
|
|
5558
5583
|
function findClosingMentions(text) {
|
|
5559
5584
|
const mentions = [];
|
|
5560
5585
|
for (const match of text.matchAll(CLOSING_MENTION_RE)) {
|
|
@@ -5588,11 +5613,12 @@ var CROSS_REPO_TOKEN_RE = /\b([\w.-]+\/[\w.-]+)#(\d+)\b/g;
|
|
|
5588
5613
|
function findCrossRepoAmbiguousClosings(text, closingNumbers, repo) {
|
|
5589
5614
|
if (!closingNumbers.length) return [];
|
|
5590
5615
|
const closing = new Set(closingNumbers);
|
|
5616
|
+
const bareClosing = new Set(findClosingMentions(text).map((m) => m.issue));
|
|
5591
5617
|
const flagged = /* @__PURE__ */ new Set();
|
|
5592
5618
|
const repoLower = repo.toLowerCase();
|
|
5593
5619
|
for (const match of text.matchAll(CROSS_REPO_TOKEN_RE)) {
|
|
5594
5620
|
const n = Number(match[2]);
|
|
5595
|
-
if (!closing.has(n)) continue;
|
|
5621
|
+
if (!closing.has(n) || !bareClosing.has(n)) continue;
|
|
5596
5622
|
if ((match[1] ?? "").toLowerCase() === repoLower) continue;
|
|
5597
5623
|
flagged.add(n);
|
|
5598
5624
|
}
|
|
@@ -8016,13 +8042,47 @@ async function defaultCompareRefs(exec, repo, base, head) {
|
|
|
8016
8042
|
return "unknown";
|
|
8017
8043
|
}
|
|
8018
8044
|
}
|
|
8045
|
+
async function publishAbsentPrHeadIfOwned(repo, head, exec) {
|
|
8046
|
+
if (isForkStyleHead(head)) return false;
|
|
8047
|
+
let originRepo;
|
|
8048
|
+
try {
|
|
8049
|
+
const { stdout } = await exec("git", ["remote", "get-url", "--push", "origin"], { timeout: GIT_TIMEOUT_MS });
|
|
8050
|
+
originRepo = repoFromRemoteUrl(stdout.trim());
|
|
8051
|
+
} catch {
|
|
8052
|
+
return false;
|
|
8053
|
+
}
|
|
8054
|
+
if (!originRepo || originRepo.toLowerCase() !== repo.toLowerCase()) return false;
|
|
8055
|
+
let currentBranch2;
|
|
8056
|
+
try {
|
|
8057
|
+
currentBranch2 = (await exec("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
8058
|
+
} catch {
|
|
8059
|
+
return false;
|
|
8060
|
+
}
|
|
8061
|
+
if (!currentBranch2 || currentBranch2 !== head) return false;
|
|
8062
|
+
let commit;
|
|
8063
|
+
try {
|
|
8064
|
+
commit = (await exec("git", ["rev-parse", "--verify", "HEAD^{commit}"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
8065
|
+
} catch {
|
|
8066
|
+
return false;
|
|
8067
|
+
}
|
|
8068
|
+
if (!commit) return false;
|
|
8069
|
+
try {
|
|
8070
|
+
await exec("git", ["push", "origin", `${commit}:refs/heads/${head}`], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8071
|
+
return true;
|
|
8072
|
+
} catch {
|
|
8073
|
+
return false;
|
|
8074
|
+
}
|
|
8075
|
+
}
|
|
8019
8076
|
async function preflightPrCreateRemoteHead(args, deps) {
|
|
8020
8077
|
const repo = flagValue(args, "--repo");
|
|
8021
8078
|
const head = flagValue(args, "--head");
|
|
8022
8079
|
if (!repo || !head || isForkStyleHead(head)) return void 0;
|
|
8023
8080
|
const readRemoteHead = deps.readRemoteHead ?? ((input) => defaultReadRemoteHead(deps.exec, input.repo, input.head));
|
|
8024
|
-
|
|
8025
|
-
if (presence === "absent")
|
|
8081
|
+
let presence = await readRemoteHead({ repo, head });
|
|
8082
|
+
if (presence === "absent") {
|
|
8083
|
+
if (!await publishAbsentPrHeadIfOwned(repo, head, deps.exec)) return unpushedHeadMessage(head);
|
|
8084
|
+
presence = "present";
|
|
8085
|
+
}
|
|
8026
8086
|
if (presence !== "present") return void 0;
|
|
8027
8087
|
const base = flagValue(args, "--base");
|
|
8028
8088
|
if (!base) return void 0;
|
|
@@ -8438,7 +8498,7 @@ async function createPrViaRestFallback(args, swappedArgs, deps, knownPools) {
|
|
|
8438
8498
|
}
|
|
8439
8499
|
async function ghCreate(args, deps = {}) {
|
|
8440
8500
|
const exec = deps.exec ?? execFileP;
|
|
8441
|
-
const sleep2 = deps.sleep ?? ((ms) => new Promise((
|
|
8501
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
8442
8502
|
const now = deps.now ?? Date.now;
|
|
8443
8503
|
const read = deps.readFile ?? import_promises.readFile;
|
|
8444
8504
|
const restCreatePr = deps.restCreatePr ?? ((input) => defaultRestCreatePr(exec, input));
|
|
@@ -9151,8 +9211,8 @@ var PRE_SPAWN_DRAIN_TIMEOUT_MS = 2e3;
|
|
|
9151
9211
|
async function drainHttpPoolBeforeSpawn() {
|
|
9152
9212
|
const drained = await Promise.race([
|
|
9153
9213
|
closeHttpPool().then(() => true),
|
|
9154
|
-
new Promise((
|
|
9155
|
-
setTimeout(() =>
|
|
9214
|
+
new Promise((resolve7) => {
|
|
9215
|
+
setTimeout(() => resolve7(false), PRE_SPAWN_DRAIN_TIMEOUT_MS).unref?.();
|
|
9156
9216
|
})
|
|
9157
9217
|
]);
|
|
9158
9218
|
if (!drained) destroyHttpPool();
|
|
@@ -9400,8 +9460,22 @@ function repoFromSelector(selector) {
|
|
|
9400
9460
|
return void 0;
|
|
9401
9461
|
}
|
|
9402
9462
|
}
|
|
9463
|
+
var REPO_OPTION_SHAPES = "owner/repo or host/owner/repo";
|
|
9464
|
+
function invalidRepoOption(repo, hint) {
|
|
9465
|
+
const base = `invalid repo "${repo}" (expected ${REPO_OPTION_SHAPES})`;
|
|
9466
|
+
return new Error(hint ? `${base} \u2014 ${hint}` : base);
|
|
9467
|
+
}
|
|
9468
|
+
async function validateRepoOption(repo) {
|
|
9469
|
+
const value = repo.trim();
|
|
9470
|
+
const parts = value.split("/");
|
|
9471
|
+
if ((parts.length === 2 || parts.length === 3) && parts.every((p) => p !== "")) return value;
|
|
9472
|
+
const origin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"])) ?? repoFromRemoteUrl(originUrlFromGitConfig() ?? "");
|
|
9473
|
+
const originName = origin?.split("/")[1];
|
|
9474
|
+
const hint = originName !== void 0 && value.toLowerCase() === originName.toLowerCase() ? `did you mean ${origin}?` : void 0;
|
|
9475
|
+
throw invalidRepoOption(repo, hint);
|
|
9476
|
+
}
|
|
9403
9477
|
async function resolveRepo(repo) {
|
|
9404
|
-
if (repo) return repo;
|
|
9478
|
+
if (repo) return validateRepoOption(repo);
|
|
9405
9479
|
const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"])) ?? repoFromRemoteUrl(originUrlFromGitConfig() ?? "");
|
|
9406
9480
|
if (fromOrigin) return fromOrigin;
|
|
9407
9481
|
try {
|
|
@@ -11660,7 +11734,16 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
|
|
|
11660
11734
|
const out = { failed: [] };
|
|
11661
11735
|
try {
|
|
11662
11736
|
const comments = await client.restPaginate(`repos/${repo}/issues/${number}/comments`);
|
|
11663
|
-
|
|
11737
|
+
const rows = comments.map((comment) => ({ body: comment.body ?? "", login: comment.user?.login ?? void 0 }));
|
|
11738
|
+
out.marker = latestClaimMarker(rows);
|
|
11739
|
+
if (out.marker) {
|
|
11740
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
11741
|
+
if (latestClaimMarker([rows[i]])) {
|
|
11742
|
+
out.markerBy = rows[i].login;
|
|
11743
|
+
break;
|
|
11744
|
+
}
|
|
11745
|
+
}
|
|
11746
|
+
}
|
|
11664
11747
|
out.markerAgeMs = out.marker ? Date.now() - Date.parse(out.marker.ts) : void 0;
|
|
11665
11748
|
if (out.marker) {
|
|
11666
11749
|
const state = probeLocalClaimSession(out.marker);
|
|
@@ -11727,17 +11810,26 @@ function laneOwnership(marker, current) {
|
|
|
11727
11810
|
}
|
|
11728
11811
|
return "unknown";
|
|
11729
11812
|
}
|
|
11730
|
-
|
|
11813
|
+
function laneResumeEvidence(evidence) {
|
|
11814
|
+
if (!evidence.marker || evidence.sessionState !== "dead" || evidence.failed.length > 0) return void 0;
|
|
11815
|
+
if (!isHostSessionId(evidence.marker.session)) return void 0;
|
|
11816
|
+
const artifacts = [evidence.openPr, evidence.branch ? `live branch ${evidence.branch}` : void 0].filter(Boolean);
|
|
11817
|
+
return `prior claim by lane ${describeClaimMarker(evidence.marker)} (@${evidence.markerBy}, ${formatClaimAge(evidence.markerAgeMs)} old) is verifiably not running on this host \u2014 its local session transcript is gone or stale${artifacts.length ? `; resuming over ${artifacts.join("; ")}` : ""}`;
|
|
11818
|
+
}
|
|
11819
|
+
async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin) {
|
|
11731
11820
|
const evidence = await gatherClaimLiveness(client, item.repository, item.number, openPullsFetcher(client));
|
|
11732
11821
|
const ownership = laneOwnership(evidence.marker, actor);
|
|
11822
|
+
const sameOwner = Boolean(viewerLogin && evidence.markerBy && evidence.markerBy.toLowerCase() === viewerLogin.toLowerCase());
|
|
11823
|
+
const resume = ownership !== "mine" && sameOwner ? laneResumeEvidence(evidence) : void 0;
|
|
11733
11824
|
const live = ownership === "mine" ? [] : liveEvidenceLines(evidence, item.repository);
|
|
11734
11825
|
const unverifiable = ownership === "mine" ? [] : evidence.failed;
|
|
11735
11826
|
return {
|
|
11736
|
-
contested: ownership !== "mine" && (live.length > 0 || unverifiable.length > 0),
|
|
11827
|
+
contested: ownership !== "mine" && !resume && (live.length > 0 || unverifiable.length > 0),
|
|
11737
11828
|
live,
|
|
11738
11829
|
unverifiable,
|
|
11739
11830
|
ownership,
|
|
11740
|
-
marker: evidence.marker
|
|
11831
|
+
marker: evidence.marker,
|
|
11832
|
+
resume
|
|
11741
11833
|
};
|
|
11742
11834
|
}
|
|
11743
11835
|
function laneContestMessage(ref, contest, verb) {
|
|
@@ -12036,8 +12128,9 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
12036
12128
|
host: ctx.session.host
|
|
12037
12129
|
};
|
|
12038
12130
|
let previousHolder;
|
|
12039
|
-
|
|
12040
|
-
const
|
|
12131
|
+
let resumeEvidence;
|
|
12132
|
+
const claimedReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "claimed", holder };
|
|
12133
|
+
const heldReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "held", holder };
|
|
12041
12134
|
if (flatItem.contentType !== "Issue") throw new Error(`${flatItem.ref} is not an issue`);
|
|
12042
12135
|
const pre = evaluateClaim(flatItem, assignedLogin);
|
|
12043
12136
|
if (!pre.ok) throw new Error(pre.reason);
|
|
@@ -12048,17 +12141,25 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
12048
12141
|
if (!verdict.ok) throw new Error(verdict.reason);
|
|
12049
12142
|
item = fresh;
|
|
12050
12143
|
const refuseIfContested = async () => {
|
|
12051
|
-
const contest = await checkLaneContest(client, item, ctx.session);
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
previousHolder = {
|
|
12144
|
+
const contest = await checkLaneContest(client, item, ctx.session, report.viewer);
|
|
12145
|
+
resumeEvidence = void 0;
|
|
12146
|
+
const displaced = () => ({
|
|
12055
12147
|
login: assignedLogin,
|
|
12056
12148
|
...contest.marker ? {
|
|
12057
12149
|
session: contest.marker.session,
|
|
12058
12150
|
surface: contest.marker.surface,
|
|
12059
12151
|
host: contest.marker.host
|
|
12060
12152
|
} : {}
|
|
12061
|
-
};
|
|
12153
|
+
});
|
|
12154
|
+
if (!contest.contested) {
|
|
12155
|
+
if (contest.resume && contest.marker) {
|
|
12156
|
+
resumeEvidence = contest.resume;
|
|
12157
|
+
previousHolder = displaced();
|
|
12158
|
+
}
|
|
12159
|
+
return;
|
|
12160
|
+
}
|
|
12161
|
+
if (!options.force) throw new Error(laneContestMessage(item.ref, contest, "claim"));
|
|
12162
|
+
previousHolder = displaced();
|
|
12062
12163
|
};
|
|
12063
12164
|
await refuseIfContested();
|
|
12064
12165
|
if (verdict.alreadyClaimed) {
|
|
@@ -12128,7 +12229,7 @@ async function claimBoardIssues(options, deps = {}) {
|
|
|
12128
12229
|
const ref = `${selector.repo}#${selector.number}`;
|
|
12129
12230
|
try {
|
|
12130
12231
|
const result = await claimOneBoardItem(ctx, selector, options);
|
|
12131
|
-
results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
|
|
12232
|
+
results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
|
|
12132
12233
|
} catch (e) {
|
|
12133
12234
|
results[index] = { ref, claimed: false, reason: e.message };
|
|
12134
12235
|
}
|
|
@@ -12271,7 +12372,7 @@ async function setBoardItemPriority(client, cfg, itemId, priority) {
|
|
|
12271
12372
|
await updateItemSingleSelect(client, cfg.projectId, itemId, cfg.priorityFieldId, optionId);
|
|
12272
12373
|
return cliPriorityToFieldName(priority);
|
|
12273
12374
|
}
|
|
12274
|
-
var defaultRetrySleep = (ms) => new Promise((
|
|
12375
|
+
var defaultRetrySleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
12275
12376
|
async function resolveProjectItemIdWithRetry(client, cfg, selector, opts = {}) {
|
|
12276
12377
|
const attempts = Math.max(1, opts.attempts ?? 5);
|
|
12277
12378
|
const delayMs = opts.delayMs ?? 300;
|
|
@@ -15274,10 +15375,10 @@ var rollout_plan_default = {
|
|
|
15274
15375
|
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)."
|
|
15275
15376
|
},
|
|
15276
15377
|
baseline: {
|
|
15277
|
-
version: "4.3.
|
|
15278
|
-
tag: "v4.3.
|
|
15279
|
-
commit: "
|
|
15280
|
-
npm: "@mutmutco/cli@4.3.
|
|
15378
|
+
version: "4.3.3",
|
|
15379
|
+
tag: "v4.3.3",
|
|
15380
|
+
commit: "851ec4c56e34",
|
|
15381
|
+
npm: "@mutmutco/cli@4.3.3"
|
|
15281
15382
|
},
|
|
15282
15383
|
exitCriterion: "fleet-n-of-n",
|
|
15283
15384
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15294,14 +15395,14 @@ var rollout_plan_default = {
|
|
|
15294
15395
|
repo: "mutmutco/mmi-hub",
|
|
15295
15396
|
role: "canary",
|
|
15296
15397
|
schedule: "train",
|
|
15297
|
-
v3Target: "v4.3.
|
|
15398
|
+
v3Target: "v4.3.3"
|
|
15298
15399
|
}
|
|
15299
15400
|
],
|
|
15300
15401
|
rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
|
|
15301
15402
|
rollback: {
|
|
15302
15403
|
independent: true,
|
|
15303
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15304
|
-
v3Target: "v4.3.
|
|
15404
|
+
mechanism: "npm dist-tag latest -> 4.3.3 and redeploy the Hub Lambda from tag v4.3.3 (851ec4c56e34); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15405
|
+
v3Target: "v4.3.3 (@mutmutco/cli@4.3.3, tag commit 851ec4c56e34 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15305
15406
|
}
|
|
15306
15407
|
},
|
|
15307
15408
|
{
|
|
@@ -16169,6 +16270,7 @@ function validateEnum(flag, allowed, value, command) {
|
|
|
16169
16270
|
var ISSUE_STATES = ["open", "closed", "all"];
|
|
16170
16271
|
var PR_STATES = ["open", "all"];
|
|
16171
16272
|
async function resolveBoardProjectId(repoOption) {
|
|
16273
|
+
if (repoOption !== void 0) await validateRepoOption(repoOption);
|
|
16172
16274
|
try {
|
|
16173
16275
|
const floor = await loadConfig();
|
|
16174
16276
|
if (!floor.sagaApiUrl) return void 0;
|
|
@@ -17270,7 +17372,7 @@ async function runMergeTreePreflight(deps, ours, theirs) {
|
|
|
17270
17372
|
async function predictMergeConflicts(deps, ours, theirs) {
|
|
17271
17373
|
return runMergeTreePreflight(deps, ours, theirs);
|
|
17272
17374
|
}
|
|
17273
|
-
async function mergeWithToleratedResolution(deps, sourceRef, label,
|
|
17375
|
+
async function mergeWithToleratedResolution(deps, sourceRef, label, resolve7, extraTolerated = []) {
|
|
17274
17376
|
try {
|
|
17275
17377
|
await deps.run("git", ["merge", sourceRef, "--no-edit"]);
|
|
17276
17378
|
return;
|
|
@@ -17284,7 +17386,7 @@ async function mergeWithToleratedResolution(deps, sourceRef, label, resolve6, ex
|
|
|
17284
17386
|
unmerged.length === 0 ? `${label} merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun` : `${label} merge conflicts on untolerated path(s): ${blocking.join(", ")} \u2014 merge aborted (the train is misaligned; reconcile the branches via an approved alignment PR, then rerun)`
|
|
17285
17387
|
);
|
|
17286
17388
|
}
|
|
17287
|
-
await deps.run("git", ["checkout", `--${
|
|
17389
|
+
await deps.run("git", ["checkout", `--${resolve7}`, "--", ...unmerged]);
|
|
17288
17390
|
await deps.run("git", ["add", "--", ...unmerged]);
|
|
17289
17391
|
await deps.run("git", ["commit", "--no-edit"]);
|
|
17290
17392
|
}
|
|
@@ -17463,7 +17565,7 @@ var CORRELATE_SKEW_SLACK_MS = 1e4;
|
|
|
17463
17565
|
var CORRELATE_PAGE_LIMIT = 50;
|
|
17464
17566
|
var RUN_CONFIRM_ATTEMPTS = 3;
|
|
17465
17567
|
var RUN_CONFIRM_DELAY_MS = 1e3;
|
|
17466
|
-
var defaultSleep = (ms) => new Promise((
|
|
17568
|
+
var defaultSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
17467
17569
|
function resolveSleep(deps) {
|
|
17468
17570
|
return deps.sleep ?? defaultSleep;
|
|
17469
17571
|
}
|
|
@@ -18292,12 +18394,6 @@ async function discoverShaWorkflowRuns(deps, repo, headSha, seenRunIds) {
|
|
|
18292
18394
|
}
|
|
18293
18395
|
return extra;
|
|
18294
18396
|
}
|
|
18295
|
-
async function deriveHistoricalDeployStatus(deps, ctx, model, releaseSha) {
|
|
18296
|
-
const targets = prodReleaseTargets(model);
|
|
18297
|
-
if (!targets.length) return "pending";
|
|
18298
|
-
const repo = model === "hub-serverless" ? HUB_REPO2 : ctx.repo;
|
|
18299
|
-
return aggregateWorkflowRuns(await listOwnWorkflowRuns(deps, repo, targets, releaseSha));
|
|
18300
|
-
}
|
|
18301
18397
|
async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
|
|
18302
18398
|
if (isCentralDispatchModel(model)) {
|
|
18303
18399
|
const since = (deps.now ?? Date.now)();
|
|
@@ -18592,9 +18688,9 @@ function renderTenantControl(r) {
|
|
|
18592
18688
|
}
|
|
18593
18689
|
|
|
18594
18690
|
// src/train-apply-phases.ts
|
|
18595
|
-
var
|
|
18596
|
-
var
|
|
18597
|
-
var
|
|
18691
|
+
var import_node_fs20 = require("node:fs");
|
|
18692
|
+
var import_promises3 = require("node:fs/promises");
|
|
18693
|
+
var import_node_path19 = require("node:path");
|
|
18598
18694
|
|
|
18599
18695
|
// src/actions-billing-preflight.ts
|
|
18600
18696
|
var CANARY_WORKFLOW = "actions-job-start-canary.yml";
|
|
@@ -18818,37 +18914,1077 @@ Resume it: ${resumeCommand}
|
|
|
18818
18914
|
);
|
|
18819
18915
|
}
|
|
18820
18916
|
|
|
18917
|
+
// src/release-ledger.ts
|
|
18918
|
+
var import_node_fs19 = require("node:fs");
|
|
18919
|
+
var import_node_path18 = require("node:path");
|
|
18920
|
+
var import_node_crypto5 = require("node:crypto");
|
|
18921
|
+
|
|
18922
|
+
// src/file-lock.ts
|
|
18923
|
+
var import_promises2 = require("node:fs/promises");
|
|
18924
|
+
var import_node_path17 = require("node:path");
|
|
18925
|
+
var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
18926
|
+
var IMMEDIATE_RETRY_BUDGET = 3;
|
|
18927
|
+
var FileLockBusyError = class extends Error {
|
|
18928
|
+
lockPath;
|
|
18929
|
+
constructor(label, lockPath, maxWaitMs) {
|
|
18930
|
+
super(`${label} busy: ${lockPath} held longer than ${maxWaitMs}ms`);
|
|
18931
|
+
this.name = "FileLockBusyError";
|
|
18932
|
+
this.lockPath = lockPath;
|
|
18933
|
+
}
|
|
18934
|
+
};
|
|
18935
|
+
function resolveFileLockOpts(opts = {}) {
|
|
18936
|
+
return {
|
|
18937
|
+
staleMs: opts.staleMs ?? 3e4,
|
|
18938
|
+
retryMs: opts.retryMs ?? 50,
|
|
18939
|
+
maxWaitMs: opts.maxWaitMs ?? 5e3,
|
|
18940
|
+
label: opts.label ?? "file lock"
|
|
18941
|
+
};
|
|
18942
|
+
}
|
|
18943
|
+
async function acquireFileLock(lockPath, opts, deadline) {
|
|
18944
|
+
let immediateRetries = 0;
|
|
18945
|
+
for (; ; ) {
|
|
18946
|
+
let handle;
|
|
18947
|
+
try {
|
|
18948
|
+
handle = await (0, import_promises2.open)(lockPath, "wx");
|
|
18949
|
+
} catch (e) {
|
|
18950
|
+
const code = e.code;
|
|
18951
|
+
if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
|
|
18952
|
+
const retryNow = async () => {
|
|
18953
|
+
if (Date.now() >= deadline) throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
|
|
18954
|
+
if (++immediateRetries > IMMEDIATE_RETRY_BUDGET) await sleep(opts.retryMs);
|
|
18955
|
+
};
|
|
18956
|
+
try {
|
|
18957
|
+
const age = Date.now() - (await (0, import_promises2.stat)(lockPath)).mtimeMs;
|
|
18958
|
+
if (age > opts.staleMs) {
|
|
18959
|
+
try {
|
|
18960
|
+
await (0, import_promises2.unlink)(lockPath);
|
|
18961
|
+
await retryNow();
|
|
18962
|
+
continue;
|
|
18963
|
+
} catch (unlinkError) {
|
|
18964
|
+
if (unlinkError instanceof FileLockBusyError) throw unlinkError;
|
|
18965
|
+
const unlinkCode = unlinkError.code;
|
|
18966
|
+
if (unlinkCode === "ENOENT") {
|
|
18967
|
+
await retryNow();
|
|
18968
|
+
continue;
|
|
18969
|
+
}
|
|
18970
|
+
}
|
|
18971
|
+
}
|
|
18972
|
+
} catch (statError) {
|
|
18973
|
+
if (statError instanceof FileLockBusyError) throw statError;
|
|
18974
|
+
if (statError.code !== "ENOENT") throw statError;
|
|
18975
|
+
await retryNow();
|
|
18976
|
+
continue;
|
|
18977
|
+
}
|
|
18978
|
+
if (Date.now() >= deadline) {
|
|
18979
|
+
throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
|
|
18980
|
+
}
|
|
18981
|
+
await sleep(opts.retryMs);
|
|
18982
|
+
continue;
|
|
18983
|
+
}
|
|
18984
|
+
const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
18985
|
+
try {
|
|
18986
|
+
await handle.writeFile(token);
|
|
18987
|
+
} catch (e) {
|
|
18988
|
+
await handle.close().catch(() => void 0);
|
|
18989
|
+
throw e;
|
|
18990
|
+
}
|
|
18991
|
+
return { token, handle };
|
|
18992
|
+
}
|
|
18993
|
+
}
|
|
18994
|
+
async function fileLockHeldBy(lockPath, token) {
|
|
18995
|
+
try {
|
|
18996
|
+
return await (0, import_promises2.readFile)(lockPath, "utf8") === token;
|
|
18997
|
+
} catch {
|
|
18998
|
+
return false;
|
|
18999
|
+
}
|
|
19000
|
+
}
|
|
19001
|
+
async function releaseFileLock(lockPath, guard) {
|
|
19002
|
+
await guard.handle.close().catch(() => void 0);
|
|
19003
|
+
if (await fileLockHeldBy(lockPath, guard.token)) {
|
|
19004
|
+
await (0, import_promises2.unlink)(lockPath).catch(() => void 0);
|
|
19005
|
+
}
|
|
19006
|
+
}
|
|
19007
|
+
async function withFileLock(lockPath, opts, fn) {
|
|
19008
|
+
const resolved = resolveFileLockOpts(opts);
|
|
19009
|
+
await (0, import_promises2.mkdir)((0, import_node_path17.dirname)(lockPath), { recursive: true }).catch(() => void 0);
|
|
19010
|
+
const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
|
|
19011
|
+
try {
|
|
19012
|
+
return await fn();
|
|
19013
|
+
} finally {
|
|
19014
|
+
await releaseFileLock(lockPath, guard);
|
|
19015
|
+
}
|
|
19016
|
+
}
|
|
19017
|
+
|
|
19018
|
+
// src/release-ledger.ts
|
|
19019
|
+
var RELEASE_LEDGER_SCHEMA = "mmi-release-ledger/v1";
|
|
19020
|
+
var RELEASE_LEDGER_PHASES = [
|
|
19021
|
+
"promotion",
|
|
19022
|
+
"ordinaryCi",
|
|
19023
|
+
"githubRelease",
|
|
19024
|
+
"publish",
|
|
19025
|
+
"deploy",
|
|
19026
|
+
"alignment"
|
|
19027
|
+
];
|
|
19028
|
+
var PHASE_SET = new Set(RELEASE_LEDGER_PHASES);
|
|
19029
|
+
var STATE_SET = /* @__PURE__ */ new Set(["pending", "complete", "failed", "skipped"]);
|
|
19030
|
+
var ReleaseLedgerError = class extends Error {
|
|
19031
|
+
kind;
|
|
19032
|
+
constructor(kind, message) {
|
|
19033
|
+
super(message);
|
|
19034
|
+
this.name = "ReleaseLedgerError";
|
|
19035
|
+
this.kind = kind;
|
|
19036
|
+
}
|
|
19037
|
+
};
|
|
19038
|
+
var SHA_RE = /^[0-9a-f]{40}$/;
|
|
19039
|
+
var MAX_TEXT = 500;
|
|
19040
|
+
function truncate(text, max) {
|
|
19041
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
|
|
19042
|
+
}
|
|
19043
|
+
function normalizeLedgerAnchors(anchors) {
|
|
19044
|
+
const repo = anchors.repo?.trim();
|
|
19045
|
+
const tag = anchors.tag?.trim();
|
|
19046
|
+
const tagSha = anchors.tagSha?.trim().toLowerCase();
|
|
19047
|
+
if (!repo || !/^[^/]+\/[^/]+$/.test(repo)) {
|
|
19048
|
+
throw new ReleaseLedgerError("invalid-record", `release ledger: invalid repo anchor ${JSON.stringify(anchors.repo ?? "")}`);
|
|
19049
|
+
}
|
|
19050
|
+
if (!tag || !/^v\d+\.\d+\.\d+(-rc\.\d+)?$/.test(tag)) {
|
|
19051
|
+
throw new ReleaseLedgerError("invalid-record", `release ledger: invalid tag anchor ${JSON.stringify(anchors.tag ?? "")}`);
|
|
19052
|
+
}
|
|
19053
|
+
if (!tagSha || !SHA_RE.test(tagSha)) {
|
|
19054
|
+
throw new ReleaseLedgerError("invalid-record", `release ledger: invalid tagSha anchor ${JSON.stringify(anchors.tagSha ?? "")}`);
|
|
19055
|
+
}
|
|
19056
|
+
return { repo, tag, tagSha };
|
|
19057
|
+
}
|
|
19058
|
+
function emptyPhases() {
|
|
19059
|
+
return Object.fromEntries(RELEASE_LEDGER_PHASES.map((p) => [p, { state: "pending", updatedAt: "never" }]));
|
|
19060
|
+
}
|
|
19061
|
+
function validatePhaseRecord(phase, raw) {
|
|
19062
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
19063
|
+
throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} is not an object`);
|
|
19064
|
+
}
|
|
19065
|
+
const rec = raw;
|
|
19066
|
+
const state = rec.state;
|
|
19067
|
+
if (typeof state !== "string" || !STATE_SET.has(state)) {
|
|
19068
|
+
throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has invalid state ${JSON.stringify(state)}`);
|
|
19069
|
+
}
|
|
19070
|
+
if (rec.sha !== void 0 && (typeof rec.sha !== "string" || !SHA_RE.test(rec.sha))) {
|
|
19071
|
+
throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has a malformed sha`);
|
|
19072
|
+
}
|
|
19073
|
+
if (rec.runId !== void 0 && (typeof rec.runId !== "number" || !Number.isSafeInteger(rec.runId) || rec.runId <= 0)) {
|
|
19074
|
+
throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has a malformed runId`);
|
|
19075
|
+
}
|
|
19076
|
+
if (rec.runRepo !== void 0 && (typeof rec.runRepo !== "string" || !/^[\w.-]+\/[\w.-]+$/.test(rec.runRepo))) {
|
|
19077
|
+
throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has a malformed runRepo`);
|
|
19078
|
+
}
|
|
19079
|
+
if (rec.runProvenance !== void 0 && rec.runProvenance !== "sha" && rec.runProvenance !== "dispatch") {
|
|
19080
|
+
throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has a malformed runProvenance ${JSON.stringify(rec.runProvenance)}`);
|
|
19081
|
+
}
|
|
19082
|
+
for (const key of ["runUrl", "note", "error", "workflow", "event"]) {
|
|
19083
|
+
if (rec[key] !== void 0 && (typeof rec[key] !== "string" || rec[key].length > MAX_TEXT * 4)) {
|
|
19084
|
+
throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has a malformed ${key}`);
|
|
19085
|
+
}
|
|
19086
|
+
}
|
|
19087
|
+
if (rec.retries !== void 0 && (typeof rec.retries !== "number" || !Number.isSafeInteger(rec.retries) || rec.retries < 0 || rec.retries > 9)) {
|
|
19088
|
+
throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has a malformed retries value`);
|
|
19089
|
+
}
|
|
19090
|
+
if (typeof rec.updatedAt !== "string" || !rec.updatedAt) {
|
|
19091
|
+
throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} is missing updatedAt`);
|
|
19092
|
+
}
|
|
19093
|
+
return rec;
|
|
19094
|
+
}
|
|
19095
|
+
function parseReleaseLedger(raw) {
|
|
19096
|
+
let parsed;
|
|
19097
|
+
try {
|
|
19098
|
+
parsed = JSON.parse(raw);
|
|
19099
|
+
} catch (e) {
|
|
19100
|
+
throw new ReleaseLedgerError("unreadable", `release ledger is not valid JSON (${e instanceof Error ? e.message : String(e)}) \u2014 refusing to resume against unreadable state (#5987)`);
|
|
19101
|
+
}
|
|
19102
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
19103
|
+
throw new ReleaseLedgerError("unreadable", "release ledger is not a JSON object");
|
|
19104
|
+
}
|
|
19105
|
+
const obj = parsed;
|
|
19106
|
+
if (obj.schema !== RELEASE_LEDGER_SCHEMA) {
|
|
19107
|
+
throw new ReleaseLedgerError(
|
|
19108
|
+
"unknown-schema",
|
|
19109
|
+
`release ledger schema ${JSON.stringify(obj.schema ?? "(missing)")} is not ${JSON.stringify(RELEASE_LEDGER_SCHEMA)} \u2014 a NEWER ledger this CLI cannot interpret must fail closed; do not delete or hand-edit the file, use a CLI that understands it`
|
|
19110
|
+
);
|
|
19111
|
+
}
|
|
19112
|
+
const repo = obj.repo;
|
|
19113
|
+
const tag = obj.tag;
|
|
19114
|
+
const tagSha = obj.tagSha;
|
|
19115
|
+
if (typeof repo !== "string" || !repo || typeof tag !== "string" || !tag || typeof tagSha !== "string" || !SHA_RE.test(tagSha)) {
|
|
19116
|
+
throw new ReleaseLedgerError("unreadable", `release ledger has malformed identity anchors (repo/tag/tagSha)`);
|
|
19117
|
+
}
|
|
19118
|
+
if (typeof obj.updatedAt !== "string" || !obj.updatedAt) {
|
|
19119
|
+
throw new ReleaseLedgerError("unreadable", "release ledger is missing updatedAt");
|
|
19120
|
+
}
|
|
19121
|
+
const phasesRaw = obj.phases;
|
|
19122
|
+
if (typeof phasesRaw !== "object" || phasesRaw === null || Array.isArray(phasesRaw)) {
|
|
19123
|
+
throw new ReleaseLedgerError("unreadable", "release ledger phases block is not an object");
|
|
19124
|
+
}
|
|
19125
|
+
const phases = { ...emptyPhases() };
|
|
19126
|
+
for (const [phase, rec] of Object.entries(phasesRaw)) {
|
|
19127
|
+
if (!PHASE_SET.has(phase)) continue;
|
|
19128
|
+
phases[phase] = validatePhaseRecord(phase, rec);
|
|
19129
|
+
}
|
|
19130
|
+
return { schema: RELEASE_LEDGER_SCHEMA, repo, tag, tagSha, phases, updatedAt: obj.updatedAt };
|
|
19131
|
+
}
|
|
19132
|
+
function validateLedgerAnchors(ledger, expected) {
|
|
19133
|
+
const anchors = normalizeAnchorsForCompare(expected);
|
|
19134
|
+
if (ledger.repo.toLowerCase() !== anchors.repo.toLowerCase()) {
|
|
19135
|
+
throw new ReleaseLedgerError(
|
|
19136
|
+
"repo-mismatch",
|
|
19137
|
+
`release ledger belongs to ${ledger.repo}, not ${anchors.repo} \u2014 refusing to mix releases across repos (#5987)`
|
|
19138
|
+
);
|
|
19139
|
+
}
|
|
19140
|
+
if (ledger.tag !== anchors.tag) {
|
|
19141
|
+
throw new ReleaseLedgerError(
|
|
19142
|
+
"tag-mismatch",
|
|
19143
|
+
`release ledger belongs to ${ledger.tag}, not ${anchors.tag} \u2014 refusing to resume a different tag against it (#5987)`
|
|
19144
|
+
);
|
|
19145
|
+
}
|
|
19146
|
+
if (ledger.tagSha.toLowerCase() !== anchors.tagSha.toLowerCase()) {
|
|
19147
|
+
throw new ReleaseLedgerError(
|
|
19148
|
+
"tag-sha-mismatch",
|
|
19149
|
+
`release ledger anchors ${ledger.tag} at ${ledger.tagSha.slice(0, 12)} but live origin reports ${anchors.tagSha.slice(0, 12)} \u2014 the immutable tag moved or the ledger is stale; failing closed instead of trusting either (#5987)`
|
|
19150
|
+
);
|
|
19151
|
+
}
|
|
19152
|
+
}
|
|
19153
|
+
function normalizeAnchorsForCompare(expected) {
|
|
19154
|
+
const repo = expected.repo?.trim() ?? "";
|
|
19155
|
+
const tag = expected.tag?.trim() ?? "";
|
|
19156
|
+
const tagSha = expected.tagSha?.trim().toLowerCase() ?? "";
|
|
19157
|
+
if (!repo || !tag || !SHA_RE.test(tagSha)) {
|
|
19158
|
+
throw new ReleaseLedgerError("invalid-record", `release ledger: invalid live anchors (repo=${JSON.stringify(repo)}, tag=${JSON.stringify(tag)})`);
|
|
19159
|
+
}
|
|
19160
|
+
return { repo, tag, tagSha };
|
|
19161
|
+
}
|
|
19162
|
+
function resolvePrimaryGitDir(cwd) {
|
|
19163
|
+
const gitDir = resolveGitDir(cwd);
|
|
19164
|
+
if (!gitDir) {
|
|
19165
|
+
throw new ReleaseLedgerError("no-checkout", `release ledger: no git checkout found above ${cwd}`);
|
|
19166
|
+
}
|
|
19167
|
+
let common;
|
|
19168
|
+
try {
|
|
19169
|
+
common = (0, import_node_fs19.readFileSync)((0, import_node_path18.join)(gitDir, "commondir"), "utf8").trim();
|
|
19170
|
+
} catch (e) {
|
|
19171
|
+
if (e.code !== "ENOENT") {
|
|
19172
|
+
throw new ReleaseLedgerError("unreadable", `release ledger: the worktree gitdir's commondir pointer could not be READ (${e instanceof Error ? e.message : String(e)}) \u2014 a failed read is unverified, not primary (#5987/#4713)`);
|
|
19173
|
+
}
|
|
19174
|
+
}
|
|
19175
|
+
if (!common) return gitDir;
|
|
19176
|
+
const commonDir = (0, import_node_path18.isAbsolute)(common) ? common : (0, import_node_path18.resolve)(gitDir, common);
|
|
19177
|
+
try {
|
|
19178
|
+
(0, import_node_fs19.readFileSync)((0, import_node_path18.join)(commonDir, "HEAD"), "utf8");
|
|
19179
|
+
} catch (e) {
|
|
19180
|
+
throw new ReleaseLedgerError("no-checkout", `release ledger: the worktree's commondir pointer names ${commonDir}, which has no readable HEAD (${e instanceof Error ? e.message : String(e)}) \u2014 the primary checkout may have been removed; refusing to write the ledger into a doomed location`);
|
|
19181
|
+
}
|
|
19182
|
+
return commonDir;
|
|
19183
|
+
}
|
|
19184
|
+
function releaseLedgerPath(cwd) {
|
|
19185
|
+
return (0, import_node_path18.join)(resolvePrimaryGitDir(cwd), "mmi-runtime", "release", "phases.json");
|
|
19186
|
+
}
|
|
19187
|
+
function atomicWriteJson(path2, ledger) {
|
|
19188
|
+
(0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(path2), { recursive: true });
|
|
19189
|
+
const tmp = `${path2}.tmp-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}`;
|
|
19190
|
+
(0, import_node_fs19.writeFileSync)(tmp, `${JSON.stringify(ledger, null, 2)}
|
|
19191
|
+
`, "utf8");
|
|
19192
|
+
(0, import_node_fs19.renameSync)(tmp, path2);
|
|
19193
|
+
}
|
|
19194
|
+
async function withReleaseLedger(path2, expected, fn, opts = {}) {
|
|
19195
|
+
return withFileLock(`${path2}.lock`, { label: "release ledger", maxWaitMs: opts.maxWaitMs ?? 5e3 }, async () => {
|
|
19196
|
+
let current;
|
|
19197
|
+
let raw;
|
|
19198
|
+
try {
|
|
19199
|
+
raw = (0, import_node_fs19.readFileSync)(path2, "utf8");
|
|
19200
|
+
} catch (e) {
|
|
19201
|
+
if (e.code !== "ENOENT") {
|
|
19202
|
+
throw new ReleaseLedgerError("unreadable", `release ledger at ${path2} could not be READ (${e instanceof Error ? e.message : String(e)}) \u2014 a failed read is unverified, not absent (#5987)`);
|
|
19203
|
+
}
|
|
19204
|
+
}
|
|
19205
|
+
if (raw !== void 0) {
|
|
19206
|
+
current = parseReleaseLedger(raw);
|
|
19207
|
+
if (expected) validateLedgerAnchors(current, expected);
|
|
19208
|
+
}
|
|
19209
|
+
const { ledger, value } = await fn(current);
|
|
19210
|
+
if (ledger) atomicWriteJson(path2, ledger);
|
|
19211
|
+
return value;
|
|
19212
|
+
});
|
|
19213
|
+
}
|
|
19214
|
+
function openReleaseLedgerWriter(cwd, opts = {}) {
|
|
19215
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
19216
|
+
let path2;
|
|
19217
|
+
try {
|
|
19218
|
+
path2 = releaseLedgerPath(cwd);
|
|
19219
|
+
} catch (e) {
|
|
19220
|
+
const failure = e;
|
|
19221
|
+
return {
|
|
19222
|
+
path: "(unresolved)",
|
|
19223
|
+
record: async () => ({ ok: false, error: failure instanceof Error ? failure.message : String(failure) }),
|
|
19224
|
+
load: async () => {
|
|
19225
|
+
throw failure;
|
|
19226
|
+
}
|
|
19227
|
+
};
|
|
19228
|
+
}
|
|
19229
|
+
return {
|
|
19230
|
+
path: path2,
|
|
19231
|
+
record: async (anchors, phase, entry) => {
|
|
19232
|
+
try {
|
|
19233
|
+
return await withReleaseLedger(
|
|
19234
|
+
path2,
|
|
19235
|
+
anchors,
|
|
19236
|
+
(current) => {
|
|
19237
|
+
const normalized = normalizeLedgerAnchors(anchors);
|
|
19238
|
+
const base = current ?? {
|
|
19239
|
+
schema: RELEASE_LEDGER_SCHEMA,
|
|
19240
|
+
repo: normalized.repo,
|
|
19241
|
+
tag: normalized.tag,
|
|
19242
|
+
tagSha: normalized.tagSha,
|
|
19243
|
+
phases: emptyPhases(),
|
|
19244
|
+
updatedAt: now().toISOString()
|
|
19245
|
+
};
|
|
19246
|
+
const previousRetries = base.phases[phase].retries ?? 0;
|
|
19247
|
+
const record = {
|
|
19248
|
+
...entry,
|
|
19249
|
+
note: entry.note !== void 0 ? truncate(entry.note, MAX_TEXT) : void 0,
|
|
19250
|
+
error: entry.error !== void 0 ? truncate(entry.error, MAX_TEXT * 2) : void 0,
|
|
19251
|
+
retries: entry.retries ?? previousRetries,
|
|
19252
|
+
updatedAt: now().toISOString()
|
|
19253
|
+
};
|
|
19254
|
+
const updated = {
|
|
19255
|
+
...base,
|
|
19256
|
+
phases: { ...base.phases, [phase]: record },
|
|
19257
|
+
updatedAt: now().toISOString()
|
|
19258
|
+
};
|
|
19259
|
+
return { ledger: updated, value: { ok: true } };
|
|
19260
|
+
},
|
|
19261
|
+
{ maxWaitMs: opts.maxWaitMs }
|
|
19262
|
+
);
|
|
19263
|
+
} catch (e) {
|
|
19264
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
19265
|
+
}
|
|
19266
|
+
},
|
|
19267
|
+
load: async (anchors) => {
|
|
19268
|
+
try {
|
|
19269
|
+
return await withReleaseLedger(
|
|
19270
|
+
path2,
|
|
19271
|
+
anchors,
|
|
19272
|
+
(current) => ({ value: current }),
|
|
19273
|
+
{ maxWaitMs: opts.maxWaitMs }
|
|
19274
|
+
);
|
|
19275
|
+
} catch (e) {
|
|
19276
|
+
if (e instanceof ReleaseLedgerError) throw e;
|
|
19277
|
+
throw new ReleaseLedgerError("unreadable", e instanceof Error ? e.message : String(e));
|
|
19278
|
+
}
|
|
19279
|
+
}
|
|
19280
|
+
};
|
|
19281
|
+
}
|
|
19282
|
+
function compactEntry(entry) {
|
|
19283
|
+
return Object.fromEntries(Object.entries(entry).filter(([, v]) => v !== void 0));
|
|
19284
|
+
}
|
|
19285
|
+
function phaseEntry(entry) {
|
|
19286
|
+
return compactEntry(entry);
|
|
19287
|
+
}
|
|
19288
|
+
var PHASE_RANK = { failed: 0, pending: 1, complete: 2, skipped: 2 };
|
|
19289
|
+
function deriveReleasePhaseStatus(ledger) {
|
|
19290
|
+
const legs = RELEASE_LEDGER_PHASES.map((phase) => {
|
|
19291
|
+
const rec = ledger.phases[phase];
|
|
19292
|
+
return {
|
|
19293
|
+
phase,
|
|
19294
|
+
state: rec.state,
|
|
19295
|
+
...rec.runId !== void 0 ? { runId: rec.runId } : {},
|
|
19296
|
+
...rec.runRepo !== void 0 ? { runRepo: rec.runRepo } : {},
|
|
19297
|
+
...rec.workflow !== void 0 ? { workflow: rec.workflow } : {},
|
|
19298
|
+
...rec.event !== void 0 ? { event: rec.event } : {},
|
|
19299
|
+
...rec.note !== void 0 ? { note: rec.note } : {},
|
|
19300
|
+
...rec.error !== void 0 ? { error: rec.error } : {}
|
|
19301
|
+
};
|
|
19302
|
+
});
|
|
19303
|
+
const worst = legs.reduce((acc, leg) => Math.min(acc, PHASE_RANK[leg.state]), 2);
|
|
19304
|
+
return { phaseStatus: worst === 0 ? "phases-failed" : worst === 1 ? "phases-pending" : "phases-green", legs };
|
|
19305
|
+
}
|
|
19306
|
+
function safeRunAction(runId, runRepo, state) {
|
|
19307
|
+
return state === "failed" ? `next: diagnose with gh run view ${runId} --repo ${runRepo} --log-failed; rerun by hand only when the cause is fixed: gh run rerun ${runId} --repo ${runRepo} --failed (never re-tag)` : `next: gh run watch ${runId} --repo ${runRepo} --exit-status`;
|
|
19308
|
+
}
|
|
19309
|
+
function formatReleaseLedgerReport(report) {
|
|
19310
|
+
const lines = [`release phase ledger: ${report.phaseStatus.toUpperCase()} (${report.path}) \u2014 phase status of the recorded boundaries, NOT the release verdict`];
|
|
19311
|
+
for (const leg of report.legs) {
|
|
19312
|
+
const detail = leg.error ?? leg.note;
|
|
19313
|
+
const run = [leg.workflow, leg.event ? `(${leg.event})` : void 0, leg.runId != null ? `run ${leg.runId}` : void 0, leg.runRepo ? `in ${leg.runRepo}` : void 0].filter(Boolean).join(" ");
|
|
19314
|
+
lines.push(` - ${leg.phase}: ${leg.state.toUpperCase()}${run ? ` \u2014 ${run}` : ""}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
19315
|
+
if ((leg.state === "failed" || leg.state === "pending") && leg.runId != null && leg.runRepo) {
|
|
19316
|
+
lines.push(` ${safeRunAction(leg.runId, leg.runRepo, leg.state === "failed" ? "failed" : "pending")}`);
|
|
19317
|
+
}
|
|
19318
|
+
}
|
|
19319
|
+
return lines;
|
|
19320
|
+
}
|
|
19321
|
+
function releaseLedgerReport(ledger, path2) {
|
|
19322
|
+
if (!ledger) return void 0;
|
|
19323
|
+
const derived = deriveReleasePhaseStatus(ledger);
|
|
19324
|
+
return { path: path2, phases: ledger.phases, phaseStatus: derived.phaseStatus, legs: derived.legs };
|
|
19325
|
+
}
|
|
19326
|
+
var TRANSIENT_TRANSPORT_MARKERS = [
|
|
19327
|
+
/connection (?:reset|refused|aborted|timed? ?out)/i,
|
|
19328
|
+
/\bECONN(?:RESET|REFUSED|ABORTED|TIMEDOUT)\b/,
|
|
19329
|
+
/\bE(?:HOST|NET)UNREACH\b/,
|
|
19330
|
+
/\bEAI_AGAIN\b/,
|
|
19331
|
+
/\bEPIPE\b/,
|
|
19332
|
+
/lost communication with (?:the )?github actions runner service/i,
|
|
19333
|
+
/runner lost communication/i,
|
|
19334
|
+
/\b(?:502|503|504)\b[^0-9]{0,40}(?:bad gateway|service unavailable|gateway time-?out|server error)/i,
|
|
19335
|
+
/(?:bad gateway|service unavailable|gateway time-?out)/i,
|
|
19336
|
+
/fetch failed/i,
|
|
19337
|
+
/network (?:error|failure|unreachable|timeout|timed? ?out)/i,
|
|
19338
|
+
/socket hang ?up/i,
|
|
19339
|
+
/remote end hung ?up unexpectedly/i,
|
|
19340
|
+
/early EOF/i,
|
|
19341
|
+
/tls handshake timeout/i
|
|
19342
|
+
];
|
|
19343
|
+
function classifyTransientTransportFailure(evidence) {
|
|
19344
|
+
if (!evidence) return false;
|
|
19345
|
+
return TRANSIENT_TRANSPORT_MARKERS.some((marker) => marker.test(evidence));
|
|
19346
|
+
}
|
|
19347
|
+
var TRANSIENT_EVIDENCE_MAX_CHARS = 16e3;
|
|
19348
|
+
var AUTO_RERUN_MAX_RETRIES = 1;
|
|
19349
|
+
function failedRunIdentity(run, repo) {
|
|
19350
|
+
return `${run.workflow}${run.event ? ` (${run.event})` : ""} run ${run.runId} in ${repo}`;
|
|
19351
|
+
}
|
|
19352
|
+
function manualRunAction(run, repo) {
|
|
19353
|
+
return `safe next action: diagnose with gh run view ${run.runId} --repo ${repo} --log-failed; rerun by hand only when the cause is fixed: gh run rerun ${run.runId} --repo ${repo} --failed (never re-tag)`;
|
|
19354
|
+
}
|
|
19355
|
+
async function readFailedRunsOnSha(deps, repo, sha) {
|
|
19356
|
+
try {
|
|
19357
|
+
const rows = JSON.parse(await deps.run("gh", [
|
|
19358
|
+
"run",
|
|
19359
|
+
"list",
|
|
19360
|
+
"--repo",
|
|
19361
|
+
repo,
|
|
19362
|
+
"--commit",
|
|
19363
|
+
sha,
|
|
19364
|
+
"--limit",
|
|
19365
|
+
"20",
|
|
19366
|
+
"--json",
|
|
19367
|
+
"databaseId,url,workflowName,event,status,conclusion"
|
|
19368
|
+
]));
|
|
19369
|
+
if (!Array.isArray(rows)) return [];
|
|
19370
|
+
return rows.filter((r) => typeof r.databaseId === "number" && r.status === "completed" && (r.conclusion === "failure" || r.conclusion === "cancelled")).map((r) => ({
|
|
19371
|
+
workflow: r.workflowName ?? `run:${r.databaseId}`,
|
|
19372
|
+
runId: r.databaseId,
|
|
19373
|
+
runUrl: r.url,
|
|
19374
|
+
repo,
|
|
19375
|
+
...r.event ? { event: r.event } : {},
|
|
19376
|
+
conclusion: r.conclusion ?? "failure"
|
|
19377
|
+
}));
|
|
19378
|
+
} catch {
|
|
19379
|
+
return [];
|
|
19380
|
+
}
|
|
19381
|
+
}
|
|
19382
|
+
async function transientEvidenceForRun(deps, repo, runId) {
|
|
19383
|
+
const parts = [];
|
|
19384
|
+
try {
|
|
19385
|
+
parts.push(await deps.run("gh", ["run", "view", String(runId), "--repo", repo, "--json", "jobs", "--jq", '[.jobs[] | select(.conclusion=="failure") | {name:.name, steps:[.steps[] | select(.conclusion=="failure") | .name]}] | tostring']));
|
|
19386
|
+
} catch {
|
|
19387
|
+
}
|
|
19388
|
+
try {
|
|
19389
|
+
parts.push(await deps.run("gh", ["run", "view", String(runId), "--repo", repo, "--log-failed"]));
|
|
19390
|
+
} catch {
|
|
19391
|
+
}
|
|
19392
|
+
const evidence = parts.filter(Boolean).join("\n");
|
|
19393
|
+
return evidence.length > TRANSIENT_EVIDENCE_MAX_CHARS ? evidence.slice(-TRANSIENT_EVIDENCE_MAX_CHARS) : evidence;
|
|
19394
|
+
}
|
|
19395
|
+
function extractFailedRequiredContexts(failure) {
|
|
19396
|
+
const match = /required train check failed:\s*([^\n(]+)/.exec(failure);
|
|
19397
|
+
if (!match) return [];
|
|
19398
|
+
return match[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
19399
|
+
}
|
|
19400
|
+
async function readFailedCheckRunsOnSha(deps, repo, sha) {
|
|
19401
|
+
try {
|
|
19402
|
+
const rows = JSON.parse(await deps.run("gh", [
|
|
19403
|
+
"api",
|
|
19404
|
+
`repos/${repo}/commits/${sha}/check-runs`,
|
|
19405
|
+
"--jq",
|
|
19406
|
+
'[.check_runs[] | select(.status == "completed" and .conclusion != "success" and .conclusion != null) | {name: .name, url: .details_url}]'
|
|
19407
|
+
]));
|
|
19408
|
+
if (!Array.isArray(rows)) return [];
|
|
19409
|
+
return rows.flatMap((r) => {
|
|
19410
|
+
if (typeof r.name !== "string" || typeof r.url !== "string") return [];
|
|
19411
|
+
const id = /\/actions\/runs\/(\d+)/.exec(r.url);
|
|
19412
|
+
return [{ name: r.name, ...id ? { runId: Number(id[1]) } : {} }];
|
|
19413
|
+
});
|
|
19414
|
+
} catch {
|
|
19415
|
+
return [];
|
|
19416
|
+
}
|
|
19417
|
+
}
|
|
19418
|
+
async function correlateCandidateRunsToRequiredContexts(deps, repo, sha, candidates, contexts) {
|
|
19419
|
+
if (contexts.length === 0) {
|
|
19420
|
+
return {
|
|
19421
|
+
unique: false,
|
|
19422
|
+
note: `the failure names no failed required context \u2014 rerunning the first failed run could rerun an unrelated workflow; manual intervention required; safe next action: gh run list --repo ${repo} --commit ${sha.slice(0, 12)} to identify the run`
|
|
19423
|
+
};
|
|
19424
|
+
}
|
|
19425
|
+
const failedChecks = await readFailedCheckRunsOnSha(deps, repo, sha);
|
|
19426
|
+
const contextRunIds = /* @__PURE__ */ new Map();
|
|
19427
|
+
for (const check of failedChecks) {
|
|
19428
|
+
if (check.runId != null && contexts.includes(check.name)) contextRunIds.set(check.runId, check.name);
|
|
19429
|
+
}
|
|
19430
|
+
const proven = candidates.filter((c) => c.runId != null && contextRunIds.has(c.runId));
|
|
19431
|
+
if (proven.length === 1) return { unique: true, candidate: proven[0] };
|
|
19432
|
+
if (proven.length > 1) {
|
|
19433
|
+
return {
|
|
19434
|
+
unique: false,
|
|
19435
|
+
note: `${proven.length} failed runs correspond to the failed required contexts (${contexts.join(", ")}): ${proven.map((c) => failedRunIdentity(c, c.repo ?? repo)).join("; ")} \u2014 ambiguous which to rerun; manual intervention required; safe next action: gh run view <run-id> --repo ${repo} --log-failed`
|
|
19436
|
+
};
|
|
19437
|
+
}
|
|
19438
|
+
return {
|
|
19439
|
+
unique: false,
|
|
19440
|
+
note: `no enumerated failed run corresponds to the failed required contexts (${contexts.join(", ")}) \u2014 the failed run(s) are unrelated to the required gate; manual intervention required; safe next action: gh run view <run-id> --repo ${repo} --log-failed`
|
|
19441
|
+
};
|
|
19442
|
+
}
|
|
19443
|
+
async function transientRecoveryForFailedPhase(deps, input) {
|
|
19444
|
+
const { ledger, anchors, phase, sha, failure } = input;
|
|
19445
|
+
const enumerated = !input.runs;
|
|
19446
|
+
const provenance = input.runsProvenance ?? "sha";
|
|
19447
|
+
const recordProvenance = provenance === "watched" ? "dispatch" : "sha";
|
|
19448
|
+
let candidates = (input.runs ?? await transientRecoveryEnumerate(deps, input.repo, sha)).filter((r) => typeof r.runId === "number" && (r.conclusion === "failure" || r.conclusion === "cancelled"));
|
|
19449
|
+
const first = candidates[0];
|
|
19450
|
+
await ledger.record(anchors, phase, phaseEntry({
|
|
19451
|
+
state: "failed",
|
|
19452
|
+
sha,
|
|
19453
|
+
error: failure,
|
|
19454
|
+
...first ? {
|
|
19455
|
+
runId: first.runId,
|
|
19456
|
+
...first.runUrl ? { runUrl: first.runUrl } : {},
|
|
19457
|
+
runRepo: first.repo ?? input.repo,
|
|
19458
|
+
runProvenance: recordProvenance,
|
|
19459
|
+
workflow: first.workflow,
|
|
19460
|
+
...first.event ? { event: first.event } : {}
|
|
19461
|
+
} : {}
|
|
19462
|
+
}));
|
|
19463
|
+
const recorded = await ledger.load(anchors).catch(() => void 0);
|
|
19464
|
+
const retriesSoFar = recorded?.phases[phase].retries ?? 0;
|
|
19465
|
+
if (retriesSoFar >= AUTO_RERUN_MAX_RETRIES) {
|
|
19466
|
+
const spent = first ? ` for ${failedRunIdentity(first, first.repo ?? input.repo)}` : "";
|
|
19467
|
+
return {
|
|
19468
|
+
reran: false,
|
|
19469
|
+
recovered: false,
|
|
19470
|
+
...first?.runId != null ? { runId: first.runId, runRepo: first.repo ?? input.repo } : {},
|
|
19471
|
+
note: `automatic rerun already spent for ${phase}${spent} \u2014 manual intervention required; ${first?.runId != null ? manualRunAction(first, first.repo ?? input.repo) : "safe next action: identify the failed run on the Actions tab"}`
|
|
19472
|
+
};
|
|
19473
|
+
}
|
|
19474
|
+
if (candidates.length === 0) {
|
|
19475
|
+
return { reran: false, recovered: false, note: `no failed run with an id could be classified on ${sha.slice(0, 12)} \u2014 manual intervention required (no marker proof, no automatic rerun); safe next action: gh run list --repo ${input.repo} --commit ${sha.slice(0, 12)} to identify the run` };
|
|
19476
|
+
}
|
|
19477
|
+
if (enumerated && phase === "ordinaryCi") {
|
|
19478
|
+
const correlation = await correlateCandidateRunsToRequiredContexts(deps, input.repo, sha, candidates, extractFailedRequiredContexts(failure));
|
|
19479
|
+
if (!correlation.unique) return { reran: false, recovered: false, note: correlation.note };
|
|
19480
|
+
candidates = [correlation.candidate];
|
|
19481
|
+
}
|
|
19482
|
+
const candidate = candidates[0];
|
|
19483
|
+
const runRepo = candidate.repo ?? input.repo;
|
|
19484
|
+
let meta;
|
|
19485
|
+
try {
|
|
19486
|
+
meta = JSON.parse(await deps.run("gh", [
|
|
19487
|
+
"run",
|
|
19488
|
+
"view",
|
|
19489
|
+
String(candidate.runId),
|
|
19490
|
+
"--repo",
|
|
19491
|
+
runRepo,
|
|
19492
|
+
"--json",
|
|
19493
|
+
"headSha,status,conclusion,url"
|
|
19494
|
+
]));
|
|
19495
|
+
} catch (e) {
|
|
19496
|
+
return { reran: false, recovered: false, note: `could not READ failed run ${failedRunIdentity(candidate, runRepo)} to prove its identity (${e instanceof Error ? e.message : String(e)}) \u2014 no automatic rerun without proof; ${manualRunAction(candidate, runRepo)}` };
|
|
19497
|
+
}
|
|
19498
|
+
const shaProven = provenance === "watched" || (meta.headSha ?? "").toLowerCase() === sha.toLowerCase();
|
|
19499
|
+
if (!shaProven || meta.conclusion !== "failure") {
|
|
19500
|
+
return { reran: false, recovered: false, note: `run ${failedRunIdentity(candidate, runRepo)} does not prove a failed run on ${sha.slice(0, 12)} \u2014 no automatic rerun; ${manualRunAction(candidate, runRepo)}` };
|
|
19501
|
+
}
|
|
19502
|
+
const evidence = await transientEvidenceForRun(deps, runRepo, candidate.runId);
|
|
19503
|
+
if (!classifyTransientTransportFailure(evidence)) {
|
|
19504
|
+
return { reran: false, recovered: false, note: `run ${failedRunIdentity(candidate, runRepo)} failed on evidence that is NOT a proven transient transport/network failure \u2014 manual intervention required; ${manualRunAction(candidate, runRepo)}` };
|
|
19505
|
+
}
|
|
19506
|
+
const reservation = await ledger.record(anchors, phase, phaseEntry({
|
|
19507
|
+
state: "pending",
|
|
19508
|
+
sha,
|
|
19509
|
+
runId: candidate.runId,
|
|
19510
|
+
...meta.url ?? candidate.runUrl ? { runUrl: meta.url ?? candidate.runUrl } : {},
|
|
19511
|
+
runRepo,
|
|
19512
|
+
runProvenance: recordProvenance,
|
|
19513
|
+
workflow: candidate.workflow,
|
|
19514
|
+
...candidate.event ? { event: candidate.event } : {},
|
|
19515
|
+
note: "ONE automatic transient rerun RESERVED (cap spent durably) \u2014 dispatch pending",
|
|
19516
|
+
retries: retriesSoFar + 1
|
|
19517
|
+
}));
|
|
19518
|
+
if (!reservation.ok) {
|
|
19519
|
+
return { reran: false, recovered: false, note: `the rerun reservation could not be durably recorded (${reservation.error}) \u2014 no automatic rerun without a durable cap; manual intervention required; ${manualRunAction(candidate, runRepo)}` };
|
|
19520
|
+
}
|
|
19521
|
+
try {
|
|
19522
|
+
await deps.run("gh", ["run", "rerun", String(candidate.runId), "--repo", runRepo, "--failed"]);
|
|
19523
|
+
} catch (e) {
|
|
19524
|
+
await ledger.record(anchors, phase, phaseEntry({
|
|
19525
|
+
state: "failed",
|
|
19526
|
+
sha,
|
|
19527
|
+
runId: candidate.runId,
|
|
19528
|
+
...meta.url ?? candidate.runUrl ? { runUrl: meta.url ?? candidate.runUrl } : {},
|
|
19529
|
+
runRepo,
|
|
19530
|
+
runProvenance: recordProvenance,
|
|
19531
|
+
workflow: candidate.workflow,
|
|
19532
|
+
...candidate.event ? { event: candidate.event } : {},
|
|
19533
|
+
error: `automatic rerun dispatch failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
19534
|
+
retries: retriesSoFar + 1
|
|
19535
|
+
}));
|
|
19536
|
+
return {
|
|
19537
|
+
reran: true,
|
|
19538
|
+
recovered: false,
|
|
19539
|
+
runId: candidate.runId,
|
|
19540
|
+
runRepo,
|
|
19541
|
+
note: `automatic rerun of ${failedRunIdentity(candidate, runRepo)} FAILED to dispatch (${e instanceof Error ? e.message : String(e)}) \u2014 the one automatic rerun remains spent; manual intervention required; ${manualRunAction(candidate, runRepo)}`
|
|
19542
|
+
};
|
|
19543
|
+
}
|
|
19544
|
+
const watched = await (input.watchRun ?? defaultWatchRun)(deps, candidate.runId, runRepo);
|
|
19545
|
+
const runFields = {
|
|
19546
|
+
sha,
|
|
19547
|
+
runId: candidate.runId,
|
|
19548
|
+
...meta.url ?? candidate.runUrl ? { runUrl: meta.url ?? candidate.runUrl } : {},
|
|
19549
|
+
runRepo,
|
|
19550
|
+
runProvenance: recordProvenance,
|
|
19551
|
+
workflow: candidate.workflow,
|
|
19552
|
+
...candidate.event ? { event: candidate.event } : {}
|
|
19553
|
+
};
|
|
19554
|
+
if (watched === "success") {
|
|
19555
|
+
await ledger.record(anchors, phase, phaseEntry({
|
|
19556
|
+
state: "complete",
|
|
19557
|
+
...runFields,
|
|
19558
|
+
note: `recovered: the ONE automatic rerun was watched green in this invocation`,
|
|
19559
|
+
retries: retriesSoFar + 1
|
|
19560
|
+
}));
|
|
19561
|
+
return {
|
|
19562
|
+
reran: true,
|
|
19563
|
+
recovered: true,
|
|
19564
|
+
runId: candidate.runId,
|
|
19565
|
+
runRepo,
|
|
19566
|
+
note: `${failedRunIdentity(candidate, runRepo)} was a proven transient transport failure \u2014 the ONE automatic rerun was dispatched and watched GREEN; continuing in this invocation`
|
|
19567
|
+
};
|
|
19568
|
+
}
|
|
19569
|
+
if (watched === "failure") {
|
|
19570
|
+
await ledger.record(anchors, phase, phaseEntry({
|
|
19571
|
+
state: "failed",
|
|
19572
|
+
...runFields,
|
|
19573
|
+
error: "the automatic rerun was watched FAILURE \u2014 not transient after all",
|
|
19574
|
+
retries: retriesSoFar + 1
|
|
19575
|
+
}));
|
|
19576
|
+
return {
|
|
19577
|
+
reran: true,
|
|
19578
|
+
recovered: false,
|
|
19579
|
+
runId: candidate.runId,
|
|
19580
|
+
runRepo,
|
|
19581
|
+
note: `the automatic rerun of ${failedRunIdentity(candidate, runRepo)} FAILED \u2014 the one automatic rerun is spent; manual intervention required; ${manualRunAction(candidate, runRepo)}`
|
|
19582
|
+
};
|
|
19583
|
+
}
|
|
19584
|
+
return {
|
|
19585
|
+
reran: true,
|
|
19586
|
+
recovered: false,
|
|
19587
|
+
runId: candidate.runId,
|
|
19588
|
+
runRepo,
|
|
19589
|
+
note: `the automatic rerun of ${failedRunIdentity(candidate, runRepo)} did not resolve within the bounded watch \u2014 typed PENDING (the one automatic rerun is spent); safe next action: gh run watch ${candidate.runId} --repo ${runRepo} --exit-status`
|
|
19590
|
+
};
|
|
19591
|
+
}
|
|
19592
|
+
async function transientRecoveryEnumerate(deps, repo, sha) {
|
|
19593
|
+
return readFailedRunsOnSha(deps, repo, sha);
|
|
19594
|
+
}
|
|
19595
|
+
var RUN_CONFIRM_ATTEMPTS2 = 3;
|
|
19596
|
+
var RUN_CONFIRM_DELAY_MS2 = 1e3;
|
|
19597
|
+
async function defaultWatchRun(deps, runId, repo) {
|
|
19598
|
+
let watchSaidSuccess = true;
|
|
19599
|
+
try {
|
|
19600
|
+
await deps.run("gh", ["run", "watch", String(runId), "--repo", repo, "--exit-status"]);
|
|
19601
|
+
} catch {
|
|
19602
|
+
watchSaidSuccess = false;
|
|
19603
|
+
}
|
|
19604
|
+
let confirmError;
|
|
19605
|
+
for (let attempt = 0; attempt < RUN_CONFIRM_ATTEMPTS2; attempt++) {
|
|
19606
|
+
if (attempt > 0) await new Promise((resolve7) => setTimeout(resolve7, RUN_CONFIRM_DELAY_MS2));
|
|
19607
|
+
try {
|
|
19608
|
+
const parsed = JSON.parse(await deps.run("gh", ["run", "view", String(runId), "--repo", repo, "--json", "status,conclusion"]));
|
|
19609
|
+
if (parsed.status === "completed") return parsed.conclusion === "success" ? "success" : "failure";
|
|
19610
|
+
confirmError = void 0;
|
|
19611
|
+
} catch (e) {
|
|
19612
|
+
confirmError = e;
|
|
19613
|
+
}
|
|
19614
|
+
}
|
|
19615
|
+
if (!watchSaidSuccess) return "failure";
|
|
19616
|
+
if (confirmError) return "pending";
|
|
19617
|
+
return "success";
|
|
19618
|
+
}
|
|
19619
|
+
function alignmentPhaseEntry(dev, rc) {
|
|
19620
|
+
if (!dev && !rc) return { state: "skipped", note: "no protected-branch alignment leg on this release" };
|
|
19621
|
+
const pending = dev?.status === "pr-pending" || rc?.status === "pr-pending";
|
|
19622
|
+
const note = [dev ? `development: ${dev.note}` : void 0, rc ? `rc: ${rc.note}` : void 0].filter(Boolean).join("; ");
|
|
19623
|
+
return { state: pending ? "pending" : "complete", note };
|
|
19624
|
+
}
|
|
19625
|
+
function runRowState(run, fallback) {
|
|
19626
|
+
if (!run) return fallback === "success" ? "complete" : fallback === "failure" ? "failed" : "pending";
|
|
19627
|
+
return run.conclusion === "success" ? "complete" : run.conclusion === "failure" ? "failed" : "pending";
|
|
19628
|
+
}
|
|
19629
|
+
function runMetadata(run, runRepo, provenance) {
|
|
19630
|
+
return {
|
|
19631
|
+
...run.runId != null ? { runId: run.runId } : {},
|
|
19632
|
+
...run.runUrl != null ? { runUrl: run.runUrl } : {},
|
|
19633
|
+
...run.runId != null ? { runRepo, runProvenance: provenance } : {},
|
|
19634
|
+
...run.workflow ? { workflow: run.workflow } : {},
|
|
19635
|
+
...run.event ? { event: run.event } : {}
|
|
19636
|
+
};
|
|
19637
|
+
}
|
|
19638
|
+
function releaseRunRepoFor(model, productRepo) {
|
|
19639
|
+
return model === "hub-serverless" || isCentralDispatchModel(model) ? HUB_REPO2 : productRepo;
|
|
19640
|
+
}
|
|
19641
|
+
function deployPhaseInput(model, dispatch, opts = {}) {
|
|
19642
|
+
const sha = opts.releaseSha;
|
|
19643
|
+
const runRepo = releaseRunRepoFor(model, "");
|
|
19644
|
+
switch (model) {
|
|
19645
|
+
case "hub-serverless": {
|
|
19646
|
+
const deployRun = (dispatch.workflowRuns ?? []).find((r) => r.workflow === "deploy.yml");
|
|
19647
|
+
return {
|
|
19648
|
+
state: runRowState(deployRun, dispatch.deployStatus),
|
|
19649
|
+
...sha ? { sha } : {},
|
|
19650
|
+
...runMetadata(deployRun ?? {}, runRepo, "sha"),
|
|
19651
|
+
note: deployRun ? `${deployRun.workflow} release run` : dispatch.note
|
|
19652
|
+
};
|
|
19653
|
+
}
|
|
19654
|
+
case "registry-publish":
|
|
19655
|
+
return { state: "skipped", ...sha ? { sha } : {}, note: "registry-publish deploys by publishing \u2014 the release-event publish.yml run is the deploy plane" };
|
|
19656
|
+
case "tenant-container":
|
|
19657
|
+
case "solo-container":
|
|
19658
|
+
case "static-cdn":
|
|
19659
|
+
return {
|
|
19660
|
+
state: dispatch.deployStatus === "success" ? "complete" : dispatch.deployStatus === "failure" ? "failed" : "pending",
|
|
19661
|
+
...sha ? { sha } : {},
|
|
19662
|
+
...runMetadata({ runId: dispatch.runId, runUrl: dispatch.runUrl, workflow: "tenant-deploy.yml", event: "workflow_dispatch" }, runRepo, "dispatch"),
|
|
19663
|
+
note: dispatch.note
|
|
19664
|
+
};
|
|
19665
|
+
default:
|
|
19666
|
+
return {
|
|
19667
|
+
state: dispatch.deployStatus === "success" ? "complete" : dispatch.deployStatus === "failure" ? "failed" : "pending",
|
|
19668
|
+
...sha ? { sha } : {},
|
|
19669
|
+
note: dispatch.note
|
|
19670
|
+
};
|
|
19671
|
+
}
|
|
19672
|
+
}
|
|
19673
|
+
function publishPhaseInput(model, dispatch, publishDispatch, publishRequired, opts = {}) {
|
|
19674
|
+
const sha = opts.releaseSha;
|
|
19675
|
+
const publishRow = publishDispatch ? {
|
|
19676
|
+
state: publishDispatch.deployStatus === "success" ? "complete" : publishDispatch.deployStatus === "failure" ? "failed" : "pending",
|
|
19677
|
+
...runMetadata({ runId: publishDispatch.runId, runUrl: publishDispatch.runUrl, workflow: "tenant-publish.yml", event: "workflow_dispatch" }, HUB_REPO2, "dispatch"),
|
|
19678
|
+
note: publishDispatch.note
|
|
19679
|
+
} : void 0;
|
|
19680
|
+
switch (model) {
|
|
19681
|
+
case "hub-serverless": {
|
|
19682
|
+
const publishRun = (dispatch.workflowRuns ?? []).find((r) => r.workflow === "publish.yml");
|
|
19683
|
+
return publishRow ?? {
|
|
19684
|
+
state: runRowState(publishRun, dispatch.deployStatus),
|
|
19685
|
+
...sha ? { sha } : {},
|
|
19686
|
+
...runMetadata(publishRun ?? {}, HUB_REPO2, "sha"),
|
|
19687
|
+
note: publishRun ? `${publishRun.workflow} release run` : dispatch.note
|
|
19688
|
+
};
|
|
19689
|
+
}
|
|
19690
|
+
case "registry-publish": {
|
|
19691
|
+
const publishRun = (dispatch.workflowRuns ?? []).find((r) => r.workflow === "publish.yml");
|
|
19692
|
+
return publishRow ?? {
|
|
19693
|
+
state: runRowState(publishRun, dispatch.deployStatus),
|
|
19694
|
+
...sha ? { sha } : {},
|
|
19695
|
+
...runMetadata(publishRun ?? {}, opts.repo ?? "", "sha"),
|
|
19696
|
+
note: publishRun ? `${publishRun.workflow} release run` : dispatch.note
|
|
19697
|
+
};
|
|
19698
|
+
}
|
|
19699
|
+
case "tenant-container":
|
|
19700
|
+
case "solo-container":
|
|
19701
|
+
case "static-cdn":
|
|
19702
|
+
return publishRow ?? (publishRequired ? { state: "skipped", ...sha ? { sha } : {}, note: opts.skipPublishNote ?? "publish skipped \u2014 deploy not confirmed" } : { state: "skipped", ...sha ? { sha } : {}, note: "publishRequired=false \u2014 no publish leg" });
|
|
19703
|
+
default:
|
|
19704
|
+
return { state: "skipped", ...sha ? { sha } : {}, note: `${model} has no publish leg` };
|
|
19705
|
+
}
|
|
19706
|
+
}
|
|
19707
|
+
function phaseInputsFromRunRows(model, rows, tagSha, opts = {}) {
|
|
19708
|
+
const deployRun = rows.find((r) => r.workflow === "deploy.yml");
|
|
19709
|
+
const publishRun = rows.find((r) => r.workflow === "publish.yml");
|
|
19710
|
+
switch (model) {
|
|
19711
|
+
case "hub-serverless":
|
|
19712
|
+
return {
|
|
19713
|
+
deploy: {
|
|
19714
|
+
state: runRowState(deployRun, "pending"),
|
|
19715
|
+
sha: tagSha,
|
|
19716
|
+
...runMetadata(deployRun ?? {}, HUB_REPO2, "sha"),
|
|
19717
|
+
note: deployRun ? `${deployRun.workflow} run on the release SHA` : "deploy.yml release run not yet correlated"
|
|
19718
|
+
},
|
|
19719
|
+
publish: publishRun ? {
|
|
19720
|
+
state: runRowState(publishRun, "pending"),
|
|
19721
|
+
sha: tagSha,
|
|
19722
|
+
...runMetadata(publishRun, HUB_REPO2, "sha"),
|
|
19723
|
+
note: `${publishRun.workflow} run on the release SHA`
|
|
19724
|
+
} : { state: "pending", sha: tagSha, note: "publish.yml release run not yet correlated" }
|
|
19725
|
+
};
|
|
19726
|
+
case "registry-publish":
|
|
19727
|
+
return {
|
|
19728
|
+
deploy: { state: "skipped", sha: tagSha, note: "registry-publish deploys by publishing \u2014 the release-event publish.yml run is the deploy plane" },
|
|
19729
|
+
publish: publishRun ? {
|
|
19730
|
+
state: runRowState(publishRun, "pending"),
|
|
19731
|
+
sha: tagSha,
|
|
19732
|
+
...runMetadata(publishRun, opts.repo ?? "", "sha"),
|
|
19733
|
+
note: `${publishRun.workflow} run on the release SHA`
|
|
19734
|
+
} : { state: "pending", sha: tagSha, note: "publish.yml release run not yet correlated" }
|
|
19735
|
+
};
|
|
19736
|
+
default:
|
|
19737
|
+
return {
|
|
19738
|
+
deploy: { state: "pending", sha: tagSha, note: `${model} deploys outside the release SHA's run set \u2014 historical outcome uncorrelatable` },
|
|
19739
|
+
publish: { state: "skipped", sha: tagSha, note: "no historical publish leg correlated for this model" }
|
|
19740
|
+
};
|
|
19741
|
+
}
|
|
19742
|
+
}
|
|
19743
|
+
async function reverifyPersistedRunPhases(deps, path2, anchors, input) {
|
|
19744
|
+
const now = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
19745
|
+
const notes = [];
|
|
19746
|
+
const result = await withReleaseLedger(
|
|
19747
|
+
path2,
|
|
19748
|
+
anchors,
|
|
19749
|
+
async (current) => {
|
|
19750
|
+
if (!current) return { value: { ledger: void 0, notes } };
|
|
19751
|
+
const phases = { ...current.phases };
|
|
19752
|
+
for (const phase of ["deploy", "publish"]) {
|
|
19753
|
+
const rec = phases[phase];
|
|
19754
|
+
if (rec.updatedAt === "never") continue;
|
|
19755
|
+
if (rec.runId == null) {
|
|
19756
|
+
if (rec.state === "complete") notes.push(`${phase}: persisted complete without a run id \u2014 the live deploy/publish re-derivation below decides the leg`);
|
|
19757
|
+
continue;
|
|
19758
|
+
}
|
|
19759
|
+
if (rec.runProvenance !== "sha" && rec.runProvenance !== "dispatch") {
|
|
19760
|
+
throw new ReleaseLedgerError(
|
|
19761
|
+
"invalid-record",
|
|
19762
|
+
`the persisted ${phase} boundary names run ${rec.runId} without valid runProvenance ('sha'|'dispatch' required, got ${JSON.stringify(rec.runProvenance ?? null)}) \u2014 a run-backed boundary without provenance cannot be re-verified; failing closed (#5987)`
|
|
19763
|
+
);
|
|
19764
|
+
}
|
|
19765
|
+
const runRepo = rec.runRepo ?? input.repo;
|
|
19766
|
+
let meta;
|
|
19767
|
+
try {
|
|
19768
|
+
meta = JSON.parse(await deps.run("gh", [
|
|
19769
|
+
"run",
|
|
19770
|
+
"view",
|
|
19771
|
+
String(rec.runId),
|
|
19772
|
+
"--repo",
|
|
19773
|
+
runRepo,
|
|
19774
|
+
"--json",
|
|
19775
|
+
"status,conclusion,headSha,url"
|
|
19776
|
+
]));
|
|
19777
|
+
} catch (e) {
|
|
19778
|
+
throw new ReleaseLedgerError(
|
|
19779
|
+
"unreadable",
|
|
19780
|
+
`resume could not re-verify the persisted ${phase} boundary (run ${rec.runId} in ${runRepo}: ${e instanceof Error ? e.message : String(e)}) \u2014 a failed live read is unverified, not passable; retry when GitHub answers (#5987/#4713)`
|
|
19781
|
+
);
|
|
19782
|
+
}
|
|
19783
|
+
if (rec.runProvenance === "sha" && (meta.headSha ?? "").toLowerCase() !== input.sha.toLowerCase()) {
|
|
19784
|
+
throw new ReleaseLedgerError(
|
|
19785
|
+
"tag-sha-mismatch",
|
|
19786
|
+
`the persisted ${phase} boundary names run ${rec.runId} at ${(meta.headSha ?? "").slice(0, 12)} but the release tag is ${input.sha.slice(0, 12)} \u2014 ledger contradicted by live proof; failing closed (#5987)`
|
|
19787
|
+
);
|
|
19788
|
+
}
|
|
19789
|
+
const liveLabel = `${rec.workflow ? `${rec.workflow}${rec.event ? ` (${rec.event})` : ""} ` : ""}run ${rec.runId} in ${runRepo}`;
|
|
19790
|
+
if (meta.status !== "completed") {
|
|
19791
|
+
phases[phase] = { ...rec, state: "pending", note: `re-verified live: ${liveLabel} still in flight`, updatedAt: now().toISOString() };
|
|
19792
|
+
notes.push(`${phase}: ${liveLabel} still in flight \u2014 leg re-verified as pending`);
|
|
19793
|
+
continue;
|
|
19794
|
+
}
|
|
19795
|
+
if (meta.conclusion === "success") {
|
|
19796
|
+
if (rec.state !== "complete") {
|
|
19797
|
+
phases[phase] = { ...rec, state: "complete", note: `re-verified live: ${liveLabel} concluded success`, updatedAt: now().toISOString() };
|
|
19798
|
+
notes.push(`${phase}: ${liveLabel} re-verified green live`);
|
|
19799
|
+
}
|
|
19800
|
+
continue;
|
|
19801
|
+
}
|
|
19802
|
+
if (rec.state === "complete") {
|
|
19803
|
+
throw new ReleaseLedgerError(
|
|
19804
|
+
"contradicted",
|
|
19805
|
+
`the persisted ${phase} boundary says COMPLETE but live ${liveLabel} concluded ${meta.conclusion ?? "not-success"} \u2014 ledger contradicted by live GitHub proof; failing closed instead of trusting either (#5987)`
|
|
19806
|
+
);
|
|
19807
|
+
}
|
|
19808
|
+
phases[phase] = { ...rec, state: "failed", error: `live re-verification: ${liveLabel} concluded ${meta.conclusion}`, updatedAt: now().toISOString() };
|
|
19809
|
+
notes.push(`${phase}: ${liveLabel} failed live \u2014 leg re-verified as failed`);
|
|
19810
|
+
}
|
|
19811
|
+
const ledger = { ...current, phases, updatedAt: now().toISOString() };
|
|
19812
|
+
return { ledger, value: { ledger, notes } };
|
|
19813
|
+
}
|
|
19814
|
+
);
|
|
19815
|
+
return result;
|
|
19816
|
+
}
|
|
19817
|
+
async function clearLedgerFile(path2, anchors, maxWaitMs) {
|
|
19818
|
+
return withFileLock(`${path2}.lock`, { label: "release ledger", maxWaitMs }, async () => {
|
|
19819
|
+
let raw;
|
|
19820
|
+
try {
|
|
19821
|
+
raw = (0, import_node_fs19.readFileSync)(path2, "utf8");
|
|
19822
|
+
} catch (e) {
|
|
19823
|
+
if (e.code === "ENOENT") return { cleared: false };
|
|
19824
|
+
return { cleared: false, note: `release ledger at ${path2} could not be read for clearing (${e instanceof Error ? e.message : String(e)}) \u2014 left in place` };
|
|
19825
|
+
}
|
|
19826
|
+
let ledger;
|
|
19827
|
+
try {
|
|
19828
|
+
ledger = parseReleaseLedger(raw);
|
|
19829
|
+
} catch (e) {
|
|
19830
|
+
return { cleared: false, note: `release ledger at ${path2} is unreadable (${e instanceof Error ? e.message : String(e)}) \u2014 left in place; inspect it by hand` };
|
|
19831
|
+
}
|
|
19832
|
+
try {
|
|
19833
|
+
validateLedgerAnchors(ledger, anchors);
|
|
19834
|
+
} catch (e) {
|
|
19835
|
+
return { cleared: false, note: `release ledger at ${path2} belongs to ${ledger.tag} @ ${ledger.tagSha.slice(0, 12)} \u2014 not this release; left in place (${e instanceof Error ? e.message : String(e)})` };
|
|
19836
|
+
}
|
|
19837
|
+
(0, import_node_fs19.unlinkSync)(path2);
|
|
19838
|
+
return { cleared: true, note: `cleared the release ledger at ${path2} for the live-proven aborted ${anchors.tag}` };
|
|
19839
|
+
});
|
|
19840
|
+
}
|
|
19841
|
+
async function clearReleaseLedger(cwd, anchors, opts = {}) {
|
|
19842
|
+
let path2;
|
|
19843
|
+
try {
|
|
19844
|
+
path2 = releaseLedgerPath(cwd);
|
|
19845
|
+
} catch {
|
|
19846
|
+
return void 0;
|
|
19847
|
+
}
|
|
19848
|
+
return (await clearLedgerFile(path2, anchors, opts.maxWaitMs)).note;
|
|
19849
|
+
}
|
|
19850
|
+
async function reconcileLedgerAfterFailedTagPush(deps, ledger, anchors) {
|
|
19851
|
+
let remoteOut;
|
|
19852
|
+
try {
|
|
19853
|
+
remoteOut = await deps.run("git", ["ls-remote", "origin", `refs/tags/${anchors.tag}`]);
|
|
19854
|
+
} catch (e) {
|
|
19855
|
+
return {
|
|
19856
|
+
action: "kept-unproven",
|
|
19857
|
+
note: `the tag-existence read for ${anchors.tag} FAILED (${e instanceof Error ? e.message : String(e)}) \u2014 the just-started all-pending ledger is LEFT in place (never cleared on a failed read); it will block the next release until this release is resolved by hand (#5987)`
|
|
19858
|
+
};
|
|
19859
|
+
}
|
|
19860
|
+
const remoteSha = remoteOut.trim().split(/\s+/)[0] ?? "";
|
|
19861
|
+
if (remoteSha.toLowerCase() === anchors.tagSha.toLowerCase()) {
|
|
19862
|
+
return {
|
|
19863
|
+
action: "kept-present",
|
|
19864
|
+
note: `origin shows ${anchors.tag} at this release SHA ${remoteSha.slice(0, 12)} \u2014 the partial release state stands and the ledger is kept; resume with mmi-cli devops release --resume (do not re-tag) (#5987)`
|
|
19865
|
+
};
|
|
19866
|
+
}
|
|
19867
|
+
let clear;
|
|
19868
|
+
try {
|
|
19869
|
+
clear = await clearLedgerFile(ledger.path, anchors);
|
|
19870
|
+
} catch (e) {
|
|
19871
|
+
clear = { cleared: false, note: `clear failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
19872
|
+
}
|
|
19873
|
+
if (!clear.cleared && clear.note) {
|
|
19874
|
+
return { action: "kept-unproven", note: `the just-started ledger could not be cleared after origin proved this run has no resumable tag (${clear.note}) \u2014 resolve it by hand (#5987)` };
|
|
19875
|
+
}
|
|
19876
|
+
return {
|
|
19877
|
+
action: "cleared",
|
|
19878
|
+
note: remoteSha ? `origin shows ${anchors.tag} at foreign SHA ${remoteSha.slice(0, 12)}, not this run's ${anchors.tagSha.slice(0, 12)} \u2014 this run's just-started ledger was cleared; let the owning release finish before retrying (#5987)` : `origin proves ${anchors.tag} was never pushed (no tag on origin) \u2014 the just-started all-pending ledger was cleared, so the next release is not blocked; rerun the release once the failure above is fixed (#5987)`
|
|
19879
|
+
};
|
|
19880
|
+
}
|
|
19881
|
+
async function startReleaseLedger(ledger, anchors, opts = {}) {
|
|
19882
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
19883
|
+
try {
|
|
19884
|
+
return await withReleaseLedger(
|
|
19885
|
+
ledger.path,
|
|
19886
|
+
anchors,
|
|
19887
|
+
(current) => {
|
|
19888
|
+
if (current) return { value: { ok: true } };
|
|
19889
|
+
const normalized = normalizeLedgerAnchors(anchors);
|
|
19890
|
+
const fresh = {
|
|
19891
|
+
schema: RELEASE_LEDGER_SCHEMA,
|
|
19892
|
+
repo: normalized.repo,
|
|
19893
|
+
tag: normalized.tag,
|
|
19894
|
+
tagSha: normalized.tagSha,
|
|
19895
|
+
phases: emptyPhases(),
|
|
19896
|
+
updatedAt: now().toISOString()
|
|
19897
|
+
};
|
|
19898
|
+
return { ledger: fresh, value: { ok: true } };
|
|
19899
|
+
}
|
|
19900
|
+
);
|
|
19901
|
+
} catch (e) {
|
|
19902
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
19903
|
+
}
|
|
19904
|
+
}
|
|
19905
|
+
async function beginReleaseLedger(deps, cwd, targetTag) {
|
|
19906
|
+
let path2;
|
|
19907
|
+
try {
|
|
19908
|
+
path2 = releaseLedgerPath(cwd);
|
|
19909
|
+
} catch {
|
|
19910
|
+
return { ok: true };
|
|
19911
|
+
}
|
|
19912
|
+
return withFileLock(`${path2}.lock`, { label: "release ledger" }, async () => {
|
|
19913
|
+
let raw;
|
|
19914
|
+
try {
|
|
19915
|
+
raw = (0, import_node_fs19.readFileSync)(path2, "utf8");
|
|
19916
|
+
} catch (e) {
|
|
19917
|
+
if (e.code === "ENOENT") return { ok: true };
|
|
19918
|
+
return { ok: false, error: `the release ledger at ${path2} could not be READ (${e instanceof Error ? e.message : String(e)}) \u2014 a failed read is unverified, not absent (#5987/#4713)` };
|
|
19919
|
+
}
|
|
19920
|
+
let prior;
|
|
19921
|
+
try {
|
|
19922
|
+
prior = parseReleaseLedger(raw);
|
|
19923
|
+
} catch (e) {
|
|
19924
|
+
return { ok: false, error: `the active release ledger at ${path2} is unreadable (${e instanceof Error ? e.message : String(e)}) \u2014 resolve it by hand before starting a release (#5987)` };
|
|
19925
|
+
}
|
|
19926
|
+
if (targetTag && prior.tag === targetTag) return { ok: true };
|
|
19927
|
+
const derived = deriveReleasePhaseStatus(prior);
|
|
19928
|
+
if (derived.phaseStatus !== "phases-green") {
|
|
19929
|
+
const open2 = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
|
|
19930
|
+
return {
|
|
19931
|
+
ok: false,
|
|
19932
|
+
error: `a prior release ledger for ${prior.tag} is ${derived.phaseStatus} (${open2.map((l) => `${l.phase}=${l.state}`).join(", ")}) \u2014 a new release must not stack on an unresolved one. Finish it first: mmi-cli devops release --resume (or, only when proven unpublished, mmi-cli devops release --abort --apply) (#5987)`
|
|
19933
|
+
};
|
|
19934
|
+
}
|
|
19935
|
+
let liveSha = "";
|
|
19936
|
+
try {
|
|
19937
|
+
liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
|
|
19938
|
+
} catch (e) {
|
|
19939
|
+
return { ok: false, error: `could not prove the completed ${prior.tag} live (${e instanceof Error ? e.message : String(e)}) \u2014 refusing to archive the ledger; retry when origin answers (#5987/#4713)` };
|
|
19940
|
+
}
|
|
19941
|
+
if (liveSha.toLowerCase() !== prior.tagSha.toLowerCase()) {
|
|
19942
|
+
return { ok: false, error: `the completed ${prior.tag} ledger says phases-green but origin reports ${liveSha.slice(0, 12) || "no tag"} \u2260 ${prior.tagSha.slice(0, 12)} \u2014 refusing to archive on unproven state; resolve by hand (#5987)` };
|
|
19943
|
+
}
|
|
19944
|
+
const archivePath = (0, import_node_path18.join)((0, import_node_path18.dirname)(path2), `phases.archive-${prior.tag}.json`);
|
|
19945
|
+
try {
|
|
19946
|
+
(0, import_node_fs19.unlinkSync)(archivePath);
|
|
19947
|
+
} catch (e) {
|
|
19948
|
+
if (e.code !== "ENOENT") {
|
|
19949
|
+
return { ok: false, error: `could not clear the stale archive at ${archivePath} (${e instanceof Error ? e.message : String(e)}) \u2014 the ledger for ${prior.tag} stays active (#5987)` };
|
|
19950
|
+
}
|
|
19951
|
+
}
|
|
19952
|
+
(0, import_node_fs19.renameSync)(path2, archivePath);
|
|
19953
|
+
return { ok: true, note: `archived the completed release ledger ${prior.tag} to ${archivePath} (live-proven at ${prior.tagSha.slice(0, 12)})` };
|
|
19954
|
+
});
|
|
19955
|
+
}
|
|
19956
|
+
|
|
18821
19957
|
// src/train-apply-phases.ts
|
|
18822
19958
|
var TRAIN_BUMP_INTENTS = ["major", "minor", "patch"];
|
|
18823
19959
|
function readLocalGateWorkflows() {
|
|
18824
|
-
const dir = (0,
|
|
19960
|
+
const dir = (0, import_node_path19.join)(".github", "workflows");
|
|
18825
19961
|
let names;
|
|
18826
19962
|
try {
|
|
18827
|
-
names = (0,
|
|
19963
|
+
names = (0, import_node_fs20.readdirSync)(dir);
|
|
18828
19964
|
} catch {
|
|
18829
19965
|
return null;
|
|
18830
19966
|
}
|
|
18831
19967
|
const files = [];
|
|
18832
19968
|
for (const name of names.filter(isGateWorkflowPath)) {
|
|
18833
19969
|
try {
|
|
18834
|
-
files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0,
|
|
19970
|
+
files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs20.readFileSync)((0, import_node_path19.join)(dir, name), "utf8") });
|
|
18835
19971
|
} catch {
|
|
18836
19972
|
}
|
|
18837
19973
|
}
|
|
18838
19974
|
return files;
|
|
18839
19975
|
}
|
|
18840
19976
|
function readLocalWorkflows() {
|
|
18841
|
-
const dir = (0,
|
|
19977
|
+
const dir = (0, import_node_path19.join)(".github", "workflows");
|
|
18842
19978
|
let names;
|
|
18843
19979
|
try {
|
|
18844
|
-
names = (0,
|
|
19980
|
+
names = (0, import_node_fs20.readdirSync)(dir);
|
|
18845
19981
|
} catch {
|
|
18846
19982
|
return null;
|
|
18847
19983
|
}
|
|
18848
19984
|
const files = [];
|
|
18849
19985
|
for (const name of names.filter((n) => /\.ya?ml$/i.test(n))) {
|
|
18850
19986
|
try {
|
|
18851
|
-
files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0,
|
|
19987
|
+
files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs20.readFileSync)((0, import_node_path19.join)(dir, name), "utf8") });
|
|
18852
19988
|
} catch {
|
|
18853
19989
|
}
|
|
18854
19990
|
}
|
|
@@ -18986,7 +20122,7 @@ async function waitForFoldIndexLock(deps, startBranch, resumeCommand) {
|
|
|
18986
20122
|
const deadline = now() + GIT_INDEX_LOCK_WAIT_MS;
|
|
18987
20123
|
for (; ; ) {
|
|
18988
20124
|
try {
|
|
18989
|
-
await (0,
|
|
20125
|
+
await (0, import_promises3.stat)(lockPath);
|
|
18990
20126
|
} catch (e) {
|
|
18991
20127
|
if (e.code === "ENOENT") return;
|
|
18992
20128
|
throw foldFailureGuidance(
|
|
@@ -19316,8 +20452,35 @@ async function gateTrueMergeCommit(deps, ctx, tag, releaseSha, requiredChecks) {
|
|
|
19316
20452
|
}
|
|
19317
20453
|
}
|
|
19318
20454
|
}
|
|
19319
|
-
async function
|
|
20455
|
+
async function recordPhase(ledger, deps, anchors, phase, entry, opts = {}) {
|
|
20456
|
+
if (!ledger) return;
|
|
20457
|
+
const res = await ledger.record(anchors, phase, entry);
|
|
20458
|
+
if (!res.ok) {
|
|
20459
|
+
const message = `release ledger write FAILED for phase '${phase}' (${res.error}) - already landed and immutable: ${opts.landed ?? "no boundary recorded yet"}. Nothing is rolled back, but this run stops here rather than mutating untracked: resolve the ledger at the recorded path and rerun mmi-cli devops release --resume (#5987)`;
|
|
20460
|
+
if (opts.strict !== false) throw new Error(message);
|
|
20461
|
+
deps.warn?.(message);
|
|
20462
|
+
}
|
|
20463
|
+
}
|
|
20464
|
+
async function recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, phase, input) {
|
|
20465
|
+
if (!ledger) return { reran: false, recovered: false, note: "" };
|
|
20466
|
+
try {
|
|
20467
|
+
return await transientRecoveryForFailedPhase(deps, {
|
|
20468
|
+
ledger,
|
|
20469
|
+
anchors,
|
|
20470
|
+
phase,
|
|
20471
|
+
repo: input.repo,
|
|
20472
|
+
sha: input.sha,
|
|
20473
|
+
failure: input.failure,
|
|
20474
|
+
...input.runs ? { runs: input.runs } : {},
|
|
20475
|
+
runsProvenance: input.watched ? "watched" : "sha"
|
|
20476
|
+
});
|
|
20477
|
+
} catch (e) {
|
|
20478
|
+
return { reran: false, recovered: false, note: `release ledger: the transient recovery read failed (${e instanceof Error ? e.message : String(e)}) - manual intervention required` };
|
|
20479
|
+
}
|
|
20480
|
+
}
|
|
20481
|
+
async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, startBranch, preFold, tagProbe, branchHints, ledger) {
|
|
19320
20482
|
const resumeCommand = options.dev ? "mmi-cli devops release --dev --apply" : "mmi-cli devops release --apply";
|
|
20483
|
+
const ledgerAnchors = { repo: ctx.repo, tag, tagSha: releaseSha };
|
|
19321
20484
|
try {
|
|
19322
20485
|
await verifyPublishDryRun(deps, ctx, meta, deployModel);
|
|
19323
20486
|
} catch (e) {
|
|
@@ -19344,45 +20507,128 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
|
|
|
19344
20507
|
}
|
|
19345
20508
|
}
|
|
19346
20509
|
let tagPush;
|
|
20510
|
+
let tagLedgerStarted = false;
|
|
20511
|
+
const recoveryInput = { repo: ctx.repo, tag, stage: "main", deployModel, meta, branchHints };
|
|
19347
20512
|
try {
|
|
19348
20513
|
await assertComputedReleaseTagStillNext(deps, tag);
|
|
19349
20514
|
if (deployModel === "hub-serverless") {
|
|
19350
20515
|
await deps.run("node", ["scripts/release-distribution.mjs", "assert-release-lineage", tag.replace(/^v/, ""), "--release-commit", releaseSha]);
|
|
19351
20516
|
}
|
|
20517
|
+
if (ledger) {
|
|
20518
|
+
const start = await startReleaseLedger(ledger, ledgerAnchors);
|
|
20519
|
+
if (!start.ok) {
|
|
20520
|
+
throw new Error(
|
|
20521
|
+
`release refused before the tag was pushed: the durable phase ledger could not be started at ${ledger.path} (${start.error}) - nothing was tagged or pushed; repair the ledger location and rerun the release (#5987)`
|
|
20522
|
+
);
|
|
20523
|
+
}
|
|
20524
|
+
tagLedgerStarted = true;
|
|
20525
|
+
}
|
|
19352
20526
|
tagPush = await ensureTagPushed(deps, tag, releaseSha, tagProbe, ctx.repo);
|
|
19353
20527
|
} catch (e) {
|
|
20528
|
+
if (ledger && tagLedgerStarted) {
|
|
20529
|
+
const decision = await reconcileLedgerAfterFailedTagPush(deps, ledger, ledgerAnchors);
|
|
20530
|
+
const annotated = new Error(`${e instanceof Error ? e.message : String(e)}
|
|
20531
|
+
|
|
20532
|
+
${decision.note}`);
|
|
20533
|
+
if (decision.action === "kept-present") throw partialTrainRecoveryError(annotated, recoveryInput);
|
|
20534
|
+
if (decision.action === "kept-unproven") throw annotated;
|
|
20535
|
+
throw await recoverFailedFold(deps, annotated, startBranch, preFold.mainSha, resumeCommand);
|
|
20536
|
+
}
|
|
19354
20537
|
throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha, resumeCommand);
|
|
19355
20538
|
}
|
|
19356
20539
|
const tagPushSince = (deps.now ?? Date.now)();
|
|
19357
20540
|
let checks;
|
|
19358
|
-
const recoveryInput = { repo: ctx.repo, tag, stage: "main", deployModel, meta, branchHints };
|
|
19359
20541
|
try {
|
|
19360
20542
|
checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
|
|
19361
20543
|
} catch (e) {
|
|
19362
|
-
|
|
20544
|
+
const failureText = e instanceof Error ? e.message : String(e);
|
|
20545
|
+
const recovery = await recoverPhaseWithMaybeTransientRerun(deps, ledger, ledgerAnchors, "ordinaryCi", {
|
|
20546
|
+
repo: ctx.repo,
|
|
20547
|
+
sha: releaseSha,
|
|
20548
|
+
failure: failureText,
|
|
20549
|
+
watched: false
|
|
20550
|
+
});
|
|
20551
|
+
if (recovery.reran && recovery.recovered) {
|
|
20552
|
+
try {
|
|
20553
|
+
checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks, void 0);
|
|
20554
|
+
} catch (e2) {
|
|
20555
|
+
const spent = await recoverPhaseWithMaybeTransientRerun(deps, ledger, ledgerAnchors, "ordinaryCi", {
|
|
20556
|
+
repo: ctx.repo,
|
|
20557
|
+
sha: releaseSha,
|
|
20558
|
+
failure: e2 instanceof Error ? e2.message : String(e2),
|
|
20559
|
+
watched: false
|
|
20560
|
+
});
|
|
20561
|
+
throw partialTrainRecoveryError(new Error(`${e2 instanceof Error ? e2.message : String(e2)}
|
|
20562
|
+
|
|
20563
|
+
${spent.note}`), recoveryInput);
|
|
20564
|
+
}
|
|
20565
|
+
} else {
|
|
20566
|
+
throw partialTrainRecoveryError(new Error(`${failureText}
|
|
20567
|
+
|
|
20568
|
+
${recovery.note}`), recoveryInput);
|
|
20569
|
+
}
|
|
19363
20570
|
}
|
|
20571
|
+
await recordPhase(ledger, deps, ledgerAnchors, "ordinaryCi", phaseEntry({ state: "complete", sha: releaseSha, note: checks }), {
|
|
20572
|
+
landed: "the immutable tag pushed at " + releaseSha.slice(0, 12)
|
|
20573
|
+
});
|
|
19364
20574
|
if (trueMergeGateNote) checks = `${trueMergeGateNote}; ${checks}`;
|
|
19365
20575
|
try {
|
|
19366
20576
|
await runGitPush(deps, ["push", "origin", "main"]);
|
|
19367
20577
|
} catch (e) {
|
|
20578
|
+
await recordPhase(ledger, deps, ledgerAnchors, "promotion", phaseEntry({ state: "failed", sha: releaseSha, error: `origin/main push failed after ${tag} was pushed: ${e instanceof Error ? e.message : String(e)}` }), { landed: "the immutable tag pushed at " + releaseSha.slice(0, 12) });
|
|
19368
20579
|
throw partialTrainRecoveryError(e, recoveryInput);
|
|
19369
20580
|
}
|
|
20581
|
+
await recordPhase(ledger, deps, ledgerAnchors, "promotion", phaseEntry({ state: "complete", sha: releaseSha, note: `immutable ${tag} pushed at ${releaseSha.slice(0, 12)}; origin/main fast-forwarded to it` }), { landed: "the immutable tag and the green required-check wall" });
|
|
19370
20582
|
const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
19371
20583
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
20584
|
+
await recordPhase(ledger, deps, ledgerAnchors, "githubRelease", phaseEntry({ state: "complete", sha: releaseSha, runUrl: releaseUrl, note: `verified GitHub Release ${tag} against ${releaseSha.slice(0, 12)}` }), { landed: "the immutable tag, the green required-check wall, and the origin/main fast-forward" });
|
|
19372
20585
|
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
19373
20586
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
19374
|
-
const
|
|
20587
|
+
const deployDispatch0 = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir);
|
|
20588
|
+
let deployDispatch = deployDispatch0;
|
|
20589
|
+
let deployRecoveryNote;
|
|
20590
|
+
if (deployDispatch0.deployStatus === "failure" && ledger) {
|
|
20591
|
+
const deployRunRepo = releaseRunRepoFor(deployModel, ctx.repo);
|
|
20592
|
+
const failedRow = (deployDispatch0.workflowRuns ?? []).find((r) => r.runId != null && r.conclusion === "failure");
|
|
20593
|
+
const dispatchRun = deployDispatch0.runId != null ? [{ workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow", runId: deployDispatch0.runId, runUrl: deployDispatch0.runUrl, conclusion: "failure", repo: deployRunRepo, ...isCentralDispatchModel(deployModel) ? { event: "workflow_dispatch" } : {} }] : [];
|
|
20594
|
+
const recovery = await recoverPhaseWithMaybeTransientRerun(deps, ledger, ledgerAnchors, failedRow?.workflow === "publish.yml" ? "publish" : "deploy", {
|
|
20595
|
+
repo: deployRunRepo,
|
|
20596
|
+
sha: releaseSha,
|
|
20597
|
+
failure: `watched deploy run(s) failed on the release SHA: ${deployDispatch0.note}`,
|
|
20598
|
+
runs: deployDispatch0.workflowRuns?.filter((r) => r.runId != null).map((r) => ({ ...r, repo: deployRunRepo })) ?? (deployDispatch0.runId != null ? [{ workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow", runId: deployDispatch0.runId, runUrl: deployDispatch0.runUrl, conclusion: "failure", repo: deployRunRepo, ...isCentralDispatchModel(deployModel) ? { event: "workflow_dispatch" } : {} }] : []),
|
|
20599
|
+
watched: isCentralDispatchModel(deployModel)
|
|
20600
|
+
});
|
|
20601
|
+
if (recovery.reran && recovery.recovered) {
|
|
20602
|
+
deployDispatch = {
|
|
20603
|
+
...deployDispatch0,
|
|
20604
|
+
deployStatus: "success",
|
|
20605
|
+
workflowRuns: deployDispatch0.workflowRuns?.map((r) => recovery.runId != null && r.runId === recovery.runId ? { ...r, conclusion: "success" } : r),
|
|
20606
|
+
note: `${deployDispatch0.note}; ${recovery.note}`
|
|
20607
|
+
};
|
|
20608
|
+
} else if (recovery.note) {
|
|
20609
|
+
deployRecoveryNote = recovery.note;
|
|
20610
|
+
}
|
|
20611
|
+
}
|
|
20612
|
+
await recordPhase(ledger, deps, ledgerAnchors, "deploy", phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha })), {
|
|
20613
|
+
landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
|
|
20614
|
+
});
|
|
19375
20615
|
const publishDispatch = deployDispatch.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report") : null;
|
|
19376
|
-
let
|
|
19377
|
-
dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, dispatch);
|
|
20616
|
+
let publishSkipNote;
|
|
19378
20617
|
if (!publishDispatch && deployDispatch.deployStatus !== "success" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
|
|
19379
|
-
|
|
19380
|
-
dispatch = {
|
|
19381
|
-
...dispatch,
|
|
19382
|
-
note: `${dispatch.note}; tenant-publish.yml skipped (${reason})`
|
|
19383
|
-
};
|
|
20618
|
+
publishSkipNote = deployDispatch.deployStatus === "failure" ? "box deploy failed - redeploy the box before publishing" : "box deploy not confirmed (run with --watch) - publish after the box deploy lands";
|
|
19384
20619
|
}
|
|
19385
|
-
|
|
20620
|
+
await recordPhase(ledger, deps, ledgerAnchors, "publish", phaseEntry(publishPhaseInput(deployModel, deployDispatch, publishDispatch, meta.publishRequired === true, {
|
|
20621
|
+
releaseSha,
|
|
20622
|
+
repo: ctx.repo,
|
|
20623
|
+
...publishSkipNote !== void 0 ? { skipPublishNote: publishSkipNote } : {}
|
|
20624
|
+
})), {
|
|
20625
|
+
landed: "the immutable tag, the green required-check wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
20626
|
+
});
|
|
20627
|
+
let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
20628
|
+
dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, dispatch);
|
|
20629
|
+
if (publishSkipNote) dispatch = { ...dispatch, note: `${dispatch.note}; tenant-publish.yml skipped (${publishSkipNote})` };
|
|
20630
|
+
if (deployRecoveryNote) dispatch = { ...dispatch, note: `${dispatch.note}; ${deployRecoveryNote}` };
|
|
20631
|
+
return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors };
|
|
19386
20632
|
}
|
|
19387
20633
|
function trainResumeExitCode(state) {
|
|
19388
20634
|
if (state === "failed") return 1;
|
|
@@ -19492,6 +20738,35 @@ async function runReleaseResumePipeline(deps, options, prepared) {
|
|
|
19492
20738
|
);
|
|
19493
20739
|
}
|
|
19494
20740
|
const base = { command: "release-resume", repo: ctx.repo, tag, tagSha, deployModel };
|
|
20741
|
+
const ledger = openReleaseLedgerWriter(process.cwd());
|
|
20742
|
+
const anchors = { repo: ctx.repo, tag, tagSha };
|
|
20743
|
+
const begin = await beginReleaseLedger(deps, process.cwd(), tag);
|
|
20744
|
+
if (!begin.ok) throw new Error(`release --resume refused: ${begin.error}`);
|
|
20745
|
+
else if (begin.note) deps.warn?.(`release ledger: ${begin.note}`);
|
|
20746
|
+
let persisted;
|
|
20747
|
+
try {
|
|
20748
|
+
persisted = await ledger.load(anchors);
|
|
20749
|
+
} catch (e) {
|
|
20750
|
+
throw new Error(
|
|
20751
|
+
`release --resume: ${e instanceof Error ? e.message : String(e)}
|
|
20752
|
+
Live origin/tag/Release proof stays authoritative; nothing was written. Inspect ${ledger.path} before retrying.`
|
|
20753
|
+
);
|
|
20754
|
+
}
|
|
20755
|
+
if (persisted) {
|
|
20756
|
+
let reverifyNotes = [];
|
|
20757
|
+
try {
|
|
20758
|
+
const reverified = await reverifyPersistedRunPhases(deps, ledger.path, anchors, { sha: tagSha, repo: ctx.repo });
|
|
20759
|
+
persisted = reverified.ledger;
|
|
20760
|
+
reverifyNotes = reverified.notes;
|
|
20761
|
+
} catch (e) {
|
|
20762
|
+
throw new Error(
|
|
20763
|
+
`release --resume: ${e instanceof Error ? e.message : String(e)}
|
|
20764
|
+
Nothing was written. Inspect ${ledger.path} (or clear it only after proving the release state by hand).`
|
|
20765
|
+
);
|
|
20766
|
+
}
|
|
20767
|
+
for (const note of reverifyNotes) deps.warn?.(`release ledger: ${note}`);
|
|
20768
|
+
}
|
|
20769
|
+
const ledgerMode = persisted ? "strict" : "migration";
|
|
19495
20770
|
const partialRelease = await isStrayUnreleasedTag(deps, tag, tagSha, ctx.repo);
|
|
19496
20771
|
if (!partialRelease) {
|
|
19497
20772
|
try {
|
|
@@ -19506,12 +20781,44 @@ async function runReleaseResumePipeline(deps, options, prepared) {
|
|
|
19506
20781
|
steps2.push(`development alignment: ${devRollForward2.status}`);
|
|
19507
20782
|
const rcAlignment2 = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
|
|
19508
20783
|
if (rcAlignment2) steps2.push(`rc alignment: ${rcAlignment2.status}`);
|
|
19509
|
-
const
|
|
20784
|
+
const historicalTargets = prodReleaseTargets(deployModel);
|
|
20785
|
+
const historicalRepo = releaseRunRepoFor(deployModel, ctx.repo);
|
|
20786
|
+
let historicalRows = historicalTargets.length ? await listOwnWorkflowRuns(deps, historicalRepo, historicalTargets, tagSha) : [];
|
|
20787
|
+
let historicalDeploy = historicalTargets.length ? aggregateWorkflowRuns(historicalRows) : "pending";
|
|
20788
|
+
let historicalNote = `historical ${deployModel} deploy/publish status: ${historicalDeploy}`;
|
|
20789
|
+
if (historicalDeploy === "failure" && ledger) {
|
|
20790
|
+
const failedRow = historicalRows.find((r) => r.runId != null && r.conclusion === "failure");
|
|
20791
|
+
const recovery = await recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, failedRow?.workflow === "publish.yml" ? "publish" : "deploy", {
|
|
20792
|
+
repo: historicalRepo,
|
|
20793
|
+
sha: tagSha,
|
|
20794
|
+
failure: `historical deploy/publish run(s) failed on the release SHA: ${historicalRows.map((r) => `${r.workflow}=${r.conclusion}`).join(", ")}`,
|
|
20795
|
+
runs: historicalRows.filter((r) => r.runId != null).map((r) => ({ ...r, repo: historicalRepo })),
|
|
20796
|
+
watched: false
|
|
20797
|
+
});
|
|
20798
|
+
if (recovery.reran && recovery.recovered) {
|
|
20799
|
+
historicalRows = historicalRows.map((r) => recovery.runId != null && r.runId === recovery.runId ? { ...r, conclusion: "success" } : r);
|
|
20800
|
+
historicalDeploy = aggregateWorkflowRuns(historicalRows);
|
|
20801
|
+
historicalNote = `historical ${deployModel} deploy/publish status: ${historicalDeploy}; ${recovery.note}`;
|
|
20802
|
+
} else if (recovery.note) {
|
|
20803
|
+
historicalNote = `${historicalNote}; ${recovery.note}`;
|
|
20804
|
+
}
|
|
20805
|
+
}
|
|
20806
|
+
if (persisted) {
|
|
20807
|
+
const phaseInputs = phaseInputsFromRunRows(deployModel, historicalRows, tagSha, { repo: ctx.repo });
|
|
20808
|
+
await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(phaseInputs.deploy), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
|
|
20809
|
+
await recordPhase(ledger, deps, anchors, "publish", phaseEntry(phaseInputs.publish), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
|
|
20810
|
+
await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified live: the GitHub Release exists at the immutable tag SHA" }), { strict: true, landed: "the promotion (verified live)" });
|
|
20811
|
+
await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified live: origin/main contains the immutable tag SHA" }), { strict: true, landed: "nothing beyond the already-public release" });
|
|
20812
|
+
}
|
|
19510
20813
|
const dispatch2 = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, {
|
|
19511
|
-
note:
|
|
20814
|
+
note: historicalNote,
|
|
19512
20815
|
deployStatus: historicalDeploy
|
|
19513
20816
|
});
|
|
19514
20817
|
if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
|
|
20818
|
+
if (persisted) {
|
|
20819
|
+
await recordPhase(ledger, deps, anchors, "alignment", phaseEntry(alignmentPhaseEntry(devRollForward2, rcAlignment2)), { strict: true, landed: "the released, verified, deploy-resolved release (alignment PRs reused/landed)" });
|
|
20820
|
+
}
|
|
20821
|
+
const ledgerReport2 = releaseLedgerReport(await ledger.load(anchors).catch(() => void 0), ledger.path);
|
|
19515
20822
|
const state2 = resumeStateOf(deriveTrainFollowUpStatus({
|
|
19516
20823
|
projectInfo: "ok",
|
|
19517
20824
|
deploy: deployFollowUpOutcome(dispatch2.deployStatus),
|
|
@@ -19528,6 +20835,7 @@ async function runReleaseResumePipeline(deps, options, prepared) {
|
|
|
19528
20835
|
devRollForward: devRollForward2,
|
|
19529
20836
|
rcAlignment: rcAlignment2,
|
|
19530
20837
|
dispatch: dispatch2,
|
|
20838
|
+
...ledgerReport2 ? { ledger: ledgerReport2 } : {},
|
|
19531
20839
|
note: state2 === "failed" ? `release ${tag} was already published, but its deploy/publish on the release SHA FAILED \u2014 re-run the failed job (do NOT re-tag); the protected alignment PRs were reused` : state2 === "pending" ? `release ${tag} was already published; reused its protected alignment PRs, but a deploy/publish or alignment follow-up is unresolved \u2014 watch it to completion` : `release ${tag} was already published; alignment is complete and the deploy on the release SHA is proven green`
|
|
19532
20840
|
};
|
|
19533
20841
|
}
|
|
@@ -19548,21 +20856,81 @@ async function runReleaseResumePipeline(deps, options, prepared) {
|
|
|
19548
20856
|
try {
|
|
19549
20857
|
steps.push(await waitForRequiredTrainChecks(deps, ctx, tagSha, requiredChecks, void 0));
|
|
19550
20858
|
} catch (e) {
|
|
19551
|
-
|
|
20859
|
+
const failureText = e instanceof Error ? e.message : String(e);
|
|
20860
|
+
const recovery = await recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, "ordinaryCi", {
|
|
20861
|
+
repo: ctx.repo,
|
|
20862
|
+
sha: tagSha,
|
|
20863
|
+
failure: failureText,
|
|
20864
|
+
watched: false
|
|
20865
|
+
});
|
|
20866
|
+
if (recovery.reran && recovery.recovered) {
|
|
20867
|
+
try {
|
|
20868
|
+
steps.push(await waitForRequiredTrainChecks(deps, ctx, tagSha, requiredChecks, void 0));
|
|
20869
|
+
} catch (e2) {
|
|
20870
|
+
const spent = await recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, "ordinaryCi", {
|
|
20871
|
+
repo: ctx.repo,
|
|
20872
|
+
sha: tagSha,
|
|
20873
|
+
failure: e2 instanceof Error ? e2.message : String(e2),
|
|
20874
|
+
watched: false
|
|
20875
|
+
});
|
|
20876
|
+
throw partialTrainRecoveryError(new Error(`${e2 instanceof Error ? e2.message : String(e2)}
|
|
20877
|
+
|
|
20878
|
+
${spent.note}`), recoveryInput);
|
|
20879
|
+
}
|
|
20880
|
+
} else {
|
|
20881
|
+
throw partialTrainRecoveryError(new Error(`${failureText}
|
|
20882
|
+
|
|
20883
|
+
${recovery.note}`), recoveryInput);
|
|
20884
|
+
}
|
|
19552
20885
|
}
|
|
20886
|
+
await recordPhase(ledger, deps, anchors, "ordinaryCi", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified the required-check wall live on this resume" }), { strict: ledgerMode === "strict", landed: "the immutable tag on origin at the resumed SHA" });
|
|
19553
20887
|
await runGitPush(deps, ["push", "origin", `${tagSha}:refs/heads/main`]);
|
|
19554
20888
|
steps.push(`pushed ${tagSha.slice(0, 12)} to origin/main`);
|
|
20889
|
+
await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: tagSha, note: `resumed: ${tagSha.slice(0, 12)} pushed to origin/main (tag preserved, never re-cut)` }), { strict: ledgerMode === "strict", landed: "the immutable tag and the re-proven required-check wall" });
|
|
19555
20890
|
const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
19556
20891
|
steps.push(`created the GitHub Release for ${tag}`);
|
|
19557
20892
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
|
|
19558
20893
|
steps.push("verified the Release published against the tagged commit");
|
|
20894
|
+
await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "complete", sha: tagSha, runUrl: releaseUrl, note: "verified the Release published against the tagged commit" }), { strict: ledgerMode === "strict", landed: "the immutable tag, the re-proven wall, and the origin/main push" });
|
|
19559
20895
|
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
19560
20896
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
19561
|
-
const
|
|
20897
|
+
const deployDispatch0 = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir);
|
|
20898
|
+
let deployDispatch = deployDispatch0;
|
|
20899
|
+
let deployRecoveryNote;
|
|
20900
|
+
if (deployDispatch0.deployStatus === "failure" && ledger) {
|
|
20901
|
+
const deployRunRepo = releaseRunRepoFor(deployModel, ctx.repo);
|
|
20902
|
+
const failedRow = (deployDispatch0.workflowRuns ?? []).find((r) => r.runId != null && r.conclusion === "failure");
|
|
20903
|
+
const recovery = await recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, failedRow?.workflow === "publish.yml" ? "publish" : "deploy", {
|
|
20904
|
+
repo: deployRunRepo,
|
|
20905
|
+
sha: tagSha,
|
|
20906
|
+
failure: `watched deploy run(s) failed on the release SHA: ${deployDispatch0.note}`,
|
|
20907
|
+
runs: deployDispatch0.workflowRuns?.filter((r) => r.runId != null).map((r) => ({ ...r, repo: deployRunRepo })) ?? (deployDispatch0.runId != null ? [{ workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow", runId: deployDispatch0.runId, runUrl: deployDispatch0.runUrl, conclusion: "failure", repo: deployRunRepo, ...isCentralDispatchModel(deployModel) ? { event: "workflow_dispatch" } : {} }] : []),
|
|
20908
|
+
watched: isCentralDispatchModel(deployModel)
|
|
20909
|
+
});
|
|
20910
|
+
if (recovery.reran && recovery.recovered) {
|
|
20911
|
+
deployDispatch = {
|
|
20912
|
+
...deployDispatch0,
|
|
20913
|
+
deployStatus: "success",
|
|
20914
|
+
workflowRuns: deployDispatch0.workflowRuns?.map((r) => recovery.runId != null && r.runId === recovery.runId ? { ...r, conclusion: "success" } : r),
|
|
20915
|
+
note: `${deployDispatch0.note}; ${recovery.note}`
|
|
20916
|
+
};
|
|
20917
|
+
} else if (recovery.note) {
|
|
20918
|
+
deployRecoveryNote = recovery.note;
|
|
20919
|
+
}
|
|
20920
|
+
}
|
|
20921
|
+
steps.push(`dispatched the ${deployModel} deploy path`);
|
|
20922
|
+
await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha })), {
|
|
20923
|
+
strict: ledgerMode === "strict",
|
|
20924
|
+
landed: "the immutable tag, the re-proven wall, the origin/main push, and the verified GitHub Release"
|
|
20925
|
+
});
|
|
19562
20926
|
const publishDispatch = deployDispatch.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report") : null;
|
|
20927
|
+
await recordPhase(ledger, deps, anchors, "publish", phaseEntry(publishPhaseInput(deployModel, deployDispatch, publishDispatch, meta.publishRequired === true, { releaseSha: tagSha, repo: ctx.repo })), {
|
|
20928
|
+
strict: ledgerMode === "strict",
|
|
20929
|
+
landed: "the immutable tag, the re-proven wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
20930
|
+
});
|
|
19563
20931
|
let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
19564
20932
|
dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, dispatch);
|
|
19565
|
-
|
|
20933
|
+
if (deployRecoveryNote) dispatch = { ...dispatch, note: `${dispatch.note}; ${deployRecoveryNote}` };
|
|
19566
20934
|
const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
19567
20935
|
steps.push(`development roll-forward: ${devRollForward.status}`);
|
|
19568
20936
|
const rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
|
|
@@ -19575,6 +20943,8 @@ async function runReleaseResumePipeline(deps, options, prepared) {
|
|
|
19575
20943
|
foldPort: "ok"
|
|
19576
20944
|
});
|
|
19577
20945
|
const state = resumeStateOf(followUp);
|
|
20946
|
+
await recordPhase(ledger, deps, anchors, "alignment", phaseEntry(alignmentPhaseEntry(devRollForward, rcAlignment)), { strict: ledgerMode === "strict", landed: "the promotion, verified Release, and deploy path of the resumed release" });
|
|
20947
|
+
const ledgerReport = releaseLedgerReport(await ledger.load(anchors).catch(() => void 0), ledger?.path ?? "(no ledger)");
|
|
19578
20948
|
return {
|
|
19579
20949
|
...base,
|
|
19580
20950
|
mode: "partial-release",
|
|
@@ -19586,6 +20956,7 @@ async function runReleaseResumePipeline(deps, options, prepared) {
|
|
|
19586
20956
|
dispatch,
|
|
19587
20957
|
devRollForward,
|
|
19588
20958
|
rcAlignment,
|
|
20959
|
+
...ledgerReport ? { ledger: ledgerReport } : {},
|
|
19589
20960
|
note: followUp === "failed" ? `resumed ${tag} (version preserved, not re-cut) \u2014 but a watched deploy/publish run FAILED; the Release stands, re-run the failed job (do NOT re-tag)` : followUp === "pending" ? `resumed ${tag} (version preserved, not re-cut) \u2014 a deploy/publish or alignment follow-up is unresolved; watch it to completion` : `resumed and completed ${tag} \u2014 the original version was preserved, not re-cut`
|
|
19590
20961
|
};
|
|
19591
20962
|
}
|
|
@@ -19679,13 +21050,14 @@ async function runReleaseAbort(deps, options = {}) {
|
|
|
19679
21050
|
if (restoredMainSha !== originMainSha) {
|
|
19680
21051
|
throw new Error(`release --abort removed ${tag}, but local main did not restore to origin/main; inspect before retrying`);
|
|
19681
21052
|
}
|
|
21053
|
+
const clearedLedgerNote = await clearReleaseLedger(process.cwd(), { repo: ctx.repo, tag, tagSha });
|
|
19682
21054
|
return {
|
|
19683
21055
|
command: "release-abort",
|
|
19684
21056
|
repo: ctx.repo,
|
|
19685
21057
|
tag,
|
|
19686
21058
|
tagSha,
|
|
19687
21059
|
restoredMainSha,
|
|
19688
|
-
note: `removed the unpublished ${tag} candidate and restored local main; repair development before recutting`
|
|
21060
|
+
note: `removed the unpublished ${tag} candidate and restored local main; repair development before recutting` + (clearedLedgerNote ? `; ${clearedLedgerNote}` : "")
|
|
19689
21061
|
};
|
|
19690
21062
|
}
|
|
19691
21063
|
async function runReleasePublishRetry(deps, runId, options = {}) {
|
|
@@ -19732,6 +21104,25 @@ async function runReleasePublishRetry(deps, runId, options = {}) {
|
|
|
19732
21104
|
if (options.watch && await watchTenantRun(deps, runId, ctx.repo) !== "success") {
|
|
19733
21105
|
throw new Error(`publish run ${runId} did not finish successfully after retry`);
|
|
19734
21106
|
}
|
|
21107
|
+
const ledger = openReleaseLedgerWriter(process.cwd());
|
|
21108
|
+
const existing = await ledger.load({ repo: ctx.repo, tag, tagSha }).catch(() => void 0);
|
|
21109
|
+
let ledgerNote;
|
|
21110
|
+
if (existing) {
|
|
21111
|
+
const record = await ledger.record({ repo: ctx.repo, tag, tagSha }, "publish", phaseEntry({
|
|
21112
|
+
state: options.watch ? "complete" : "pending",
|
|
21113
|
+
sha: tagSha,
|
|
21114
|
+
runId,
|
|
21115
|
+
runUrl: run.url ?? `https://github.com/${ctx.repo}/actions/runs/${runId}`,
|
|
21116
|
+
runRepo: ctx.repo,
|
|
21117
|
+
runProvenance: "sha",
|
|
21118
|
+
workflow: "publish.yml",
|
|
21119
|
+
event: "release",
|
|
21120
|
+
note: options.watch ? `publish run ${runId} retried via release --retry-publish and passed (live-proven)` : `publish run ${runId} queued for retry via release --retry-publish`
|
|
21121
|
+
}));
|
|
21122
|
+
ledgerNote = record.ok ? options.watch ? "publish boundary updated to complete in the release ledger" : "publish boundary updated to pending in the release ledger" : `release ledger not updated: ${record.error}`;
|
|
21123
|
+
} else {
|
|
21124
|
+
ledgerNote = "no release ledger on disk for this release \u2014 nothing to update";
|
|
21125
|
+
}
|
|
19735
21126
|
return {
|
|
19736
21127
|
command: "release-retry-publish",
|
|
19737
21128
|
repo: ctx.repo,
|
|
@@ -19740,12 +21131,18 @@ async function runReleasePublishRetry(deps, runId, options = {}) {
|
|
|
19740
21131
|
runId,
|
|
19741
21132
|
runUrl: run.url ?? `https://github.com/${ctx.repo}/actions/runs/${runId}`,
|
|
19742
21133
|
status: options.watch ? "success" : "pending",
|
|
19743
|
-
note: options.watch ? `publish run ${runId} retried and passed` : `publish run ${runId} queued for retry`
|
|
21134
|
+
note: options.watch ? `publish run ${runId} retried and passed; ${ledgerNote}` : `publish run ${runId} queued for retry; ${ledgerNote}`
|
|
19744
21135
|
};
|
|
19745
21136
|
}
|
|
19746
21137
|
async function runTrainApplyPipeline(mode, input) {
|
|
19747
21138
|
const { deps, ctx, command, meta, branchHints, watch, options, bumpIntent } = input;
|
|
19748
21139
|
const directTrack = input.directTrack ?? false;
|
|
21140
|
+
const ledger = mode === "rcand" ? void 0 : openReleaseLedgerWriter(process.cwd());
|
|
21141
|
+
if (ledger) {
|
|
21142
|
+
const begin = await beginReleaseLedger(deps, process.cwd());
|
|
21143
|
+
if (!begin.ok) throw new Error(`release refused before any mutation: ${begin.error}`);
|
|
21144
|
+
else if (begin.note) deps.warn?.(`release ledger: ${begin.note}`);
|
|
21145
|
+
}
|
|
19749
21146
|
if (mode === "rcand") {
|
|
19750
21147
|
await requireBranch(deps, "development");
|
|
19751
21148
|
if (directTrack) {
|
|
@@ -19832,8 +21229,10 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
19832
21229
|
const releaseSha3 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
19833
21230
|
return { versionFold: versionFold3, releaseSha: releaseSha3 };
|
|
19834
21231
|
});
|
|
19835
|
-
const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2, "development", preFold2, tagProbe2, branchHints);
|
|
21232
|
+
const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2, ledgerAnchors: ledgerAnchors2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2, "development", preFold2, tagProbe2, branchHints, ledger);
|
|
19836
21233
|
const devRollForward2 = await rollDevelopmentForward(deps, ctx, tag2);
|
|
21234
|
+
await recordPhase(ledger, deps, ledgerAnchors2, "alignment", phaseEntry(alignmentPhaseEntry(devRollForward2)), { landed: "the immutable tag, the green wall, origin/main, the verified Release, and the deploy/publish path" });
|
|
21235
|
+
const ledgerReport2 = releaseLedgerReport(await ledger?.load(ledgerAnchors2).catch(() => void 0), ledger?.path ?? "(no ledger)");
|
|
19837
21236
|
if (directTrack) {
|
|
19838
21237
|
return {
|
|
19839
21238
|
...ctx,
|
|
@@ -19856,11 +21255,14 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
19856
21255
|
devRollForward: devRollForward2,
|
|
19857
21256
|
announceNote: announceNote2,
|
|
19858
21257
|
devNote: options.dev ? "--dev is a no-op on a direct-track repo \u2014 it already releases development -> main" : void 0,
|
|
19859
|
-
release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 }
|
|
21258
|
+
release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 },
|
|
21259
|
+
...ledgerReport2 ? { ledger: ledgerReport2 } : {}
|
|
19860
21260
|
};
|
|
19861
21261
|
}
|
|
19862
21262
|
const retirement2 = hasRcBranch ? await retireRcRuntime(deps, ctx, deployModel2, d2.deployStatus, rcShaAtRelease) : { status: "not-applicable", note: "no origin/rc branch \u2014 rc runtime retirement skipped" };
|
|
19863
21263
|
const rcAlignment2 = hasRcBranch ? await alignRcForward(deps, ctx, tag2) : void 0;
|
|
21264
|
+
await recordPhase(ledger, deps, ledgerAnchors2, "alignment", phaseEntry(alignmentPhaseEntry(devRollForward2, rcAlignment2)), { landed: "the immutable tag, the green wall, origin/main, the verified Release, and the deploy/publish path" });
|
|
21265
|
+
const fullLedgerReport = releaseLedgerReport(await ledger?.load(ledgerAnchors2).catch(() => void 0), ledger?.path ?? "(no ledger)");
|
|
19864
21266
|
const environments2 = await buildEnvironments(deps, ctx, deployModel2, d2.deployStatus, retirement2);
|
|
19865
21267
|
return {
|
|
19866
21268
|
...ctx,
|
|
@@ -19886,7 +21288,8 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
19886
21288
|
announceNote: announceNote2,
|
|
19887
21289
|
devNote: "released development -> main (--dev), skipping the rc candidate",
|
|
19888
21290
|
release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 },
|
|
19889
|
-
environments: environments2
|
|
21291
|
+
environments: environments2,
|
|
21292
|
+
...fullLedgerReport ? { ledger: fullLedgerReport } : {}
|
|
19890
21293
|
};
|
|
19891
21294
|
}
|
|
19892
21295
|
await requireBranch(deps, "rc");
|
|
@@ -19919,10 +21322,12 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
19919
21322
|
const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
19920
21323
|
return { versionFold: versionFold2, releaseSha: releaseSha2 };
|
|
19921
21324
|
});
|
|
19922
|
-
const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, "rc", preFold, tagProbe, branchHints);
|
|
21325
|
+
const { checks, releaseUrl, announceNote, dispatch: d, ledgerAnchors } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, "rc", preFold, tagProbe, branchHints, ledger);
|
|
19923
21326
|
const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
|
|
19924
21327
|
const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
19925
21328
|
const rcAlignment = await alignRcForward(deps, ctx, tag);
|
|
21329
|
+
await recordPhase(ledger, deps, ledgerAnchors, "alignment", phaseEntry(alignmentPhaseEntry(devRollForward, rcAlignment)), { landed: "the immutable tag, the green wall, origin/main, the verified Release, and the deploy/publish path" });
|
|
21330
|
+
const ledgerReport = releaseLedgerReport(await ledger?.load(ledgerAnchors).catch(() => void 0), ledger?.path ?? "(no ledger)");
|
|
19926
21331
|
const environments = await buildEnvironments(deps, ctx, deployModel, d.deployStatus, retirement);
|
|
19927
21332
|
return {
|
|
19928
21333
|
...ctx,
|
|
@@ -19947,7 +21352,8 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
19947
21352
|
devRollForward,
|
|
19948
21353
|
announceNote,
|
|
19949
21354
|
release: { tag, url: releaseUrl, targetSha: releaseSha },
|
|
19950
|
-
environments
|
|
21355
|
+
environments,
|
|
21356
|
+
...ledgerReport ? { ledger: ledgerReport } : {}
|
|
19951
21357
|
};
|
|
19952
21358
|
}
|
|
19953
21359
|
async function latestLocalReleaseTag(deps) {
|
|
@@ -21015,20 +22421,20 @@ async function applyCiReconcileRepo(repo, deps) {
|
|
|
21015
22421
|
}
|
|
21016
22422
|
|
|
21017
22423
|
// src/claude-binary-doctor.ts
|
|
21018
|
-
var
|
|
22424
|
+
var import_node_fs22 = require("node:fs");
|
|
21019
22425
|
var import_node_os11 = require("node:os");
|
|
21020
|
-
var
|
|
22426
|
+
var import_node_path21 = require("node:path");
|
|
21021
22427
|
|
|
21022
22428
|
// src/jerv-cli-spawn.ts
|
|
21023
|
-
var
|
|
22429
|
+
var import_node_fs21 = require("node:fs");
|
|
21024
22430
|
var import_node_os10 = require("node:os");
|
|
21025
|
-
var
|
|
22431
|
+
var import_node_path20 = require("node:path");
|
|
21026
22432
|
var WIN_NAMES = ["jerv-cli.cmd", "jerv-cli.exe", "jerv-cli"];
|
|
21027
22433
|
var POSIX_NAMES = ["jerv-cli"];
|
|
21028
|
-
var JERV_CLI_ENTRY = (0,
|
|
22434
|
+
var JERV_CLI_ENTRY = (0, import_node_path20.join)("node_modules", "@jervaise", "jerv-cli", "dist", "index.cjs");
|
|
21029
22435
|
function pathEnvEntries(pathEnv, platform2 = process.platform) {
|
|
21030
22436
|
if (platform2 !== "win32") {
|
|
21031
|
-
return pathEnv.split(
|
|
22437
|
+
return pathEnv.split(import_node_path20.delimiter).map((e) => e.trim()).filter(Boolean);
|
|
21032
22438
|
}
|
|
21033
22439
|
if (pathEnv.includes(";")) {
|
|
21034
22440
|
return pathEnv.split(";").map((e) => e.trim()).filter(Boolean);
|
|
@@ -21061,10 +22467,10 @@ function jervCliCandidateDirs(env = process.env, home = (0, import_node_os10.hom
|
|
|
21061
22467
|
push(normalizeSpawnPathEntry(entry, platform2));
|
|
21062
22468
|
}
|
|
21063
22469
|
if (platform2 === "win32") {
|
|
21064
|
-
if (env.APPDATA) push((0,
|
|
21065
|
-
if (env.LOCALAPPDATA) push((0,
|
|
22470
|
+
if (env.APPDATA) push((0, import_node_path20.join)(env.APPDATA, "npm"));
|
|
22471
|
+
if (env.LOCALAPPDATA) push((0, import_node_path20.join)(env.LOCALAPPDATA, "npm"));
|
|
21066
22472
|
} else {
|
|
21067
|
-
push((0,
|
|
22473
|
+
push((0, import_node_path20.join)(home, ".local", "bin"));
|
|
21068
22474
|
}
|
|
21069
22475
|
return out;
|
|
21070
22476
|
}
|
|
@@ -21072,23 +22478,23 @@ function jervCliCandidatePaths(env = process.env, home = (0, import_node_os10.ho
|
|
|
21072
22478
|
const names = platform2 === "win32" ? WIN_NAMES : POSIX_NAMES;
|
|
21073
22479
|
const out = [];
|
|
21074
22480
|
for (const dir of jervCliCandidateDirs(env, home, platform2)) {
|
|
21075
|
-
for (const name of names) out.push((0,
|
|
22481
|
+
for (const name of names) out.push((0, import_node_path20.join)(dir, name));
|
|
21076
22482
|
}
|
|
21077
22483
|
return out;
|
|
21078
22484
|
}
|
|
21079
|
-
function resolveJervCliPath(env = process.env, home = (0, import_node_os10.homedir)(), platform2 = process.platform, exists =
|
|
22485
|
+
function resolveJervCliPath(env = process.env, home = (0, import_node_os10.homedir)(), platform2 = process.platform, exists = import_node_fs21.existsSync) {
|
|
21080
22486
|
for (const candidate of jervCliCandidatePaths(env, home, platform2)) {
|
|
21081
22487
|
if (exists(candidate)) return candidate;
|
|
21082
22488
|
}
|
|
21083
22489
|
return void 0;
|
|
21084
22490
|
}
|
|
21085
|
-
function resolveJervCliNodeEntry(shimPath, exists =
|
|
21086
|
-
const entry = (0,
|
|
22491
|
+
function resolveJervCliNodeEntry(shimPath, exists = import_node_fs21.existsSync) {
|
|
22492
|
+
const entry = (0, import_node_path20.join)((0, import_node_path20.dirname)(shimPath), JERV_CLI_ENTRY);
|
|
21087
22493
|
return exists(entry) ? entry : void 0;
|
|
21088
22494
|
}
|
|
21089
22495
|
function jervCliExecFileArgs(args, opts = {}) {
|
|
21090
22496
|
const platform2 = opts.platform ?? process.platform;
|
|
21091
|
-
const exists = opts.exists ??
|
|
22497
|
+
const exists = opts.exists ?? import_node_fs21.existsSync;
|
|
21092
22498
|
const resolved = resolveJervCliPath(opts.env ?? process.env, opts.home ?? (0, import_node_os10.homedir)(), platform2, exists);
|
|
21093
22499
|
if (resolved) {
|
|
21094
22500
|
const entry = resolveJervCliNodeEntry(resolved, exists);
|
|
@@ -21178,26 +22584,26 @@ function globalNodeModulesRoots(host) {
|
|
|
21178
22584
|
out.push(dir);
|
|
21179
22585
|
};
|
|
21180
22586
|
const prefix = env.npm_config_prefix?.trim();
|
|
21181
|
-
if (prefix) push(platform2 === "win32" ? (0,
|
|
22587
|
+
if (prefix) push(platform2 === "win32" ? (0, import_node_path21.join)(prefix, "node_modules") : (0, import_node_path21.join)(prefix, "lib", "node_modules"));
|
|
21182
22588
|
for (const dir of jervCliCandidateDirs(env, host.home ?? (0, import_node_os11.homedir)(), platform2)) {
|
|
21183
|
-
push((0,
|
|
21184
|
-
push((0,
|
|
22589
|
+
push((0, import_node_path21.join)(dir, "node_modules"));
|
|
22590
|
+
push((0, import_node_path21.join)((0, import_node_path21.dirname)(dir), "lib", "node_modules"));
|
|
21185
22591
|
}
|
|
21186
22592
|
return out;
|
|
21187
22593
|
}
|
|
21188
22594
|
function readHead(path2) {
|
|
21189
22595
|
let fd;
|
|
21190
22596
|
try {
|
|
21191
|
-
fd = (0,
|
|
22597
|
+
fd = (0, import_node_fs22.openSync)(path2, "r");
|
|
21192
22598
|
const buffer = new Uint8Array(MAGIC_HEAD_BYTES);
|
|
21193
|
-
const read = (0,
|
|
22599
|
+
const read = (0, import_node_fs22.readSync)(fd, buffer, 0, MAGIC_HEAD_BYTES, 0);
|
|
21194
22600
|
return buffer.subarray(0, read);
|
|
21195
22601
|
} catch {
|
|
21196
22602
|
return void 0;
|
|
21197
22603
|
} finally {
|
|
21198
22604
|
if (fd !== void 0) {
|
|
21199
22605
|
try {
|
|
21200
|
-
(0,
|
|
22606
|
+
(0, import_node_fs22.closeSync)(fd);
|
|
21201
22607
|
} catch {
|
|
21202
22608
|
}
|
|
21203
22609
|
}
|
|
@@ -21205,7 +22611,7 @@ function readHead(path2) {
|
|
|
21205
22611
|
}
|
|
21206
22612
|
function fileBytes(path2) {
|
|
21207
22613
|
try {
|
|
21208
|
-
return (0,
|
|
22614
|
+
return (0, import_node_fs22.statSync)(path2).size;
|
|
21209
22615
|
} catch {
|
|
21210
22616
|
return void 0;
|
|
21211
22617
|
}
|
|
@@ -21219,17 +22625,17 @@ function readClaudeBinaryState(host = {}) {
|
|
|
21219
22625
|
const arch = host.arch ?? process.arch;
|
|
21220
22626
|
const magic = EXECUTABLE_MAGIC[platform2];
|
|
21221
22627
|
if (!magic) return void 0;
|
|
21222
|
-
const packageRoot = globalNodeModulesRoots(host).map((root) => (0,
|
|
22628
|
+
const packageRoot = globalNodeModulesRoots(host).map((root) => (0, import_node_path21.join)(root, ...PACKAGE.split("/"))).find((dir) => (0, import_node_fs22.existsSync)((0, import_node_path21.join)(dir, "package.json")));
|
|
21223
22629
|
if (!packageRoot) return void 0;
|
|
21224
22630
|
const keys = platformPackageKeys(platform2, arch);
|
|
21225
22631
|
const fallbackPackage = `${PACKAGE}-${keys[0]}`;
|
|
21226
22632
|
let manifest;
|
|
21227
22633
|
try {
|
|
21228
|
-
manifest = JSON.parse((0,
|
|
22634
|
+
manifest = JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path21.join)(packageRoot, "package.json"), "utf8"));
|
|
21229
22635
|
} catch (e) {
|
|
21230
22636
|
return {
|
|
21231
22637
|
state: "unreadable",
|
|
21232
|
-
binPath: (0,
|
|
22638
|
+
binPath: (0, import_node_path21.join)(packageRoot, "package.json"),
|
|
21233
22639
|
expectedMagic: magic.name,
|
|
21234
22640
|
platformPackage: fallbackPackage,
|
|
21235
22641
|
error: `package.json could not be read \u2014 ${e.message}`
|
|
@@ -21246,17 +22652,17 @@ function readClaudeBinaryState(host = {}) {
|
|
|
21246
22652
|
error: "package.json declares no `bin` entry \u2014 the executed file cannot be resolved"
|
|
21247
22653
|
};
|
|
21248
22654
|
}
|
|
21249
|
-
const binPath = (0,
|
|
21250
|
-
const binName = (0,
|
|
22655
|
+
const binPath = (0, import_node_path21.join)(packageRoot, binRelative);
|
|
22656
|
+
const binName = (0, import_node_path21.basename)(binRelative);
|
|
21251
22657
|
const optional = manifest.optionalDependencies;
|
|
21252
22658
|
const publishes = (name) => typeof optional === "object" && optional !== null && Object.prototype.hasOwnProperty.call(optional, name);
|
|
21253
22659
|
const published = keys.map((key) => `${PACKAGE}-${key}`).filter(publishes);
|
|
21254
22660
|
if (published.length === 0) return void 0;
|
|
21255
22661
|
const binIn = (name) => [
|
|
21256
|
-
(0,
|
|
21257
|
-
(0,
|
|
22662
|
+
(0, import_node_path21.join)(packageRoot, "node_modules", ...name.split("/"), binName),
|
|
22663
|
+
(0, import_node_path21.join)((0, import_node_path21.dirname)((0, import_node_path21.dirname)(packageRoot)), ...name.split("/"), binName)
|
|
21258
22664
|
];
|
|
21259
|
-
const found = published.map((name) => ({ name, path: binIn(name).find((file) => (0,
|
|
22665
|
+
const found = published.map((name) => ({ name, path: binIn(name).find((file) => (0, import_node_fs22.existsSync)(file)) })).find((c) => c.path);
|
|
21260
22666
|
const platformPackage = found?.name ?? published[0];
|
|
21261
22667
|
let source;
|
|
21262
22668
|
let sourceProblem;
|
|
@@ -21267,7 +22673,7 @@ function readClaudeBinaryState(host = {}) {
|
|
|
21267
22673
|
} else {
|
|
21268
22674
|
source = { path: found.path, bytes: fileBytes(found.path) ?? 0 };
|
|
21269
22675
|
}
|
|
21270
|
-
if (!(0,
|
|
22676
|
+
if (!(0, import_node_fs22.existsSync)(binPath)) {
|
|
21271
22677
|
return { state: "missing", binPath, expectedMagic: magic.name, platformPackage, ...source ? { source } : {}, ...sourceProblem ? { sourceProblem } : {} };
|
|
21272
22678
|
}
|
|
21273
22679
|
const head = readHead(binPath);
|
|
@@ -21310,9 +22716,9 @@ function healClaudeBinary(host = {}, onStep) {
|
|
|
21310
22716
|
const platform2 = host.platform ?? process.platform;
|
|
21311
22717
|
const aside = `${probe.binPath}.stub-${Date.now()}`;
|
|
21312
22718
|
let renamed = false;
|
|
21313
|
-
if ((0,
|
|
22719
|
+
if ((0, import_node_fs22.existsSync)(probe.binPath)) {
|
|
21314
22720
|
try {
|
|
21315
|
-
(0,
|
|
22721
|
+
(0, import_node_fs22.renameSync)(probe.binPath, aside);
|
|
21316
22722
|
renamed = true;
|
|
21317
22723
|
onStep?.(`renamed the stub aside: ${aside}`);
|
|
21318
22724
|
} catch (e) {
|
|
@@ -21321,12 +22727,12 @@ function healClaudeBinary(host = {}, onStep) {
|
|
|
21321
22727
|
}
|
|
21322
22728
|
try {
|
|
21323
22729
|
onStep?.(`copying ${probe.source.path} \u2192 ${probe.binPath} (${(probe.source.bytes / 1e6).toFixed(0)} MB)`);
|
|
21324
|
-
(0,
|
|
21325
|
-
if (platform2 !== "win32") (0,
|
|
22730
|
+
(0, import_node_fs22.copyFileSync)(probe.source.path, probe.binPath);
|
|
22731
|
+
if (platform2 !== "win32") (0, import_node_fs22.chmodSync)(probe.binPath, 493);
|
|
21326
22732
|
} catch (e) {
|
|
21327
22733
|
if (renamed) {
|
|
21328
22734
|
try {
|
|
21329
|
-
(0,
|
|
22735
|
+
(0, import_node_fs22.renameSync)(aside, probe.binPath);
|
|
21330
22736
|
} catch {
|
|
21331
22737
|
return { ok: false, detail: `copy failed (${e.message}) and the stub could not be restored \u2014 the original is at ${aside}` };
|
|
21332
22738
|
}
|
|
@@ -21340,7 +22746,7 @@ function healClaudeBinary(host = {}, onStep) {
|
|
|
21340
22746
|
let kept = false;
|
|
21341
22747
|
if (renamed) {
|
|
21342
22748
|
try {
|
|
21343
|
-
(0,
|
|
22749
|
+
(0, import_node_fs22.rmSync)(aside);
|
|
21344
22750
|
} catch {
|
|
21345
22751
|
kept = true;
|
|
21346
22752
|
}
|
|
@@ -22192,7 +23598,7 @@ function renderPluginCachePlan(plan, applied) {
|
|
|
22192
23598
|
}
|
|
22193
23599
|
|
|
22194
23600
|
// src/app-actor.ts
|
|
22195
|
-
var
|
|
23601
|
+
var import_node_crypto6 = require("node:crypto");
|
|
22196
23602
|
var APP_ACTOR_ENV = "MMI_ACTOR";
|
|
22197
23603
|
var APP_VAULT_REPO = "mutmutco/MMI-Hub";
|
|
22198
23604
|
var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
|
|
@@ -22236,7 +23642,7 @@ function mintAppJwt(appId, privateKeyPem, nowSec) {
|
|
|
22236
23642
|
exp: now + APP_JWT_TTL_S,
|
|
22237
23643
|
iss: appId
|
|
22238
23644
|
}));
|
|
22239
|
-
const signer = (0,
|
|
23645
|
+
const signer = (0, import_node_crypto6.createSign)("RSA-SHA256");
|
|
22240
23646
|
signer.update(`${header}.${payload}`);
|
|
22241
23647
|
return `${header}.${payload}.${signer.sign(privateKeyPem, "base64url")}`;
|
|
22242
23648
|
}
|
|
@@ -22412,19 +23818,23 @@ function registerBoardCommands(program3) {
|
|
|
22412
23818
|
if (result.checked) {
|
|
22413
23819
|
if (result.outcome === "held") return `Check ${ref}: held by ${holder} - claim would renew the lease (nothing written)`;
|
|
22414
23820
|
if (result.outcome === "took-over") return `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
|
|
23821
|
+
if (result.outcome === "resumed") return `Check ${ref}: prior lane ${previousHolder} is verifiably dead - claim would resume its work for ${holder} (nothing written; ${result.resumeEvidence})`;
|
|
22415
23822
|
return `Check ${ref}: free - claim would proceed for ${holder} (nothing written)`;
|
|
22416
23823
|
}
|
|
22417
23824
|
if (result.partial) {
|
|
22418
|
-
|
|
23825
|
+
if (result.outcome === "took-over") return `Partially took over ${ref} from ${previousHolder}: ${result.warning}`;
|
|
23826
|
+
if (result.outcome === "resumed") return `Partially resumed ${ref} from ${previousHolder}: ${result.warning}`;
|
|
23827
|
+
return `Partially claimed ${ref} for ${holder}: ${result.warning}`;
|
|
22419
23828
|
}
|
|
22420
23829
|
if (result.outcome === "took-over") return `Took over ${ref} from ${previousHolder} for ${holder} - In Progress`;
|
|
23830
|
+
if (result.outcome === "resumed") return `Resumed ${ref} from ${previousHolder} for ${holder} - In Progress (${result.resumeEvidence})`;
|
|
22421
23831
|
if (result.outcome === "held") return `${ref} is held by ${holder} - In Progress`;
|
|
22422
23832
|
return `Claimed ${ref} for ${holder} - In Progress`;
|
|
22423
23833
|
}
|
|
22424
23834
|
const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
|
|
22425
23835
|
board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nNever capture the JSON with a shell redirect on Windows: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
|
|
22426
23836
|
withExamples(mutating(
|
|
22427
|
-
board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs)").addHelpText("after", "\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
|
|
23837
|
+
board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs)").addHelpText("after", "\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
|
|
22428
23838
|
(_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
|
|
22429
23839
|
).action(async (issueRefs, o) => {
|
|
22430
23840
|
if (issueRefs.length === 1) {
|
|
@@ -22637,9 +24047,9 @@ function registerBoardCommands(program3) {
|
|
|
22637
24047
|
}
|
|
22638
24048
|
|
|
22639
24049
|
// src/bootstrap-commands.ts
|
|
22640
|
-
var
|
|
24050
|
+
var import_node_fs24 = require("node:fs");
|
|
22641
24051
|
var import_node_os12 = require("node:os");
|
|
22642
|
-
var
|
|
24052
|
+
var import_node_path23 = require("node:path");
|
|
22643
24053
|
|
|
22644
24054
|
// src/command-plans.ts
|
|
22645
24055
|
function parseTrainTag(tag) {
|
|
@@ -22799,8 +24209,8 @@ function renderSteps(title, steps) {
|
|
|
22799
24209
|
}
|
|
22800
24210
|
|
|
22801
24211
|
// src/port-registry.ts
|
|
22802
|
-
var
|
|
22803
|
-
var
|
|
24212
|
+
var import_node_fs23 = require("node:fs");
|
|
24213
|
+
var import_node_path22 = require("node:path");
|
|
22804
24214
|
|
|
22805
24215
|
// ../infra/port-geometry.mjs
|
|
22806
24216
|
var PORT_BLOCK = 100;
|
|
@@ -22814,8 +24224,8 @@ function nextPortBlock(registry2) {
|
|
|
22814
24224
|
return [base, base + PORT_SPAN];
|
|
22815
24225
|
}
|
|
22816
24226
|
function loadPortRegistry(path2) {
|
|
22817
|
-
if (!(0,
|
|
22818
|
-
const raw = JSON.parse((0,
|
|
24227
|
+
if (!(0, import_node_fs23.existsSync)(path2)) return {};
|
|
24228
|
+
const raw = JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8"));
|
|
22819
24229
|
const out = {};
|
|
22820
24230
|
for (const [key, value] of Object.entries(raw)) {
|
|
22821
24231
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -22829,9 +24239,9 @@ function ensurePortRange(repo, path2) {
|
|
|
22829
24239
|
const existing = registry2[repo];
|
|
22830
24240
|
if (existing) return existing;
|
|
22831
24241
|
const range = nextPortBlock(registry2);
|
|
22832
|
-
const raw = (0,
|
|
24242
|
+
const raw = (0, import_node_fs23.existsSync)(path2) ? JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8")) : {};
|
|
22833
24243
|
raw[repo] = range;
|
|
22834
|
-
(0,
|
|
24244
|
+
(0, import_node_fs23.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
22835
24245
|
return range;
|
|
22836
24246
|
}
|
|
22837
24247
|
function portCursorSeed(registry2) {
|
|
@@ -22853,22 +24263,22 @@ function existingPortRange(repo, registry2) {
|
|
|
22853
24263
|
return registry2[repo] ?? null;
|
|
22854
24264
|
}
|
|
22855
24265
|
function portRangeInfraAt(root, source) {
|
|
22856
|
-
const registryPath = (0,
|
|
22857
|
-
const ddbScriptPath = (0,
|
|
22858
|
-
if (!(0,
|
|
24266
|
+
const registryPath = (0, import_node_path22.join)(root, "infra", "port-ranges.json");
|
|
24267
|
+
const ddbScriptPath = (0, import_node_path22.join)(root, "infra", "port-ddb.mjs");
|
|
24268
|
+
if (!(0, import_node_fs23.existsSync)(registryPath) || !(0, import_node_fs23.existsSync)(ddbScriptPath)) return null;
|
|
22859
24269
|
return { root, source, registryPath, ddbScriptPath };
|
|
22860
24270
|
}
|
|
22861
24271
|
function resolvePortRangeInfra(cwd, packageDir) {
|
|
22862
24272
|
const direct = portRangeInfraAt(cwd, "cwd");
|
|
22863
24273
|
if (direct) return direct;
|
|
22864
|
-
for (let dir = cwd; ; dir = (0,
|
|
22865
|
-
const sibling = portRangeInfraAt((0,
|
|
24274
|
+
for (let dir = cwd; ; dir = (0, import_node_path22.dirname)(dir)) {
|
|
24275
|
+
const sibling = portRangeInfraAt((0, import_node_path22.join)(dir, "MMI-Hub"), "sibling-hub");
|
|
22866
24276
|
if (sibling) return sibling;
|
|
22867
|
-
const parent = (0,
|
|
24277
|
+
const parent = (0, import_node_path22.dirname)(dir);
|
|
22868
24278
|
if (parent === dir) break;
|
|
22869
24279
|
}
|
|
22870
24280
|
if (packageDir) {
|
|
22871
|
-
const pkgRoot = (0,
|
|
24281
|
+
const pkgRoot = (0, import_node_path22.join)(packageDir, "..", "..");
|
|
22872
24282
|
const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
|
|
22873
24283
|
if (pkgFrom) return pkgFrom;
|
|
22874
24284
|
}
|
|
@@ -22930,7 +24340,7 @@ async function assignPersistedPortRange(repo, slug, reg, opts) {
|
|
|
22930
24340
|
}
|
|
22931
24341
|
|
|
22932
24342
|
// src/bootstrap-drift.ts
|
|
22933
|
-
var
|
|
24343
|
+
var import_node_crypto7 = require("node:crypto");
|
|
22934
24344
|
function byteComparableSeeds(manifest, cls) {
|
|
22935
24345
|
return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
|
|
22936
24346
|
}
|
|
@@ -22950,7 +24360,7 @@ function compareSeedBytes(hubContent, repoContent) {
|
|
|
22950
24360
|
return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
|
|
22951
24361
|
}
|
|
22952
24362
|
function seedContentHash(content) {
|
|
22953
|
-
return (0,
|
|
24363
|
+
return (0, import_node_crypto7.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
|
|
22954
24364
|
}
|
|
22955
24365
|
function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
|
|
22956
24366
|
const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
|
|
@@ -23112,7 +24522,7 @@ function renderPropagationReport(plan) {
|
|
|
23112
24522
|
}
|
|
23113
24523
|
|
|
23114
24524
|
// src/bootstrap-propagation-identity.ts
|
|
23115
|
-
var
|
|
24525
|
+
var import_node_crypto8 = require("node:crypto");
|
|
23116
24526
|
var PROPAGATION_BRANCH_PREFIX = "seed-propagate-";
|
|
23117
24527
|
var TARGET_MARKER_NAME = "mmi-bootstrap-propagation-target";
|
|
23118
24528
|
function safeBranchPart(value, maxLength, fallback) {
|
|
@@ -23125,7 +24535,7 @@ function repoSlug2(repo) {
|
|
|
23125
24535
|
function propagationBranch(repo, target) {
|
|
23126
24536
|
const repoPart = safeBranchPart(repoSlug2(repo), 32, "repo");
|
|
23127
24537
|
const targetPart = safeBranchPart(target, 48, "target");
|
|
23128
|
-
const hash = (0,
|
|
24538
|
+
const hash = (0, import_node_crypto8.createHash)("sha256").update(repo.trim().toLowerCase()).update("\0").update(target).digest("hex").slice(0, 12);
|
|
23129
24539
|
return `${PROPAGATION_BRANCH_PREFIX}${repoPart}-${targetPart}-${hash}`;
|
|
23130
24540
|
}
|
|
23131
24541
|
function legacyPropagationBranch(repo) {
|
|
@@ -23932,13 +25342,13 @@ function registerBootstrapCommands(program3) {
|
|
|
23932
25342
|
client: defaultGitHubClient(),
|
|
23933
25343
|
projectMeta: meta,
|
|
23934
25344
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
23935
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
25345
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs24.existsSync)(path2) ? (0, import_node_fs24.readFileSync)(path2, "utf8") : null,
|
|
23936
25346
|
// requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
|
|
23937
25347
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
23938
25348
|
// #3689: the same committed map the org access audit reads (#3664), so a sanctioned admin is not a
|
|
23939
25349
|
// permanent bootstrap failure on one surface and an intended state on the other. Absent file → no
|
|
23940
25350
|
// sanction, which is the pre-#3664 behaviour.
|
|
23941
|
-
sanctionedAdmins: (0,
|
|
25351
|
+
sanctionedAdmins: (0, import_node_fs24.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs24.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
|
|
23942
25352
|
requiredGcpApis: (() => {
|
|
23943
25353
|
const v = meta?.requiredGcpApis;
|
|
23944
25354
|
if (Array.isArray(v)) return v;
|
|
@@ -23991,14 +25401,14 @@ function registerBootstrapCommands(program3) {
|
|
|
23991
25401
|
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 () => {
|
|
23992
25402
|
const o = { repo: rawValue("--repo", ""), json: rawFlag("--json") };
|
|
23993
25403
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
23994
|
-
if (!(0,
|
|
25404
|
+
if (!(0, import_node_fs24.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`);
|
|
23995
25405
|
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
23996
25406
|
if (!seedSource.ok) return fail(`bootstrap drift: ${seedSource.reason}`);
|
|
23997
|
-
const manifest = loadBootstrapSeeds((0,
|
|
25407
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs24.readFileSync)(manifestPath, "utf8"));
|
|
23998
25408
|
const hubContents = /* @__PURE__ */ new Map();
|
|
23999
25409
|
for (const s of manifest.seeds) {
|
|
24000
25410
|
if (s.ownership !== "org" || s.source !== "self") continue;
|
|
24001
|
-
hubContents.set(s.target, (0,
|
|
25411
|
+
hubContents.set(s.target, (0, import_node_fs24.existsSync)(s.target) ? (0, import_node_fs24.readFileSync)(s.target, "utf8") : null);
|
|
24002
25412
|
}
|
|
24003
25413
|
let targets;
|
|
24004
25414
|
let classOf = (_repo) => "deployable";
|
|
@@ -24116,10 +25526,10 @@ function registerBootstrapCommands(program3) {
|
|
|
24116
25526
|
return;
|
|
24117
25527
|
}
|
|
24118
25528
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
24119
|
-
if (!(0,
|
|
25529
|
+
if (!(0, import_node_fs24.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`);
|
|
24120
25530
|
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
24121
25531
|
if (!seedSource.ok) return fail(`bootstrap apply: ${seedSource.reason}`);
|
|
24122
|
-
const manifest = loadBootstrapSeeds((0,
|
|
25532
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs24.readFileSync)(manifestPath, "utf8"));
|
|
24123
25533
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
24124
25534
|
const slug = parsedRepo.slug;
|
|
24125
25535
|
const onlyTarget = o.only.trim();
|
|
@@ -24131,16 +25541,16 @@ function registerBootstrapCommands(program3) {
|
|
|
24131
25541
|
}
|
|
24132
25542
|
const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
|
|
24133
25543
|
const gh = async (args) => execFileP("gh", args, { timeout: 2e4 });
|
|
24134
|
-
const readFile8 = (p) => (0,
|
|
25544
|
+
const readFile8 = (p) => (0, import_node_fs24.existsSync)(p) ? (0, import_node_fs24.readFileSync)(p, "utf8") : null;
|
|
24135
25545
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
24136
25546
|
const putSeed = async (target, content, ref, sha) => {
|
|
24137
|
-
const tmp = (0,
|
|
24138
|
-
(0,
|
|
25547
|
+
const tmp = (0, import_node_path23.join)((0, import_node_os12.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
25548
|
+
(0, import_node_fs24.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
|
|
24139
25549
|
try {
|
|
24140
25550
|
await gh(contentPutInputArgs(repo, target, tmp));
|
|
24141
25551
|
} finally {
|
|
24142
25552
|
try {
|
|
24143
|
-
(0,
|
|
25553
|
+
(0, import_node_fs24.unlinkSync)(tmp);
|
|
24144
25554
|
} catch {
|
|
24145
25555
|
}
|
|
24146
25556
|
}
|
|
@@ -24455,10 +25865,10 @@ LIVE apply to ${repo}:
|
|
|
24455
25865
|
bootstrap.command("propagate").description("#4238: re-entrant canary\u2192wave tick \u2014 plan (or, with --execute, open) per-repo PRs fanning an org-owned seed out to the fleet").option("--target <path>", "the manifest target to propagate (an org-owned whole file or declared Hub-managed block)").option("--execute", "LIVE tick via gh (master-gated) \u2014 opens/reuses per-repo seed-propagate PRs; dry-run prints the plan only").option("--json", "machine-readable output").action(async () => {
|
|
24456
25866
|
const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
|
|
24457
25867
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
24458
|
-
if (!(0,
|
|
25868
|
+
if (!(0, import_node_fs24.existsSync)(manifestPath)) return fail(`bootstrap propagate: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the desired state this tick propagates`);
|
|
24459
25869
|
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
24460
25870
|
if (!seedSource.ok) return fail(`bootstrap propagate: ${seedSource.reason}`);
|
|
24461
|
-
const manifest = loadBootstrapSeeds((0,
|
|
25871
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs24.readFileSync)(manifestPath, "utf8"));
|
|
24462
25872
|
const propagatable = manifest.seeds.filter(isPropagatableSeed);
|
|
24463
25873
|
if (!o.target) {
|
|
24464
25874
|
return fail(`bootstrap propagate: --target <path> is required \u2014 one of:
|
|
@@ -24467,9 +25877,9 @@ LIVE apply to ${repo}:
|
|
|
24467
25877
|
const seed = propagatable.find((s) => s.target === o.target);
|
|
24468
25878
|
if (!seed) return fail(`bootstrap propagate: --target '${o.target}' names no centrally propagatable seed in ${manifestPath}. Propagatable targets:
|
|
24469
25879
|
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
24470
|
-
if (!seed.managedBlock && !(0,
|
|
24471
|
-
const hubContent = seed.managedBlock ? null : (0,
|
|
24472
|
-
const readSeedFile = (path2) => (0,
|
|
25880
|
+
if (!seed.managedBlock && !(0, import_node_fs24.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
|
|
25881
|
+
const hubContent = seed.managedBlock ? null : (0, import_node_fs24.readFileSync)(seed.target, "utf8");
|
|
25882
|
+
const readSeedFile = (path2) => (0, import_node_fs24.existsSync)(path2) ? (0, import_node_fs24.readFileSync)(path2, "utf8") : null;
|
|
24473
25883
|
const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
|
|
24474
25884
|
const cfg = await loadConfig();
|
|
24475
25885
|
const projects = await fetchProjectsList(registryClientDeps(cfg));
|
|
@@ -24478,9 +25888,9 @@ LIVE apply to ${repo}:
|
|
|
24478
25888
|
}
|
|
24479
25889
|
const rosterRepos = collectRegistryRepos(projects).filter((r) => r.toLowerCase() !== "mutmutco/mmi-hub");
|
|
24480
25890
|
let independentCount = rosterRepos.length;
|
|
24481
|
-
if ((0,
|
|
25891
|
+
if ((0, import_node_fs24.existsSync)("projects.json")) {
|
|
24482
25892
|
try {
|
|
24483
|
-
const local = JSON.parse((0,
|
|
25893
|
+
const local = JSON.parse((0, import_node_fs24.readFileSync)("projects.json", "utf8"));
|
|
24484
25894
|
const localRepos = /* @__PURE__ */ new Set();
|
|
24485
25895
|
for (const p of local.projects ?? []) for (const r of p.repos ?? []) {
|
|
24486
25896
|
const full = (r.includes("/") ? r : `mutmutco/${r}`).toLowerCase();
|
|
@@ -24640,15 +26050,15 @@ LIVE apply to ${repo}:
|
|
|
24640
26050
|
} catch {
|
|
24641
26051
|
existingSha = void 0;
|
|
24642
26052
|
}
|
|
24643
|
-
const tmp = (0,
|
|
26053
|
+
const tmp = (0, import_node_path23.join)((0, import_node_os12.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
24644
26054
|
const desiredContent = desiredByRepo.get(rec.repo);
|
|
24645
26055
|
if (desiredContent == null) return fail(`bootstrap propagate: no resolved content for ${rec.repo} ${seed.target} \u2014 refusing to write`);
|
|
24646
|
-
(0,
|
|
26056
|
+
(0, import_node_fs24.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
|
|
24647
26057
|
try {
|
|
24648
26058
|
await gh(contentPutInputArgs(rec.repo, seed.target, tmp));
|
|
24649
26059
|
} finally {
|
|
24650
26060
|
try {
|
|
24651
|
-
(0,
|
|
26061
|
+
(0, import_node_fs24.unlinkSync)(tmp);
|
|
24652
26062
|
} catch {
|
|
24653
26063
|
}
|
|
24654
26064
|
}
|
|
@@ -24715,10 +26125,10 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
|
|
|
24715
26125
|
return fail(`bootstrap rollback: ${e.message}`);
|
|
24716
26126
|
}
|
|
24717
26127
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
24718
|
-
if (!(0,
|
|
26128
|
+
if (!(0, import_node_fs24.existsSync)(manifestPath)) return fail(`bootstrap rollback: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the manifest names which targets are org-owned and therefore propagated (and rollback-able)`);
|
|
24719
26129
|
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
24720
26130
|
if (!seedSource.ok) return fail(`bootstrap rollback: ${seedSource.reason}`);
|
|
24721
|
-
const manifest = loadBootstrapSeeds((0,
|
|
26131
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs24.readFileSync)(manifestPath, "utf8"));
|
|
24722
26132
|
const propagatable = manifest.seeds.filter(isPropagatableSeed);
|
|
24723
26133
|
if (!o.target) {
|
|
24724
26134
|
return fail(`bootstrap rollback: --target <path> is required \u2014 one of:
|
|
@@ -24735,10 +26145,10 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
|
|
|
24735
26145
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
24736
26146
|
let candidates;
|
|
24737
26147
|
if (o.record) {
|
|
24738
|
-
if (!(0,
|
|
26148
|
+
if (!(0, import_node_fs24.existsSync)(o.record)) return fail(`bootstrap rollback: --record '${o.record}' not found`);
|
|
24739
26149
|
let parsed;
|
|
24740
26150
|
try {
|
|
24741
|
-
parsed = JSON.parse((0,
|
|
26151
|
+
parsed = JSON.parse((0, import_node_fs24.readFileSync)(o.record, "utf8"));
|
|
24742
26152
|
} catch (e) {
|
|
24743
26153
|
return fail(`bootstrap rollback: --record '${o.record}' is not valid JSON: ${e.message}`);
|
|
24744
26154
|
}
|
|
@@ -24815,13 +26225,13 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
|
|
|
24815
26225
|
} catch {
|
|
24816
26226
|
existingSha = void 0;
|
|
24817
26227
|
}
|
|
24818
|
-
const tmp = (0,
|
|
24819
|
-
(0,
|
|
26228
|
+
const tmp = (0, import_node_path23.join)((0, import_node_os12.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
26229
|
+
(0, import_node_fs24.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
|
|
24820
26230
|
try {
|
|
24821
26231
|
await gh(contentPutInputArgs(repo, seed.target, tmp));
|
|
24822
26232
|
} finally {
|
|
24823
26233
|
try {
|
|
24824
|
-
(0,
|
|
26234
|
+
(0, import_node_fs24.unlinkSync)(tmp);
|
|
24825
26235
|
} catch {
|
|
24826
26236
|
}
|
|
24827
26237
|
}
|
|
@@ -25814,7 +27224,7 @@ function renderDeployPortDoctor(report) {
|
|
|
25814
27224
|
}
|
|
25815
27225
|
|
|
25816
27226
|
// src/wave-status.ts
|
|
25817
|
-
var
|
|
27227
|
+
var import_node_fs27 = require("node:fs");
|
|
25818
27228
|
|
|
25819
27229
|
// src/git-worktree-observation.ts
|
|
25820
27230
|
function parseGitWorktreePorcelain(text) {
|
|
@@ -25845,8 +27255,8 @@ function deriveComposeProjectName(worktreePath) {
|
|
|
25845
27255
|
|
|
25846
27256
|
// src/stage-runner.ts
|
|
25847
27257
|
var import_node_child_process8 = require("node:child_process");
|
|
25848
|
-
var
|
|
25849
|
-
var
|
|
27258
|
+
var import_node_fs26 = require("node:fs");
|
|
27259
|
+
var import_node_path25 = require("node:path");
|
|
25850
27260
|
var import_node_net = require("node:net");
|
|
25851
27261
|
var import_node_util4 = require("node:util");
|
|
25852
27262
|
|
|
@@ -25856,8 +27266,8 @@ function normalizeEol2(s) {
|
|
|
25856
27266
|
}
|
|
25857
27267
|
|
|
25858
27268
|
// src/env-detection.ts
|
|
25859
|
-
var
|
|
25860
|
-
var
|
|
27269
|
+
var import_node_fs25 = require("node:fs");
|
|
27270
|
+
var import_node_path24 = require("node:path");
|
|
25861
27271
|
var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", ".jerv", ".pi", ".venv", "venv"]);
|
|
25862
27272
|
function isEnvBaseName(name) {
|
|
25863
27273
|
return name.toLowerCase() === ".env";
|
|
@@ -25867,7 +27277,7 @@ function findEnvFiles(root) {
|
|
|
25867
27277
|
const walk2 = (dir, rel) => {
|
|
25868
27278
|
let entries;
|
|
25869
27279
|
try {
|
|
25870
|
-
entries = (0,
|
|
27280
|
+
entries = (0, import_node_fs25.readdirSync)(dir, { withFileTypes: true });
|
|
25871
27281
|
} catch {
|
|
25872
27282
|
return;
|
|
25873
27283
|
}
|
|
@@ -25875,13 +27285,13 @@ function findEnvFiles(root) {
|
|
|
25875
27285
|
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
25876
27286
|
if (entry.isDirectory()) {
|
|
25877
27287
|
if (SKIPPED_DIRS.has(entry.name)) continue;
|
|
25878
|
-
walk2((0,
|
|
27288
|
+
walk2((0, import_node_path24.join)(dir, entry.name), relPath);
|
|
25879
27289
|
continue;
|
|
25880
27290
|
}
|
|
25881
27291
|
if (isEnvBaseName(entry.name)) {
|
|
25882
27292
|
let isDir = false;
|
|
25883
27293
|
try {
|
|
25884
|
-
isDir = (0,
|
|
27294
|
+
isDir = (0, import_node_fs25.lstatSync)((0, import_node_path24.join)(dir, entry.name)).isDirectory();
|
|
25885
27295
|
} catch {
|
|
25886
27296
|
}
|
|
25887
27297
|
if (!isDir) found.push(relPath);
|
|
@@ -25912,7 +27322,7 @@ function healthPollIntervalMs() {
|
|
|
25912
27322
|
return HEALTH_POLL_INTERVAL_MS;
|
|
25913
27323
|
}
|
|
25914
27324
|
function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
|
|
25915
|
-
return new Promise((
|
|
27325
|
+
return new Promise((resolve7, reject) => {
|
|
25916
27326
|
let settled = false;
|
|
25917
27327
|
const finish = (fn) => {
|
|
25918
27328
|
if (settled) return;
|
|
@@ -25922,7 +27332,7 @@ function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
|
|
|
25922
27332
|
child2.removeAllListeners("exit");
|
|
25923
27333
|
fn();
|
|
25924
27334
|
};
|
|
25925
|
-
const timer = setTimeout(() => finish(
|
|
27335
|
+
const timer = setTimeout(() => finish(resolve7), graceMs);
|
|
25926
27336
|
child2.on("error", (err) => finish(() => reject(new Error(`stage process failed to start: ${err.message}`))));
|
|
25927
27337
|
child2.on("exit", (code, signal) => {
|
|
25928
27338
|
const detail = code != null ? `code ${code}` : signal ? `signal ${signal}` : "unknown reason";
|
|
@@ -26022,7 +27432,7 @@ function appendForceRecreate(up) {
|
|
|
26022
27432
|
return `${up.trimEnd()} --force-recreate`;
|
|
26023
27433
|
}
|
|
26024
27434
|
function stageStatePath(cwd = process.cwd()) {
|
|
26025
|
-
return (0,
|
|
27435
|
+
return (0, import_node_path25.join)(cwd, "tmp", "stage", "state.json");
|
|
26026
27436
|
}
|
|
26027
27437
|
function normPath(path2) {
|
|
26028
27438
|
return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
@@ -26128,10 +27538,10 @@ function pickStagePort(range, isFree) {
|
|
|
26128
27538
|
throw new Error(`no free stage port in range ${start}-${end} \u2014 every port is in use`);
|
|
26129
27539
|
}
|
|
26130
27540
|
function isPortFree(port) {
|
|
26131
|
-
return new Promise((
|
|
27541
|
+
return new Promise((resolve7) => {
|
|
26132
27542
|
const srv = (0, import_node_net.createServer)();
|
|
26133
|
-
srv.once("error", () =>
|
|
26134
|
-
srv.once("listening", () => srv.close(() =>
|
|
27543
|
+
srv.once("error", () => resolve7(false));
|
|
27544
|
+
srv.once("listening", () => srv.close(() => resolve7(true)));
|
|
26135
27545
|
srv.listen(port, "127.0.0.1");
|
|
26136
27546
|
});
|
|
26137
27547
|
}
|
|
@@ -26246,14 +27656,14 @@ function stageProcessEnv(stagePort, extraEnv) {
|
|
|
26246
27656
|
}
|
|
26247
27657
|
async function ensureStageRuntimeEnv(config, opts, cwd) {
|
|
26248
27658
|
if (!config.ensureEnv) return;
|
|
26249
|
-
const target = (0,
|
|
26250
|
-
const example = (0,
|
|
26251
|
-
if (!(0,
|
|
26252
|
-
(0,
|
|
26253
|
-
} else if ((0,
|
|
26254
|
-
const stale = detectStaleEnvFile((0,
|
|
26255
|
-
exampleMtimeMs: (0,
|
|
26256
|
-
targetMtimeMs: (0,
|
|
27659
|
+
const target = (0, import_node_path25.join)(cwd, config.ensureEnv.target);
|
|
27660
|
+
const example = (0, import_node_path25.join)(cwd, config.ensureEnv.example);
|
|
27661
|
+
if (!(0, import_node_fs26.existsSync)(target) && (0, import_node_fs26.existsSync)(example)) {
|
|
27662
|
+
(0, import_node_fs26.copyFileSync)(example, target);
|
|
27663
|
+
} else if ((0, import_node_fs26.existsSync)(target) && (0, import_node_fs26.existsSync)(example)) {
|
|
27664
|
+
const stale = detectStaleEnvFile((0, import_node_fs26.readFileSync)(example, "utf8"), (0, import_node_fs26.readFileSync)(target, "utf8"), {
|
|
27665
|
+
exampleMtimeMs: (0, import_node_fs26.statSync)(example).mtimeMs,
|
|
27666
|
+
targetMtimeMs: (0, import_node_fs26.statSync)(target).mtimeMs
|
|
26257
27667
|
});
|
|
26258
27668
|
if (stale) {
|
|
26259
27669
|
const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
|
|
@@ -26261,8 +27671,8 @@ async function ensureStageRuntimeEnv(config, opts, cwd) {
|
|
|
26261
27671
|
console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
|
|
26262
27672
|
}
|
|
26263
27673
|
}
|
|
26264
|
-
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0,
|
|
26265
|
-
(0,
|
|
27674
|
+
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs26.existsSync)(target)) {
|
|
27675
|
+
(0, import_node_fs26.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs26.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
|
|
26266
27676
|
}
|
|
26267
27677
|
}
|
|
26268
27678
|
async function gitText(cwd, args) {
|
|
@@ -26292,20 +27702,20 @@ async function resolveGlobalStatePath(cwd, explicit) {
|
|
|
26292
27702
|
return void 0;
|
|
26293
27703
|
}
|
|
26294
27704
|
function readState(path2) {
|
|
26295
|
-
if (!(0,
|
|
27705
|
+
if (!(0, import_node_fs26.existsSync)(path2)) return null;
|
|
26296
27706
|
try {
|
|
26297
|
-
return JSON.parse((0,
|
|
27707
|
+
return JSON.parse((0, import_node_fs26.readFileSync)(path2, "utf8"));
|
|
26298
27708
|
} catch {
|
|
26299
27709
|
return null;
|
|
26300
27710
|
}
|
|
26301
27711
|
}
|
|
26302
27712
|
function mkdirFor(path2) {
|
|
26303
27713
|
const dir = path2.slice(0, Math.max(path2.lastIndexOf("/"), path2.lastIndexOf("\\")));
|
|
26304
|
-
(0,
|
|
27714
|
+
(0, import_node_fs26.mkdirSync)(dir, { recursive: true });
|
|
26305
27715
|
}
|
|
26306
27716
|
function writeState(path2, state) {
|
|
26307
27717
|
mkdirFor(path2);
|
|
26308
|
-
(0,
|
|
27718
|
+
(0, import_node_fs26.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
|
|
26309
27719
|
}
|
|
26310
27720
|
function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
|
|
26311
27721
|
const reservation = {
|
|
@@ -26325,7 +27735,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
|
|
|
26325
27735
|
await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, Math.max(timeoutMs, 1e4));
|
|
26326
27736
|
}
|
|
26327
27737
|
for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
|
|
26328
|
-
(0,
|
|
27738
|
+
(0, import_node_fs26.rmSync)(path2, { force: true });
|
|
26329
27739
|
}
|
|
26330
27740
|
}
|
|
26331
27741
|
async function killTree(pid) {
|
|
@@ -26342,7 +27752,7 @@ async function killTree(pid) {
|
|
|
26342
27752
|
} catch {
|
|
26343
27753
|
}
|
|
26344
27754
|
}
|
|
26345
|
-
await new Promise((
|
|
27755
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
26346
27756
|
try {
|
|
26347
27757
|
process.kill(-pid, "SIGKILL");
|
|
26348
27758
|
} catch {
|
|
@@ -26363,7 +27773,7 @@ async function waitForHealth(url, timeoutMs, anyStatus = false) {
|
|
|
26363
27773
|
} catch (e) {
|
|
26364
27774
|
last = e.message;
|
|
26365
27775
|
}
|
|
26366
|
-
await new Promise((
|
|
27776
|
+
await new Promise((resolve7) => setTimeout(resolve7, healthPollIntervalMs()));
|
|
26367
27777
|
}
|
|
26368
27778
|
throw new Error(`stage health check timed out for ${url}${last ? ` (${last})` : ""}`);
|
|
26369
27779
|
}
|
|
@@ -26495,8 +27905,8 @@ async function runStage(config = {}, opts = {}) {
|
|
|
26495
27905
|
});
|
|
26496
27906
|
}
|
|
26497
27907
|
} catch (e) {
|
|
26498
|
-
(0,
|
|
26499
|
-
if (globalStatePath && globalStatePath !== statePath) (0,
|
|
27908
|
+
(0, import_node_fs26.rmSync)(statePath, { force: true });
|
|
27909
|
+
if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs26.rmSync)(globalStatePath, { force: true });
|
|
26500
27910
|
throw e;
|
|
26501
27911
|
}
|
|
26502
27912
|
const started = await startStage(config, {
|
|
@@ -26513,9 +27923,9 @@ async function runStage(config = {}, opts = {}) {
|
|
|
26513
27923
|
// src/wave-status.ts
|
|
26514
27924
|
function readStageSummary(worktreePath) {
|
|
26515
27925
|
const statePath = stageStatePath(worktreePath);
|
|
26516
|
-
if (!(0,
|
|
27926
|
+
if (!(0, import_node_fs27.existsSync)(statePath)) return void 0;
|
|
26517
27927
|
try {
|
|
26518
|
-
const state = JSON.parse((0,
|
|
27928
|
+
const state = JSON.parse((0, import_node_fs27.readFileSync)(statePath, "utf8"));
|
|
26519
27929
|
const port = typeof state.port === "number" ? state.port : void 0;
|
|
26520
27930
|
if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
|
|
26521
27931
|
return { port, url: typeof state.url === "string" ? state.url : void 0 };
|
|
@@ -26570,9 +27980,9 @@ async function collectWaveStatus(deps) {
|
|
|
26570
27980
|
}
|
|
26571
27981
|
|
|
26572
27982
|
// src/discovery-commands.ts
|
|
26573
|
-
var
|
|
27983
|
+
var import_node_fs28 = require("node:fs");
|
|
26574
27984
|
var import_node_os13 = require("node:os");
|
|
26575
|
-
var
|
|
27985
|
+
var import_node_path26 = require("node:path");
|
|
26576
27986
|
var GC_GH_TIMEOUT_MS = 2e4;
|
|
26577
27987
|
async function collectStatus() {
|
|
26578
27988
|
const repo = await resolveRepo();
|
|
@@ -26762,8 +28172,8 @@ async function collectOnboardStatus(opts = {}) {
|
|
|
26762
28172
|
}
|
|
26763
28173
|
const home = (0, import_node_os13.homedir)();
|
|
26764
28174
|
const plugin = onboardPluginGate({
|
|
26765
|
-
readKnown: () => readFileSyncSafe((0,
|
|
26766
|
-
readSettings: () => readFileSyncSafe((0,
|
|
28175
|
+
readKnown: () => readFileSyncSafe((0, import_node_path26.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs28.readFileSync),
|
|
28176
|
+
readSettings: () => readFileSyncSafe((0, import_node_path26.join)(home, ".claude", "settings.json"), import_node_fs28.readFileSync)
|
|
26767
28177
|
});
|
|
26768
28178
|
return { track, board, registry: registry2, secrets, plugin, doors: opts.doors ?? [], nextCommand };
|
|
26769
28179
|
}
|
|
@@ -28794,9 +30204,9 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
28794
30204
|
}
|
|
28795
30205
|
|
|
28796
30206
|
// src/doctor-io.ts
|
|
28797
|
-
var
|
|
30207
|
+
var import_node_fs29 = require("node:fs");
|
|
28798
30208
|
var import_node_os14 = require("node:os");
|
|
28799
|
-
var
|
|
30209
|
+
var import_node_path27 = require("node:path");
|
|
28800
30210
|
var import_node_child_process9 = require("node:child_process");
|
|
28801
30211
|
var import_node_util5 = require("node:util");
|
|
28802
30212
|
|
|
@@ -28809,8 +30219,8 @@ function nodeDiscardSinkPath(platform2 = process.platform) {
|
|
|
28809
30219
|
var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process9.execFile);
|
|
28810
30220
|
function execFileCapture(file, args, opts = {}) {
|
|
28811
30221
|
const sink = nodeDiscardSinkPath();
|
|
28812
|
-
const inFd = (0,
|
|
28813
|
-
const errFd = (0,
|
|
30222
|
+
const inFd = (0, import_node_fs29.openSync)(sink, "r");
|
|
30223
|
+
const errFd = (0, import_node_fs29.openSync)(sink, "w");
|
|
28814
30224
|
try {
|
|
28815
30225
|
return (0, import_node_child_process9.execFileSync)(file, args, {
|
|
28816
30226
|
...opts,
|
|
@@ -28818,17 +30228,17 @@ function execFileCapture(file, args, opts = {}) {
|
|
|
28818
30228
|
stdio: [inFd, "pipe", errFd]
|
|
28819
30229
|
});
|
|
28820
30230
|
} finally {
|
|
28821
|
-
(0,
|
|
28822
|
-
(0,
|
|
30231
|
+
(0, import_node_fs29.closeSync)(inFd);
|
|
30232
|
+
(0, import_node_fs29.closeSync)(errFd);
|
|
28823
30233
|
}
|
|
28824
30234
|
}
|
|
28825
30235
|
var MMI_PLUGIN_ID2 = "mmi@mutmutco";
|
|
28826
30236
|
function stagedClaudePluginVersion() {
|
|
28827
30237
|
try {
|
|
28828
|
-
const stagingRoot = (0,
|
|
28829
|
-
const record = JSON.parse((0,
|
|
30238
|
+
const stagingRoot = (0, import_node_path27.join)((0, import_node_os14.homedir)(), ".claude", "plugins", "staging");
|
|
30239
|
+
const record = JSON.parse((0, import_node_fs29.readFileSync)((0, import_node_path27.join)(stagingRoot, "launch-target.json"), "utf8"));
|
|
28830
30240
|
if (typeof record.target !== "string" || typeof record.incoming !== "string") return void 0;
|
|
28831
|
-
const manifest = JSON.parse((0,
|
|
30241
|
+
const manifest = JSON.parse((0, import_node_fs29.readFileSync)((0, import_node_path27.join)(record.incoming, ".claude-plugin", "plugin.json"), "utf8"));
|
|
28832
30242
|
if (typeof manifest.version !== "string") return void 0;
|
|
28833
30243
|
return manifest.version === record.target ? record.target : void 0;
|
|
28834
30244
|
} catch {
|
|
@@ -28838,7 +30248,7 @@ function stagedClaudePluginVersion() {
|
|
|
28838
30248
|
function installedClaudePluginVersion() {
|
|
28839
30249
|
try {
|
|
28840
30250
|
const file = JSON.parse(
|
|
28841
|
-
(0,
|
|
30251
|
+
(0, import_node_fs29.readFileSync)((0, import_node_path27.join)((0, import_node_os14.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
|
|
28842
30252
|
);
|
|
28843
30253
|
const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
|
|
28844
30254
|
if (versions.length === 0) return void 0;
|
|
@@ -28849,7 +30259,7 @@ function installedClaudePluginVersion() {
|
|
|
28849
30259
|
}
|
|
28850
30260
|
function manifestVersion(path2) {
|
|
28851
30261
|
try {
|
|
28852
|
-
const manifest = JSON.parse((0,
|
|
30262
|
+
const manifest = JSON.parse((0, import_node_fs29.readFileSync)(path2, "utf8"));
|
|
28853
30263
|
return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
|
|
28854
30264
|
} catch {
|
|
28855
30265
|
return void 0;
|
|
@@ -28858,12 +30268,12 @@ function manifestVersion(path2) {
|
|
|
28858
30268
|
function readHermesPluginEvidence(env = process.env) {
|
|
28859
30269
|
const host = hermesConfigRoot(env);
|
|
28860
30270
|
const root = hermesPluginRoot(env);
|
|
28861
|
-
const installRecordPresent = (0,
|
|
28862
|
-
const manifestPath = (0,
|
|
30271
|
+
const installRecordPresent = (0, import_node_fs29.existsSync)(root);
|
|
30272
|
+
const manifestPath = (0, import_node_path27.join)(root, "plugin.yaml");
|
|
28863
30273
|
let installedVersion;
|
|
28864
30274
|
let manifest = "missing";
|
|
28865
30275
|
try {
|
|
28866
|
-
const text = (0,
|
|
30276
|
+
const text = (0, import_node_fs29.readFileSync)(manifestPath, "utf8");
|
|
28867
30277
|
let version;
|
|
28868
30278
|
try {
|
|
28869
30279
|
const parsed = JSON.parse(text).version;
|
|
@@ -28877,20 +30287,20 @@ function readHermesPluginEvidence(env = process.env) {
|
|
|
28877
30287
|
if (version) {
|
|
28878
30288
|
installedVersion = version;
|
|
28879
30289
|
manifest = "valid";
|
|
28880
|
-
} else if ((0,
|
|
30290
|
+
} else if ((0, import_node_fs29.existsSync)(manifestPath)) manifest = "invalid";
|
|
28881
30291
|
} catch {
|
|
28882
|
-
if ((0,
|
|
30292
|
+
if ((0, import_node_fs29.existsSync)(manifestPath)) manifest = "invalid";
|
|
28883
30293
|
}
|
|
28884
30294
|
let skills = false;
|
|
28885
30295
|
try {
|
|
28886
|
-
skills = (0,
|
|
30296
|
+
skills = (0, import_node_fs29.existsSync)((0, import_node_path27.join)(root, "skills")) && (0, import_node_fs29.statSync)((0, import_node_path27.join)(root, "skills")).isDirectory();
|
|
28887
30297
|
} catch {
|
|
28888
30298
|
}
|
|
28889
30299
|
return {
|
|
28890
|
-
hostPresent: (0,
|
|
30300
|
+
hostPresent: (0, import_node_fs29.existsSync)(host),
|
|
28891
30301
|
installRecordPresent,
|
|
28892
30302
|
manifest,
|
|
28893
|
-
payloadPresent: (0,
|
|
30303
|
+
payloadPresent: (0, import_node_fs29.existsSync)((0, import_node_path27.join)(root, "__init__.py")) && skills && manifest === "valid",
|
|
28894
30304
|
...installedVersion ? { installedVersion } : {}
|
|
28895
30305
|
};
|
|
28896
30306
|
}
|
|
@@ -28898,7 +30308,7 @@ function installedSurfacePluginVersion(surface) {
|
|
|
28898
30308
|
const token = surfaceToken(surface);
|
|
28899
30309
|
if (token === "hermes") return readHermesPluginEvidence().installedVersion;
|
|
28900
30310
|
if (token === "cursor") {
|
|
28901
|
-
return manifestVersion((0,
|
|
30311
|
+
return manifestVersion((0, import_node_path27.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
|
|
28902
30312
|
}
|
|
28903
30313
|
if (token === "jervcode") {
|
|
28904
30314
|
return installedJervCodePackageVersion();
|
|
@@ -28935,13 +30345,13 @@ function worktreeRootSync() {
|
|
|
28935
30345
|
}
|
|
28936
30346
|
var gitignorePath = () => {
|
|
28937
30347
|
const root = worktreeRootSync();
|
|
28938
|
-
return root === null ? null : (0,
|
|
30348
|
+
return root === null ? null : (0, import_node_path27.join)(root, ".gitignore");
|
|
28939
30349
|
};
|
|
28940
30350
|
function readGitignore() {
|
|
28941
30351
|
const path2 = gitignorePath();
|
|
28942
30352
|
if (path2 === null) return null;
|
|
28943
30353
|
try {
|
|
28944
|
-
return (0,
|
|
30354
|
+
return (0, import_node_fs29.readFileSync)(path2, "utf8");
|
|
28945
30355
|
} catch {
|
|
28946
30356
|
return null;
|
|
28947
30357
|
}
|
|
@@ -28950,14 +30360,14 @@ function writeGitignore(content) {
|
|
|
28950
30360
|
const path2 = gitignorePath();
|
|
28951
30361
|
if (path2 === null) return false;
|
|
28952
30362
|
try {
|
|
28953
|
-
(0,
|
|
30363
|
+
(0, import_node_fs29.writeFileSync)(path2, content, "utf8");
|
|
28954
30364
|
return true;
|
|
28955
30365
|
} catch {
|
|
28956
30366
|
return false;
|
|
28957
30367
|
}
|
|
28958
30368
|
}
|
|
28959
30369
|
function lineEndingState(root) {
|
|
28960
|
-
const attributesPresent = (0,
|
|
30370
|
+
const attributesPresent = (0, import_node_fs29.existsSync)((0, import_node_path27.join)(root, ".gitattributes"));
|
|
28961
30371
|
try {
|
|
28962
30372
|
const output = execFileCapture("git", ["-C", root, "ls-files", "--eol", "--", ":(glob)**/*.sh"], {
|
|
28963
30373
|
windowsHide: true
|
|
@@ -29277,102 +30687,6 @@ function registerExplainCommand(program3) {
|
|
|
29277
30687
|
});
|
|
29278
30688
|
}
|
|
29279
30689
|
|
|
29280
|
-
// src/file-lock.ts
|
|
29281
|
-
var import_promises3 = require("node:fs/promises");
|
|
29282
|
-
var import_node_path26 = require("node:path");
|
|
29283
|
-
var sleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
29284
|
-
var IMMEDIATE_RETRY_BUDGET = 3;
|
|
29285
|
-
var FileLockBusyError = class extends Error {
|
|
29286
|
-
lockPath;
|
|
29287
|
-
constructor(label, lockPath, maxWaitMs) {
|
|
29288
|
-
super(`${label} busy: ${lockPath} held longer than ${maxWaitMs}ms`);
|
|
29289
|
-
this.name = "FileLockBusyError";
|
|
29290
|
-
this.lockPath = lockPath;
|
|
29291
|
-
}
|
|
29292
|
-
};
|
|
29293
|
-
function resolveFileLockOpts(opts = {}) {
|
|
29294
|
-
return {
|
|
29295
|
-
staleMs: opts.staleMs ?? 3e4,
|
|
29296
|
-
retryMs: opts.retryMs ?? 50,
|
|
29297
|
-
maxWaitMs: opts.maxWaitMs ?? 5e3,
|
|
29298
|
-
label: opts.label ?? "file lock"
|
|
29299
|
-
};
|
|
29300
|
-
}
|
|
29301
|
-
async function acquireFileLock(lockPath, opts, deadline) {
|
|
29302
|
-
let immediateRetries = 0;
|
|
29303
|
-
for (; ; ) {
|
|
29304
|
-
let handle;
|
|
29305
|
-
try {
|
|
29306
|
-
handle = await (0, import_promises3.open)(lockPath, "wx");
|
|
29307
|
-
} catch (e) {
|
|
29308
|
-
const code = e.code;
|
|
29309
|
-
if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
|
|
29310
|
-
const retryNow = async () => {
|
|
29311
|
-
if (Date.now() >= deadline) throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
|
|
29312
|
-
if (++immediateRetries > IMMEDIATE_RETRY_BUDGET) await sleep(opts.retryMs);
|
|
29313
|
-
};
|
|
29314
|
-
try {
|
|
29315
|
-
const age = Date.now() - (await (0, import_promises3.stat)(lockPath)).mtimeMs;
|
|
29316
|
-
if (age > opts.staleMs) {
|
|
29317
|
-
try {
|
|
29318
|
-
await (0, import_promises3.unlink)(lockPath);
|
|
29319
|
-
await retryNow();
|
|
29320
|
-
continue;
|
|
29321
|
-
} catch (unlinkError) {
|
|
29322
|
-
if (unlinkError instanceof FileLockBusyError) throw unlinkError;
|
|
29323
|
-
const unlinkCode = unlinkError.code;
|
|
29324
|
-
if (unlinkCode === "ENOENT") {
|
|
29325
|
-
await retryNow();
|
|
29326
|
-
continue;
|
|
29327
|
-
}
|
|
29328
|
-
}
|
|
29329
|
-
}
|
|
29330
|
-
} catch (statError) {
|
|
29331
|
-
if (statError instanceof FileLockBusyError) throw statError;
|
|
29332
|
-
if (statError.code !== "ENOENT") throw statError;
|
|
29333
|
-
await retryNow();
|
|
29334
|
-
continue;
|
|
29335
|
-
}
|
|
29336
|
-
if (Date.now() >= deadline) {
|
|
29337
|
-
throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
|
|
29338
|
-
}
|
|
29339
|
-
await sleep(opts.retryMs);
|
|
29340
|
-
continue;
|
|
29341
|
-
}
|
|
29342
|
-
const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
29343
|
-
try {
|
|
29344
|
-
await handle.writeFile(token);
|
|
29345
|
-
} catch (e) {
|
|
29346
|
-
await handle.close().catch(() => void 0);
|
|
29347
|
-
throw e;
|
|
29348
|
-
}
|
|
29349
|
-
return { token, handle };
|
|
29350
|
-
}
|
|
29351
|
-
}
|
|
29352
|
-
async function fileLockHeldBy(lockPath, token) {
|
|
29353
|
-
try {
|
|
29354
|
-
return await (0, import_promises3.readFile)(lockPath, "utf8") === token;
|
|
29355
|
-
} catch {
|
|
29356
|
-
return false;
|
|
29357
|
-
}
|
|
29358
|
-
}
|
|
29359
|
-
async function releaseFileLock(lockPath, guard) {
|
|
29360
|
-
await guard.handle.close().catch(() => void 0);
|
|
29361
|
-
if (await fileLockHeldBy(lockPath, guard.token)) {
|
|
29362
|
-
await (0, import_promises3.unlink)(lockPath).catch(() => void 0);
|
|
29363
|
-
}
|
|
29364
|
-
}
|
|
29365
|
-
async function withFileLock(lockPath, opts, fn) {
|
|
29366
|
-
const resolved = resolveFileLockOpts(opts);
|
|
29367
|
-
await (0, import_promises3.mkdir)((0, import_node_path26.dirname)(lockPath), { recursive: true }).catch(() => void 0);
|
|
29368
|
-
const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
|
|
29369
|
-
try {
|
|
29370
|
-
return await fn();
|
|
29371
|
-
} finally {
|
|
29372
|
-
await releaseFileLock(lockPath, guard);
|
|
29373
|
-
}
|
|
29374
|
-
}
|
|
29375
|
-
|
|
29376
30690
|
// src/gh-account-visibility.ts
|
|
29377
30691
|
function parseGhAuthAccounts(stdout) {
|
|
29378
30692
|
const accounts = [];
|
|
@@ -29392,8 +30706,8 @@ function parseOriginRepo(remoteUrl) {
|
|
|
29392
30706
|
}
|
|
29393
30707
|
|
|
29394
30708
|
// src/issue-commands.ts
|
|
29395
|
-
var
|
|
29396
|
-
var
|
|
30709
|
+
var import_node_fs30 = require("node:fs");
|
|
30710
|
+
var import_node_crypto9 = require("node:crypto");
|
|
29397
30711
|
|
|
29398
30712
|
// src/issue-body.ts
|
|
29399
30713
|
var import_node_os15 = require("node:os");
|
|
@@ -29642,7 +30956,7 @@ async function editIssue(client, options, deps = {}) {
|
|
|
29642
30956
|
const url = `https://github.com/${repo}/issues/${parsed.number}`;
|
|
29643
30957
|
const patch = {};
|
|
29644
30958
|
let bodyChanged = false;
|
|
29645
|
-
const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0,
|
|
30959
|
+
const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs30.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
|
|
29646
30960
|
if (options.titleFile !== void 0) {
|
|
29647
30961
|
patch.title = await resolveIssueTitle({ title: options.title, titleFile: options.titleFile }, textDeps());
|
|
29648
30962
|
} else if (options.title !== void 0) {
|
|
@@ -29923,7 +31237,7 @@ function rowIdempotencyKey(batchKey, spec) {
|
|
|
29923
31237
|
const identity = `${spec.type}
|
|
29924
31238
|
${spec.title.trim()}
|
|
29925
31239
|
${spec.body ?? ""}`;
|
|
29926
|
-
const hash = (0,
|
|
31240
|
+
const hash = (0, import_node_crypto9.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
29927
31241
|
return `${batchKey}:${hash}`;
|
|
29928
31242
|
}
|
|
29929
31243
|
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
|
|
@@ -30243,8 +31557,8 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
30243
31557
|
(_opts, args) => ({ command: "issue unlink-child", parent: args[0], child: args[1] })
|
|
30244
31558
|
).action(async (parentRef, childRef, o) => {
|
|
30245
31559
|
try {
|
|
30246
|
-
parseIssueRef(parentRef
|
|
30247
|
-
parseIssueRef(childRef
|
|
31560
|
+
parseIssueRef(parentRef);
|
|
31561
|
+
parseIssueRef(childRef);
|
|
30248
31562
|
const defaultRepo = await resolveRepo(o.repo);
|
|
30249
31563
|
const result = await unlinkSubIssue(ghRunner, parentRef, childRef, defaultRepo);
|
|
30250
31564
|
console.log(JSON.stringify(result));
|
|
@@ -30267,7 +31581,7 @@ function extendCreateCommand(issue, batchAttach) {
|
|
|
30267
31581
|
if (opts.batch) {
|
|
30268
31582
|
let specs;
|
|
30269
31583
|
try {
|
|
30270
|
-
const raw = (0,
|
|
31584
|
+
const raw = (0, import_node_fs30.readFileSync)(opts.batch, "utf8");
|
|
30271
31585
|
specs = JSON.parse(raw);
|
|
30272
31586
|
if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
|
|
30273
31587
|
} catch (e) {
|
|
@@ -30739,7 +32053,7 @@ async function readRestPrSnapshotWithRetry(prNumber, repo, gh = defaultGhApi, op
|
|
|
30739
32053
|
const retries = options?.retries ?? PR_SNAPSHOT_READ_RETRIES;
|
|
30740
32054
|
const untilMs = options?.retryTransientUntilMs;
|
|
30741
32055
|
const delayMs = options?.delayMs ?? (untilMs !== void 0 ? PR_CHECKS_POLL_MS : PR_SNAPSHOT_READ_DELAY_MS);
|
|
30742
|
-
const sleep2 = options?.sleep ?? ((ms) => new Promise((
|
|
32056
|
+
const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
30743
32057
|
const now = options?.now ?? (() => Date.now());
|
|
30744
32058
|
let lastError = "no attempt completed";
|
|
30745
32059
|
let attempt = 0;
|
|
@@ -31394,8 +32708,8 @@ function registerPrLifecycleCommands(program3) {
|
|
|
31394
32708
|
}
|
|
31395
32709
|
|
|
31396
32710
|
// src/project-info-sync.ts
|
|
31397
|
-
var
|
|
31398
|
-
var
|
|
32711
|
+
var import_node_fs31 = require("node:fs");
|
|
32712
|
+
var import_node_path28 = require("node:path");
|
|
31399
32713
|
var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!, $readme: String!) {
|
|
31400
32714
|
updateProjectV2(input: { projectId: $projectId, shortDescription: $shortDescription, readme: $readme }) {
|
|
31401
32715
|
projectV2 { id }
|
|
@@ -31440,14 +32754,14 @@ function sharedName(entries, fallback) {
|
|
|
31440
32754
|
}
|
|
31441
32755
|
function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
|
|
31442
32756
|
if (!project2.projectId) throw new Error(`org project sync-info: ${targetRepo2} registry META has no projectId`);
|
|
31443
|
-
const readmePath = (0,
|
|
31444
|
-
if (!(0,
|
|
32757
|
+
const readmePath = (0, import_node_path28.join)(repoRoot2, "README.md");
|
|
32758
|
+
if (!(0, import_node_fs31.existsSync)(readmePath)) throw new Error(`org project sync-info: ${targetRepo2} has no README.md`);
|
|
31445
32759
|
const entries = entriesFor(project2, projects);
|
|
31446
32760
|
const memberRepos = [...new Set(entries.flatMap((entry) => entry.repos ?? []))].filter((repo) => /^[^/]+\/[^/]+$/.test(repo)).sort((a, b) => a.localeCompare(b));
|
|
31447
32761
|
const projectName = sharedName(entries, project2.name?.trim() || targetRepo2.split("/").pop() || targetRepo2);
|
|
31448
32762
|
if (!memberRepos.length) throw new Error(`org project sync-info: project ${projectName} has no registered member repos`);
|
|
31449
32763
|
const entryNames = entries.map((entry) => entry.name?.trim()).filter((name) => Boolean(name));
|
|
31450
|
-
const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0,
|
|
32764
|
+
const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0, import_node_fs31.readFileSync)(readmePath, "utf8")) : `Shared work across ${new Intl.ListFormat("en", { type: "conjunction" }).format(entryNames)}.`;
|
|
31451
32765
|
const lines = [
|
|
31452
32766
|
`# ${projectName}`,
|
|
31453
32767
|
"",
|
|
@@ -31466,8 +32780,8 @@ function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
|
|
|
31466
32780
|
const targetBase = `https://github.com/${targetRepo2}`;
|
|
31467
32781
|
const targetBranch = branchFor(targetRepo2, projects);
|
|
31468
32782
|
const orgDocs = [
|
|
31469
|
-
(0,
|
|
31470
|
-
(0,
|
|
32783
|
+
(0, import_node_fs31.existsSync)((0, import_node_path28.join)(repoRoot2, "architecture.md")) ? `- [Hub architecture](${targetBase}/blob/${targetBranch}/architecture.md)` : "",
|
|
32784
|
+
(0, import_node_fs31.existsSync)((0, import_node_path28.join)(repoRoot2, "docs", "Architecture", "agentic-dev-environment.md")) ? `- [Agentic development environment](${targetBase}/blob/${targetBranch}/docs/Architecture/agentic-dev-environment.md)` : ""
|
|
31471
32785
|
].filter(Boolean);
|
|
31472
32786
|
if (orgDocs.length) lines.push("", "## Organisation docs", "", ...orgDocs);
|
|
31473
32787
|
return { projectId: project2.projectId, projectName, targetRepo: targetRepo2, memberRepos, shortDescription, readme: `${lines.join("\n")}
|
|
@@ -31682,7 +32996,7 @@ async function awsJson(args) {
|
|
|
31682
32996
|
try {
|
|
31683
32997
|
return await run();
|
|
31684
32998
|
} catch {
|
|
31685
|
-
await new Promise((
|
|
32999
|
+
await new Promise((resolve7) => setTimeout(resolve7, AWS_RETRY_DELAY_MS));
|
|
31686
33000
|
return run();
|
|
31687
33001
|
}
|
|
31688
33002
|
}
|
|
@@ -31885,8 +33199,8 @@ function registerSchedulesCommands(program3) {
|
|
|
31885
33199
|
}
|
|
31886
33200
|
|
|
31887
33201
|
// src/secrets-commands.ts
|
|
31888
|
-
var
|
|
31889
|
-
var
|
|
33202
|
+
var import_node_fs32 = require("node:fs");
|
|
33203
|
+
var import_node_path29 = require("node:path");
|
|
31890
33204
|
var import_node_os16 = require("node:os");
|
|
31891
33205
|
|
|
31892
33206
|
// src/secrets-diff.ts
|
|
@@ -31989,18 +33303,18 @@ function collectMap(value, previous = []) {
|
|
|
31989
33303
|
return [...previous, value];
|
|
31990
33304
|
}
|
|
31991
33305
|
async function decryptRailsCredentials(input) {
|
|
31992
|
-
const appDir = (0,
|
|
33306
|
+
const appDir = (0, import_node_path29.resolve)(input.appDir ?? process.cwd());
|
|
31993
33307
|
const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
|
|
31994
33308
|
const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
|
|
31995
|
-
const credentialsPath = (0,
|
|
31996
|
-
const masterKeyPath = (0,
|
|
33309
|
+
const credentialsPath = (0, import_node_path29.resolve)(appDir, credentialsFile);
|
|
33310
|
+
const masterKeyPath = (0, import_node_path29.resolve)(appDir, masterKeyFile);
|
|
31997
33311
|
const env = {
|
|
31998
33312
|
...process.env,
|
|
31999
33313
|
MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
|
|
32000
33314
|
MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
|
|
32001
33315
|
};
|
|
32002
|
-
if ((0,
|
|
32003
|
-
env.RAILS_MASTER_KEY = (0,
|
|
33316
|
+
if ((0, import_node_fs32.existsSync)(masterKeyPath)) {
|
|
33317
|
+
env.RAILS_MASTER_KEY = (0, import_node_fs32.readFileSync)(masterKeyPath, "utf8").trim();
|
|
32004
33318
|
}
|
|
32005
33319
|
const script = [
|
|
32006
33320
|
'require "json"',
|
|
@@ -32010,9 +33324,9 @@ async function decryptRailsCredentials(input) {
|
|
|
32010
33324
|
'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
|
|
32011
33325
|
"puts JSON.generate(config.config)"
|
|
32012
33326
|
].join("\n");
|
|
32013
|
-
const scriptDir = (0,
|
|
32014
|
-
const scriptPath = (0,
|
|
32015
|
-
(0,
|
|
33327
|
+
const scriptDir = (0, import_node_fs32.mkdtempSync)((0, import_node_path29.join)((0, import_node_os16.tmpdir)(), "mmi-rails-decrypt-"));
|
|
33328
|
+
const scriptPath = (0, import_node_path29.join)(scriptDir, "decrypt.rb");
|
|
33329
|
+
(0, import_node_fs32.writeFileSync)(scriptPath, script, "utf8");
|
|
32016
33330
|
try {
|
|
32017
33331
|
const args = ["exec", "ruby", scriptPath];
|
|
32018
33332
|
const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
|
|
@@ -32024,7 +33338,7 @@ async function decryptRailsCredentials(input) {
|
|
|
32024
33338
|
});
|
|
32025
33339
|
return JSON.parse(stdout);
|
|
32026
33340
|
} finally {
|
|
32027
|
-
(0,
|
|
33341
|
+
(0, import_node_fs32.rmSync)(scriptDir, { recursive: true, force: true });
|
|
32028
33342
|
}
|
|
32029
33343
|
}
|
|
32030
33344
|
async function readSecretStdin() {
|
|
@@ -32114,7 +33428,7 @@ function registerSecretsCommands(program3) {
|
|
|
32114
33428
|
let body;
|
|
32115
33429
|
if (o.file) {
|
|
32116
33430
|
try {
|
|
32117
|
-
body = (0,
|
|
33431
|
+
body = (0, import_node_fs32.readFileSync)((0, import_node_path29.resolve)(o.file), "utf8");
|
|
32118
33432
|
} catch (e) {
|
|
32119
33433
|
return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
|
|
32120
33434
|
}
|
|
@@ -32219,7 +33533,7 @@ function registerSecretsCommands(program3) {
|
|
|
32219
33533
|
{
|
|
32220
33534
|
...d,
|
|
32221
33535
|
decryptRailsCredentials,
|
|
32222
|
-
removeFile: (path2) => (0,
|
|
33536
|
+
removeFile: (path2) => (0, import_node_fs32.unlinkSync)((0, import_node_path29.resolve)(o.appDir ?? process.cwd(), path2))
|
|
32223
33537
|
},
|
|
32224
33538
|
{
|
|
32225
33539
|
repo: o.repo,
|
|
@@ -32380,8 +33694,8 @@ function registerSessionReport(program3) {
|
|
|
32380
33694
|
}
|
|
32381
33695
|
|
|
32382
33696
|
// src/stage-commands.ts
|
|
32383
|
-
var
|
|
32384
|
-
var
|
|
33697
|
+
var import_node_fs33 = require("node:fs");
|
|
33698
|
+
var import_node_path30 = require("node:path");
|
|
32385
33699
|
|
|
32386
33700
|
// src/stage-default.ts
|
|
32387
33701
|
function shellFor(platform2 = process.platform) {
|
|
@@ -32665,8 +33979,8 @@ function registerStageCommands(program3) {
|
|
|
32665
33979
|
return {
|
|
32666
33980
|
resolution: decideStage({
|
|
32667
33981
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
32668
|
-
hasCompose: (0,
|
|
32669
|
-
hasEnvExample: (0,
|
|
33982
|
+
hasCompose: (0, import_node_fs33.existsSync)((0, import_node_path30.join)(process.cwd(), "docker-compose.yml")),
|
|
33983
|
+
hasEnvExample: (0, import_node_fs33.existsSync)((0, import_node_path30.join)(process.cwd(), ".env.example")),
|
|
32670
33984
|
repo
|
|
32671
33985
|
}),
|
|
32672
33986
|
project: project2,
|
|
@@ -32916,20 +34230,20 @@ function registerStageCommands(program3) {
|
|
|
32916
34230
|
}
|
|
32917
34231
|
|
|
32918
34232
|
// src/tenant-artifact.ts
|
|
32919
|
-
var
|
|
32920
|
-
var
|
|
34233
|
+
var import_node_crypto10 = require("node:crypto");
|
|
34234
|
+
var import_node_fs34 = require("node:fs");
|
|
32921
34235
|
var import_promises6 = require("node:fs/promises");
|
|
32922
|
-
var
|
|
34236
|
+
var import_node_path31 = require("node:path");
|
|
32923
34237
|
var ARTIFACT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
32924
34238
|
var MAX_BYTES = 5 * 1024 * 1024 * 1024;
|
|
32925
34239
|
async function sha256File(path2) {
|
|
32926
|
-
const hash = (0,
|
|
32927
|
-
for await (const chunk of (0,
|
|
34240
|
+
const hash = (0, import_node_crypto10.createHash)("sha256");
|
|
34241
|
+
for await (const chunk of (0, import_node_fs34.createReadStream)(path2)) hash.update(chunk);
|
|
32928
34242
|
return hash.digest("hex");
|
|
32929
34243
|
}
|
|
32930
34244
|
async function putTenantArtifact(repo, stage, inputPath, deps) {
|
|
32931
34245
|
if (!["dev", "rc", "main"].includes(stage)) throw new Error("tenant artifact put: <stage> must be dev, rc, or main");
|
|
32932
|
-
const path2 = (0,
|
|
34246
|
+
const path2 = (0, import_node_path31.resolve)(inputPath);
|
|
32933
34247
|
const info = await (0, import_promises6.stat)(path2);
|
|
32934
34248
|
if (!info.isFile()) throw new Error("tenant artifact put: input path must be a file");
|
|
32935
34249
|
if (!Number.isSafeInteger(info.size) || info.size < 1 || info.size > MAX_BYTES) throw new Error(`tenant artifact put: file must be 1..${MAX_BYTES} bytes`);
|
|
@@ -32947,7 +34261,7 @@ async function putTenantArtifact(repo, stage, inputPath, deps) {
|
|
|
32947
34261
|
return [key, value];
|
|
32948
34262
|
}));
|
|
32949
34263
|
headers["content-length"] = String(info.size);
|
|
32950
|
-
const stream = (0,
|
|
34264
|
+
const stream = (0, import_node_fs34.createReadStream)(path2);
|
|
32951
34265
|
let uploaded;
|
|
32952
34266
|
try {
|
|
32953
34267
|
uploaded = await fetch(body.uploadUrl, {
|
|
@@ -33087,7 +34401,7 @@ function renderVerifySecrets(body) {
|
|
|
33087
34401
|
|
|
33088
34402
|
// src/command-register-collaboration.ts
|
|
33089
34403
|
var import_node_child_process14 = require("node:child_process");
|
|
33090
|
-
var
|
|
34404
|
+
var import_node_fs41 = require("node:fs");
|
|
33091
34405
|
var import_promises7 = require("node:fs/promises");
|
|
33092
34406
|
|
|
33093
34407
|
// src/session-runtime.ts
|
|
@@ -33107,7 +34421,7 @@ function spawnDetachedSelf(args, deps, opts = {}) {
|
|
|
33107
34421
|
}
|
|
33108
34422
|
|
|
33109
34423
|
// src/command-register-collaboration.ts
|
|
33110
|
-
var
|
|
34424
|
+
var import_node_path38 = require("node:path");
|
|
33111
34425
|
|
|
33112
34426
|
// src/attach-to-project.ts
|
|
33113
34427
|
function boardAttachRateLimitedReceipt(resetEpochSeconds) {
|
|
@@ -33194,7 +34508,7 @@ async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
|
|
|
33194
34508
|
if (first.mergeStatus !== "failed") return first;
|
|
33195
34509
|
const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false }));
|
|
33196
34510
|
if (!ready.open || !ready.mergeable || !ready.checksPassing) return first;
|
|
33197
|
-
const sleep2 = deps.sleep ?? ((ms) => new Promise((
|
|
34511
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
33198
34512
|
await sleep2(PR_LAND_MERGE_RETRY_DELAY_MS);
|
|
33199
34513
|
const retried = await deps.mergeAuto(prNumber, repo);
|
|
33200
34514
|
if (retried.mergeStatus !== "failed") return retried;
|
|
@@ -33205,7 +34519,7 @@ var AUTO_MERGE_CONFIRM_DELAY_MS = 3e3;
|
|
|
33205
34519
|
async function confirmAutoMergeEnqueued(deps, options) {
|
|
33206
34520
|
const retries = options?.retries ?? AUTO_MERGE_CONFIRM_RETRIES;
|
|
33207
34521
|
const delayMs = options?.delayMs ?? AUTO_MERGE_CONFIRM_DELAY_MS;
|
|
33208
|
-
const sleep2 = deps.sleep ?? ((ms) => new Promise((
|
|
34522
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
33209
34523
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
33210
34524
|
if (await deps.readMerged().catch(() => false)) return "merged";
|
|
33211
34525
|
const stuck = await deps.readAutoMergeRequest().then((s) => s.trim()).catch(() => "");
|
|
@@ -33222,7 +34536,7 @@ async function confirmAutoMergeEnqueued(deps, options) {
|
|
|
33222
34536
|
async function readGhPrStateWithRetry(fetchState, options) {
|
|
33223
34537
|
const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
|
|
33224
34538
|
const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
|
|
33225
|
-
const sleep2 = options?.sleep ?? ((ms) => new Promise((
|
|
34539
|
+
const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
33226
34540
|
let lastError = "empty state";
|
|
33227
34541
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
33228
34542
|
try {
|
|
@@ -33295,8 +34609,8 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
33295
34609
|
}
|
|
33296
34610
|
|
|
33297
34611
|
// src/merge-cleanup.ts
|
|
33298
|
-
var
|
|
33299
|
-
var
|
|
34612
|
+
var import_node_fs36 = require("node:fs");
|
|
34613
|
+
var import_node_path33 = require("node:path");
|
|
33300
34614
|
var import_node_os17 = require("node:os");
|
|
33301
34615
|
|
|
33302
34616
|
// src/board-advance.ts
|
|
@@ -33399,8 +34713,8 @@ function boardAdvanceFailureMessage(result) {
|
|
|
33399
34713
|
|
|
33400
34714
|
// src/test-policy-core.ts
|
|
33401
34715
|
var import_node_child_process12 = require("node:child_process");
|
|
33402
|
-
var
|
|
33403
|
-
var
|
|
34716
|
+
var import_node_fs35 = require("node:fs");
|
|
34717
|
+
var import_node_path32 = require("node:path");
|
|
33404
34718
|
|
|
33405
34719
|
// src/test-command-policy-shared.mjs
|
|
33406
34720
|
var import_node_child_process11 = require("node:child_process");
|
|
@@ -33855,7 +35169,7 @@ function isMeaningfulRow(file) {
|
|
|
33855
35169
|
return file.meaningful !== false;
|
|
33856
35170
|
}
|
|
33857
35171
|
function loadPolicy(root, readFile8 = readFileOrNull2) {
|
|
33858
|
-
const raw = readFile8((0,
|
|
35172
|
+
const raw = readFile8((0, import_node_path32.join)(root, POLICY_FILE));
|
|
33859
35173
|
if (raw == null) return { mandatory: [], declared: false };
|
|
33860
35174
|
try {
|
|
33861
35175
|
return { ...JSON.parse(raw), declared: true };
|
|
@@ -33865,7 +35179,7 @@ function loadPolicy(root, readFile8 = readFileOrNull2) {
|
|
|
33865
35179
|
}
|
|
33866
35180
|
function readFileOrNull2(path2) {
|
|
33867
35181
|
try {
|
|
33868
|
-
return (0,
|
|
35182
|
+
return (0, import_node_fs35.readFileSync)(path2, "utf8");
|
|
33869
35183
|
} catch {
|
|
33870
35184
|
return null;
|
|
33871
35185
|
}
|
|
@@ -33893,12 +35207,12 @@ function classify(changed, policy, present = () => false) {
|
|
|
33893
35207
|
const removedProtected = [...removed].filter((p) => protectedBy.has(p)).map((p) => ({ path: p, why: protectedBy.get(p) ?? "" }));
|
|
33894
35208
|
return { mandatoryHits, untestedHits, testChanges, meaningfulTestChanges, addedTests, removedProtected };
|
|
33895
35209
|
}
|
|
33896
|
-
function unresolvedProtectedEntries(policy, root, exists = (path2) => (0,
|
|
33897
|
-
return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0,
|
|
35210
|
+
function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs35.existsSync)(path2)) {
|
|
35211
|
+
return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path32.join)(root, p)));
|
|
33898
35212
|
}
|
|
33899
|
-
function unresolvedSatisfiers(policy, root, exists = (path2) => (0,
|
|
35213
|
+
function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_fs35.existsSync)(path2)) {
|
|
33900
35214
|
const declared = (policy.mandatory ?? []).flatMap((m) => m.satisfiedBy ?? []);
|
|
33901
|
-
return [...new Set(declared)].filter((p) => !exists((0,
|
|
35215
|
+
return [...new Set(declared)].filter((p) => !exists((0, import_node_path32.join)(root, p)));
|
|
33902
35216
|
}
|
|
33903
35217
|
function evaluate(changed, policy, present = () => false) {
|
|
33904
35218
|
const { mandatoryHits, untestedHits, meaningfulTestChanges, addedTests, removedProtected } = classify(changed, policy, present);
|
|
@@ -34026,17 +35340,17 @@ function blobAt(base, path2, cwd) {
|
|
|
34026
35340
|
}
|
|
34027
35341
|
function runTestPolicy(root, deps = {}) {
|
|
34028
35342
|
const policy = deps.policy ?? loadPolicy(root);
|
|
34029
|
-
const exists = deps.exists ?? ((path2) => (0,
|
|
35343
|
+
const exists = deps.exists ?? ((path2) => (0, import_node_fs35.existsSync)(path2));
|
|
34030
35344
|
const counts = { mandatoryCount: (policy.mandatory ?? []).length, protectedCount: (policy.protected ?? []).length };
|
|
34031
35345
|
const base = deps.changed ? "(injected)" : resolveBase(root, deps.base);
|
|
34032
35346
|
const refusal = deps.changed ? null : untrustworthyRange(root, base);
|
|
34033
35347
|
const raw = deps.changed ?? (refusal ? [] : changedFilesSince(base, root));
|
|
34034
35348
|
const changed = deps.changed ? raw : annotateChangeMeaning(raw, policy, (path2) => ({
|
|
34035
35349
|
before: blobAt(base, path2, root),
|
|
34036
|
-
after: readFileOrNull2((0,
|
|
35350
|
+
after: readFileOrNull2((0, import_node_path32.join)(root, path2))
|
|
34037
35351
|
}));
|
|
34038
35352
|
const lookup = deps.override !== void 0 ? { override: deps.override, refusals: [] } : !deps.changed && !refusal ? readOverride2(base, root) : { override: null, refusals: [] };
|
|
34039
|
-
const present = (path2) => exists((0,
|
|
35353
|
+
const present = (path2) => exists((0, import_node_path32.join)(root, path2));
|
|
34040
35354
|
const removedByThisDiff = removedPaths(changed);
|
|
34041
35355
|
const staleFindings = [];
|
|
34042
35356
|
const unresolved = unresolvedProtectedEntries(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
|
|
@@ -34236,13 +35550,13 @@ function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
|
|
|
34236
35550
|
return rewritten === base ? null : rewritten;
|
|
34237
35551
|
}
|
|
34238
35552
|
function writeSquashBodyFile(body) {
|
|
34239
|
-
const dir = (0,
|
|
34240
|
-
const path2 = (0,
|
|
34241
|
-
(0,
|
|
35553
|
+
const dir = (0, import_node_fs36.mkdtempSync)((0, import_node_path33.join)((0, import_node_os17.tmpdir)(), "mmi-squash-body-"));
|
|
35554
|
+
const path2 = (0, import_node_path33.join)(dir, "body.txt");
|
|
35555
|
+
(0, import_node_fs36.writeFileSync)(path2, body.endsWith("\n") ? body : `${body}
|
|
34242
35556
|
`, "utf8");
|
|
34243
35557
|
return { path: path2, cleanup: () => {
|
|
34244
35558
|
try {
|
|
34245
|
-
(0,
|
|
35559
|
+
(0, import_node_fs36.rmSync)(dir, { recursive: true, force: true });
|
|
34246
35560
|
} catch {
|
|
34247
35561
|
}
|
|
34248
35562
|
} };
|
|
@@ -34359,7 +35673,8 @@ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile) {
|
|
|
34359
35673
|
return confirmEnqueueOutcome();
|
|
34360
35674
|
}
|
|
34361
35675
|
function cleanupGitArgs(cwd, args) {
|
|
34362
|
-
|
|
35676
|
+
if (!cwd || args[0] === "-C") return args;
|
|
35677
|
+
return ["-C", cwd, ...args];
|
|
34363
35678
|
}
|
|
34364
35679
|
async function remoteBranchExists2(branch, options = {}) {
|
|
34365
35680
|
if (!branch) return void 0;
|
|
@@ -34444,8 +35759,8 @@ async function deleteMergedRemoteBranch(options) {
|
|
|
34444
35759
|
}
|
|
34445
35760
|
|
|
34446
35761
|
// src/post-merge-recon.ts
|
|
34447
|
-
var
|
|
34448
|
-
var
|
|
35762
|
+
var import_node_fs37 = require("node:fs");
|
|
35763
|
+
var import_node_path34 = require("node:path");
|
|
34449
35764
|
|
|
34450
35765
|
// src/cross-repo-filing-issue.ts
|
|
34451
35766
|
function crossRepoFilingRetryCommand(prRepo, prNumber) {
|
|
@@ -34611,16 +35926,16 @@ function buildPostMergeReconRecovery(input) {
|
|
|
34611
35926
|
}
|
|
34612
35927
|
function writePostMergeReconRecovery(cwd, recovery) {
|
|
34613
35928
|
const path2 = postMergeReconStatePath(cwd, recovery.repo, recovery.pr);
|
|
34614
|
-
(0,
|
|
34615
|
-
(0,
|
|
35929
|
+
(0, import_node_fs37.mkdirSync)((0, import_node_path34.dirname)(path2), { recursive: true });
|
|
35930
|
+
(0, import_node_fs37.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
|
|
34616
35931
|
`, "utf8");
|
|
34617
35932
|
return path2;
|
|
34618
35933
|
}
|
|
34619
35934
|
function clearPostMergeReconRecovery(cwd, repo, pr) {
|
|
34620
35935
|
const path2 = postMergeReconStatePath(cwd, repo, pr);
|
|
34621
|
-
if (!(0,
|
|
35936
|
+
if (!(0, import_node_fs37.existsSync)(path2)) return;
|
|
34622
35937
|
try {
|
|
34623
|
-
(0,
|
|
35938
|
+
(0, import_node_fs37.unlinkSync)(path2);
|
|
34624
35939
|
} catch {
|
|
34625
35940
|
}
|
|
34626
35941
|
}
|
|
@@ -34655,7 +35970,7 @@ var import_node_child_process13 = require("node:child_process");
|
|
|
34655
35970
|
var GIT_TIMEOUT_MS2 = 15e3;
|
|
34656
35971
|
function catFileBatch(root, ref, paths) {
|
|
34657
35972
|
if (paths.length === 0) return Promise.resolve([]);
|
|
34658
|
-
return new Promise((
|
|
35973
|
+
return new Promise((resolve7) => {
|
|
34659
35974
|
const chunks = [];
|
|
34660
35975
|
let settled = false;
|
|
34661
35976
|
const child2 = (0, import_node_child_process13.spawn)("git", ["-C", root, "cat-file", "--batch", "--buffer"], { windowsHide: true });
|
|
@@ -34663,7 +35978,7 @@ function catFileBatch(root, ref, paths) {
|
|
|
34663
35978
|
if (settled) return;
|
|
34664
35979
|
settled = true;
|
|
34665
35980
|
clearTimeout(timer);
|
|
34666
|
-
|
|
35981
|
+
resolve7(parseCatFileBatch(Buffer.concat(chunks), paths.length));
|
|
34667
35982
|
};
|
|
34668
35983
|
const timer = setTimeout(() => {
|
|
34669
35984
|
child2.kill("SIGKILL");
|
|
@@ -34771,13 +36086,88 @@ async function checkDocsIndexAtHead(opts, deps) {
|
|
|
34771
36086
|
};
|
|
34772
36087
|
}
|
|
34773
36088
|
|
|
36089
|
+
// src/pr-create-claim-guard.ts
|
|
36090
|
+
async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
36091
|
+
const issues = [...new Set(findClosingMentions(body).map((mention) => mention.issue))];
|
|
36092
|
+
if (!issues.length) return void 0;
|
|
36093
|
+
const repo = await requireRepo(repoOption);
|
|
36094
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
36095
|
+
const actor = deps.actor ?? describeSessionIdentity();
|
|
36096
|
+
const checkContest = deps.checkContest ?? checkLaneContest;
|
|
36097
|
+
for (const number of issues) {
|
|
36098
|
+
const contest = await checkContest(client, { repository: repo, number }, actor);
|
|
36099
|
+
if (!contest.contested) continue;
|
|
36100
|
+
const ref = `${repo}#${number}`;
|
|
36101
|
+
const holder = contest.marker ? `lane ${describeClaimMarker(contest.marker)}` : "another lane";
|
|
36102
|
+
return `pr create: REFUSED \u2014 ${ref} is held by ${holder} with live or unreadable work evidence; run \`mmi-cli oracle board claim ${ref} --force\` before creating this PR`;
|
|
36103
|
+
}
|
|
36104
|
+
return void 0;
|
|
36105
|
+
}
|
|
36106
|
+
|
|
34774
36107
|
// src/worktree-merge-cleanup.ts
|
|
36108
|
+
var import_node_fs40 = require("node:fs");
|
|
36109
|
+
var import_node_path37 = require("node:path");
|
|
36110
|
+
|
|
36111
|
+
// src/jervcode-node-modules-cleanup.ts
|
|
34775
36112
|
var import_node_fs38 = require("node:fs");
|
|
36113
|
+
var import_node_os18 = require("node:os");
|
|
34776
36114
|
var import_node_path35 = require("node:path");
|
|
36115
|
+
var JERVCODE_PACKAGE_ENTRY = (0, import_node_path35.join)("node_modules", "@jervaise", "jervcode", "dist", "launcher-entry.js");
|
|
36116
|
+
var WIN_NAMES2 = ["jervcode.cmd", "jervcode"];
|
|
36117
|
+
var POSIX_NAMES2 = ["jervcode"];
|
|
36118
|
+
var NODE_MODULES_CLEANUP_TIMEOUT_MS = 3e5;
|
|
36119
|
+
function jervcodeCandidatePaths(env = process.env, home = (0, import_node_os18.homedir)(), platform2 = process.platform) {
|
|
36120
|
+
const names = platform2 === "win32" ? WIN_NAMES2 : POSIX_NAMES2;
|
|
36121
|
+
const out = [];
|
|
36122
|
+
for (const dir of jervCliCandidateDirs(env, home, platform2)) {
|
|
36123
|
+
for (const name of names) out.push((0, import_node_path35.join)(dir, name));
|
|
36124
|
+
}
|
|
36125
|
+
return out;
|
|
36126
|
+
}
|
|
36127
|
+
function resolveJervcodePath(env = process.env, home = (0, import_node_os18.homedir)(), platform2 = process.platform, exists = import_node_fs38.existsSync) {
|
|
36128
|
+
for (const candidate of jervcodeCandidatePaths(env, home, platform2)) {
|
|
36129
|
+
if (exists(candidate)) return candidate;
|
|
36130
|
+
}
|
|
36131
|
+
return void 0;
|
|
36132
|
+
}
|
|
36133
|
+
function jervcodeExecFileArgs(args, opts = {}) {
|
|
36134
|
+
const platform2 = opts.platform ?? process.platform;
|
|
36135
|
+
const exists = opts.exists ?? import_node_fs38.existsSync;
|
|
36136
|
+
const resolved = resolveJervcodePath(opts.env ?? process.env, opts.home ?? (0, import_node_os18.homedir)(), platform2, exists);
|
|
36137
|
+
if (resolved) {
|
|
36138
|
+
const entry = (0, import_node_path35.join)((0, import_node_path35.join)(resolved, ".."), JERVCODE_PACKAGE_ENTRY);
|
|
36139
|
+
if (exists(entry)) {
|
|
36140
|
+
return { file: opts.execPath ?? process.execPath, args: [entry, ...args], via: "node-entry" };
|
|
36141
|
+
}
|
|
36142
|
+
}
|
|
36143
|
+
const bin = resolved ?? "jervcode";
|
|
36144
|
+
if (platform2 === "win32") {
|
|
36145
|
+
return { file: "cmd.exe", args: ["/c", bin, ...args], via: resolved ? "cmd-shim" : "bare" };
|
|
36146
|
+
}
|
|
36147
|
+
return { file: bin, args: [...args], via: resolved ? "posix" : "bare" };
|
|
36148
|
+
}
|
|
36149
|
+
async function removeWorktreeNodeModulesViaHelper(wtPath, opts = {}) {
|
|
36150
|
+
const { cwd, timeoutMs = NODE_MODULES_CLEANUP_TIMEOUT_MS } = opts;
|
|
36151
|
+
const env = opts.env ?? process.env;
|
|
36152
|
+
const exec = opts.exec ?? execFileP;
|
|
36153
|
+
const candidates = jervcodeCandidatePaths(env, opts.home, opts.platform ?? process.platform);
|
|
36154
|
+
const plan = jervcodeExecFileArgs(["worktree-node-modules-cleanup", "--worktree", wtPath], opts);
|
|
36155
|
+
const execOptions = { timeout: timeoutMs, ...cwd ? { cwd } : {} };
|
|
36156
|
+
try {
|
|
36157
|
+
await exec(plan.file, plan.args, execOptions);
|
|
36158
|
+
return { ok: true };
|
|
36159
|
+
} catch (e) {
|
|
36160
|
+
const err = e;
|
|
36161
|
+
const base = formatJervCliSpawnFailure(err, plan, candidates);
|
|
36162
|
+
const stderr = typeof err.stderr === "string" ? err.stderr.trim() : "";
|
|
36163
|
+
const detail = stderr.split("\n")[0] || base;
|
|
36164
|
+
return { ok: false, error: `jervcode worktree-node-modules-cleanup failed: ${detail}` };
|
|
36165
|
+
}
|
|
36166
|
+
}
|
|
34777
36167
|
|
|
34778
36168
|
// src/worktree-evidence-archive.ts
|
|
34779
|
-
var
|
|
34780
|
-
var
|
|
36169
|
+
var import_node_fs39 = require("node:fs");
|
|
36170
|
+
var import_node_path36 = require("node:path");
|
|
34781
36171
|
var JERV_ARTIFACT_RUN_SCOPE_ENV_VARS = ["JERV_RUN_ID", ...SESSION_ID_ENV_VARS];
|
|
34782
36172
|
function sanitizeArchiveSegment(value, max = 80) {
|
|
34783
36173
|
const scrubbed = value.replace(/[^A-Za-z0-9._@+-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -34792,24 +36182,24 @@ function resolveArtifactRunScope(env = process.env) {
|
|
|
34792
36182
|
}
|
|
34793
36183
|
function defaultIsDirectory(path2) {
|
|
34794
36184
|
try {
|
|
34795
|
-
return (0,
|
|
36185
|
+
return (0, import_node_fs39.lstatSync)(path2).isDirectory();
|
|
34796
36186
|
} catch {
|
|
34797
36187
|
return false;
|
|
34798
36188
|
}
|
|
34799
36189
|
}
|
|
34800
36190
|
function archiveWorktreeJervArtifacts(args, deps = {}) {
|
|
34801
|
-
const exists = deps.exists ??
|
|
36191
|
+
const exists = deps.exists ?? import_node_fs39.existsSync;
|
|
34802
36192
|
const isDirectory = deps.isDirectory ?? defaultIsDirectory;
|
|
34803
|
-
const copyDir = deps.copyDir ?? ((from, to) => (0,
|
|
36193
|
+
const copyDir = deps.copyDir ?? ((from, to) => (0, import_node_fs39.cpSync)(from, to, { recursive: true, force: true }));
|
|
34804
36194
|
const mkdirp = deps.mkdirp ?? ((path2) => {
|
|
34805
|
-
(0,
|
|
36195
|
+
(0, import_node_fs39.mkdirSync)(path2, { recursive: true });
|
|
34806
36196
|
});
|
|
34807
36197
|
const env = deps.env ?? process.env;
|
|
34808
36198
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
34809
36199
|
const resolveRoot = deps.resolveArchiveRoot ?? repoRuntimeStatePath;
|
|
34810
36200
|
if (!args.primaryRoot?.trim()) return { status: "skipped", reason: "missing-primary-root" };
|
|
34811
36201
|
if (!args.worktreePath?.trim()) return { status: "skipped", reason: "missing-worktree-path" };
|
|
34812
|
-
const source = (0,
|
|
36202
|
+
const source = (0, import_node_path36.join)(args.worktreePath, ".jerv");
|
|
34813
36203
|
if (!exists(source)) return { status: "absent" };
|
|
34814
36204
|
if (!isDirectory(source)) return { status: "skipped", reason: "jerv-not-a-directory" };
|
|
34815
36205
|
const runScope = resolveArtifactRunScope(env);
|
|
@@ -34817,7 +36207,7 @@ function archiveWorktreeJervArtifacts(args, deps = {}) {
|
|
|
34817
36207
|
const stamp = now().toISOString().replace(/[:.]/g, "-");
|
|
34818
36208
|
const dest = resolveRoot(args.primaryRoot, "jerv-artifacts", runScope, branchSlug, stamp, ".jerv");
|
|
34819
36209
|
try {
|
|
34820
|
-
mkdirp((0,
|
|
36210
|
+
mkdirp((0, import_node_path36.dirname)(dest));
|
|
34821
36211
|
copyDir(source, dest);
|
|
34822
36212
|
if (!exists(dest)) return { status: "failed", error: `archive write left no directory at ${dest}` };
|
|
34823
36213
|
return { status: "archived", path: dest, runScope };
|
|
@@ -34826,14 +36216,14 @@ function archiveWorktreeJervArtifacts(args, deps = {}) {
|
|
|
34826
36216
|
}
|
|
34827
36217
|
}
|
|
34828
36218
|
function defaultStat(path2) {
|
|
34829
|
-
const st = (0,
|
|
36219
|
+
const st = (0, import_node_fs39.statSync)(path2);
|
|
34830
36220
|
return { mtimeMs: st.mtimeMs, size: st.size, isDirectory: () => st.isDirectory() };
|
|
34831
36221
|
}
|
|
34832
36222
|
function scanWorktreeTmpEvidence(worktreePath, newerThanMs, deps = {}) {
|
|
34833
|
-
const exists = deps.exists ??
|
|
36223
|
+
const exists = deps.exists ?? import_node_fs39.existsSync;
|
|
34834
36224
|
const stat4 = deps.stat ?? defaultStat;
|
|
34835
|
-
const readdir2 = deps.readdir ??
|
|
34836
|
-
const tmpRoot = (0,
|
|
36225
|
+
const readdir2 = deps.readdir ?? import_node_fs39.readdirSync;
|
|
36226
|
+
const tmpRoot = (0, import_node_path36.join)(worktreePath, "tmp");
|
|
34837
36227
|
if (!exists(tmpRoot)) return [];
|
|
34838
36228
|
const entries = [];
|
|
34839
36229
|
const walk2 = (dir) => {
|
|
@@ -34844,14 +36234,14 @@ function scanWorktreeTmpEvidence(worktreePath, newerThanMs, deps = {}) {
|
|
|
34844
36234
|
return;
|
|
34845
36235
|
}
|
|
34846
36236
|
for (const name of names) {
|
|
34847
|
-
const full = (0,
|
|
36237
|
+
const full = (0, import_node_path36.join)(dir, name);
|
|
34848
36238
|
let st;
|
|
34849
36239
|
try {
|
|
34850
36240
|
st = stat4(full);
|
|
34851
36241
|
} catch {
|
|
34852
36242
|
continue;
|
|
34853
36243
|
}
|
|
34854
|
-
const relPath = (0,
|
|
36244
|
+
const relPath = (0, import_node_path36.relative)(worktreePath, full).replace(/\\/g, "/");
|
|
34855
36245
|
if (st.isDirectory()) {
|
|
34856
36246
|
if (st.mtimeMs > newerThanMs) entries.push({ relPath, bytes: 0, mtimeMs: st.mtimeMs });
|
|
34857
36247
|
walk2(full);
|
|
@@ -34865,10 +36255,10 @@ function scanWorktreeTmpEvidence(worktreePath, newerThanMs, deps = {}) {
|
|
|
34865
36255
|
return entries;
|
|
34866
36256
|
}
|
|
34867
36257
|
function archiveWorktreeTmpArtifacts(args, deps = {}) {
|
|
34868
|
-
const exists = deps.exists ??
|
|
34869
|
-
const copyDir = deps.copyDir ?? ((from, to) => (0,
|
|
36258
|
+
const exists = deps.exists ?? import_node_fs39.existsSync;
|
|
36259
|
+
const copyDir = deps.copyDir ?? ((from, to) => (0, import_node_fs39.cpSync)(from, to, { recursive: true, force: true }));
|
|
34870
36260
|
const mkdirp = deps.mkdirp ?? ((path2) => {
|
|
34871
|
-
(0,
|
|
36261
|
+
(0, import_node_fs39.mkdirSync)(path2, { recursive: true });
|
|
34872
36262
|
});
|
|
34873
36263
|
const env = deps.env ?? process.env;
|
|
34874
36264
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -34876,7 +36266,7 @@ function archiveWorktreeTmpArtifacts(args, deps = {}) {
|
|
|
34876
36266
|
if (!args.primaryRoot?.trim()) return { status: "skipped", reason: "missing-primary-root" };
|
|
34877
36267
|
if (!args.worktreePath?.trim()) return { status: "skipped", reason: "missing-worktree-path" };
|
|
34878
36268
|
const scanned = scanWorktreeTmpEvidence(args.worktreePath, args.newerThanMs, deps);
|
|
34879
|
-
const source = (0,
|
|
36269
|
+
const source = (0, import_node_path36.join)(args.worktreePath, "tmp");
|
|
34880
36270
|
if (!scanned.length) return { status: "absent" };
|
|
34881
36271
|
if (!exists(source)) return { status: "absent" };
|
|
34882
36272
|
const runScope = resolveArtifactRunScope(env);
|
|
@@ -34884,7 +36274,7 @@ function archiveWorktreeTmpArtifacts(args, deps = {}) {
|
|
|
34884
36274
|
const stamp = now().toISOString().replace(/[:.]/g, "-");
|
|
34885
36275
|
const dest = resolveRoot(args.primaryRoot, "worktree-artifacts", runScope, branchSlug, stamp, "tmp");
|
|
34886
36276
|
try {
|
|
34887
|
-
mkdirp((0,
|
|
36277
|
+
mkdirp((0, import_node_path36.dirname)(dest));
|
|
34888
36278
|
copyDir(source, dest);
|
|
34889
36279
|
if (!exists(dest)) return { status: "failed", error: `archive write left no directory at ${dest}` };
|
|
34890
36280
|
const bytes = scanned.reduce((sum, e) => sum + e.bytes, 0);
|
|
@@ -34953,9 +36343,9 @@ function normPath2(p) {
|
|
|
34953
36343
|
return p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
34954
36344
|
}
|
|
34955
36345
|
function unlinkNodeModulesJunction(wtPath) {
|
|
34956
|
-
const nm = (0,
|
|
36346
|
+
const nm = (0, import_node_path37.join)(wtPath, "node_modules");
|
|
34957
36347
|
try {
|
|
34958
|
-
if ((0,
|
|
36348
|
+
if ((0, import_node_fs40.lstatSync)(nm).isSymbolicLink()) (0, import_node_fs40.rmdirSync)(nm);
|
|
34959
36349
|
return { ok: true };
|
|
34960
36350
|
} catch (e) {
|
|
34961
36351
|
if (e.code === "ENOENT") return { ok: true };
|
|
@@ -34963,11 +36353,11 @@ function unlinkNodeModulesJunction(wtPath) {
|
|
|
34963
36353
|
}
|
|
34964
36354
|
}
|
|
34965
36355
|
function defaultSleep2(ms) {
|
|
34966
|
-
return new Promise((
|
|
36356
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
34967
36357
|
}
|
|
34968
36358
|
async function removeResidueDirectory(wtPath, options = {}) {
|
|
34969
|
-
const { sleep: sleep2 = defaultSleep2, removeEmptyDir =
|
|
34970
|
-
const removeRecursive = options.removeRecursive ?? ((path2) => (0,
|
|
36359
|
+
const { sleep: sleep2 = defaultSleep2, removeEmptyDir = import_node_fs40.rmdirSync, exists = import_node_fs40.existsSync } = options;
|
|
36360
|
+
const removeRecursive = options.removeRecursive ?? ((path2) => (0, import_node_fs40.rmSync)(path2, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }));
|
|
34971
36361
|
const junction = unlinkNodeModulesJunction(wtPath);
|
|
34972
36362
|
if (!junction.ok) return junction;
|
|
34973
36363
|
try {
|
|
@@ -35167,6 +36557,10 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
|
|
|
35167
36557
|
})
|
|
35168
36558
|
};
|
|
35169
36559
|
}
|
|
36560
|
+
function isPrMergeWorktreePartial(cleanup) {
|
|
36561
|
+
const worktree = cleanup?.worktree;
|
|
36562
|
+
return Boolean(worktree?.path && worktree.status !== "removed" && worktree.status !== "preserved");
|
|
36563
|
+
}
|
|
35170
36564
|
function prMergeLocalCleanupExitCode(cleanup) {
|
|
35171
36565
|
return cleanup?.worktree?.status === "failed" || cleanup?.localBranch?.status === "failed" ? 1 : void 0;
|
|
35172
36566
|
}
|
|
@@ -35181,7 +36575,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
35181
36575
|
return report;
|
|
35182
36576
|
}
|
|
35183
36577
|
const execGit = options.execGit ?? (async (args) => (await execFileP("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
|
|
35184
|
-
const pathExists = options.pathExists ??
|
|
36578
|
+
const pathExists = options.pathExists ?? import_node_fs40.existsSync;
|
|
35185
36579
|
let afterWorktrees = [];
|
|
35186
36580
|
try {
|
|
35187
36581
|
afterWorktrees = parseGitWorktreePorcelain(await execGit(["worktree", "list", "--porcelain"]));
|
|
@@ -35273,6 +36667,23 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
35273
36667
|
report.localBranch = { name: branch, status: "not-attempted", reason: "archive-failed" };
|
|
35274
36668
|
return report;
|
|
35275
36669
|
}
|
|
36670
|
+
if (pathExists((0, import_node_path37.join)(wtPath, "node_modules"))) {
|
|
36671
|
+
const nmRemoved = await (options.removeRealNodeModules ?? ((p) => removeWorktreeNodeModulesViaHelper(p, { cwd: mainWorktreePath })))(wtPath);
|
|
36672
|
+
if (!nmRemoved.ok) {
|
|
36673
|
+
report.worktree = {
|
|
36674
|
+
path: wtPath,
|
|
36675
|
+
status: "failed",
|
|
36676
|
+
reason: "pre-clean-failed",
|
|
36677
|
+
error: `real-node_modules cleanup failed before worktree removal: ${nmRemoved.error}`,
|
|
36678
|
+
artifactsArchive,
|
|
36679
|
+
stageTeardown,
|
|
36680
|
+
tmpEvidenceCount: tmpEvidence.length,
|
|
36681
|
+
remediation: `jervcode worktree-node-modules-cleanup --worktree '${wtPath.replace(/'/g, "''")}'`
|
|
36682
|
+
};
|
|
36683
|
+
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-pre-clean-failed" };
|
|
36684
|
+
return report;
|
|
36685
|
+
}
|
|
36686
|
+
}
|
|
35276
36687
|
const preClean = await preCleanWorktreeForRemoval(wtPath, execGit);
|
|
35277
36688
|
if (!preClean.ok) {
|
|
35278
36689
|
report.worktree = {
|
|
@@ -35399,10 +36810,10 @@ function argvWantsJson2() {
|
|
|
35399
36810
|
return process.argv.some((a) => a === "--json" || a.startsWith("--json="));
|
|
35400
36811
|
}
|
|
35401
36812
|
function hubRoot() {
|
|
35402
|
-
const fromPkg = (0,
|
|
36813
|
+
const fromPkg = (0, import_node_path38.join)(__dirname, "..", "..");
|
|
35403
36814
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
35404
|
-
if ((0,
|
|
35405
|
-
if ((0,
|
|
36815
|
+
if ((0, import_node_fs41.existsSync)((0, import_node_path38.join)(fromPkg, marker))) return fromPkg;
|
|
36816
|
+
if ((0, import_node_fs41.existsSync)((0, import_node_path38.join)(process.cwd(), marker))) return process.cwd();
|
|
35406
36817
|
return null;
|
|
35407
36818
|
}
|
|
35408
36819
|
function ciAuditDeps() {
|
|
@@ -35414,8 +36825,8 @@ function ciAuditDeps() {
|
|
|
35414
36825
|
getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise)),
|
|
35415
36826
|
readSeedFile: (path2) => {
|
|
35416
36827
|
if (!root) return null;
|
|
35417
|
-
const fullPath = (0,
|
|
35418
|
-
return (0,
|
|
36828
|
+
const fullPath = (0, import_node_path38.join)(root, path2);
|
|
36829
|
+
return (0, import_node_fs41.existsSync)(fullPath) ? (0, import_node_fs41.readFileSync)(fullPath, "utf8") : null;
|
|
35419
36830
|
}
|
|
35420
36831
|
};
|
|
35421
36832
|
}
|
|
@@ -35631,7 +37042,7 @@ function registerCollaborationCommands(program3) {
|
|
|
35631
37042
|
repo: targetRepo2,
|
|
35632
37043
|
labels: extraLabels.length ? extraLabels : void 0
|
|
35633
37044
|
});
|
|
35634
|
-
if (o.parent !== void 0) parseIssueRef(o.parent
|
|
37045
|
+
if (o.parent !== void 0) parseIssueRef(o.parent);
|
|
35635
37046
|
} catch (e) {
|
|
35636
37047
|
return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
|
|
35637
37048
|
}
|
|
@@ -35788,14 +37199,14 @@ function registerCollaborationCommands(program3) {
|
|
|
35788
37199
|
jsonParity(issue.command("link-child <parent> <child>").description("link an existing issue as a native sub-issue of a parent and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)")).action(async (parentRef, childRef, o) => {
|
|
35789
37200
|
const defaultRepo = await resolveRepo(o.repo);
|
|
35790
37201
|
try {
|
|
35791
|
-
parseIssueRef(parentRef
|
|
35792
|
-
parseIssueRef(childRef
|
|
37202
|
+
parseIssueRef(parentRef);
|
|
37203
|
+
parseIssueRef(childRef);
|
|
35793
37204
|
const result = await linkSubIssue(ghRunner2, parentRef, childRef, defaultRepo);
|
|
35794
37205
|
console.log(JSON.stringify(result));
|
|
35795
37206
|
} catch (e) {
|
|
35796
37207
|
let conflict;
|
|
35797
37208
|
try {
|
|
35798
|
-
const child2 = parseIssueRef(childRef
|
|
37209
|
+
const child2 = parseIssueRef(childRef);
|
|
35799
37210
|
const childRepo = child2.repo ?? defaultRepo;
|
|
35800
37211
|
if (childRepo) conflict = await classifyReparentFailure(e, ghRunner2, childRepo, child2.number, parentRef);
|
|
35801
37212
|
} catch {
|
|
@@ -36000,10 +37411,20 @@ ${list}`);
|
|
|
36000
37411
|
}
|
|
36001
37412
|
body = normalizeClosingDirectives(body);
|
|
36002
37413
|
body = rewriteNegatedClosingPhrases(body);
|
|
37414
|
+
const prose = rewriteCloseKeywordProse(body);
|
|
37415
|
+
if (prose.rewritten.length) {
|
|
37416
|
+
body = prose.text;
|
|
37417
|
+
process.stderr.write(
|
|
37418
|
+
`pr create: WARNING \u2014 rewrote close-keyword prose to "part of #N" for ${prose.rewritten.map((n) => `#${n}`).join(", ")}: GitHub parses close/fix/resolve + #N even mid-sentence (#6039). Keep closing keywords on a dedicated "Closes #N" line.
|
|
37419
|
+
`
|
|
37420
|
+
);
|
|
37421
|
+
}
|
|
36003
37422
|
const docsCheck = await checkDocsIndexAtHead({ repo: o.repo, head: o.head }, createPrCreateDocsIndexDeps());
|
|
36004
37423
|
if (docsCheck && !docsCheck.ok) {
|
|
36005
37424
|
return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
|
|
36006
37425
|
}
|
|
37426
|
+
const claimRefusal = await prCreateClaimRefusal(body, o.repo);
|
|
37427
|
+
if (claimRefusal) return fail(claimRefusal);
|
|
36007
37428
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
|
|
36008
37429
|
if (isGhCreateRateLimited(created)) {
|
|
36009
37430
|
console.log(JSON.stringify(created));
|
|
@@ -36018,7 +37439,8 @@ ${list}`);
|
|
|
36018
37439
|
], [
|
|
36019
37440
|
"--head and --base default to the current branch and the repo default; only pass them to override.",
|
|
36020
37441
|
"Use --body-file for multiline PR bodies instead of shell-escaped inline markdown.",
|
|
36021
|
-
"Write that file under .jerv/ inside the host workspace (#4405); host policy governs untracked files."
|
|
37442
|
+
"Write that file under .jerv/ inside the host workspace (#4405); host policy governs untracked files.",
|
|
37443
|
+
'Keep closing keywords on a dedicated line ("Closes #N") \u2014 GitHub parses close/fix/resolve + #N even mid-sentence, so pr create rewrites such prose to "part of #N" (#6039).'
|
|
36022
37444
|
]);
|
|
36023
37445
|
pr.command("view <ref>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347). --comments folds in every comment; --context also adds linkedIssues (the issues it closes/references, #2894)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "state,baseRefName,mergeCommit"').option("--comments", "include every comment (body + comments in one call) \u2014 read the whole PR before landing it (#2894)").option("--context", "full working context in one call: implies --comments and also adds linkedIssues (the issues the PR closes/references) (#2894)").action(async (ref, o) => {
|
|
36024
37446
|
let parsed;
|
|
@@ -36046,11 +37468,11 @@ ${list}`);
|
|
|
36046
37468
|
}
|
|
36047
37469
|
});
|
|
36048
37470
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
36049
|
-
const wfDir = (0,
|
|
36050
|
-
if (!(0,
|
|
36051
|
-
return (0,
|
|
37471
|
+
const wfDir = (0, import_node_path38.join)(cwd, ".github", "workflows");
|
|
37472
|
+
if (!(0, import_node_fs41.existsSync)(wfDir)) return [];
|
|
37473
|
+
return (0, import_node_fs41.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
|
|
36052
37474
|
try {
|
|
36053
|
-
return workflowReportsPrChecks((0,
|
|
37475
|
+
return workflowReportsPrChecks((0, import_node_fs41.readFileSync)((0, import_node_path38.join)(wfDir, name), "utf8"));
|
|
36054
37476
|
} catch {
|
|
36055
37477
|
return true;
|
|
36056
37478
|
}
|
|
@@ -36208,7 +37630,7 @@ ${list}`);
|
|
|
36208
37630
|
// #5400: after grace, name "GitHub delivered zero runs" instead of burning the full budget as pending.
|
|
36209
37631
|
pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr checks-wait", number, repo),
|
|
36210
37632
|
baseBranch,
|
|
36211
|
-
sleep: (ms) => new Promise((
|
|
37633
|
+
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
|
|
36212
37634
|
log: (message) => console.warn(message),
|
|
36213
37635
|
timeoutMs: Math.max(1, budgetMs - (Date.now() - waitStarted)),
|
|
36214
37636
|
// Liveness on stderr, one line per poll. A silent bounded wait is indistinguishable from a hang, and
|
|
@@ -36312,7 +37734,7 @@ ${list}`);
|
|
|
36312
37734
|
// #5400: same zero-runs delivery probe as checks-wait — do not burn the land budget on silence.
|
|
36313
37735
|
pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr land", prNumber, repo),
|
|
36314
37736
|
baseBranch: "development",
|
|
36315
|
-
sleep: (ms) => new Promise((
|
|
37737
|
+
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
|
|
36316
37738
|
log: (message) => console.warn(message),
|
|
36317
37739
|
// `pr land` inherits the same (raised) checks budget, so it needs the same liveness — otherwise the
|
|
36318
37740
|
// 30m wait is SILENT and reads exactly like the hang #2940 was filed about, only three times longer.
|
|
@@ -36358,7 +37780,7 @@ ${list}`);
|
|
|
36358
37780
|
} else {
|
|
36359
37781
|
lastFailure = void 0;
|
|
36360
37782
|
}
|
|
36361
|
-
await new Promise((
|
|
37783
|
+
await new Promise((resolve7) => setTimeout(resolve7, PR_LAND_POLL_MS));
|
|
36362
37784
|
}
|
|
36363
37785
|
if (lastFailure) {
|
|
36364
37786
|
throw new Error(
|
|
@@ -36407,7 +37829,7 @@ ${list}`);
|
|
|
36407
37829
|
else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
36408
37830
|
if (result.status === "failed") process.exitCode = 1;
|
|
36409
37831
|
});
|
|
36410
|
-
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
|
|
37832
|
+
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
|
|
36411
37833
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
36412
37834
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
36413
37835
|
if (o.disableAuto) {
|
|
@@ -36445,7 +37867,7 @@ ${list}`);
|
|
|
36445
37867
|
const mergeSquashBody = squashBodyTextForMerge(
|
|
36446
37868
|
closingGuardInput,
|
|
36447
37869
|
method === "--squash",
|
|
36448
|
-
o.squashBodyFile ? (0,
|
|
37870
|
+
o.squashBodyFile ? (0, import_node_fs41.readFileSync)(o.squashBodyFile, "utf8") : void 0
|
|
36449
37871
|
);
|
|
36450
37872
|
if (!o.squashBodyFile) warnSquashBodyClosingStrip("pr merge", closingGuardInput, mergeSquashBody);
|
|
36451
37873
|
const closingGuardVerdict = evaluateClosingGuard(closingGuardInput, {
|
|
@@ -36471,6 +37893,7 @@ ${list}`);
|
|
|
36471
37893
|
const ciHeadRef = repoForPostCleanup ? await prHeadRefForCiProbe(number, repoForPostCleanup) : void 0;
|
|
36472
37894
|
const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo, ciHeadRef);
|
|
36473
37895
|
if (o.wait) {
|
|
37896
|
+
console.warn(`pr merge: --wait can hold the shell up to the full ${PR_CHECKS_TIMEOUT_MS / 6e4}m budget plus the merge \u2014 run under a shell timeout above that budget or as a background/monitor task (#6027)`);
|
|
36474
37897
|
const repo = await requireRepo(o.repo);
|
|
36475
37898
|
const budgetMs = PR_CHECKS_TIMEOUT_MS;
|
|
36476
37899
|
const waitStarted = Date.now();
|
|
@@ -36495,7 +37918,7 @@ ${list}`);
|
|
|
36495
37918
|
checksDiagnosticCommand: `gh pr checks ${number} --repo ${repo}`,
|
|
36496
37919
|
pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr merge --wait", number, repo),
|
|
36497
37920
|
baseBranch,
|
|
36498
|
-
sleep: (ms) => new Promise((
|
|
37921
|
+
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
|
|
36499
37922
|
log: (message) => console.warn(message),
|
|
36500
37923
|
timeoutMs: Math.max(1, budgetMs - (Date.now() - waitStarted)),
|
|
36501
37924
|
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr merge: --wait checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
@@ -36549,7 +37972,7 @@ ${list}`);
|
|
|
36549
37972
|
}
|
|
36550
37973
|
if (!repoForPostCleanup) throw e;
|
|
36551
37974
|
console.warn(`pr merge: gh GraphQL rate-limited \u2014 merging PR #${number} via REST PUT instead (#4588).`);
|
|
36552
|
-
const commitMessage = bodyFile ? (0,
|
|
37975
|
+
const commitMessage = bodyFile ? (0, import_node_fs41.readFileSync)(bodyFile, "utf8") : void 0;
|
|
36553
37976
|
await defaultGitHubClient().rest("PUT", `repos/${repoForPostCleanup}/pulls/${number}/merge`, {
|
|
36554
37977
|
body: { merge_method: method.slice(2), ...commitMessage ? { commit_message: commitMessage } : {} },
|
|
36555
37978
|
timeoutMs: GH_MUTATION_TIMEOUT_MS
|
|
@@ -36637,7 +38060,7 @@ ${list}`);
|
|
|
36637
38060
|
preserveWorktree: o.preserveWorktree,
|
|
36638
38061
|
gcAcknowledged: o.gc,
|
|
36639
38062
|
expectedHeadOid: headRefOid,
|
|
36640
|
-
pathExists: (p) => (0,
|
|
38063
|
+
pathExists: (p) => (0, import_node_fs41.existsSync)(p),
|
|
36641
38064
|
// #5899: pin cleanup git calls to the main checkout — the task worktree this process may be
|
|
36642
38065
|
// standing in is removed mid-cleanup, so a cwd-relative invocation fails with
|
|
36643
38066
|
// 'fatal: not a git repository' and leaves a spurious partial-cleanup exit.
|
|
@@ -36664,12 +38087,13 @@ ${list}`);
|
|
|
36664
38087
|
execGit: async (args) => (await execFileP("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
36665
38088
|
branchExists: (b) => remoteBranchExists2(b, { cwd: primaryRoot })
|
|
36666
38089
|
});
|
|
36667
|
-
const
|
|
38090
|
+
const worktree = localCleanup?.worktree;
|
|
38091
|
+
const worktreePartial = isPrMergeWorktreePartial(localCleanup) && worktree?.path ? {
|
|
36668
38092
|
kind: "worktree-directory",
|
|
36669
|
-
path:
|
|
36670
|
-
reason:
|
|
36671
|
-
error:
|
|
36672
|
-
remediation:
|
|
38093
|
+
path: worktree.path,
|
|
38094
|
+
reason: worktree.reason ?? worktree.status,
|
|
38095
|
+
error: worktree.error ?? worktree.residueError,
|
|
38096
|
+
remediation: worktree.remediation ?? `Remove-Item -LiteralPath '${worktree.path.replace(/'/g, "''")}' -Recurse -Force`
|
|
36673
38097
|
} : void 0;
|
|
36674
38098
|
const partialCleanup = [
|
|
36675
38099
|
...worktreePartial ? [worktreePartial] : [],
|
|
@@ -36752,7 +38176,7 @@ ${list}`);
|
|
|
36752
38176
|
}
|
|
36753
38177
|
|
|
36754
38178
|
// src/command-register-developer.ts
|
|
36755
|
-
var
|
|
38179
|
+
var import_node_fs46 = require("node:fs");
|
|
36756
38180
|
|
|
36757
38181
|
// src/whoami.ts
|
|
36758
38182
|
async function resolveWhoami(deps) {
|
|
@@ -36782,7 +38206,7 @@ async function resolveWhoami(deps) {
|
|
|
36782
38206
|
}
|
|
36783
38207
|
|
|
36784
38208
|
// src/command-register-developer.ts
|
|
36785
|
-
var
|
|
38209
|
+
var import_node_path43 = require("node:path");
|
|
36786
38210
|
|
|
36787
38211
|
// src/wave-land.ts
|
|
36788
38212
|
function planWaveLand(prs) {
|
|
@@ -36813,7 +38237,7 @@ async function executeWaveLand(plan, deps) {
|
|
|
36813
38237
|
}
|
|
36814
38238
|
|
|
36815
38239
|
// src/box-commands.ts
|
|
36816
|
-
var
|
|
38240
|
+
var import_node_fs42 = require("node:fs");
|
|
36817
38241
|
|
|
36818
38242
|
// src/box.ts
|
|
36819
38243
|
var BOX_KEYS = {
|
|
@@ -37014,11 +38438,16 @@ function registerBoxCommands(program3) {
|
|
|
37014
38438
|
await failGraceful("runtime box get: --script requires --ssh (it writes the ssh connect recipe)");
|
|
37015
38439
|
return;
|
|
37016
38440
|
}
|
|
37017
|
-
|
|
37018
|
-
|
|
37019
|
-
|
|
37020
|
-
console.log(
|
|
37021
|
-
|
|
38441
|
+
const wroteScript = o.ssh && o.script ? o.script : null;
|
|
38442
|
+
if (wroteScript) (0, import_node_fs42.writeFileSync)(wroteScript, sshRecipeScript(found), "utf8");
|
|
38443
|
+
if (o.json) {
|
|
38444
|
+
console.log(JSON.stringify({
|
|
38445
|
+
box: found,
|
|
38446
|
+
incomplete,
|
|
38447
|
+
...wroteScript ? { script: { path: wroteScript, run: `bash "${wroteScript}"` } } : {}
|
|
38448
|
+
}, null, 2));
|
|
38449
|
+
} else if (wroteScript) console.log(`wrote ${wroteScript} \u2014 run: bash "${wroteScript}"`);
|
|
38450
|
+
else if (o.ssh) console.log(`${formatSshRecipe(found)}
|
|
37022
38451
|
${SSH_RECIPE_AGENT_NOTE}`);
|
|
37023
38452
|
else console.log(formatBoxTable([found]));
|
|
37024
38453
|
if (!o.json) warnIncomplete2(incomplete);
|
|
@@ -37031,28 +38460,28 @@ ${SSH_RECIPE_AGENT_NOTE}`);
|
|
|
37031
38460
|
|
|
37032
38461
|
// src/dist-drift.ts
|
|
37033
38462
|
var import_node_child_process15 = require("node:child_process");
|
|
37034
|
-
var
|
|
37035
|
-
var
|
|
37036
|
-
var
|
|
37037
|
-
var
|
|
38463
|
+
var import_node_crypto12 = require("node:crypto");
|
|
38464
|
+
var import_node_fs44 = require("node:fs");
|
|
38465
|
+
var import_node_os19 = require("node:os");
|
|
38466
|
+
var import_node_path40 = require("node:path");
|
|
37038
38467
|
|
|
37039
38468
|
// ../scripts/distribution-digest.mjs
|
|
37040
|
-
var
|
|
37041
|
-
var
|
|
37042
|
-
var
|
|
38469
|
+
var import_node_crypto11 = require("node:crypto");
|
|
38470
|
+
var import_node_fs43 = require("node:fs");
|
|
38471
|
+
var import_node_path39 = require("node:path");
|
|
37043
38472
|
var slash = (value) => value.replaceAll("\\", "/");
|
|
37044
38473
|
function repoPath(root, declaredPath, label) {
|
|
37045
|
-
const absoluteRoot = (0,
|
|
37046
|
-
const target = (0,
|
|
37047
|
-
if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${
|
|
38474
|
+
const absoluteRoot = (0, import_node_path39.resolve)(root);
|
|
38475
|
+
const target = (0, import_node_path39.resolve)(root, declaredPath);
|
|
38476
|
+
if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${import_node_path39.sep}`)) {
|
|
37048
38477
|
throw new Error(`${label} ${declaredPath} escapes the repository root`);
|
|
37049
38478
|
}
|
|
37050
38479
|
return target;
|
|
37051
38480
|
}
|
|
37052
38481
|
function digestFiles(files) {
|
|
37053
|
-
const hash = (0,
|
|
38482
|
+
const hash = (0, import_node_crypto11.createHash)("sha256");
|
|
37054
38483
|
for (const file of [...files].sort((a, b) => a.relative.localeCompare(b.relative))) {
|
|
37055
|
-
const content = file.stat.isSymbolicLink() ? Buffer.from((0,
|
|
38484
|
+
const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs43.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs43.readFileSync)(file.absolute);
|
|
37056
38485
|
hash.update(file.relative, "utf8");
|
|
37057
38486
|
hash.update("\0");
|
|
37058
38487
|
hash.update(file.stat.isSymbolicLink() ? "symlink" : "file", "utf8");
|
|
@@ -37067,8 +38496,8 @@ function digestFiles(files) {
|
|
|
37067
38496
|
function digestPackedFiles(packageRoot, packedFiles) {
|
|
37068
38497
|
return digestFiles(packedFiles.map((path2) => {
|
|
37069
38498
|
const absolute = repoPath(packageRoot, path2, "packed artifact identity path");
|
|
37070
|
-
if (!(0,
|
|
37071
|
-
return { absolute, relative: slash(path2), stat: (0,
|
|
38499
|
+
if (!(0, import_node_fs43.existsSync)(absolute)) throw new Error(`packed artifact identity path ${path2} does not exist`);
|
|
38500
|
+
return { absolute, relative: slash(path2), stat: (0, import_node_fs43.lstatSync)(absolute) };
|
|
37072
38501
|
}));
|
|
37073
38502
|
}
|
|
37074
38503
|
|
|
@@ -37080,7 +38509,7 @@ var DIST_ARTIFACTS = [
|
|
|
37080
38509
|
];
|
|
37081
38510
|
var BOM_DIST_TREE_ID = "mmi-cli-dist";
|
|
37082
38511
|
var ABSENT = "absent";
|
|
37083
|
-
var sha256 = (bytes) => `sha256:${(0,
|
|
38512
|
+
var sha256 = (bytes) => `sha256:${(0, import_node_crypto12.createHash)("sha256").update(bytes).digest("hex")}`;
|
|
37084
38513
|
function artifactDrift(path2, committedBytes, rebuiltBytes) {
|
|
37085
38514
|
const committed = committedBytes ? sha256(committedBytes) : ABSENT;
|
|
37086
38515
|
const rebuiltExpected = rebuiltBytes ? sha256(rebuiltBytes) : ABSENT;
|
|
@@ -37152,13 +38581,13 @@ function renderDistDriftReceipt(receipt) {
|
|
|
37152
38581
|
return lines;
|
|
37153
38582
|
}
|
|
37154
38583
|
function readOrNull(path2) {
|
|
37155
|
-
return (0,
|
|
38584
|
+
return (0, import_node_fs44.existsSync)(path2) ? (0, import_node_fs44.readFileSync)(path2) : null;
|
|
37156
38585
|
}
|
|
37157
38586
|
function walkFiles(root) {
|
|
37158
38587
|
const files = [];
|
|
37159
38588
|
const walk2 = (directory) => {
|
|
37160
|
-
for (const entry of (0,
|
|
37161
|
-
const child2 = (0,
|
|
38589
|
+
for (const entry of (0, import_node_fs44.readdirSync)(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
38590
|
+
const child2 = (0, import_node_path40.join)(directory, entry.name);
|
|
37162
38591
|
if (entry.isDirectory()) walk2(child2);
|
|
37163
38592
|
else files.push(child2);
|
|
37164
38593
|
}
|
|
@@ -37168,10 +38597,10 @@ function walkFiles(root) {
|
|
|
37168
38597
|
}
|
|
37169
38598
|
function bomPathFor(root) {
|
|
37170
38599
|
try {
|
|
37171
|
-
const registry2 = JSON.parse((0,
|
|
37172
|
-
return (0,
|
|
38600
|
+
const registry2 = JSON.parse((0, import_node_fs44.readFileSync)((0, import_node_path40.join)(root, "surfaces.json"), "utf8"));
|
|
38601
|
+
return (0, import_node_path40.join)(root, registry2?.sharedAgentCore?.releaseMetadata?.bomPath ?? "distribution-bom.json");
|
|
37173
38602
|
} catch {
|
|
37174
|
-
return (0,
|
|
38603
|
+
return (0, import_node_path40.join)(root, "distribution-bom.json");
|
|
37175
38604
|
}
|
|
37176
38605
|
}
|
|
37177
38606
|
function rebuildTo(packageRoot, outDir) {
|
|
@@ -37184,35 +38613,35 @@ function rebuildTo(packageRoot, outDir) {
|
|
|
37184
38613
|
});
|
|
37185
38614
|
}
|
|
37186
38615
|
function runDistStatus(root) {
|
|
37187
|
-
const stage = (0,
|
|
38616
|
+
const stage = (0, import_node_fs44.mkdtempSync)((0, import_node_path40.join)((0, import_node_os19.tmpdir)(), "mmi-dist-drift-"));
|
|
37188
38617
|
let overlayCount = 0;
|
|
37189
38618
|
try {
|
|
37190
|
-
const cliOut = (0,
|
|
37191
|
-
const hubOut = (0,
|
|
37192
|
-
rebuildTo((0,
|
|
37193
|
-
rebuildTo((0,
|
|
38619
|
+
const cliOut = (0, import_node_path40.join)(stage, "cli-dist");
|
|
38620
|
+
const hubOut = (0, import_node_path40.join)(stage, "hub-dist");
|
|
38621
|
+
rebuildTo((0, import_node_path40.join)(root, "cli"), cliOut);
|
|
38622
|
+
rebuildTo((0, import_node_path40.join)(root, "updater"), hubOut);
|
|
37194
38623
|
const outDirFor = (packageDir) => packageDir === "cli" ? cliOut : hubOut;
|
|
37195
38624
|
const rebuilt = (path2) => {
|
|
37196
38625
|
const spec = DIST_ARTIFACTS.find((entry) => entry.path === path2);
|
|
37197
|
-
return spec ? readOrNull((0,
|
|
38626
|
+
return spec ? readOrNull((0, import_node_path40.join)(outDirFor(spec.packageDir), spec.output)) : null;
|
|
37198
38627
|
};
|
|
37199
|
-
const committed = (path2) => readOrNull((0,
|
|
37200
|
-
const tree = (path2) => readOrNull((0,
|
|
37201
|
-
const distRoot = (0,
|
|
37202
|
-
const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0,
|
|
37203
|
-
const bom = JSON.parse((0,
|
|
38628
|
+
const committed = (path2) => readOrNull((0, import_node_path40.join)(root, path2));
|
|
38629
|
+
const tree = (path2) => readOrNull((0, import_node_path40.join)(root, path2));
|
|
38630
|
+
const distRoot = (0, import_node_path40.join)(root, "cli", "dist");
|
|
38631
|
+
const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0, import_node_path40.relative)(distRoot, absolute).replaceAll("\\", "/")}`);
|
|
38632
|
+
const bom = JSON.parse((0, import_node_fs44.readFileSync)(bomPathFor(root), "utf8"));
|
|
37204
38633
|
const digest = (entries) => {
|
|
37205
|
-
const overlay = (0,
|
|
38634
|
+
const overlay = (0, import_node_path40.join)(stage, `overlay-${overlayCount++}`);
|
|
37206
38635
|
for (const entry of entries) {
|
|
37207
|
-
const target = (0,
|
|
37208
|
-
(0,
|
|
37209
|
-
(0,
|
|
38636
|
+
const target = (0, import_node_path40.join)(overlay, entry.path);
|
|
38637
|
+
(0, import_node_fs44.mkdirSync)((0, import_node_path40.dirname)(target), { recursive: true });
|
|
38638
|
+
(0, import_node_fs44.writeFileSync)(target, entry.bytes);
|
|
37210
38639
|
}
|
|
37211
38640
|
return digestPackedFiles(overlay, entries.map((entry) => entry.path));
|
|
37212
38641
|
};
|
|
37213
38642
|
return computeDistDriftReceipt({ committed, tree, rebuilt, distTree, bom, digest });
|
|
37214
38643
|
} finally {
|
|
37215
|
-
(0,
|
|
38644
|
+
(0, import_node_fs44.rmSync)(stage, { recursive: true, force: true });
|
|
37216
38645
|
}
|
|
37217
38646
|
}
|
|
37218
38647
|
|
|
@@ -37284,7 +38713,7 @@ function registerEdgeCommands(program3) {
|
|
|
37284
38713
|
|
|
37285
38714
|
// src/schedules-lift-command.ts
|
|
37286
38715
|
var import_promises8 = require("node:fs/promises");
|
|
37287
|
-
var
|
|
38716
|
+
var import_node_path41 = require("node:path");
|
|
37288
38717
|
var DEFAULT_WORKFLOWS_DIR = ".github/workflows";
|
|
37289
38718
|
var SCHEDULE_REPO_RE = /^[A-Za-z0-9_.-]+$/;
|
|
37290
38719
|
var SchedulesLiftUsageError = class extends Error {
|
|
@@ -37311,7 +38740,7 @@ async function readWorkflowFiles(dir) {
|
|
|
37311
38740
|
const files = [];
|
|
37312
38741
|
for (const name of names.sort()) {
|
|
37313
38742
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
37314
|
-
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises8.readFile)((0,
|
|
38743
|
+
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises8.readFile)((0, import_node_path41.join)(dir, name), "utf8") });
|
|
37315
38744
|
}
|
|
37316
38745
|
return files;
|
|
37317
38746
|
}
|
|
@@ -37394,8 +38823,8 @@ function registerSchedulesLiftCommand(program3, deps = {}) {
|
|
|
37394
38823
|
|
|
37395
38824
|
// src/spawn-policy-core.ts
|
|
37396
38825
|
var import_node_child_process16 = require("node:child_process");
|
|
37397
|
-
var
|
|
37398
|
-
var
|
|
38826
|
+
var import_node_fs45 = require("node:fs");
|
|
38827
|
+
var import_node_path42 = require("node:path");
|
|
37399
38828
|
var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
|
|
37400
38829
|
var CALL_SOURCE = String.raw`(^|[^.\w$])(${SPAWNERS.join("|")})\s*\(`;
|
|
37401
38830
|
var SOURCE_EXT = /\.(ts|mts|cts|js|mjs|cjs)$/;
|
|
@@ -37481,7 +38910,7 @@ function runSpawnPolicy(root) {
|
|
|
37481
38910
|
for (const file of files) {
|
|
37482
38911
|
let raw;
|
|
37483
38912
|
try {
|
|
37484
|
-
raw = (0,
|
|
38913
|
+
raw = (0, import_node_fs45.readFileSync)((0, import_node_path42.join)(root, file), "utf8");
|
|
37485
38914
|
} catch {
|
|
37486
38915
|
continue;
|
|
37487
38916
|
}
|
|
@@ -37501,19 +38930,19 @@ function runSpawnPolicy(root) {
|
|
|
37501
38930
|
function registerDeveloperCommands(program3) {
|
|
37502
38931
|
const rules = program3.command("rules").description("org-managed .gitignore delivery");
|
|
37503
38932
|
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) => {
|
|
37504
|
-
const path2 = (0,
|
|
37505
|
-
const current = (0,
|
|
38933
|
+
const path2 = (0, import_node_path43.join)(process.cwd(), ".gitignore");
|
|
38934
|
+
const current = (0, import_node_fs46.existsSync)(path2) ? (0, import_node_fs46.readFileSync)(path2, "utf8") : null;
|
|
37506
38935
|
const plan = planManagedGitignore(current);
|
|
37507
38936
|
const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
37508
38937
|
if (opts.json) {
|
|
37509
|
-
if (opts.write && plan.changed) (0,
|
|
38938
|
+
if (opts.write && plan.changed) (0, import_node_fs46.writeFileSync)(path2, plan.content, "utf8");
|
|
37510
38939
|
console.log(JSON.stringify(plan, null, 2));
|
|
37511
38940
|
if (!opts.write && plan.changed) process.exitCode = 1;
|
|
37512
38941
|
return;
|
|
37513
38942
|
}
|
|
37514
38943
|
if (opts.write) {
|
|
37515
38944
|
if (plan.changed) {
|
|
37516
|
-
(0,
|
|
38945
|
+
(0, import_node_fs46.writeFileSync)(path2, plan.content, "utf8");
|
|
37517
38946
|
console.log(`mmi-cli devops org rules gitignore: updated .gitignore (${drift})`);
|
|
37518
38947
|
} else {
|
|
37519
38948
|
console.log("mmi-cli devops org rules gitignore: up to date");
|
|
@@ -37771,11 +39200,11 @@ var defaultDeps = {
|
|
|
37771
39200
|
// #5813: the caller supplies that command line. `process.argv` is NOT it — the house shim splices
|
|
37772
39201
|
// the house root out in place, so re-reading it here re-ran the removed flat alias (`release …`
|
|
37773
39202
|
// instead of `devops release …`) and the converged child refused with exit 2.
|
|
37774
|
-
reexec: (argv) => new Promise((
|
|
39203
|
+
reexec: (argv) => new Promise((resolve7) => {
|
|
37775
39204
|
const args = [...argv];
|
|
37776
39205
|
const child2 = isWin2 ? (0, import_node_child_process17.spawn)("cmd.exe", ["/c", "mmi-cli", ...args], { stdio: "inherit", windowsHide: true }) : (0, import_node_child_process17.spawn)("mmi-cli", args, { stdio: "inherit", windowsHide: true });
|
|
37777
|
-
child2.on("exit", (code) =>
|
|
37778
|
-
child2.on("error", () =>
|
|
39206
|
+
child2.on("exit", (code) => resolve7(code ?? 1));
|
|
39207
|
+
child2.on("error", () => resolve7(1));
|
|
37779
39208
|
}),
|
|
37780
39209
|
// stderr only — stdout may be carrying --json output.
|
|
37781
39210
|
warn: (line) => console.error(line)
|
|
@@ -37817,7 +39246,7 @@ function clean3(out) {
|
|
|
37817
39246
|
return out.trim();
|
|
37818
39247
|
}
|
|
37819
39248
|
function sleeper(deps) {
|
|
37820
|
-
return deps.sleep ?? ((ms) => new Promise((
|
|
39249
|
+
return deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
37821
39250
|
}
|
|
37822
39251
|
function normalizeHotfixVersion(input) {
|
|
37823
39252
|
const m = /^v?(\d+\.\d+\.\d+)$/.exec(input.trim());
|
|
@@ -38846,8 +40275,8 @@ async function announceRelease(deps, args) {
|
|
|
38846
40275
|
}
|
|
38847
40276
|
|
|
38848
40277
|
// src/train-commands.ts
|
|
38849
|
-
var
|
|
38850
|
-
var
|
|
40278
|
+
var import_node_fs47 = require("node:fs");
|
|
40279
|
+
var import_node_path44 = require("node:path");
|
|
38851
40280
|
var RELEASE_BUMP_INTENTS = ["major", "minor", "patch"];
|
|
38852
40281
|
function resolveReleaseBumpIntent(raw) {
|
|
38853
40282
|
const intent = typeof raw === "string" ? raw.trim() : "";
|
|
@@ -38858,7 +40287,7 @@ function resolveReleaseBumpIntent(raw) {
|
|
|
38858
40287
|
}
|
|
38859
40288
|
function readRepoVersion() {
|
|
38860
40289
|
try {
|
|
38861
|
-
return JSON.parse((0,
|
|
40290
|
+
return JSON.parse((0, import_node_fs47.readFileSync)((0, import_node_path44.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
38862
40291
|
} catch {
|
|
38863
40292
|
return void 0;
|
|
38864
40293
|
}
|
|
@@ -39041,6 +40470,7 @@ function renderReleaseResume(r) {
|
|
|
39041
40470
|
if (r.devRollForward) lines.push(` development: ${r.devRollForward.note}`);
|
|
39042
40471
|
if (r.rcAlignment) lines.push(` rc: ${r.rcAlignment.note}`);
|
|
39043
40472
|
if (r.checkout) lines.push(` checkout: ${r.checkout.note}`);
|
|
40473
|
+
if (r.ledger) lines.push(...formatReleaseLedgerReport(r.ledger).map((l, i) => i === 0 ? ` ${l}` : ` ${l}`));
|
|
39044
40474
|
return lines.join("\n");
|
|
39045
40475
|
}
|
|
39046
40476
|
function renderReleaseAbort(r) {
|
|
@@ -39120,6 +40550,27 @@ async function runPostReleaseJervDoctor(repo) {
|
|
|
39120
40550
|
};
|
|
39121
40551
|
}
|
|
39122
40552
|
}
|
|
40553
|
+
function buildReleaseVerdict(commandName, result, projectInfoSync) {
|
|
40554
|
+
const alignmentPending = result.devRollForward?.status === "pr-pending" || result.rcAlignment?.status === "pr-pending";
|
|
40555
|
+
const followUpStatus = deriveTrainFollowUpStatus({
|
|
40556
|
+
projectInfo: projectInfoSync && "error" in projectInfoSync ? "failure" : "ok",
|
|
40557
|
+
deploy: deployFollowUpOutcome(result.deployStatus),
|
|
40558
|
+
rcRetirement: result.rcRetirement === "failed" ? result.rcRetirementCategory === "wait-timeout" ? "unresolved" : "failure" : "ok",
|
|
40559
|
+
alignment: alignmentPending ? "unresolved" : "ok",
|
|
40560
|
+
foldPort: "ok"
|
|
40561
|
+
});
|
|
40562
|
+
return {
|
|
40563
|
+
followUpStatus,
|
|
40564
|
+
releaseVerdict: commandName === "release" ? {
|
|
40565
|
+
releaseStatus: "succeeded",
|
|
40566
|
+
followUpStatus,
|
|
40567
|
+
promoted: result.promoted,
|
|
40568
|
+
tag: result.tag,
|
|
40569
|
+
releaseUrl: result.release?.url ?? null,
|
|
40570
|
+
legs: releaseFollowUpLegs(result, projectInfoSync)
|
|
40571
|
+
} : void 0
|
|
40572
|
+
};
|
|
40573
|
+
}
|
|
39123
40574
|
function renderTrainApply(commandName, r) {
|
|
39124
40575
|
let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2014 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
39125
40576
|
if (r.versionFold) base = `${base}; ${r.versionFold}`;
|
|
@@ -39148,9 +40599,14 @@ function renderTrainApply(commandName, r) {
|
|
|
39148
40599
|
const failedLegs = v.legs.filter((leg) => leg.status === "failed");
|
|
39149
40600
|
return [
|
|
39150
40601
|
`${base}; release verdict: ${verdict}; promoted=${v.promoted}; tag=${v.tag}; release=${v.releaseUrl ?? "unreported"}; follow-up: ${v.followUpStatus.toUpperCase()}`,
|
|
39151
|
-
...failedLegs.map((leg) => ` - follow-up leg ${leg.leg}: FAILED \u2014 ${leg.error ?? "no diagnostic recorded"}`)
|
|
40602
|
+
...failedLegs.map((leg) => ` - follow-up leg ${leg.leg}: FAILED \u2014 ${leg.error ?? "no diagnostic recorded"}`),
|
|
40603
|
+
// #5987: the durable ledger's phase legs + derived typed verdict ride the same receipt.
|
|
40604
|
+
...r.ledger ? formatReleaseLedgerReport(r.ledger).map((line) => ` ${line}`) : []
|
|
39152
40605
|
].join("\n");
|
|
39153
40606
|
}
|
|
40607
|
+
if (r.ledger) {
|
|
40608
|
+
return [base, ...formatReleaseLedgerReport(r.ledger).map((line) => ` ${line}`)].join("\n");
|
|
40609
|
+
}
|
|
39154
40610
|
return base;
|
|
39155
40611
|
}
|
|
39156
40612
|
function applyTrainFollowUpExit(status) {
|
|
@@ -39175,7 +40631,7 @@ function registerTrainCommandsAndInventory(program3) {
|
|
|
39175
40631
|
}
|
|
39176
40632
|
function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
39177
40633
|
for (const commandName of ["rcand", "release"]) {
|
|
39178
|
-
const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)");
|
|
40634
|
+
const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)").option("--out <path>", "with --apply, write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983)");
|
|
39179
40635
|
const RELEASE_ONLY_FLAGS = [
|
|
39180
40636
|
{ flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line Hub Slack summary; required for a new MMI-Hub --apply (#883/#3901)" },
|
|
39181
40637
|
{ 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)" },
|
|
@@ -39211,10 +40667,13 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
39211
40667
|
if (o.retryPublish && commandName !== "release") {
|
|
39212
40668
|
return fail(`${commandName}: --retry-publish applies only to release. Run: mmi-cli devops release --retry-publish <run-id> --apply --watch`);
|
|
39213
40669
|
}
|
|
40670
|
+
if (o.out && !o.apply) {
|
|
40671
|
+
return fail(`${commandName}: --out writes the --apply result receipt only \u2014 the dry-run plan and --resume keep their stdout/JSON output. Drop --out${o.resume ? "" : " or rerun with --apply"}`);
|
|
40672
|
+
}
|
|
39214
40673
|
if (o.retryPublish) {
|
|
39215
40674
|
if (o.resume || o.abort) return fail("release: --retry-publish cannot be combined with --resume or --abort");
|
|
39216
40675
|
if (!o.apply) return fail("release: --retry-publish requires --apply after explicit approval; nothing was written");
|
|
39217
|
-
if (o.announceSummaryFile || o.ack || o.dev) return fail("release: --retry-publish accepts only --apply, --watch, --repo and --json");
|
|
40676
|
+
if (o.announceSummaryFile || o.ack || o.dev || o.out) return fail("release: --retry-publish accepts only --apply, --watch, --repo and --json");
|
|
39218
40677
|
if (o.repo) {
|
|
39219
40678
|
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli devops release --retry-publish ${o.retryPublish} --apply${o.watch ? " --watch" : ""}`);
|
|
39220
40679
|
if (!guard.ok) return fail(`release: ${guard.message}`);
|
|
@@ -39238,7 +40697,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
39238
40697
|
if (o.abort) {
|
|
39239
40698
|
if (o.resume) return fail("release: --abort and --resume are mutually exclusive \u2014 abort removes an unpublished tag while resume preserves and promotes it");
|
|
39240
40699
|
if (!o.apply) return fail("release: --abort requires --apply after explicit approval; nothing was written");
|
|
39241
|
-
if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
|
|
40700
|
+
if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.out) {
|
|
39242
40701
|
return fail("release: --abort accepts only --apply, --repo and --json; promotion flags cannot be combined with rollback");
|
|
39243
40702
|
}
|
|
39244
40703
|
if (o.repo) {
|
|
@@ -39313,29 +40772,16 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
39313
40772
|
projectInfoSync = { applied: false, note: `FAILED \u2014 ${error}`, error };
|
|
39314
40773
|
}
|
|
39315
40774
|
}
|
|
39316
|
-
const
|
|
39317
|
-
const followUpStatus = deriveTrainFollowUpStatus({
|
|
39318
|
-
projectInfo: projectInfoSync && "error" in projectInfoSync ? "failure" : "ok",
|
|
39319
|
-
deploy: deployFollowUpOutcome(result.deployStatus),
|
|
39320
|
-
rcRetirement: result.rcRetirement === "failed" ? result.rcRetirementCategory === "wait-timeout" ? "unresolved" : "failure" : "ok",
|
|
39321
|
-
alignment: alignmentPending ? "unresolved" : "ok",
|
|
39322
|
-
foldPort: "ok"
|
|
39323
|
-
});
|
|
39324
|
-
const releaseVerdict = commandName === "release" ? {
|
|
39325
|
-
releaseStatus: "succeeded",
|
|
39326
|
-
followUpStatus,
|
|
39327
|
-
promoted: result.promoted,
|
|
39328
|
-
tag: result.tag,
|
|
39329
|
-
releaseUrl: result.release?.url ?? null,
|
|
39330
|
-
legs: releaseFollowUpLegs(result, projectInfoSync)
|
|
39331
|
-
} : void 0;
|
|
40775
|
+
const { followUpStatus, releaseVerdict } = buildReleaseVerdict(commandName, result, projectInfoSync);
|
|
39332
40776
|
const reported = {
|
|
39333
40777
|
...result,
|
|
39334
40778
|
...projectInfoSync ? { projectInfoSync } : {},
|
|
39335
40779
|
...postReleaseJervDoctor ? { postReleaseJervDoctor } : {},
|
|
39336
40780
|
...releaseVerdict ? { releaseVerdict } : {}
|
|
39337
40781
|
};
|
|
39338
|
-
|
|
40782
|
+
const output = o.json ? JSON.stringify(reported, null, 2) : renderTrainApply(commandName, reported);
|
|
40783
|
+
if (!o.out) printLine(output);
|
|
40784
|
+
else printLine(`Wrote ${commandName} apply result to ${o.out} (UTF-8, ${writeUtf8Receipt(o.out, output)} bytes)`);
|
|
39339
40785
|
applyTrainFollowUpExit(followUpStatus);
|
|
39340
40786
|
return;
|
|
39341
40787
|
} catch (e) {
|
|
@@ -39477,12 +40923,12 @@ ${r.stderr ?? ""}`).catch(() => "");
|
|
|
39477
40923
|
var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
|
|
39478
40924
|
var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
|
|
39479
40925
|
function envHealLockPath(home) {
|
|
39480
|
-
return (0,
|
|
40926
|
+
return (0, import_node_path45.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
|
|
39481
40927
|
}
|
|
39482
40928
|
async function withEnvHealLock(what, run) {
|
|
39483
40929
|
try {
|
|
39484
40930
|
return await withFileLock(
|
|
39485
|
-
envHealLockPath((0,
|
|
40931
|
+
envHealLockPath((0, import_node_os20.homedir)()),
|
|
39486
40932
|
{ staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
|
|
39487
40933
|
run
|
|
39488
40934
|
);
|
|
@@ -39578,7 +41024,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
39578
41024
|
const configRoot = surfaceConfigRoot(surface);
|
|
39579
41025
|
const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
|
|
39580
41026
|
const plan = buildPluginCachePlan(
|
|
39581
|
-
(0,
|
|
41027
|
+
(0, import_node_os20.homedir)(),
|
|
39582
41028
|
running,
|
|
39583
41029
|
pluginCacheFsDeps(configRoot, () => 0),
|
|
39584
41030
|
{ configRoot, includeStaging: surface !== "codex" }
|
|
@@ -39606,14 +41052,14 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
39606
41052
|
const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
|
|
39607
41053
|
const installed = installedActivePluginVersion(surface);
|
|
39608
41054
|
const plan = buildPluginCachePlan(
|
|
39609
|
-
(0,
|
|
41055
|
+
(0, import_node_os20.homedir)(),
|
|
39610
41056
|
running,
|
|
39611
41057
|
pluginCacheFsDeps(configRoot, () => 0),
|
|
39612
41058
|
{ configRoot, includeStaging: surface !== "codex", installedVersion: installed }
|
|
39613
41059
|
);
|
|
39614
41060
|
const result = applyPluginCachePlan(
|
|
39615
41061
|
plan,
|
|
39616
|
-
(p) => (0,
|
|
41062
|
+
(p) => (0, import_node_fs48.rmSync)(p, { recursive: true }),
|
|
39617
41063
|
stagingApplyFsGuard(configRoot)
|
|
39618
41064
|
);
|
|
39619
41065
|
return {
|
|
@@ -39648,7 +41094,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
39648
41094
|
// has no generated routing index would
|
|
39649
41095
|
// get a permanent — demanding an artifact it never asked for.
|
|
39650
41096
|
docsIndexState: (root) => {
|
|
39651
|
-
if (!(0,
|
|
41097
|
+
if (!(0, import_node_fs48.existsSync)((0, import_node_path45.join)(root, DOCS_INDEX_PATH))) return void 0;
|
|
39652
41098
|
const real = createDocsIndexDeps(root);
|
|
39653
41099
|
let docs;
|
|
39654
41100
|
const listDocs = () => docs ??= real.listDocs();
|
|
@@ -39657,7 +41103,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
39657
41103
|
},
|
|
39658
41104
|
// #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
|
|
39659
41105
|
healDocsIndex: (root) => {
|
|
39660
|
-
if (!(0,
|
|
41106
|
+
if (!(0, import_node_fs48.existsSync)((0, import_node_path45.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
|
|
39661
41107
|
const real = createDocsIndexDeps(root);
|
|
39662
41108
|
let docs;
|
|
39663
41109
|
const listDocs = () => docs ??= real.listDocs();
|
|
@@ -39678,8 +41124,8 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
39678
41124
|
});
|
|
39679
41125
|
const raced = await Promise.race([
|
|
39680
41126
|
work.then((r) => ({ ...r, timedOut: false })),
|
|
39681
|
-
new Promise((
|
|
39682
|
-
ceiling = setTimeout(() =>
|
|
41127
|
+
new Promise((resolve7) => {
|
|
41128
|
+
ceiling = setTimeout(() => resolve7({ timedOut: true, scanned: 0, findings: 0, fixed: 0, failed: 0 }), BOARD_DOCTOR_TIMEOUT_MS);
|
|
39683
41129
|
})
|
|
39684
41130
|
]);
|
|
39685
41131
|
if (raced.timedOut) return { scanned: 0, findings: 0, fixed: 0, failed: 0, timedOut: true };
|
|
@@ -39712,8 +41158,8 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
39712
41158
|
incomplete: nb.incomplete,
|
|
39713
41159
|
timedOut: false
|
|
39714
41160
|
})),
|
|
39715
|
-
new Promise((
|
|
39716
|
-
ceiling = setTimeout(() =>
|
|
41161
|
+
new Promise((resolve7) => {
|
|
41162
|
+
ceiling = setTimeout(() => resolve7({ driftLines: [], incomplete: [], timedOut: true }), SCHEDULES_DRIFT_TIMEOUT_MS);
|
|
39717
41163
|
})
|
|
39718
41164
|
]);
|
|
39719
41165
|
if (!raced.timedOut && raced.incomplete.length === 0) writeSchedulesDriftCache(cachePath, raced.driftLines);
|
|
@@ -40228,7 +41674,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
40228
41674
|
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`);
|
|
40229
41675
|
if (o.secretsFile) {
|
|
40230
41676
|
try {
|
|
40231
|
-
vars.push(`secrets=${(0,
|
|
41677
|
+
vars.push(`secrets=${(0, import_node_fs48.readFileSync)(o.secretsFile, "utf8")}`);
|
|
40232
41678
|
} catch (e) {
|
|
40233
41679
|
return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
|
|
40234
41680
|
}
|
|
@@ -40541,16 +41987,16 @@ function ciAuditDeps2() {
|
|
|
40541
41987
|
// gate re-seed step is skipped gracefully rather than failing mid-run.
|
|
40542
41988
|
readSeedFile: (path2) => {
|
|
40543
41989
|
if (!root) return null;
|
|
40544
|
-
const fullPath = (0,
|
|
40545
|
-
return (0,
|
|
41990
|
+
const fullPath = (0, import_node_path45.join)(root, path2);
|
|
41991
|
+
return (0, import_node_fs48.existsSync)(fullPath) ? (0, import_node_fs48.readFileSync)(fullPath, "utf8") : null;
|
|
40546
41992
|
}
|
|
40547
41993
|
};
|
|
40548
41994
|
}
|
|
40549
41995
|
function hubRoot2() {
|
|
40550
|
-
const fromPkg = (0,
|
|
41996
|
+
const fromPkg = (0, import_node_path45.join)(__dirname, "..", "..");
|
|
40551
41997
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
40552
|
-
if ((0,
|
|
40553
|
-
if ((0,
|
|
41998
|
+
if ((0, import_node_fs48.existsSync)((0, import_node_path45.join)(fromPkg, marker))) return fromPkg;
|
|
41999
|
+
if ((0, import_node_fs48.existsSync)((0, import_node_path45.join)(process.cwd(), marker))) return process.cwd();
|
|
40554
42000
|
return null;
|
|
40555
42001
|
}
|
|
40556
42002
|
registerQueryCommands(program2);
|
|
@@ -40650,10 +42096,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
40650
42096
|
targets = resolution.targets;
|
|
40651
42097
|
}
|
|
40652
42098
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
40653
|
-
const fileMatrix = (0,
|
|
42099
|
+
const fileMatrix = (0, import_node_fs48.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs48.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
40654
42100
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
40655
42101
|
const dataAccess = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
40656
|
-
const sanctioned = (0,
|
|
42102
|
+
const sanctioned = (0, import_node_fs48.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs48.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
40657
42103
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
|
|
40658
42104
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
40659
42105
|
if (!report.ok) process.exitCode = 1;
|
|
@@ -40684,16 +42130,16 @@ function directoryBytes(path2) {
|
|
|
40684
42130
|
let total = 0;
|
|
40685
42131
|
let entries;
|
|
40686
42132
|
try {
|
|
40687
|
-
entries = (0,
|
|
42133
|
+
entries = (0, import_node_fs48.readdirSync)(path2, { withFileTypes: true });
|
|
40688
42134
|
} catch {
|
|
40689
42135
|
return 0;
|
|
40690
42136
|
}
|
|
40691
42137
|
for (const entry of entries) {
|
|
40692
|
-
const child2 = (0,
|
|
42138
|
+
const child2 = (0, import_node_path45.join)(path2, entry.name);
|
|
40693
42139
|
if (entry.isDirectory()) total += directoryBytes(child2);
|
|
40694
42140
|
else {
|
|
40695
42141
|
try {
|
|
40696
|
-
total += (0,
|
|
42142
|
+
total += (0, import_node_fs48.statSync)(child2).size;
|
|
40697
42143
|
} catch {
|
|
40698
42144
|
}
|
|
40699
42145
|
}
|
|
@@ -40701,25 +42147,25 @@ function directoryBytes(path2) {
|
|
|
40701
42147
|
return total;
|
|
40702
42148
|
}
|
|
40703
42149
|
function listDirEntries(dir) {
|
|
40704
|
-
return (0,
|
|
42150
|
+
return (0, import_node_fs48.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
|
|
40705
42151
|
}
|
|
40706
42152
|
function readInstalledPluginRefs(configRoot) {
|
|
40707
42153
|
const p = installedPluginsPathForConfig(configRoot);
|
|
40708
|
-
if (!(0,
|
|
42154
|
+
if (!(0, import_node_fs48.existsSync)(p)) return [];
|
|
40709
42155
|
try {
|
|
40710
|
-
return installedPluginPaths((0,
|
|
42156
|
+
return installedPluginPaths((0, import_node_fs48.readFileSync)(p, "utf8"));
|
|
40711
42157
|
} catch {
|
|
40712
42158
|
return null;
|
|
40713
42159
|
}
|
|
40714
42160
|
}
|
|
40715
42161
|
function pluginCacheFsDeps(configRoot, dirBytes) {
|
|
40716
42162
|
return {
|
|
40717
|
-
exists: (p) => (0,
|
|
40718
|
-
listVersionDirs: (root) => (0,
|
|
42163
|
+
exists: (p) => (0, import_node_fs48.existsSync)(p),
|
|
42164
|
+
listVersionDirs: (root) => (0, import_node_fs48.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
|
|
40719
42165
|
dirBytes,
|
|
40720
|
-
listStagingDirs: (root) => (0,
|
|
42166
|
+
listStagingDirs: (root) => (0, import_node_fs48.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
40721
42167
|
try {
|
|
40722
|
-
return { name: d.name, mtimeMs: newestMtimeMs((0,
|
|
42168
|
+
return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path45.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs48.statSync)(p).mtimeMs) };
|
|
40723
42169
|
} catch {
|
|
40724
42170
|
return { name: d.name, mtimeMs: Date.now() };
|
|
40725
42171
|
}
|
|
@@ -40733,10 +42179,10 @@ function stagingApplyFsGuard(configRoot) {
|
|
|
40733
42179
|
return {
|
|
40734
42180
|
referencedPaths: () => readInstalledPluginRefs(configRoot),
|
|
40735
42181
|
mtimeMs: (name) => {
|
|
40736
|
-
const p = (0,
|
|
40737
|
-
if (!(0,
|
|
42182
|
+
const p = (0, import_node_path45.join)(stagingRoot, name);
|
|
42183
|
+
if (!(0, import_node_fs48.existsSync)(p)) return null;
|
|
40738
42184
|
try {
|
|
40739
|
-
return newestMtimeMs(p, listDirEntries, (q) => (0,
|
|
42185
|
+
return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs48.statSync)(q).mtimeMs);
|
|
40740
42186
|
} catch {
|
|
40741
42187
|
return null;
|
|
40742
42188
|
}
|
|
@@ -40756,13 +42202,13 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
|
|
|
40756
42202
|
return;
|
|
40757
42203
|
}
|
|
40758
42204
|
const plan = buildPluginCachePlan(
|
|
40759
|
-
(0,
|
|
42205
|
+
(0, import_node_os20.homedir)(),
|
|
40760
42206
|
running,
|
|
40761
42207
|
pluginCacheFsDeps(configRoot, directoryBytes),
|
|
40762
42208
|
{ withBytes: true, configRoot, includeStaging: surface !== "codex" }
|
|
40763
42209
|
);
|
|
40764
42210
|
const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
|
|
40765
|
-
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0,
|
|
42211
|
+
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs48.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
|
|
40766
42212
|
const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
|
|
40767
42213
|
if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
|
|
40768
42214
|
else console.log(renderPluginCachePlan(plan, result));
|