@mutmutco/cli 4.4.16 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +666 -115
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -5436,7 +5436,7 @@ function resolveVerbAliasShim(argv) {
|
|
|
5436
5436
|
}
|
|
5437
5437
|
|
|
5438
5438
|
// src/command-composition.ts
|
|
5439
|
-
var
|
|
5439
|
+
var import_node_os22 = require("node:os");
|
|
5440
5440
|
var import_node_path47 = require("node:path");
|
|
5441
5441
|
|
|
5442
5442
|
// src/board-read.ts
|
|
@@ -6245,6 +6245,44 @@ function collectPullRequestWorkflowContexts(workflows) {
|
|
|
6245
6245
|
}
|
|
6246
6246
|
return [...contexts].sort((a, b) => a.localeCompare(b));
|
|
6247
6247
|
}
|
|
6248
|
+
function jobNeeds(job) {
|
|
6249
|
+
const lines2 = job.body.split(/\r?\n/).slice(1);
|
|
6250
|
+
const at = lines2.findIndex((line) => /^\s+needs:/.test(line));
|
|
6251
|
+
if (at < 0) return [];
|
|
6252
|
+
const indent = lines2[at].match(/^(\s*)/)?.[1].length ?? 0;
|
|
6253
|
+
const inline = lines2[at].replace(/^\s*needs:\s*/, "").replace(/#.*$/, "").trim();
|
|
6254
|
+
if (inline) return inline.replace(/^\[|\]$/g, "").split(",").map(unquote).filter(Boolean);
|
|
6255
|
+
const items = [];
|
|
6256
|
+
for (const line of lines2.slice(at + 1)) {
|
|
6257
|
+
if (!line.trim()) continue;
|
|
6258
|
+
if ((line.match(/^(\s*)/)?.[1].length ?? 0) <= indent) break;
|
|
6259
|
+
items.push(unquote(line.replace(/^\s*-\s*/, "")));
|
|
6260
|
+
}
|
|
6261
|
+
return items.filter(Boolean);
|
|
6262
|
+
}
|
|
6263
|
+
function unquote(s) {
|
|
6264
|
+
return s.replace(/#.*$/, "").trim().replace(/^['"]|['"]$/g, "");
|
|
6265
|
+
}
|
|
6266
|
+
function unblockingGateJobs(workflows, required) {
|
|
6267
|
+
const unblocking = /* @__PURE__ */ new Set();
|
|
6268
|
+
for (const wf of workflows) {
|
|
6269
|
+
if (!workflowTriggersPullRequest(wf.body)) continue;
|
|
6270
|
+
const jobs = parseWorkflowJobs(wf.body);
|
|
6271
|
+
const needs = new Map(jobs.map((job) => [job.id, jobNeeds(job)]));
|
|
6272
|
+
const covered = /* @__PURE__ */ new Set();
|
|
6273
|
+
const stack = jobs.map((job) => job.id).filter((id) => required.has(id));
|
|
6274
|
+
while (stack.length) {
|
|
6275
|
+
const id = stack.pop();
|
|
6276
|
+
if (covered.has(id)) continue;
|
|
6277
|
+
covered.add(id);
|
|
6278
|
+
stack.push(...needs.get(id) ?? []);
|
|
6279
|
+
}
|
|
6280
|
+
for (const job of jobs) {
|
|
6281
|
+
if (!covered.has(job.id) && jobReportsPullRequestCheck(job)) unblocking.add(job.id);
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
6284
|
+
return [...unblocking].sort((a, b) => a.localeCompare(b));
|
|
6285
|
+
}
|
|
6248
6286
|
function pathFilteredPullRequestWorkflows(workflows) {
|
|
6249
6287
|
const filtered = [];
|
|
6250
6288
|
for (const wf of workflows) {
|
|
@@ -15938,10 +15976,10 @@ var rollout_plan_default = {
|
|
|
15938
15976
|
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)."
|
|
15939
15977
|
},
|
|
15940
15978
|
baseline: {
|
|
15941
|
-
version: "4.
|
|
15942
|
-
tag: "v4.
|
|
15943
|
-
commit: "
|
|
15944
|
-
npm: "@mutmutco/cli@4.
|
|
15979
|
+
version: "4.5.0",
|
|
15980
|
+
tag: "v4.5.0",
|
|
15981
|
+
commit: "e9c83872b853",
|
|
15982
|
+
npm: "@mutmutco/cli@4.5.0"
|
|
15945
15983
|
},
|
|
15946
15984
|
exitCriterion: "fleet-n-of-n",
|
|
15947
15985
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15958,14 +15996,14 @@ var rollout_plan_default = {
|
|
|
15958
15996
|
repo: "mutmutco/mmi-hub",
|
|
15959
15997
|
role: "canary",
|
|
15960
15998
|
schedule: "train",
|
|
15961
|
-
v3Target: "v4.
|
|
15999
|
+
v3Target: "v4.5.0"
|
|
15962
16000
|
}
|
|
15963
16001
|
],
|
|
15964
16002
|
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.",
|
|
15965
16003
|
rollback: {
|
|
15966
16004
|
independent: true,
|
|
15967
|
-
mechanism: "npm dist-tag latest -> 4.
|
|
15968
|
-
v3Target: "v4.
|
|
16005
|
+
mechanism: "npm dist-tag latest -> 4.5.0 and redeploy the Hub Lambda from tag v4.5.0 (e9c83872b853); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
16006
|
+
v3Target: "v4.5.0 (@mutmutco/cli@4.5.0, tag commit e9c83872b853 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15969
16007
|
}
|
|
15970
16008
|
},
|
|
15971
16009
|
{
|
|
@@ -18487,6 +18525,13 @@ async function awaitAlignmentLanded(deps, ctx, leg, base, opts = {}) {
|
|
|
18487
18525
|
await sleep2(ALIGNMENT_LAND_POLL_MS);
|
|
18488
18526
|
}
|
|
18489
18527
|
}
|
|
18528
|
+
async function awaitAlignmentsLanded(deps, ctx, legs) {
|
|
18529
|
+
const [dev, rc] = await Promise.all([
|
|
18530
|
+
awaitAlignmentLanded(deps, ctx, legs.dev, "development"),
|
|
18531
|
+
legs.rc ? awaitAlignmentLanded(deps, ctx, legs.rc, "rc") : void 0
|
|
18532
|
+
]);
|
|
18533
|
+
return { dev, ...rc ? { rc } : {} };
|
|
18534
|
+
}
|
|
18490
18535
|
async function alignRcForward(deps, ctx, tag) {
|
|
18491
18536
|
deps.warn?.("Release: Bringing the release candidate up to date");
|
|
18492
18537
|
await runGitRemoteRead(deps, ["fetch", "origin", "main", "rc"]);
|
|
@@ -18803,6 +18848,7 @@ function isReleaseShaDeployEnumerationCandidate(row, nonDeployEvents) {
|
|
|
18803
18848
|
|
|
18804
18849
|
// src/train-apply-deploy.ts
|
|
18805
18850
|
var import_node_fs19 = require("node:fs");
|
|
18851
|
+
var import_node_os10 = require("node:os");
|
|
18806
18852
|
var import_node_path17 = require("node:path");
|
|
18807
18853
|
|
|
18808
18854
|
// src/vercel-deployments.ts
|
|
@@ -18888,6 +18934,12 @@ async function observePublishIfRequired(deps, ctx, meta, model, stage, releaseSh
|
|
|
18888
18934
|
deployStatus: aggregateWorkflowRuns(workflowRuns)
|
|
18889
18935
|
};
|
|
18890
18936
|
}
|
|
18937
|
+
async function joinDeployAndPublish(deploy, publish) {
|
|
18938
|
+
const [d, p] = await Promise.all([deploy, publish]);
|
|
18939
|
+
if (!p) return [d, p];
|
|
18940
|
+
const mark = (x) => x.workflowRuns ? { ...x, workflowRuns: x.workflowRuns.map((r) => ({ ...r, parallel: true })) } : x;
|
|
18941
|
+
return [mark(d), mark(p)];
|
|
18942
|
+
}
|
|
18891
18943
|
function appendPublishDispatch(deploy, publish) {
|
|
18892
18944
|
if (!publish) return deploy;
|
|
18893
18945
|
return {
|
|
@@ -19052,19 +19104,29 @@ async function appendServedChannelPublish(deps, input, dispatch) {
|
|
|
19052
19104
|
};
|
|
19053
19105
|
}
|
|
19054
19106
|
const probe = await (deps.readServedChannelVersion ?? fetchServedChannelVersion)(channelUrl);
|
|
19107
|
+
const evidence = (conclusion, text) => [...runs, servedChannelRow(HUB_SERVED_PRODUCT, conclusion, text)];
|
|
19108
|
+
if (!probe.ok && staleServedProcess(probe)) {
|
|
19109
|
+
const redeploy = mmiGetRedeployCommand(input.repo);
|
|
19110
|
+
return {
|
|
19111
|
+
...dispatch,
|
|
19112
|
+
note: `${dispatch.note}; get-publish.yml succeeded but ${channelUrl} answered HTTP ${probe.status} \u2014 STALE SERVED PROCESS: the files may be current, but the process serving them predates GET /release/version; recover with \`${redeploy}\``,
|
|
19113
|
+
workflowRuns: evidence("failure", `${channelUrl} answered HTTP ${probe.status} (stale served process)`),
|
|
19114
|
+
deployStatus: "failure"
|
|
19115
|
+
};
|
|
19116
|
+
}
|
|
19055
19117
|
if (!probe.ok) {
|
|
19056
19118
|
const why = probe.error ?? `HTTP ${probe.status ?? "unknown"}`;
|
|
19057
19119
|
return {
|
|
19058
19120
|
...dispatch,
|
|
19059
19121
|
note: `${dispatch.note}; get-publish.yml succeeded but the served channel version could NOT be read at ${channelUrl} (${why}) \u2014 the move is unverified`,
|
|
19060
|
-
workflowRuns:
|
|
19122
|
+
workflowRuns: evidence("pending", `${channelUrl} unreadable (${why})`)
|
|
19061
19123
|
};
|
|
19062
19124
|
}
|
|
19063
19125
|
if (expected && probe.version !== expected) {
|
|
19064
19126
|
return {
|
|
19065
19127
|
...dispatch,
|
|
19066
19128
|
note: `${dispatch.note}; get-publish.yml succeeded but the served channel still reports ${probe.version} (expected launcher ${expected}) \u2014 THE CHANNEL DID NOT MOVE; recover with \`${remedy}\``,
|
|
19067
|
-
workflowRuns:
|
|
19129
|
+
workflowRuns: evidence("failure", `${channelUrl} reports ${probe.version}, expected ${expected}`),
|
|
19068
19130
|
deployStatus: "failure"
|
|
19069
19131
|
};
|
|
19070
19132
|
}
|
|
@@ -19072,16 +19134,107 @@ async function appendServedChannelPublish(deps, input, dispatch) {
|
|
|
19072
19134
|
return {
|
|
19073
19135
|
...dispatch,
|
|
19074
19136
|
note: `${dispatch.note}; get-publish.yml succeeded and ${channelUrl} reports ${probe.version}, but the local launcher version could not be read \u2014 nothing was compared, so the move is unverified`,
|
|
19075
|
-
workflowRuns:
|
|
19137
|
+
workflowRuns: evidence("pending", `${channelUrl} reports ${probe.version}; nothing to compare against`)
|
|
19076
19138
|
};
|
|
19077
19139
|
}
|
|
19078
19140
|
return {
|
|
19079
19141
|
...dispatch,
|
|
19080
19142
|
note: `${dispatch.note}; served channel moved \u2014 ${channelUrl} now reports ${probe.version}`,
|
|
19081
|
-
workflowRuns:
|
|
19143
|
+
workflowRuns: evidence("success", `${channelUrl} reports ${probe.version}`)
|
|
19082
19144
|
};
|
|
19083
19145
|
}
|
|
19146
|
+
function staleServedProcess(probe) {
|
|
19147
|
+
return probe.status === 401 || probe.status === 403 || probe.status === 404;
|
|
19148
|
+
}
|
|
19149
|
+
function servedChannelRow(product, conclusion, evidence) {
|
|
19150
|
+
return { workflow: `served-channel(${product})`, runUrlNote: evidence, conclusion };
|
|
19151
|
+
}
|
|
19152
|
+
var JERV_GATEWAY_REMEDY = "from the Jerv-Hub checkout, re-run the Gateway deploy on its current release tag: `bash scripts/jerv-gateway-release-deploy.sh --tag <jerv-hub tag>`";
|
|
19153
|
+
var SERVED_CHANNELS = {
|
|
19154
|
+
"mutmutco/mm-strategy": {
|
|
19155
|
+
product: "mm-strategy",
|
|
19156
|
+
url: "https://strategy.mutmut.work/release/version",
|
|
19157
|
+
mover: "the MM-Strategy container deploy (the running server's own version is the pointer)",
|
|
19158
|
+
remedy: "`mmi-cli devops runtime tenant redeploy mutmutco/MM-Strategy main`"
|
|
19159
|
+
},
|
|
19160
|
+
"mutmutco/jerv-hub": {
|
|
19161
|
+
product: "jerv",
|
|
19162
|
+
url: "https://get.jerv.ai/jerv/release/version",
|
|
19163
|
+
mover: "the Jerv Gateway deploy (`jerv-gate fetch-release --product jerv`)",
|
|
19164
|
+
remedy: JERV_GATEWAY_REMEDY
|
|
19165
|
+
},
|
|
19166
|
+
"mutmutco/jerv-jervcode": {
|
|
19167
|
+
product: "jervcode",
|
|
19168
|
+
url: "https://get.jerv.ai/jervcode/release/version",
|
|
19169
|
+
mover: "the Jerv Gateway deploy, which fetches JervCode's latest release \u2014 a JervCode release alone does not move it",
|
|
19170
|
+
remedy: JERV_GATEWAY_REMEDY
|
|
19171
|
+
}
|
|
19172
|
+
};
|
|
19173
|
+
function servedChannelFor(repo) {
|
|
19174
|
+
return SERVED_CHANNELS[repo.toLowerCase()];
|
|
19175
|
+
}
|
|
19176
|
+
var SERVED_CHANNEL_PROBE_ATTEMPTS = 3;
|
|
19177
|
+
var SERVED_CHANNEL_PROBE_BACKOFF_MS = 1e4;
|
|
19178
|
+
async function appendServedChannelProof(deps, input, dispatch) {
|
|
19179
|
+
const channel = servedChannelFor(input.repo);
|
|
19180
|
+
if (!channel) return dispatch;
|
|
19181
|
+
const expected = input.tag.replace(/^v/, "");
|
|
19182
|
+
const withProof = (conclusion, evidence, note, failed = false) => ({
|
|
19183
|
+
...dispatch,
|
|
19184
|
+
note: `${dispatch.note}; ${note}`,
|
|
19185
|
+
workflowRuns: [...dispatch.workflowRuns ?? [], servedChannelRow(channel.product, conclusion, evidence)],
|
|
19186
|
+
...failed ? { deployStatus: "failure" } : {}
|
|
19187
|
+
});
|
|
19188
|
+
if (!input.shipped) {
|
|
19189
|
+
return withProof("pending", `${channel.url} not probed`, `served channel ${channel.url} not probed \u2014 deploy/publish are not proven green, so it is not expected to have moved`);
|
|
19190
|
+
}
|
|
19191
|
+
const read = deps.readServedChannelVersion ?? fetchServedChannelVersion;
|
|
19192
|
+
let probe = await read(channel.url);
|
|
19193
|
+
for (let attempt = 1; attempt < SERVED_CHANNEL_PROBE_ATTEMPTS && probe.ok && probe.version !== expected; attempt++) {
|
|
19194
|
+
await (deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))))(SERVED_CHANNEL_PROBE_BACKOFF_MS * attempt);
|
|
19195
|
+
probe = await read(channel.url);
|
|
19196
|
+
}
|
|
19197
|
+
if (!probe.ok && staleServedProcess(probe)) {
|
|
19198
|
+
return withProof(
|
|
19199
|
+
"failure",
|
|
19200
|
+
`${channel.url} answered HTTP ${probe.status} (stale served process)`,
|
|
19201
|
+
`${channel.url} answered HTTP ${probe.status} \u2014 STALE SERVED PROCESS: the process serving ${channel.product} predates GET /release/version; it moves with ${channel.mover}; recover with ${channel.remedy}`,
|
|
19202
|
+
true
|
|
19203
|
+
);
|
|
19204
|
+
}
|
|
19205
|
+
if (!probe.ok) {
|
|
19206
|
+
const why = probe.error ?? `HTTP ${probe.status ?? "unknown"}`;
|
|
19207
|
+
return withProof("pending", `${channel.url} unreadable (${why})`, `served channel ${channel.url} could NOT be read (${why}) \u2014 the move is unverified`);
|
|
19208
|
+
}
|
|
19209
|
+
if (probe.version !== expected) {
|
|
19210
|
+
return withProof(
|
|
19211
|
+
"failure",
|
|
19212
|
+
`${channel.url} reports ${probe.version}, expected ${expected}`,
|
|
19213
|
+
`served channel ${channel.url} still reports ${probe.version} (expected ${expected}) \u2014 THE CHANNEL DID NOT MOVE: new installs get ${probe.version}; it moves with ${channel.mover}; recover with ${channel.remedy}`,
|
|
19214
|
+
true
|
|
19215
|
+
);
|
|
19216
|
+
}
|
|
19217
|
+
return withProof("success", `${channel.url} reports ${probe.version}`, `served channel moved \u2014 ${channel.url} reports ${probe.version}`);
|
|
19218
|
+
}
|
|
19084
19219
|
var JERV_GATEWAY_RUN_URL_NOTE = "no run URL emitted \u2014 host health leg runs outside GitHub Actions";
|
|
19220
|
+
var JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT = "scripts/jerv-gateway-release-deploy.sh";
|
|
19221
|
+
async function runTaggedJervGatewayLocalWrapper(deps, tag, tagSha) {
|
|
19222
|
+
let script;
|
|
19223
|
+
try {
|
|
19224
|
+
script = await deps.run("git", ["show", `${tagSha}:${JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT}`]);
|
|
19225
|
+
} catch (error) {
|
|
19226
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
19227
|
+
throw new Error(`could not extract ${JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT} from tagged commit ${tagSha.slice(0, 12)}: ${reason}`);
|
|
19228
|
+
}
|
|
19229
|
+
const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path17.join)((0, import_node_os10.tmpdir)(), "jerv-gateway-release-deploy-"));
|
|
19230
|
+
const extracted = (0, import_node_path17.join)(dir, "jerv-gateway-release-deploy.sh");
|
|
19231
|
+
try {
|
|
19232
|
+
(0, import_node_fs19.writeFileSync)(extracted, script, "utf8");
|
|
19233
|
+
return await deps.run("bash", [extracted, "--tag", tag]);
|
|
19234
|
+
} finally {
|
|
19235
|
+
(0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
|
|
19236
|
+
}
|
|
19237
|
+
}
|
|
19085
19238
|
async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch) {
|
|
19086
19239
|
if (!isJervHubRepo(repo)) return dispatch;
|
|
19087
19240
|
if (dispatch.deployStatus !== "success") {
|
|
@@ -19092,7 +19245,7 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
|
|
|
19092
19245
|
};
|
|
19093
19246
|
}
|
|
19094
19247
|
try {
|
|
19095
|
-
const output = await deps
|
|
19248
|
+
const output = await runTaggedJervGatewayLocalWrapper(deps, tag, tagSha);
|
|
19096
19249
|
const receipt = output.trim().split(/\r?\n/).reverse().map((line) => {
|
|
19097
19250
|
try {
|
|
19098
19251
|
return JSON.parse(line);
|
|
@@ -19110,26 +19263,27 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
|
|
|
19110
19263
|
deployStatus: "success"
|
|
19111
19264
|
};
|
|
19112
19265
|
} catch (error) {
|
|
19113
|
-
const
|
|
19266
|
+
const full = error instanceof Error ? error.message : String(error);
|
|
19114
19267
|
return {
|
|
19115
19268
|
...dispatch,
|
|
19116
19269
|
// #6095: the helper's receipt, health or rollback failed after a proven publish — the release resumes, never re-tags.
|
|
19117
|
-
|
|
19270
|
+
// #6985: keep the FULL failing output in the note so the ledger can persist its tail (the first-line 240-char clip hid the real cause).
|
|
19271
|
+
note: `${dispatch.note}; Jerv Gateway deploy FAILED (${full}) \u2014 see docs/Guides/train-troubleshooting.md#jerv-gateway-deploy`,
|
|
19118
19272
|
workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "jerv-gateway", runUrlNote: JERV_GATEWAY_RUN_URL_NOTE, conclusion: "failure" }],
|
|
19119
19273
|
deployStatus: "failure"
|
|
19120
19274
|
};
|
|
19121
19275
|
}
|
|
19122
19276
|
}
|
|
19123
19277
|
async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha, enumerateSha = false) {
|
|
19124
|
-
const
|
|
19125
|
-
|
|
19278
|
+
const parallel = targets.length > 1 ? { parallel: true } : {};
|
|
19279
|
+
const workflowRuns = await Promise.all(targets.map(async (target) => {
|
|
19126
19280
|
try {
|
|
19127
19281
|
const run = await correlateWorkflowRun(deps, { ...target, since, headSha, repo });
|
|
19128
|
-
|
|
19282
|
+
return { ...await watchWorkflowRun(deps, target.workflow, run, repo), ...parallel };
|
|
19129
19283
|
} catch {
|
|
19130
|
-
|
|
19284
|
+
return { workflow: target.workflow, conclusion: "failure", ...parallel };
|
|
19131
19285
|
}
|
|
19132
|
-
}
|
|
19286
|
+
}));
|
|
19133
19287
|
if (!enumerateSha) return workflowRuns;
|
|
19134
19288
|
const seen = new Set(workflowRuns.map((r) => r.runId).filter((id) => typeof id === "number"));
|
|
19135
19289
|
workflowRuns.push(...await discoverShaWorkflowRuns(deps, repo, headSha, seen));
|
|
@@ -19184,8 +19338,8 @@ async function vercelDeployDispatch(deps, ctx, ref, watch, headSha) {
|
|
|
19184
19338
|
return { note: `${note}; ${detail}`, ...read?.logUrl ? { runUrl: read.logUrl } : {}, workflowRuns: [row(read, deployStatus)], deployStatus };
|
|
19185
19339
|
}
|
|
19186
19340
|
function shipLegsGreen(deploy, publish) {
|
|
19187
|
-
const green = (
|
|
19188
|
-
return green(deploy
|
|
19341
|
+
const green = (leg) => leg?.superseded !== true && (leg?.state === "complete" || leg?.state === "skipped");
|
|
19342
|
+
return green(deploy) && green(publish);
|
|
19189
19343
|
}
|
|
19190
19344
|
async function announceShippedRelease(deps, args, model, legs, deferred) {
|
|
19191
19345
|
if (!deps.announce) return void 0;
|
|
@@ -19851,12 +20005,12 @@ Resume it: ${resumeCommand}
|
|
|
19851
20005
|
// src/train-doctor.ts
|
|
19852
20006
|
var import_node_child_process9 = require("node:child_process");
|
|
19853
20007
|
var import_node_fs23 = require("node:fs");
|
|
19854
|
-
var
|
|
20008
|
+
var import_node_os14 = require("node:os");
|
|
19855
20009
|
var import_node_path23 = require("node:path");
|
|
19856
20010
|
|
|
19857
20011
|
// src/doctor-io.ts
|
|
19858
20012
|
var import_node_fs20 = require("node:fs");
|
|
19859
|
-
var
|
|
20013
|
+
var import_node_os11 = require("node:os");
|
|
19860
20014
|
var import_node_path18 = require("node:path");
|
|
19861
20015
|
var import_node_child_process7 = require("node:child_process");
|
|
19862
20016
|
var import_node_util4 = require("node:util");
|
|
@@ -19886,7 +20040,7 @@ function execFileCapture(file, args, opts = {}) {
|
|
|
19886
20040
|
var MMI_PLUGIN_ID2 = "mmi@mutmutco";
|
|
19887
20041
|
function stagedClaudePluginVersion() {
|
|
19888
20042
|
try {
|
|
19889
|
-
const stagingRoot = (0, import_node_path18.join)((0,
|
|
20043
|
+
const stagingRoot = (0, import_node_path18.join)((0, import_node_os11.homedir)(), ".claude", "plugins", "staging");
|
|
19890
20044
|
const record = JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path18.join)(stagingRoot, "launch-target.json"), "utf8"));
|
|
19891
20045
|
if (typeof record.target !== "string" || typeof record.incoming !== "string") return void 0;
|
|
19892
20046
|
const manifest = JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path18.join)(record.incoming, ".claude-plugin", "plugin.json"), "utf8"));
|
|
@@ -19899,7 +20053,7 @@ function stagedClaudePluginVersion() {
|
|
|
19899
20053
|
function installedClaudePluginVersion() {
|
|
19900
20054
|
try {
|
|
19901
20055
|
const file = JSON.parse(
|
|
19902
|
-
(0, import_node_fs20.readFileSync)((0, import_node_path18.join)((0,
|
|
20056
|
+
(0, import_node_fs20.readFileSync)((0, import_node_path18.join)((0, import_node_os11.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
|
|
19903
20057
|
);
|
|
19904
20058
|
const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
|
|
19905
20059
|
if (versions.length === 0) return void 0;
|
|
@@ -20071,11 +20225,11 @@ function compatHolds(range, version) {
|
|
|
20071
20225
|
|
|
20072
20226
|
// src/claude-binary-doctor.ts
|
|
20073
20227
|
var import_node_fs21 = require("node:fs");
|
|
20074
|
-
var
|
|
20228
|
+
var import_node_os13 = require("node:os");
|
|
20075
20229
|
var import_node_path20 = require("node:path");
|
|
20076
20230
|
|
|
20077
20231
|
// src/jerv-cli-spawn.ts
|
|
20078
|
-
var
|
|
20232
|
+
var import_node_os12 = require("node:os");
|
|
20079
20233
|
var import_node_path19 = require("node:path");
|
|
20080
20234
|
init_cli_shared();
|
|
20081
20235
|
var JERV_CLI_ENTRY = (0, import_node_path19.join)("node_modules", "@jervaise", "jerv-cli", "dist", "index.cjs");
|
|
@@ -20100,7 +20254,7 @@ function normalizeSpawnPathEntry(entry, platform2 = process.platform) {
|
|
|
20100
20254
|
if (msys) return `${msys[1].toUpperCase()}:\\${msys[2].replace(/\//g, "\\")}`;
|
|
20101
20255
|
return trimmed;
|
|
20102
20256
|
}
|
|
20103
|
-
function jervCliCandidateDirs(env = process.env, home = (0,
|
|
20257
|
+
function jervCliCandidateDirs(env = process.env, home = (0, import_node_os12.homedir)(), platform2 = process.platform) {
|
|
20104
20258
|
const seen = /* @__PURE__ */ new Set();
|
|
20105
20259
|
const out = [];
|
|
20106
20260
|
const push = (dir) => {
|
|
@@ -20171,7 +20325,7 @@ function globalNodeModulesRoots(host) {
|
|
|
20171
20325
|
};
|
|
20172
20326
|
const prefix = env.npm_config_prefix?.trim();
|
|
20173
20327
|
if (prefix) push(platform2 === "win32" ? (0, import_node_path20.join)(prefix, "node_modules") : (0, import_node_path20.join)(prefix, "lib", "node_modules"));
|
|
20174
|
-
for (const dir of jervCliCandidateDirs(env, host.home ?? (0,
|
|
20328
|
+
for (const dir of jervCliCandidateDirs(env, host.home ?? (0, import_node_os13.homedir)(), platform2)) {
|
|
20175
20329
|
push((0, import_node_path20.join)(dir, "node_modules"));
|
|
20176
20330
|
push((0, import_node_path20.join)((0, import_node_path20.dirname)(dir), "lib", "node_modules"));
|
|
20177
20331
|
}
|
|
@@ -20818,6 +20972,53 @@ var MAX_TEXT = 500;
|
|
|
20818
20972
|
function truncate(text, max) {
|
|
20819
20973
|
return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
|
|
20820
20974
|
}
|
|
20975
|
+
var FAILING_OUTPUT_LEGS = /* @__PURE__ */ new Set(["publish", "deploy", "alignment"]);
|
|
20976
|
+
var LEDGER_OUTPUT_FILE_LINES = 50;
|
|
20977
|
+
var LEDGER_OUTPUT_NOTE_LINES = 5;
|
|
20978
|
+
function lastNonEmptyLines(text, n) {
|
|
20979
|
+
const lines2 = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
20980
|
+
while (lines2.length > 0 && lines2[lines2.length - 1] === "") lines2.pop();
|
|
20981
|
+
return lines2.slice(-n);
|
|
20982
|
+
}
|
|
20983
|
+
function attachOutputNote(note, suffix) {
|
|
20984
|
+
const room = Math.max(0, MAX_TEXT - suffix.length - 3);
|
|
20985
|
+
const head = note && room > 20 ? truncate(note, room) : "";
|
|
20986
|
+
return truncate(head ? `${head} \u2014 ${suffix}` : suffix, MAX_TEXT);
|
|
20987
|
+
}
|
|
20988
|
+
function writeFailingLegOutput(artifactPath, output) {
|
|
20989
|
+
const fileLines = lastNonEmptyLines(output, LEDGER_OUTPUT_FILE_LINES);
|
|
20990
|
+
(0, import_node_fs22.mkdirSync)((0, import_node_path22.dirname)(artifactPath), { recursive: true });
|
|
20991
|
+
(0, import_node_fs22.writeFileSync)(artifactPath, `${fileLines.join("\n")}
|
|
20992
|
+
`, "utf8");
|
|
20993
|
+
const tail = fileLines.slice(-LEDGER_OUTPUT_NOTE_LINES).join("\n");
|
|
20994
|
+
const noteSuffix = truncate(`failing-leg output: ${artifactPath}
|
|
20995
|
+
${tail}`, MAX_TEXT);
|
|
20996
|
+
return { artifactPath, noteSuffix };
|
|
20997
|
+
}
|
|
20998
|
+
function stampPhaseRecord(ledgerPath, tag, phase, entry, previousRetries, now) {
|
|
20999
|
+
const { output: failingOutput, ...fields } = entry;
|
|
21000
|
+
let note = fields.note;
|
|
21001
|
+
if (fields.state === "failed" && FAILING_OUTPUT_LEGS.has(phase)) {
|
|
21002
|
+
const raw = failingOutput ?? fields.error ?? fields.note;
|
|
21003
|
+
if (raw) {
|
|
21004
|
+
try {
|
|
21005
|
+
const persisted = writeFailingLegOutput(
|
|
21006
|
+
(0, import_node_path22.join)((0, import_node_path22.dirname)((0, import_node_path22.dirname)(ledgerPath)), "ledger-output", `${tag}-${phase}.txt`),
|
|
21007
|
+
raw
|
|
21008
|
+
);
|
|
21009
|
+
note = attachOutputNote(note, persisted.noteSuffix);
|
|
21010
|
+
} catch {
|
|
21011
|
+
}
|
|
21012
|
+
}
|
|
21013
|
+
}
|
|
21014
|
+
return {
|
|
21015
|
+
...fields,
|
|
21016
|
+
note: note !== void 0 ? truncate(note, MAX_TEXT) : void 0,
|
|
21017
|
+
error: fields.error !== void 0 ? truncate(fields.error, MAX_TEXT * 2) : void 0,
|
|
21018
|
+
retries: fields.retries ?? previousRetries,
|
|
21019
|
+
updatedAt: now().toISOString()
|
|
21020
|
+
};
|
|
21021
|
+
}
|
|
20821
21022
|
function normalizeLedgerAnchors(anchors) {
|
|
20822
21023
|
const repo = anchors.repo?.trim();
|
|
20823
21024
|
const tag = anchors.tag?.trim();
|
|
@@ -21029,13 +21230,7 @@ function openReleaseLedgerWriter(cwd, opts = {}) {
|
|
|
21029
21230
|
updatedAt: now().toISOString()
|
|
21030
21231
|
};
|
|
21031
21232
|
const previousRetries = base.phases[phase].retries ?? 0;
|
|
21032
|
-
const record =
|
|
21033
|
-
...entry,
|
|
21034
|
-
note: entry.note !== void 0 ? truncate(entry.note, MAX_TEXT) : void 0,
|
|
21035
|
-
error: entry.error !== void 0 ? truncate(entry.error, MAX_TEXT * 2) : void 0,
|
|
21036
|
-
retries: entry.retries ?? previousRetries,
|
|
21037
|
-
updatedAt: now().toISOString()
|
|
21038
|
-
};
|
|
21233
|
+
const record = stampPhaseRecord(path2, normalized.tag, phase, entry, previousRetries, now);
|
|
21039
21234
|
const updated = {
|
|
21040
21235
|
...base,
|
|
21041
21236
|
phases: { ...base.phases, [phase]: record },
|
|
@@ -21123,7 +21318,9 @@ var TRANSIENT_TRANSPORT_MARKERS = [
|
|
|
21123
21318
|
/socket hang ?up/i,
|
|
21124
21319
|
/remote end hung ?up unexpectedly/i,
|
|
21125
21320
|
/early EOF/i,
|
|
21126
|
-
/tls handshake timeout/i
|
|
21321
|
+
/tls handshake timeout/i,
|
|
21322
|
+
/kex_exchange_identification: connection closed by remote host/i,
|
|
21323
|
+
/connection closed by remote host/i
|
|
21127
21324
|
];
|
|
21128
21325
|
function classifyTransientTransportFailure(evidence) {
|
|
21129
21326
|
if (!evidence) return false;
|
|
@@ -21707,6 +21904,48 @@ function ledgerLaneResumeCommand(prior) {
|
|
|
21707
21904
|
if (prior.lane === "hotfix") return `mmi-cli devops hotfix release ${prior.tag}`;
|
|
21708
21905
|
return "mmi-cli devops release --resume (or, only when proven unpublished, mmi-cli devops release --abort --apply)";
|
|
21709
21906
|
}
|
|
21907
|
+
var RETRY_DEPLOY_LEGS = /* @__PURE__ */ new Set(["deploy", "alignment"]);
|
|
21908
|
+
function joinLegNames(legs) {
|
|
21909
|
+
if (legs.length <= 1) return legs[0] ?? "";
|
|
21910
|
+
if (legs.length === 2) return `${legs[0]} and ${legs[1]}`;
|
|
21911
|
+
return `${legs.slice(0, -1).join(", ")} and ${legs[legs.length - 1]}`;
|
|
21912
|
+
}
|
|
21913
|
+
function formatFailedLegTails(ledger) {
|
|
21914
|
+
const chunks = [];
|
|
21915
|
+
for (const phase of ["publish", "deploy", "alignment"]) {
|
|
21916
|
+
const rec = ledger.phases[phase];
|
|
21917
|
+
if (rec.state !== "failed") continue;
|
|
21918
|
+
const text = rec.note ?? rec.error ?? "";
|
|
21919
|
+
const marker = "failing-leg output: ";
|
|
21920
|
+
const idx = text.indexOf(marker);
|
|
21921
|
+
if (idx < 0) continue;
|
|
21922
|
+
chunks.push(`${phase} ${text.slice(idx)}`);
|
|
21923
|
+
}
|
|
21924
|
+
return chunks.length ? `; ${chunks.join("; ")}` : "";
|
|
21925
|
+
}
|
|
21926
|
+
function hotfixRetryDeployShape(ledger) {
|
|
21927
|
+
if (ledger.lane !== "hotfix") return false;
|
|
21928
|
+
if (ledger.phases.githubRelease.state !== "complete") return false;
|
|
21929
|
+
const open2 = RELEASE_LEDGER_PHASES.filter((phase) => {
|
|
21930
|
+
const state = ledger.phases[phase].state;
|
|
21931
|
+
return state === "pending" || state === "failed";
|
|
21932
|
+
});
|
|
21933
|
+
if (open2.length === 0) return false;
|
|
21934
|
+
if (!open2.every((phase) => RETRY_DEPLOY_LEGS.has(phase))) return false;
|
|
21935
|
+
return open2.some((phase) => ledger.phases[phase].state === "failed");
|
|
21936
|
+
}
|
|
21937
|
+
function ledgerFailedRemedy(ledger) {
|
|
21938
|
+
const derived = deriveReleasePhaseStatus(ledger);
|
|
21939
|
+
const openLegs = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
|
|
21940
|
+
const tails = formatFailedLegTails(ledger);
|
|
21941
|
+
if (hotfixRetryDeployShape(ledger)) {
|
|
21942
|
+
const failed = openLegs.filter((l) => l.state === "failed").map((l) => l.phase);
|
|
21943
|
+
return `the published ${ledger.tag} hotfix can retry the failed ${joinLegNames(failed)} of the same tag: \`mmi-cli devops hotfix release ${ledger.tag} --retry-deploy\` (the deploy helper is idempotent; alignment re-attempts the fold-port PR) (#6985)${tails}`;
|
|
21944
|
+
}
|
|
21945
|
+
const deployOnlyDeadlock = openLegs.length > 0 && openLegs.every((l) => l.phase === "deploy" && supersedeableDeployLegShape(l));
|
|
21946
|
+
const supersession = deployOnlyDeadlock ? `; when the deploy failure is content-caused on the already-published ${ledger.tag} (the immutable tag can never deploy green), the sanctioned exit is \`mmi-cli devops release --supersede-deploy --apply\` after explicit master approval \u2014 the next patch train carries the cause fix (#6553)` : "";
|
|
21947
|
+
return `diagnose the failed leg, then ${ledgerLaneResumeCommand(ledger)} once its cause is fixed (an abort only with fresh approval)${supersession}${tails}`;
|
|
21948
|
+
}
|
|
21710
21949
|
function alignmentPrNumbers(note) {
|
|
21711
21950
|
return [...new Set([...(note ?? "").matchAll(/\/pull\/(\d+)/g)].map((m) => Number.parseInt(m[1], 10)))];
|
|
21712
21951
|
}
|
|
@@ -21752,6 +21991,70 @@ async function healStaleAlignmentLeg(deps, cwd, expected) {
|
|
|
21752
21991
|
return prior.tag;
|
|
21753
21992
|
});
|
|
21754
21993
|
}
|
|
21994
|
+
async function healUnfoldedRunLegs(deps, cwd, model) {
|
|
21995
|
+
const path2 = releaseLedgerPath(cwd);
|
|
21996
|
+
let prior;
|
|
21997
|
+
try {
|
|
21998
|
+
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
21999
|
+
} catch {
|
|
22000
|
+
return void 0;
|
|
22001
|
+
}
|
|
22002
|
+
const legs = ["deploy", "publish"];
|
|
22003
|
+
const openLeg = (rec) => rec.state === "pending" && rec.superseded !== true;
|
|
22004
|
+
if (!legs.some((phase) => openLeg(prior.phases[phase]))) return void 0;
|
|
22005
|
+
const anchors = { repo: prior.repo, tag: prior.tag, tagSha: prior.tagSha };
|
|
22006
|
+
const folded = [];
|
|
22007
|
+
let current = prior;
|
|
22008
|
+
if (legs.some((phase) => openLeg(prior.phases[phase]) && prior.phases[phase].runId != null)) {
|
|
22009
|
+
let reverified;
|
|
22010
|
+
try {
|
|
22011
|
+
reverified = await reverifyPersistedRunPhases(deps, path2, anchors, { sha: prior.tagSha, repo: prior.repo });
|
|
22012
|
+
} catch {
|
|
22013
|
+
return void 0;
|
|
22014
|
+
}
|
|
22015
|
+
if (!reverified.ledger) return void 0;
|
|
22016
|
+
current = reverified.ledger;
|
|
22017
|
+
for (const phase of legs) {
|
|
22018
|
+
const before = prior.phases[phase];
|
|
22019
|
+
const after = current.phases[phase];
|
|
22020
|
+
if (openLeg(before) && before.runId != null && after.state !== "pending") folded.push({ phase, state: after.state, runId: after.runId });
|
|
22021
|
+
}
|
|
22022
|
+
}
|
|
22023
|
+
const targets = prodReleaseTargets(model);
|
|
22024
|
+
const runless = legs.filter((phase) => openLeg(current.phases[phase]) && current.phases[phase].runId == null);
|
|
22025
|
+
if (runless.length > 0 && targets.length > 0) {
|
|
22026
|
+
let rows;
|
|
22027
|
+
try {
|
|
22028
|
+
rows = await listOwnWorkflowRuns(deps, releaseRunRepoFor(model, prior.repo), targets, prior.tagSha);
|
|
22029
|
+
} catch {
|
|
22030
|
+
rows = [];
|
|
22031
|
+
}
|
|
22032
|
+
const inputs = phaseInputsFromRunRows(model, rows, prior.tagSha, { repo: prior.repo });
|
|
22033
|
+
const foldable = runless.filter((phase) => inputs[phase].runId != null && inputs[phase].state !== "pending");
|
|
22034
|
+
if (foldable.length > 0) {
|
|
22035
|
+
const written = await withReleaseLedger(path2, anchors, (ledger) => {
|
|
22036
|
+
if (!ledger) return { value: [] };
|
|
22037
|
+
const phases = { ...ledger.phases };
|
|
22038
|
+
const done = [];
|
|
22039
|
+
for (const phase of foldable) {
|
|
22040
|
+
const live = phases[phase];
|
|
22041
|
+
if (!openLeg(live) || live.runId != null) continue;
|
|
22042
|
+
const entry = compactEntry(inputs[phase]);
|
|
22043
|
+
phases[phase] = {
|
|
22044
|
+
...entry,
|
|
22045
|
+
note: `${entry.note ?? `${phase} run on the release SHA`} \u2014 folded by train doctor --heal from the concluded run (#7003)`,
|
|
22046
|
+
retries: live.retries,
|
|
22047
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22048
|
+
};
|
|
22049
|
+
done.push({ phase, state: entry.state, runId: entry.runId });
|
|
22050
|
+
}
|
|
22051
|
+
return done.length ? { ledger: { ...ledger, phases, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, value: done } : { value: done };
|
|
22052
|
+
}).catch(() => []);
|
|
22053
|
+
folded.push(...written);
|
|
22054
|
+
}
|
|
22055
|
+
}
|
|
22056
|
+
return folded.length ? { tag: prior.tag, folded } : void 0;
|
|
22057
|
+
}
|
|
21755
22058
|
function supersedeableDeployLegShape(rec) {
|
|
21756
22059
|
return rec.state === "failed" || rec.state === "pending" && rec.runId == null;
|
|
21757
22060
|
}
|
|
@@ -22098,9 +22401,10 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
|
|
|
22098
22401
|
}
|
|
22099
22402
|
if (derived.phaseStatus !== "phases-green") {
|
|
22100
22403
|
const open2 = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
|
|
22404
|
+
const next = derived.phaseStatus === "phases-failed" ? ledgerFailedRemedy(prior) : `Finish it first: ${ledgerLaneResumeCommand(prior)} (#5987/#6068)`;
|
|
22101
22405
|
return {
|
|
22102
22406
|
ok: false,
|
|
22103
|
-
error: `a prior ${prior.lane ?? "release"} ledger for ${prior.tag} is ${derived.phaseStatus} (${open2.map((l) => `${l.phase}=${l.state}`).join(", ")}) \u2014 a new train run must not stack on an unresolved one.
|
|
22407
|
+
error: `a prior ${prior.lane ?? "release"} ledger for ${prior.tag} is ${derived.phaseStatus} (${open2.map((l) => `${l.phase}=${l.state}`).join(", ")}) \u2014 a new train run must not stack on an unresolved one. ${next}`
|
|
22104
22408
|
};
|
|
22105
22409
|
}
|
|
22106
22410
|
let liveSha = "";
|
|
@@ -22124,6 +22428,124 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
|
|
|
22124
22428
|
return { ok: true, note: `archived the completed release ledger ${prior.tag} to ${archivePath} (live-proven at ${prior.tagSha.slice(0, 12)})` };
|
|
22125
22429
|
});
|
|
22126
22430
|
}
|
|
22431
|
+
async function defaultRetryDeploy(deps, prior) {
|
|
22432
|
+
const input = {
|
|
22433
|
+
note: `hotfix --retry-deploy ${prior.tag}`,
|
|
22434
|
+
deployStatus: "success",
|
|
22435
|
+
workflowRuns: []
|
|
22436
|
+
};
|
|
22437
|
+
const dispatch = await appendJervGatewayReleaseDeploy(deps, prior.repo, prior.tag, prior.tagSha, input);
|
|
22438
|
+
const host = (dispatch.workflowRuns ?? []).find((r) => r.workflow === "jerv-gateway");
|
|
22439
|
+
if (!host) {
|
|
22440
|
+
throw new Error(
|
|
22441
|
+
`hotfix --retry-deploy: the ${prior.tag} deploy helper was not invoked (not the operator-host Gateway) \u2014 nothing was written (#6985)`
|
|
22442
|
+
);
|
|
22443
|
+
}
|
|
22444
|
+
const entry = deployPhaseInput("registry-publish", dispatch, { releaseSha: prior.tagSha });
|
|
22445
|
+
if (entry.state === "failed") {
|
|
22446
|
+
return { ...entry, error: entry.note ?? dispatch.note, output: dispatch.note };
|
|
22447
|
+
}
|
|
22448
|
+
return entry;
|
|
22449
|
+
}
|
|
22450
|
+
async function retryHotfixDeployLegs(deps, cwd, opts = {}) {
|
|
22451
|
+
const path2 = releaseLedgerPath(cwd);
|
|
22452
|
+
return withFileLock(`${path2}.lock`, { label: "release ledger" }, async () => {
|
|
22453
|
+
let prior;
|
|
22454
|
+
try {
|
|
22455
|
+
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
22456
|
+
} catch (e) {
|
|
22457
|
+
throw new ReleaseLedgerError(
|
|
22458
|
+
"unreadable",
|
|
22459
|
+
`hotfix --retry-deploy: the active release ledger at ${path2} could not be read (${e instanceof Error ? e.message : String(e)}) \u2014 resolve it by hand; nothing was written (#6985)`
|
|
22460
|
+
);
|
|
22461
|
+
}
|
|
22462
|
+
if (prior.lane !== "hotfix") {
|
|
22463
|
+
throw new Error(
|
|
22464
|
+
`hotfix --retry-deploy: the active ledger is a ${prior.lane ?? "release"} lane ledger for ${prior.tag} \u2014 --retry-deploy is the hotfix lane's recovery for a published hotfix; nothing was written (#6985)`
|
|
22465
|
+
);
|
|
22466
|
+
}
|
|
22467
|
+
if (prior.phases.githubRelease.state !== "complete") {
|
|
22468
|
+
throw new Error(
|
|
22469
|
+
`hotfix --retry-deploy: ${prior.tag} is not a published hotfix (githubRelease=${prior.phases.githubRelease.state}) \u2014 --retry-deploy re-runs deploy/alignment of a PUBLISHED tag; nothing was written (#6985)`
|
|
22470
|
+
);
|
|
22471
|
+
}
|
|
22472
|
+
if (!hotfixRetryDeployShape(prior)) {
|
|
22473
|
+
const derived = deriveReleasePhaseStatus(prior);
|
|
22474
|
+
const open2 = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
|
|
22475
|
+
const extra = open2.filter((l) => l.phase !== "deploy" && l.phase !== "alignment");
|
|
22476
|
+
if (extra.length > 0) {
|
|
22477
|
+
throw new Error(
|
|
22478
|
+
`hotfix --retry-deploy: ${prior.tag} has unresolved legs besides deploy/alignment (${extra.map((l) => `${l.phase}=${l.state}`).join(", ")}) \u2014 it re-runs only the failed deploy and/or fold-port alignment of a published hotfix of the same tag; nothing was written (#6985)`
|
|
22479
|
+
);
|
|
22480
|
+
}
|
|
22481
|
+
throw new Error(
|
|
22482
|
+
`hotfix --retry-deploy: ${prior.tag} has no failed deploy or alignment leg to retry (${open2.map((l) => `${l.phase}=${l.state}`).join(", ") || "phases-green"}) \u2014 nothing was written (#6985)`
|
|
22483
|
+
);
|
|
22484
|
+
}
|
|
22485
|
+
if (prior.phases.alignment.state === "failed" && !opts.retryAlignment) {
|
|
22486
|
+
throw new Error(
|
|
22487
|
+
`hotfix --retry-deploy: the ${prior.tag} alignment (fold-port) leg is failed \u2014 nothing was written (#6985)`
|
|
22488
|
+
);
|
|
22489
|
+
}
|
|
22490
|
+
if (prior.phases.deploy.state === "failed" && !isJervHubRepo(prior.repo) && prior.phases.deploy.workflow !== "jerv-gateway") {
|
|
22491
|
+
throw new Error(
|
|
22492
|
+
`hotfix --retry-deploy: the ${prior.tag} deploy leg is ${prior.phases.deploy.workflow ?? "not the operator-host helper"} \u2014 diagnose that run; nothing was written (#6985)`
|
|
22493
|
+
);
|
|
22494
|
+
}
|
|
22495
|
+
let liveSha = "";
|
|
22496
|
+
try {
|
|
22497
|
+
liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
|
|
22498
|
+
} catch (e) {
|
|
22499
|
+
throw new Error(
|
|
22500
|
+
`hotfix --retry-deploy: could not prove ${prior.tag} live on origin (${e instanceof Error ? e.message : String(e)}) \u2014 refusing to retry on an unproven state; retry when origin answers (#6985)`
|
|
22501
|
+
);
|
|
22502
|
+
}
|
|
22503
|
+
if (liveSha.toLowerCase() !== prior.tagSha.toLowerCase()) {
|
|
22504
|
+
throw new Error(
|
|
22505
|
+
`hotfix --retry-deploy: origin reports ${prior.tag} at ${liveSha.slice(0, 12) || "no tag"} but the ledger anchors ${prior.tagSha.slice(0, 12)} \u2014 an absent or divergent tag is not retryable; nothing was written (#6985)`
|
|
22506
|
+
);
|
|
22507
|
+
}
|
|
22508
|
+
const retried = [];
|
|
22509
|
+
const phases = { ...prior.phases };
|
|
22510
|
+
const now = () => /* @__PURE__ */ new Date();
|
|
22511
|
+
if (phases.deploy.state === "failed") {
|
|
22512
|
+
retried.push("deploy");
|
|
22513
|
+
const entry = opts.retryDeploy ? await opts.retryDeploy(prior) : await defaultRetryDeploy(deps, prior);
|
|
22514
|
+
phases.deploy = stampPhaseRecord(path2, prior.tag, "deploy", entry, phases.deploy.retries ?? 0, now);
|
|
22515
|
+
}
|
|
22516
|
+
if (phases.alignment.state === "failed") {
|
|
22517
|
+
retried.push("alignment");
|
|
22518
|
+
try {
|
|
22519
|
+
const entry = await opts.retryAlignment(prior);
|
|
22520
|
+
phases.alignment = stampPhaseRecord(path2, prior.tag, "alignment", entry, phases.alignment.retries ?? 0, now);
|
|
22521
|
+
} catch (e) {
|
|
22522
|
+
const message2 = e instanceof Error ? e.message : String(e);
|
|
22523
|
+
phases.alignment = stampPhaseRecord(
|
|
22524
|
+
path2,
|
|
22525
|
+
prior.tag,
|
|
22526
|
+
"alignment",
|
|
22527
|
+
{ state: "failed", sha: prior.tagSha, error: message2, output: message2 },
|
|
22528
|
+
phases.alignment.retries ?? 0,
|
|
22529
|
+
now
|
|
22530
|
+
);
|
|
22531
|
+
}
|
|
22532
|
+
}
|
|
22533
|
+
atomicWriteJson(path2, { ...prior, phases, updatedAt: now().toISOString() });
|
|
22534
|
+
const stillFailed = retried.filter((phase) => phases[phase].state === "failed");
|
|
22535
|
+
const status = stillFailed.length === 0 ? "success" : "failed";
|
|
22536
|
+
return {
|
|
22537
|
+
command: "hotfix-retry-deploy",
|
|
22538
|
+
repo: prior.repo,
|
|
22539
|
+
tag: prior.tag,
|
|
22540
|
+
tagSha: prior.tagSha,
|
|
22541
|
+
retried,
|
|
22542
|
+
status,
|
|
22543
|
+
note: status === "success" ? `retried the failed ${joinLegNames(retried)} of published ${prior.tag} \u2014 the ledger is phases-green (#6985)` : `retried the failed ${joinLegNames(retried)} of published ${prior.tag} \u2014 ${joinLegNames(stillFailed)} still failed (#6985)`,
|
|
22544
|
+
...retried.includes("deploy") ? { deploy: phases.deploy } : {},
|
|
22545
|
+
...retried.includes("alignment") ? { alignment: phases.alignment } : {}
|
|
22546
|
+
};
|
|
22547
|
+
});
|
|
22548
|
+
}
|
|
22127
22549
|
|
|
22128
22550
|
// src/train-doctor.ts
|
|
22129
22551
|
init_client_version();
|
|
@@ -22221,7 +22643,7 @@ function readOriginWorkflowsDefault(root, ref) {
|
|
|
22221
22643
|
}
|
|
22222
22644
|
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
22223
22645
|
async function probeRegistryWhoami(train) {
|
|
22224
|
-
const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path23.join)((0,
|
|
22646
|
+
const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path23.join)((0, import_node_os14.tmpdir)(), "mmi-npmrc-"));
|
|
22225
22647
|
const rc = (0, import_node_path23.join)(dir, "npmrc");
|
|
22226
22648
|
try {
|
|
22227
22649
|
(0, import_node_fs23.writeFileSync)(rc, "//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n");
|
|
@@ -22260,6 +22682,7 @@ var defaultDeps2 = {
|
|
|
22260
22682
|
}
|
|
22261
22683
|
},
|
|
22262
22684
|
healAlignmentLeg: healStaleAlignmentLeg,
|
|
22685
|
+
healRunLegs: healUnfoldedRunLegs,
|
|
22263
22686
|
readSurfacesRaw: (cwd) => {
|
|
22264
22687
|
try {
|
|
22265
22688
|
return (0, import_node_fs23.readFileSync)((0, import_node_path23.join)(cwd, "surfaces.json"), "utf8");
|
|
@@ -22606,6 +23029,21 @@ async function runTrainDoctor(input) {
|
|
|
22606
23029
|
try {
|
|
22607
23030
|
let ledger = parseReleaseLedger(ledgerRaw);
|
|
22608
23031
|
let derived = deriveReleasePhaseStatus(ledger);
|
|
23032
|
+
const deployModel = typeof meta?.deployModel === "string" && isDeployModel(meta.deployModel) ? meta.deployModel : void 0;
|
|
23033
|
+
if (heal && deployModel && derived.legs.some((l) => (l.phase === "deploy" || l.phase === "publish") && l.state === "pending")) {
|
|
23034
|
+
const run = await deps.healRunLegs(train, cwd, deployModel);
|
|
23035
|
+
if (run) {
|
|
23036
|
+
add({
|
|
23037
|
+
code: "ledger-unfolded",
|
|
23038
|
+
severity: "healed",
|
|
23039
|
+
source: "origin",
|
|
23040
|
+
title: `folded ${run.folded.map((f) => `${f.phase}=${f.state}${f.runId ? ` (run ${f.runId})` : ""}`).join(", ")} into the ${run.tag} ledger from the concluded run(s) on the tag SHA (#7003)`,
|
|
23041
|
+
remedy: "nothing \u2014 the leg now records what GitHub already concluded"
|
|
23042
|
+
});
|
|
23043
|
+
ledger = parseReleaseLedger(deps.readLedgerRaw(ledgerPath) ?? ledgerRaw);
|
|
23044
|
+
derived = deriveReleasePhaseStatus(ledger);
|
|
23045
|
+
}
|
|
23046
|
+
}
|
|
22609
23047
|
if (heal && derived.phaseStatus === "phases-pending" && derived.legs.every((l) => l.state !== "pending" || l.phase === "alignment")) {
|
|
22610
23048
|
const closedTag = await deps.healAlignmentLeg(train, cwd);
|
|
22611
23049
|
if (closedTag) {
|
|
@@ -22644,7 +23082,7 @@ async function runTrainDoctor(input) {
|
|
|
22644
23082
|
// #6429: each lane finishes its own run (#6068) — an rcand-lane ledger named as a release
|
|
22645
23083
|
// ledger, with `release --resume` as its remedy, sends the operator to a verb that refuses it.
|
|
22646
23084
|
title: `a prior ${ledger.lane ?? "release"} ledger for ${ledger.tag} is ${derived.phaseStatus} (${open2}) \u2014 a new train must not stack on it (#5987)`,
|
|
22647
|
-
remedy: failed ?
|
|
23085
|
+
remedy: failed ? ledgerFailedRemedy(ledger) : `finish it first: ${ledgerLaneResumeCommand(ledger)} (alignment-only pending legs close themselves with \`train doctor --heal\` once the PR merges, and a deploy/publish leg whose run already concluded folds under the same heal)${supersession}`
|
|
22648
23086
|
});
|
|
22649
23087
|
} else if (newestRemoteReleaseTag && newestRemoteReleaseTag !== ledger.tag) {
|
|
22650
23088
|
add({
|
|
@@ -23400,6 +23838,8 @@ async function recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, phase,
|
|
|
23400
23838
|
}
|
|
23401
23839
|
async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch, input, recover = recoverPhaseWithMaybeTransientRerun) {
|
|
23402
23840
|
if (dispatch.deployStatus !== "failure") return dispatch;
|
|
23841
|
+
const persisted = await ledger?.load(anchors).catch(() => void 0);
|
|
23842
|
+
if (persisted?.phases[phase]?.superseded === true) return dispatch;
|
|
23403
23843
|
let workflowRuns = dispatch.workflowRuns?.length ? [...dispatch.workflowRuns] : void 0;
|
|
23404
23844
|
const notes = [];
|
|
23405
23845
|
let fallbackRecovered = false;
|
|
@@ -23407,6 +23847,7 @@ async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch
|
|
|
23407
23847
|
if (failedRows.length > 0) {
|
|
23408
23848
|
for (const failed of failedRows) {
|
|
23409
23849
|
const rowPhase = failed.workflow.includes("publish") ? "publish" : phase;
|
|
23850
|
+
if (persisted?.phases[rowPhase]?.superseded === true) continue;
|
|
23410
23851
|
const recovery = await recover(deps, ledger, anchors, rowPhase, {
|
|
23411
23852
|
repo: input.repo,
|
|
23412
23853
|
sha: input.sha,
|
|
@@ -23581,7 +24022,10 @@ ${recovery.note}`), recoveryInput);
|
|
|
23581
24022
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
23582
24023
|
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" });
|
|
23583
24024
|
let alignment = alignEarly ? await alignEarly() : void 0;
|
|
23584
|
-
const deployDispatch0 = await
|
|
24025
|
+
const [deployDispatch0, publishDispatch0] = await joinDeployAndPublish(
|
|
24026
|
+
dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir),
|
|
24027
|
+
observePublishIfRequired(deps, ctx, meta, deployModel, "main", releaseSha, watch)
|
|
24028
|
+
);
|
|
23585
24029
|
const deployRunRepo = releaseRunRepoFor(deployModel, ctx.repo);
|
|
23586
24030
|
const failedReleaseRun = deployDispatch0.workflowRuns?.find((run) => run.conclusion === "failure");
|
|
23587
24031
|
const dispatchFailurePhase = failedReleaseRun?.workflow === "publish.yml" || deployModel === "registry-publish" ? "publish" : "deploy";
|
|
@@ -23603,7 +24047,6 @@ ${recovery.note}`), recoveryInput);
|
|
|
23603
24047
|
await recordPhase(ledger, deps, ledgerAnchors, "deploy", deployLeg, {
|
|
23604
24048
|
landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
|
|
23605
24049
|
});
|
|
23606
|
-
const publishDispatch0 = await observePublishIfRequired(deps, ctx, meta, deployModel, "main", releaseSha, watch);
|
|
23607
24050
|
const publishDispatch = publishDispatch0 ? await recoverFailedDispatchPhase(
|
|
23608
24051
|
deps,
|
|
23609
24052
|
ledger,
|
|
@@ -23619,6 +24062,14 @@ ${recovery.note}`), recoveryInput);
|
|
|
23619
24062
|
await recordPhase(ledger, deps, ledgerAnchors, "publish", publishLeg, {
|
|
23620
24063
|
landed: "the immutable tag, the green required-check wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
23621
24064
|
});
|
|
24065
|
+
const announcing = announceShippedRelease(
|
|
24066
|
+
deps,
|
|
24067
|
+
{ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
|
|
24068
|
+
deployModel,
|
|
24069
|
+
{ deploy: deployLeg, publish: publishLeg },
|
|
24070
|
+
releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
|
|
24071
|
+
);
|
|
24072
|
+
const aligning = watch && alignment ? awaitAlignmentsLanded(deps, ctx, alignment) : Promise.resolve(alignment);
|
|
23622
24073
|
let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
23623
24074
|
dispatch = await appendMmiGetRedeploy(
|
|
23624
24075
|
deps,
|
|
@@ -23630,19 +24081,8 @@ ${recovery.note}`), recoveryInput);
|
|
|
23630
24081
|
{ repo: ctx.repo, publishStatus: publishLeg.state === "complete" ? "success" : "failure", watch },
|
|
23631
24082
|
dispatch
|
|
23632
24083
|
);
|
|
23633
|
-
|
|
23634
|
-
|
|
23635
|
-
{ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
|
|
23636
|
-
deployModel,
|
|
23637
|
-
{ deploy: deployLeg, publish: publishLeg },
|
|
23638
|
-
releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
|
|
23639
|
-
);
|
|
23640
|
-
if (watch && alignment) {
|
|
23641
|
-
alignment = {
|
|
23642
|
-
dev: await awaitAlignmentLanded(deps, ctx, alignment.dev, "development"),
|
|
23643
|
-
...alignment.rc ? { rc: await awaitAlignmentLanded(deps, ctx, alignment.rc, "rc") } : {}
|
|
23644
|
-
};
|
|
23645
|
-
}
|
|
24084
|
+
dispatch = await appendServedChannelProof(deps, { repo: ctx.repo, tag, shipped: shipLegsGreen(deployLeg, publishLeg) }, dispatch);
|
|
24085
|
+
alignment = await aligning;
|
|
23646
24086
|
const announceNote = await announcing;
|
|
23647
24087
|
return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors, ...alignment ? { alignment } : {} };
|
|
23648
24088
|
}
|
|
@@ -23958,6 +24398,8 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
23958
24398
|
dispatch2.deployStatus = historicalDeploy;
|
|
23959
24399
|
dispatch2.note = `historical ${deployModel} deploy/publish status: ${historicalDeploy}; ${companion.note}`;
|
|
23960
24400
|
}
|
|
24401
|
+
if (persisted?.phases.deploy.superseded === true) phaseInputs.deploy = persisted.phases.deploy;
|
|
24402
|
+
if (persisted?.phases.publish.superseded === true) phaseInputs.publish = persisted.phases.publish;
|
|
23961
24403
|
if (persisted) {
|
|
23962
24404
|
if (persisted.phases.deploy.superseded !== true) {
|
|
23963
24405
|
await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(phaseInputs.deploy), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
|
|
@@ -23969,7 +24411,8 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
23969
24411
|
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" });
|
|
23970
24412
|
}
|
|
23971
24413
|
if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
|
|
23972
|
-
const
|
|
24414
|
+
const settledBySupersede = persisted?.phases.deploy.superseded === true || persisted?.phases.publish.superseded === true;
|
|
24415
|
+
const announceNote2 = persisted && !shippedBefore && !settledBySupersede && (prodReleaseTargets(deployModel).length > 0 || companion !== null) ? await announceShippedRelease(
|
|
23973
24416
|
deps,
|
|
23974
24417
|
{ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
|
|
23975
24418
|
deployModel,
|
|
@@ -24059,7 +24502,10 @@ ${recovery.note}`), recoveryInput);
|
|
|
24059
24502
|
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" });
|
|
24060
24503
|
let devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
24061
24504
|
let rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
|
|
24062
|
-
const deployDispatch0 = await
|
|
24505
|
+
const [deployDispatch0, publishDispatch0] = await joinDeployAndPublish(
|
|
24506
|
+
dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir),
|
|
24507
|
+
observePublishIfRequired(deps, ctx, meta, deployModel, "main", tagSha, watch)
|
|
24508
|
+
);
|
|
24063
24509
|
const recoveredDeployDispatch = await recoverFailedDispatchPhase(
|
|
24064
24510
|
deps,
|
|
24065
24511
|
ledger,
|
|
@@ -24075,12 +24521,13 @@ ${recovery.note}`), recoveryInput);
|
|
|
24075
24521
|
);
|
|
24076
24522
|
const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch);
|
|
24077
24523
|
steps.push(`dispatched the ${deployModel} deploy path`);
|
|
24078
|
-
const deployLeg = phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
|
|
24079
|
-
|
|
24080
|
-
|
|
24081
|
-
|
|
24082
|
-
|
|
24083
|
-
|
|
24524
|
+
const deployLeg = persisted?.phases.deploy.superseded === true ? persisted.phases.deploy : phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
|
|
24525
|
+
if (persisted?.phases.deploy.superseded !== true) {
|
|
24526
|
+
await recordPhase(ledger, deps, anchors, "deploy", deployLeg, {
|
|
24527
|
+
strict: ledgerMode === "strict",
|
|
24528
|
+
landed: "the immutable tag, the re-proven wall, the origin/main push, and the verified GitHub Release"
|
|
24529
|
+
});
|
|
24530
|
+
}
|
|
24084
24531
|
const publishDispatch = publishDispatch0 ? await recoverFailedDispatchPhase(
|
|
24085
24532
|
deps,
|
|
24086
24533
|
ledger,
|
|
@@ -24089,11 +24536,13 @@ ${recovery.note}`), recoveryInput);
|
|
|
24089
24536
|
publishDispatch0,
|
|
24090
24537
|
{ repo: ctx.repo, sha: tagSha, watched: false, workflow: "publish.yml" }
|
|
24091
24538
|
) : null;
|
|
24092
|
-
const publishLeg = phaseEntry(publishPhaseInput(deployModel, deployDispatch, publishDispatch, meta.publishRequired === true, { releaseSha: tagSha, repo: ctx.repo }));
|
|
24093
|
-
|
|
24094
|
-
|
|
24095
|
-
|
|
24096
|
-
|
|
24539
|
+
const publishLeg = persisted?.phases.publish.superseded === true ? persisted.phases.publish : phaseEntry(publishPhaseInput(deployModel, deployDispatch, publishDispatch, meta.publishRequired === true, { releaseSha: tagSha, repo: ctx.repo }));
|
|
24540
|
+
if (persisted?.phases.publish.superseded !== true) {
|
|
24541
|
+
await recordPhase(ledger, deps, anchors, "publish", publishLeg, {
|
|
24542
|
+
strict: ledgerMode === "strict",
|
|
24543
|
+
landed: "the immutable tag, the re-proven wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
24544
|
+
});
|
|
24545
|
+
}
|
|
24097
24546
|
const dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
24098
24547
|
const announcing = announceShippedRelease(
|
|
24099
24548
|
deps,
|
|
@@ -24103,8 +24552,9 @@ ${recovery.note}`), recoveryInput);
|
|
|
24103
24552
|
releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
|
|
24104
24553
|
);
|
|
24105
24554
|
if (watch) {
|
|
24106
|
-
|
|
24107
|
-
|
|
24555
|
+
const landed = await awaitAlignmentsLanded(deps, ctx, { dev: devRollForward, rc: rcAlignment });
|
|
24556
|
+
devRollForward = landed.dev;
|
|
24557
|
+
rcAlignment = landed.rc;
|
|
24108
24558
|
}
|
|
24109
24559
|
const announceNote = await announcing;
|
|
24110
24560
|
steps.push(`development roll-forward: ${devRollForward.status}`);
|
|
@@ -24759,6 +25209,7 @@ var RULESET_TEMPLATE_SEED = "seed:mmi-product-required-checks.template.json";
|
|
|
24759
25209
|
var RULESET_REFERENCE_MATCH_LABEL = "committed ruleset reference matches the live ruleset (#3816)";
|
|
24760
25210
|
var GATE_GREEN_ON_BASE_LABEL = "gate is green on the default branch (#3819)";
|
|
24761
25211
|
var REQUIRED_CONTEXTS_EMITTED_LABEL = "required check contexts match PR workflows";
|
|
25212
|
+
var UNBLOCKING_GATE_JOBS_LABEL = "every gate job can block a merge (#7012)";
|
|
24762
25213
|
var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressable (#3880)";
|
|
24763
25214
|
var RELEASE_BRANCH_REACHABLE_LABEL = "rc/main required contexts are reachable from a release-base head (#5193)";
|
|
24764
25215
|
var RULESET_TRACK_SCOPE_LABEL = "product ruleset branch scope covers the release track (#6327)";
|
|
@@ -25191,6 +25642,17 @@ async function auditRepoCi(repo, deps) {
|
|
|
25191
25642
|
remediation: missingEmitters.length ? `Fix the pull_request workflows or ${contextAuthority.source} before reconciling` : void 0
|
|
25192
25643
|
});
|
|
25193
25644
|
}
|
|
25645
|
+
if (hasRequiredChecks) {
|
|
25646
|
+
const gateFiles = new Set(gateWorkflowFiles(prWorkflowPaths));
|
|
25647
|
+
const gateWorkflows = (await getBranchWorkflows() ?? []).filter((wf) => gateFiles.has(wf.path.slice(wf.path.lastIndexOf("/") + 1)));
|
|
25648
|
+
const unblocking = unblockingGateJobs(gateWorkflows, new Set(liveContexts)).filter((job) => !AGENT_PR_BOOKKEEPING_CONTEXTS.has(job));
|
|
25649
|
+
checks.push({
|
|
25650
|
+
ok: unblocking.length === 0,
|
|
25651
|
+
label: UNBLOCKING_GATE_JOBS_LABEL,
|
|
25652
|
+
detail: unblocking.length ? `gate job(s) [${unblocking.join(", ")}] run on every PR but are neither required nor needed by a required job (required: [${liveContexts.join(", ")}]) \u2014 a red one still merges and turns ${baseBranch} red for every later PR` : void 0,
|
|
25653
|
+
remediation: unblocking.length ? `mmi-cli oracle org project set ${repo} --var requiredChecks=${JSON.stringify(sortedUnique([...liveContexts, ...unblocking]))} then mmi-cli devops ci reconcile --repo ${repo} --apply \u2014 or make a required job \`needs:\` them` : void 0
|
|
25654
|
+
});
|
|
25655
|
+
}
|
|
25194
25656
|
const prOnly = contextAuthority.coversReleaseBranches ? contextAuthority.contexts.filter((context) => TRAIN_PR_ONLY_CONTEXTS.has(context)) : [];
|
|
25195
25657
|
const tagBlind = contextAuthority.coversReleaseBranches ? contextsUnreachableOnTagPush(contextAuthority.contexts, await getBranchWorkflows() ?? [], TRAIN_PR_ONLY_CONTEXTS) : [];
|
|
25196
25658
|
checks.push({
|
|
@@ -27122,7 +27584,7 @@ function registerBoardCommands(program3) {
|
|
|
27122
27584
|
|
|
27123
27585
|
// src/bootstrap-commands.ts
|
|
27124
27586
|
var import_node_fs26 = require("node:fs");
|
|
27125
|
-
var
|
|
27587
|
+
var import_node_os15 = require("node:os");
|
|
27126
27588
|
var import_node_path26 = require("node:path");
|
|
27127
27589
|
init_cli_shared();
|
|
27128
27590
|
init_clean_exit();
|
|
@@ -29130,7 +29592,7 @@ function registerBootstrapCommands(program3) {
|
|
|
29130
29592
|
const readFile9 = (p) => (0, import_node_fs26.existsSync)(p) ? (0, import_node_fs26.readFileSync)(p, "utf8") : null;
|
|
29131
29593
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
29132
29594
|
const putSeed = async (target, content, ref, sha) => {
|
|
29133
|
-
const tmp = (0, import_node_path26.join)((0,
|
|
29595
|
+
const tmp = (0, import_node_path26.join)((0, import_node_os15.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
29134
29596
|
(0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
|
|
29135
29597
|
try {
|
|
29136
29598
|
await gh(contentPutInputArgs(repo, target, tmp));
|
|
@@ -29771,7 +30233,7 @@ LIVE apply to ${repo}:
|
|
|
29771
30233
|
} catch {
|
|
29772
30234
|
existingSha = void 0;
|
|
29773
30235
|
}
|
|
29774
|
-
const tmp = (0, import_node_path26.join)((0,
|
|
30236
|
+
const tmp = (0, import_node_path26.join)((0, import_node_os15.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
29775
30237
|
const desiredContent = desiredByRepo.get(rec.repo);
|
|
29776
30238
|
if (desiredContent == null) return fail(`bootstrap propagate: no resolved content for ${rec.repo} ${seed.target} \u2014 refusing to write`);
|
|
29777
30239
|
(0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
|
|
@@ -29946,7 +30408,7 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
|
|
|
29946
30408
|
} catch {
|
|
29947
30409
|
existingSha = void 0;
|
|
29948
30410
|
}
|
|
29949
|
-
const tmp = (0, import_node_path26.join)((0,
|
|
30411
|
+
const tmp = (0, import_node_path26.join)((0, import_node_os15.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
29950
30412
|
(0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
|
|
29951
30413
|
try {
|
|
29952
30414
|
await gh(contentPutInputArgs(repo, seed.target, tmp));
|
|
@@ -31897,7 +32359,7 @@ async function collectWaveStatus(deps) {
|
|
|
31897
32359
|
init_github_client();
|
|
31898
32360
|
init_cli_shared();
|
|
31899
32361
|
var import_node_fs30 = require("node:fs");
|
|
31900
|
-
var
|
|
32362
|
+
var import_node_os16 = require("node:os");
|
|
31901
32363
|
var import_node_path29 = require("node:path");
|
|
31902
32364
|
var GC_GH_TIMEOUT_MS = 2e4;
|
|
31903
32365
|
async function collectStatus() {
|
|
@@ -32086,7 +32548,7 @@ async function collectOnboardStatus(opts = {}) {
|
|
|
32086
32548
|
else if (top) nextCommand = `mmi-cli oracle board claim ${top.number} # ${top.title}`;
|
|
32087
32549
|
else nextCommand = "mmi-cli oracle board read \u2014 no claimable items found";
|
|
32088
32550
|
}
|
|
32089
|
-
const home = (0,
|
|
32551
|
+
const home = (0, import_node_os16.homedir)();
|
|
32090
32552
|
const plugin = onboardPluginGate({
|
|
32091
32553
|
readKnown: () => readFileSyncSafe((0, import_node_path29.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs30.readFileSync),
|
|
32092
32554
|
readSettings: () => readFileSyncSafe((0, import_node_path29.join)(home, ".claude", "settings.json"), import_node_fs30.readFileSync)
|
|
@@ -34114,7 +34576,7 @@ init_error_codes();
|
|
|
34114
34576
|
init_github_client();
|
|
34115
34577
|
|
|
34116
34578
|
// src/issue-body.ts
|
|
34117
|
-
var
|
|
34579
|
+
var import_node_os17 = require("node:os");
|
|
34118
34580
|
init_error_codes();
|
|
34119
34581
|
var TextArgError = class extends Error {
|
|
34120
34582
|
constructor(message2, code, offendingFlag) {
|
|
@@ -34127,7 +34589,7 @@ var TextArgError = class extends Error {
|
|
|
34127
34589
|
offendingFlag;
|
|
34128
34590
|
};
|
|
34129
34591
|
function emptyStdinMessage(fileFlag) {
|
|
34130
|
-
if ((0,
|
|
34592
|
+
if ((0, import_node_os17.platform)() === "win32") {
|
|
34131
34593
|
return `${fileFlag} - read empty stdin (on Windows, ${fileFlag} - is unreliable through the npm .cmd shim \u2014 use ${fileFlag} <path>, or pipe to \`node cli/dist/index.cjs\` directly)`;
|
|
34132
34594
|
}
|
|
34133
34595
|
return `${fileFlag} - read empty stdin (nothing piped \u2014 pass a heredoc/pipe, or ${fileFlag} <path>)`;
|
|
@@ -36642,7 +37104,7 @@ function registerSchedulesCommands(program3) {
|
|
|
36642
37104
|
// src/secrets-commands.ts
|
|
36643
37105
|
var import_node_fs33 = require("node:fs");
|
|
36644
37106
|
var import_node_path31 = require("node:path");
|
|
36645
|
-
var
|
|
37107
|
+
var import_node_os18 = require("node:os");
|
|
36646
37108
|
init_cli_shared();
|
|
36647
37109
|
init_hub_auth();
|
|
36648
37110
|
init_github_client();
|
|
@@ -36768,7 +37230,7 @@ async function decryptRailsCredentials(input) {
|
|
|
36768
37230
|
'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
|
|
36769
37231
|
"puts JSON.generate(config.config)"
|
|
36770
37232
|
].join("\n");
|
|
36771
|
-
const scriptDir = (0, import_node_fs33.mkdtempSync)((0, import_node_path31.join)((0,
|
|
37233
|
+
const scriptDir = (0, import_node_fs33.mkdtempSync)((0, import_node_path31.join)((0, import_node_os18.tmpdir)(), "mmi-rails-decrypt-"));
|
|
36772
37234
|
const scriptPath = (0, import_node_path31.join)(scriptDir, "decrypt.rb");
|
|
36773
37235
|
(0, import_node_fs33.writeFileSync)(scriptPath, script, "utf8");
|
|
36774
37236
|
try {
|
|
@@ -38164,7 +38626,7 @@ init_github_client();
|
|
|
38164
38626
|
// src/merge-cleanup.ts
|
|
38165
38627
|
var import_node_fs37 = require("node:fs");
|
|
38166
38628
|
var import_node_path35 = require("node:path");
|
|
38167
|
-
var
|
|
38629
|
+
var import_node_os19 = require("node:os");
|
|
38168
38630
|
init_cli_shared();
|
|
38169
38631
|
init_github_client();
|
|
38170
38632
|
|
|
@@ -39234,7 +39696,7 @@ function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
|
|
|
39234
39696
|
return rewritten === base ? null : rewritten;
|
|
39235
39697
|
}
|
|
39236
39698
|
function writeSquashBodyFile(body) {
|
|
39237
|
-
const dir = (0, import_node_fs37.mkdtempSync)((0, import_node_path35.join)((0,
|
|
39699
|
+
const dir = (0, import_node_fs37.mkdtempSync)((0, import_node_path35.join)((0, import_node_os19.tmpdir)(), "mmi-squash-body-"));
|
|
39238
39700
|
const path2 = (0, import_node_path35.join)(dir, "body.txt");
|
|
39239
39701
|
(0, import_node_fs37.writeFileSync)(path2, body.endsWith("\n") ? body : `${body}
|
|
39240
39702
|
`, "utf8");
|
|
@@ -39657,7 +40119,7 @@ function postMergeReconWarnings(input) {
|
|
|
39657
40119
|
// src/review-verdict.ts
|
|
39658
40120
|
var import_node_child_process13 = require("node:child_process");
|
|
39659
40121
|
var import_node_fs39 = require("node:fs");
|
|
39660
|
-
var
|
|
40122
|
+
var import_node_os20 = require("node:os");
|
|
39661
40123
|
var import_node_path37 = require("node:path");
|
|
39662
40124
|
init_cli_shared();
|
|
39663
40125
|
var REVIEW_VERDICT_MARKER = "<!-- zeroci-review v1 -->";
|
|
@@ -39823,7 +40285,7 @@ async function readPrIssueComments(number, repo) {
|
|
|
39823
40285
|
return comments;
|
|
39824
40286
|
}
|
|
39825
40287
|
async function postPrCommentFromFile(number, repo, body) {
|
|
39826
|
-
const dir = (0, import_node_fs39.mkdtempSync)((0, import_node_path37.join)((0,
|
|
40288
|
+
const dir = (0, import_node_fs39.mkdtempSync)((0, import_node_path37.join)((0, import_node_os20.tmpdir)(), "mmi-review-verdict-"));
|
|
39827
40289
|
const path2 = (0, import_node_path37.join)(dir, "body.md");
|
|
39828
40290
|
try {
|
|
39829
40291
|
(0, import_node_fs39.writeFileSync)(path2, body, "utf8");
|
|
@@ -42808,7 +43270,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
|
|
|
42808
43270
|
var import_node_child_process16 = require("node:child_process");
|
|
42809
43271
|
var import_node_crypto13 = require("node:crypto");
|
|
42810
43272
|
var import_node_fs45 = require("node:fs");
|
|
42811
|
-
var
|
|
43273
|
+
var import_node_os21 = require("node:os");
|
|
42812
43274
|
var import_node_path42 = require("node:path");
|
|
42813
43275
|
|
|
42814
43276
|
// ../scripts/distribution-digest.mjs
|
|
@@ -42959,7 +43421,7 @@ function rebuildTo(packageRoot, outDir) {
|
|
|
42959
43421
|
});
|
|
42960
43422
|
}
|
|
42961
43423
|
function runDistStatus(root) {
|
|
42962
|
-
const stage = (0, import_node_fs45.mkdtempSync)((0, import_node_path42.join)((0,
|
|
43424
|
+
const stage = (0, import_node_fs45.mkdtempSync)((0, import_node_path42.join)((0, import_node_os21.tmpdir)(), "mmi-dist-drift-"));
|
|
42963
43425
|
let overlayCount = 0;
|
|
42964
43426
|
try {
|
|
42965
43427
|
const cliOut = (0, import_node_path42.join)(stage, "cli-dist");
|
|
@@ -43776,6 +44238,32 @@ function formatHotfixBatchPreflightRefusal(failures, fromSpecs) {
|
|
|
43776
44238
|
${lines2.join("\n")}
|
|
43777
44239
|
Land ONE development-based cite-only synthesis PR for the whole batch (#4643 supersedes the old cut-from-origin/main wording, which produced main-lineage PRs into development \u2014 the shape portFoldToDevelopment forbids, #4365/#4371): cut the branch FROM origin/development, regenerate the listed conflict content so it cherry-picks cleanly onto current origin/main, and land that development-parented PR with the cite-only marker (the proven #5096 flow), then rerun \`mmi-cli devops hotfix start --from ${batchFrom}\` with the port merge SHA(s) \u2014 never hand-resolve onto main; never a main-parented branch into development; never N serial ports (#3167) \u2014 see docs/Guides/train-troubleshooting.md#hotfix-pick-refused`;
|
|
43778
44240
|
}
|
|
44241
|
+
var CHERRY_PICKED_FROM = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
44242
|
+
async function findHotfixPickGaps(deps, sources, tolerated) {
|
|
44243
|
+
if (sources.length === 0) return [];
|
|
44244
|
+
const namedShas = new Set(sources.map((s) => s.sha.toLowerCase()));
|
|
44245
|
+
const carried = /* @__PURE__ */ new Set();
|
|
44246
|
+
const mainOnly = await deps.run("git", ["log", "--no-merges", "--format=%B", "origin/development..origin/main"]);
|
|
44247
|
+
for (const m of mainOnly.matchAll(CHERRY_PICKED_FROM)) carried.add(m[1].toLowerCase());
|
|
44248
|
+
const gaps = [];
|
|
44249
|
+
const devOnly = (await deps.run("git", ["log", "--no-merges", "--reverse", "--format=%H%x09%s", "origin/main..origin/development"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
44250
|
+
for (const line of devOnly) {
|
|
44251
|
+
const [sha, ...subjectParts] = line.split(" ");
|
|
44252
|
+
if (!sha) continue;
|
|
44253
|
+
const lower = sha.toLowerCase();
|
|
44254
|
+
if (namedShas.has(lower) || carried.has(lower)) continue;
|
|
44255
|
+
const paths = (await deps.run("git", ["show", "--no-color", "--name-only", "--pretty=format:", sha])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
44256
|
+
if (paths.length === 0 || paths.every((p) => pathIsTolerated(p, tolerated))) continue;
|
|
44257
|
+
gaps.push({ sha, subject: subjectParts.join(" ") });
|
|
44258
|
+
}
|
|
44259
|
+
return gaps;
|
|
44260
|
+
}
|
|
44261
|
+
function formatHotfixPickGapRefusal(gaps, tag, fromSpecs) {
|
|
44262
|
+
const lines2 = gaps.map((g) => ` - ${g.sha.slice(0, 7)} ${g.subject}`);
|
|
44263
|
+
return `hotfix start refused: ${tag} would omit ${gaps.length} merged development fix(es) not on origin/main:
|
|
44264
|
+
${lines2.join("\n")}
|
|
44265
|
+
A hotfix branch is cut from origin/main and carries ONLY the fixes --from names, in order (#4411) \u2014 every other merged development fix stays unshipped while the channel reports healthy (#6994). Add each omitted fix to the pick list and rerun \`mmi-cli devops hotfix start --from ${[...fromSpecs, ...gaps.map((g) => g.sha.slice(0, 7))].join(",")}\`, or, when a listed fix is provably not needed on main (a development-only change, already superseded), rerun with \`--ack ${gaps.map((g) => g.sha.slice(0, 7)).join(",")}\` naming exactly the sha(s) a human excluded \u2014 the ack is recorded in the PR body, never silent.`;
|
|
44266
|
+
}
|
|
43779
44267
|
async function preflightHotfixBatchPicks(deps, sources, pickTolerated) {
|
|
43780
44268
|
if (sources.length === 0) return [];
|
|
43781
44269
|
const startBranch = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
@@ -43962,6 +44450,18 @@ async function runHotfixStart(deps, options, doctor = runTrainDoctor) {
|
|
|
43962
44450
|
}
|
|
43963
44451
|
const label = sources.map((s) => s.label).join(", ");
|
|
43964
44452
|
const { foldPaths, pickTolerated } = await resolveHotfixPickTolerated(deps, deployModel);
|
|
44453
|
+
const ackedShas = (options.ack ?? []).map((a) => a.trim().toLowerCase()).filter(Boolean);
|
|
44454
|
+
const pickGaps = await findHotfixPickGaps(deps, sources, pickTolerated);
|
|
44455
|
+
const unacked = pickGaps.filter((g) => {
|
|
44456
|
+
const sha = g.sha.toLowerCase();
|
|
44457
|
+
return !ackedShas.some((a) => sha.startsWith(a) || a.startsWith(sha));
|
|
44458
|
+
});
|
|
44459
|
+
if (unacked.length > 0) {
|
|
44460
|
+
throw new Error(formatHotfixPickGapRefusal(unacked, tag, specs));
|
|
44461
|
+
}
|
|
44462
|
+
if (pickGaps.length > 0) {
|
|
44463
|
+
notes.push(`pick-completeness ack: excluded ${pickGaps.map((g) => `${g.sha.slice(0, 7)} ${g.subject}`).join("; ")} by --ack (#6994)`);
|
|
44464
|
+
}
|
|
43965
44465
|
const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
43966
44466
|
if (remoteBranch) {
|
|
43967
44467
|
await deps.run("git", ["checkout", branch]);
|
|
@@ -44026,7 +44526,9 @@ async function runHotfixStart(deps, options, doctor = runTrainDoctor) {
|
|
|
44026
44526
|
"--body",
|
|
44027
44527
|
`Hotfix ${tag}: cherry-pick of ${label} onto origin/main${bumpNote}.
|
|
44028
44528
|
|
|
44029
|
-
<!-- mmi-hotfix-carries: ${sources.map((s) => s.sha).join(",")}
|
|
44529
|
+
<!-- mmi-hotfix-carries: ${sources.map((s) => s.sha).join(",")} -->${pickGaps.length > 0 ? `
|
|
44530
|
+
|
|
44531
|
+
<!-- mmi-hotfix-acked-excluded: ${pickGaps.map((g) => g.sha).join(",")} (#6994 pick-completeness) -->` : ""}
|
|
44030
44532
|
|
|
44031
44533
|
Merge this PR (human-initiated), then run \`mmi-cli devops hotfix release ${tag}\`.`
|
|
44032
44534
|
]));
|
|
@@ -44416,9 +44918,9 @@ ${decision.note}`);
|
|
|
44416
44918
|
let deployNote;
|
|
44417
44919
|
let tenantPublishDispatch = null;
|
|
44418
44920
|
if (deployModel === "hub-serverless") {
|
|
44419
|
-
|
|
44420
|
-
|
|
44421
|
-
runs.push(await recoverHotfixReleaseRun(deps, ledger, anchors, ctx, deployModel, workflow === "publish.yml" ? "publish" : "deploy", watched));
|
|
44921
|
+
const watchedRuns = await Promise.all(HOTFIX_RELEASE_WORKFLOWS.map((workflow) => watchReleaseRun(deps, ctx, workflow, mergedSha)));
|
|
44922
|
+
for (const watched of watchedRuns) {
|
|
44923
|
+
runs.push(await recoverHotfixReleaseRun(deps, ledger, anchors, ctx, deployModel, watched.workflow === "publish.yml" ? "publish" : "deploy", watched));
|
|
44422
44924
|
}
|
|
44423
44925
|
deployNote = "watched release-triggered deploy.yml + publish.yml";
|
|
44424
44926
|
} else if (deployModel === "registry-publish") {
|
|
@@ -44427,17 +44929,9 @@ ${decision.note}`);
|
|
|
44427
44929
|
runs.push(run);
|
|
44428
44930
|
deployNote = "watched this repo's own release-triggered publish.yml (#2428 \u2014 no central dispatch)";
|
|
44429
44931
|
} else if (deployModel === "tenant-container" || deployModel === "solo-container") {
|
|
44430
|
-
const deploy0 = await
|
|
44431
|
-
deps,
|
|
44432
|
-
ctx,
|
|
44433
|
-
"main",
|
|
44434
|
-
"main",
|
|
44435
|
-
deployModel,
|
|
44436
|
-
true,
|
|
44437
|
-
(deps.now ?? Date.now)(),
|
|
44438
|
-
mergedSha,
|
|
44439
|
-
"report",
|
|
44440
|
-
meta.publishDir
|
|
44932
|
+
const [deploy0, publish0] = await joinDeployAndPublish(
|
|
44933
|
+
dispatchDeploy(deps, ctx, "main", "main", deployModel, true, (deps.now ?? Date.now)(), mergedSha, "report", meta.publishDir),
|
|
44934
|
+
observePublishIfRequired(deps, ctx, meta, deployModel, "main", mergedSha, true)
|
|
44441
44935
|
);
|
|
44442
44936
|
const deploy = await recoverFailedDispatchPhase(
|
|
44443
44937
|
deps,
|
|
@@ -44447,7 +44941,6 @@ ${decision.note}`);
|
|
|
44447
44941
|
deploy0,
|
|
44448
44942
|
{ repo: releaseRunRepoFor(deployModel, ctx.repo), sha: mergedSha, watched: true, workflow: "tenant-deploy.yml" }
|
|
44449
44943
|
);
|
|
44450
|
-
const publish0 = await observePublishIfRequired(deps, ctx, meta, deployModel, "main", mergedSha, true);
|
|
44451
44944
|
const publish = publish0 ? await recoverFailedDispatchPhase(
|
|
44452
44945
|
deps,
|
|
44453
44946
|
ledger,
|
|
@@ -44542,6 +45035,48 @@ ${decision.note}`);
|
|
|
44542
45035
|
const localSync = await syncLocalTrainBranches(deps.run, { fetch: false, warn: deps.warn }).catch(() => void 0);
|
|
44543
45036
|
return { ...result, ...localSync ? { localSync } : {} };
|
|
44544
45037
|
}
|
|
45038
|
+
async function runHotfixRetryDeploy(deps, versionInput) {
|
|
45039
|
+
const ctx = await buildTrainApplyContext(deps);
|
|
45040
|
+
const { tag } = normalizeHotfixVersion(versionInput);
|
|
45041
|
+
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
45042
|
+
if (porcelainHasBlockingChanges(status)) {
|
|
45043
|
+
throw new Error(`working tree must be clean before hotfix --retry-deploy${describeBlockingPaths(status)}`);
|
|
45044
|
+
}
|
|
45045
|
+
const root = await hotfixCheckoutRoot(deps);
|
|
45046
|
+
await deps.run("git", ["fetch", "origin", "--tags"]);
|
|
45047
|
+
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
45048
|
+
const deployModel = requireDeployModel(meta, ctx.repo);
|
|
45049
|
+
const result = await retryHotfixDeployLegs(deps, root, {
|
|
45050
|
+
retryAlignment: async (prior) => {
|
|
45051
|
+
let foldNote2;
|
|
45052
|
+
let foldStatus2 = "ok";
|
|
45053
|
+
try {
|
|
45054
|
+
foldNote2 = await portFoldToDevelopment(deps, ctx, deployModel, prior.tag);
|
|
45055
|
+
} catch (e) {
|
|
45056
|
+
foldStatus2 = "failure";
|
|
45057
|
+
foldNote2 = `development fold port FAILED: ${e.message ?? e} \u2014 the release stands; port it by hand on a fresh branch: git checkout -B ${devFoldBranch(prior.tag)}-manual origin/development && ${foldPortManualCommand(deployModel, prior.tag.replace(/^v/, ""))}, then push it as the canonical head (git push origin ${devFoldBranch(prior.tag)}-manual:refs/heads/${devFoldBranch(prior.tag)}) and open a development-base PR \u2014 see docs/Guides/train-troubleshooting.md#hotfix-fold-port`;
|
|
45058
|
+
}
|
|
45059
|
+
return hotfixFoldAlignmentEntry(foldStatus2, foldNote2, prior.tagSha);
|
|
45060
|
+
}
|
|
45061
|
+
});
|
|
45062
|
+
const ledger = openReleaseLedgerWriter(root);
|
|
45063
|
+
const report = releaseLedgerReport(
|
|
45064
|
+
await ledger.load({ repo: result.repo, tag: result.tag, tagSha: result.tagSha }).catch(() => void 0),
|
|
45065
|
+
ledger.path
|
|
45066
|
+
);
|
|
45067
|
+
const deployNote = result.deploy?.note ?? result.deploy?.error;
|
|
45068
|
+
const foldNote = result.alignment?.note ?? result.alignment?.error;
|
|
45069
|
+
const foldStatus = result.alignment ? result.alignment.state === "failed" ? "failure" : result.alignment.state === "pending" ? "unresolved" : "ok" : void 0;
|
|
45070
|
+
const runs = result.deploy ? [{ workflow: result.deploy.workflow ?? "jerv-gateway", conclusion: result.deploy.state === "complete" ? "success" : result.deploy.state === "failed" ? "failure" : "pending" }] : void 0;
|
|
45071
|
+
return {
|
|
45072
|
+
...result,
|
|
45073
|
+
...deployNote ? { deployNote } : {},
|
|
45074
|
+
...foldNote ? { foldNote } : {},
|
|
45075
|
+
...foldStatus ? { foldStatus } : {},
|
|
45076
|
+
...runs ? { runs } : {},
|
|
45077
|
+
...report ? { ledger: report } : {}
|
|
45078
|
+
};
|
|
45079
|
+
}
|
|
44545
45080
|
function deriveHotfixState(f) {
|
|
44546
45081
|
if (!f.branchExists && !f.pr && !f.tagPushed && !f.releaseExists) {
|
|
44547
45082
|
return { state: "not-started", next: `mmi-cli devops hotfix start --from <pr#|sha> (would mint ${f.tag})` };
|
|
@@ -45117,7 +45652,7 @@ async function announceSummaryRefusal(path2) {
|
|
|
45117
45652
|
function trainApplyDeps() {
|
|
45118
45653
|
return {
|
|
45119
45654
|
run: async (file, args) => {
|
|
45120
|
-
const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : file === "node" && args[1] === "verify" ? NODE_VERIFY_TIMEOUT_MS : file === "npm" && (args[0] === "version" || args[0] === "ci" || args[0] === "install") ? NPM_FOLD_TIMEOUT_MS : file === "npm" ? NPM_TRAIN_TIMEOUT_MS : file === "bash" &&
|
|
45655
|
+
const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : file === "node" && args[1] === "verify" ? NODE_VERIFY_TIMEOUT_MS : file === "npm" && (args[0] === "version" || args[0] === "ci" || args[0] === "install") ? NPM_FOLD_TIMEOUT_MS : file === "npm" ? NPM_TRAIN_TIMEOUT_MS : file === "bash" && /jerv-gateway-release-deploy\.sh$/.test(args[0] ?? "") ? JERV_GATEWAY_DEPLOY_TIMEOUT_MS : file !== "gh" ? GIT_TIMEOUT_MS : args[0] === "run" && args[1] === "watch" ? GH_RUN_WATCH_TIMEOUT_MS : GH_TRAIN_TIMEOUT_MS;
|
|
45121
45656
|
try {
|
|
45122
45657
|
return isWin3 && file === "npm" ? (await execFileP("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP(file, args, { timeout })).stdout;
|
|
45123
45658
|
} catch (e) {
|
|
@@ -45227,7 +45762,7 @@ function trainApplyDeps() {
|
|
|
45227
45762
|
}
|
|
45228
45763
|
function formatWorkflowRun(r) {
|
|
45229
45764
|
const evidenced = workflowRunWithEvidence(r);
|
|
45230
|
-
return `${evidenced.workflow} ${evidenced.runUrl ?? evidenced.runUrlNote} ${evidenced.conclusion.toUpperCase()}`;
|
|
45765
|
+
return `${evidenced.workflow} ${evidenced.runUrl ?? evidenced.runUrlNote} ${evidenced.conclusion.toUpperCase()}${r.parallel ? " (parallel)" : ""}`;
|
|
45231
45766
|
}
|
|
45232
45767
|
function renderDeployLine(d) {
|
|
45233
45768
|
const parts = [d.dispatch];
|
|
@@ -45792,6 +46327,14 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
45792
46327
|
const evidenced = hotfixRunWithEvidence(run);
|
|
45793
46328
|
return ` - ${evidenced.workflow}: ${evidenced.conclusion}${evidenced.conclusion === "failure" ? " \u2014 error: workflow reported failure" : ""} (${evidenced.url ?? evidenced.runUrlNote})`;
|
|
45794
46329
|
}
|
|
46330
|
+
function renderHotfixRetryDeploy(r) {
|
|
46331
|
+
return [
|
|
46332
|
+
`mmi-cli devops hotfix release --retry-deploy: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`,
|
|
46333
|
+
...r.deployNote ? [` - deploy: ${r.deployNote}`] : [],
|
|
46334
|
+
...r.foldNote ? [` - fold: ${r.foldNote}`] : [],
|
|
46335
|
+
...r.ledger ? formatReleaseLedgerReport(r.ledger).map((line) => ` ${line}`) : []
|
|
46336
|
+
].join("\n");
|
|
46337
|
+
}
|
|
45795
46338
|
function renderHotfixRelease(r) {
|
|
45796
46339
|
return [
|
|
45797
46340
|
`mmi-cli devops hotfix release: ${r.tag} at ${r.mergedSha.slice(0, 7)} on ${r.repo} (deployModel=${r.deployModel})`,
|
|
@@ -45864,12 +46407,20 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
45864
46407
|
const steps = trainPlan("hotfix");
|
|
45865
46408
|
console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli devops hotfix: dry-run plan", steps));
|
|
45866
46409
|
});
|
|
45867
|
-
hotfixCmd.command("start").description("cherry-pick one or more merged development PRs (or SHAs) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha[,pr#|sha...]>", "merged development PR number(s) or commit SHA(s) to cherry-pick, in pick order \u2014 one hotfix cycle carries as many fixes as you name (#4411)").option("--json", "machine-readable output").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o, renderHotfixStart));
|
|
45868
|
-
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line release Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068), optional for a product repo announcing to its project release channel (#6517)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) =>
|
|
45869
|
-
|
|
45870
|
-
|
|
45871
|
-
|
|
45872
|
-
|
|
46410
|
+
hotfixCmd.command("start").description("cherry-pick one or more merged development PRs (or SHAs) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha[,pr#|sha...]>", "merged development PR number(s) or commit SHA(s) to cherry-pick, in pick order \u2014 one hotfix cycle carries as many fixes as you name (#4411)").option("--ack <shas>", "comma-separated dev shas a human verified are NOT needed on main, overriding the pick-completeness guard for exactly those fixes \u2014 recorded in the PR body, never silent (#6994)").option("--json", "machine-readable output").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from, ack: o.ack ? o.ack.split(",").map((s) => s.trim()).filter(Boolean) : void 0 }), o, renderHotfixStart));
|
|
46411
|
+
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line release Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068), optional for a product repo announcing to its project release channel (#6517)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--retry-deploy", "re-run only the failed deploy and/or fold-port alignment legs of a published hotfix of this same tag (#6985)").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) => {
|
|
46412
|
+
if (o.retryDeploy) {
|
|
46413
|
+
if (o.announceSummaryFile || o.carries) {
|
|
46414
|
+
return fail("hotfix release: --retry-deploy accepts only --json and --out; it re-runs failed deploy/alignment legs of the published tag and does not recut");
|
|
46415
|
+
}
|
|
46416
|
+
return runHotfixSub("retry-deploy", () => runHotfixRetryDeploy(trainApplyDeps(), version), o, renderHotfixRetryDeploy);
|
|
46417
|
+
}
|
|
46418
|
+
return runHotfixSub("release", async () => {
|
|
46419
|
+
const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] });
|
|
46420
|
+
const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
46421
|
+
return { ...result, projectInfoSync };
|
|
46422
|
+
}, o, renderHotfixRelease);
|
|
46423
|
+
});
|
|
45873
46424
|
function hotfixStatusDeps() {
|
|
45874
46425
|
const STATUS_GH_MS = 8e3;
|
|
45875
46426
|
const STATUS_GIT_MS = 1e4;
|
|
@@ -45911,7 +46462,7 @@ function envHealLockPath(home) {
|
|
|
45911
46462
|
async function withEnvHealLock(what, run) {
|
|
45912
46463
|
try {
|
|
45913
46464
|
return await withFileLock(
|
|
45914
|
-
envHealLockPath((0,
|
|
46465
|
+
envHealLockPath((0, import_node_os22.homedir)()),
|
|
45915
46466
|
{ staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
|
|
45916
46467
|
run
|
|
45917
46468
|
);
|
|
@@ -46008,7 +46559,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
46008
46559
|
const configRoot = surfaceConfigRoot(surface);
|
|
46009
46560
|
const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
|
|
46010
46561
|
const plan = buildPluginCachePlan(
|
|
46011
|
-
(0,
|
|
46562
|
+
(0, import_node_os22.homedir)(),
|
|
46012
46563
|
running,
|
|
46013
46564
|
pluginCacheFsDeps(configRoot, () => 0),
|
|
46014
46565
|
{ configRoot, includeStaging: surface !== "codex" }
|
|
@@ -46036,7 +46587,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
46036
46587
|
const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
|
|
46037
46588
|
const installed = installedActivePluginVersion(surface);
|
|
46038
46589
|
const plan = buildPluginCachePlan(
|
|
46039
|
-
(0,
|
|
46590
|
+
(0, import_node_os22.homedir)(),
|
|
46040
46591
|
running,
|
|
46041
46592
|
pluginCacheFsDeps(configRoot, () => 0),
|
|
46042
46593
|
{ configRoot, includeStaging: surface !== "codex", installedVersion: installed }
|
|
@@ -47261,7 +47812,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
|
|
|
47261
47812
|
return;
|
|
47262
47813
|
}
|
|
47263
47814
|
const plan = buildPluginCachePlan(
|
|
47264
|
-
(0,
|
|
47815
|
+
(0, import_node_os22.homedir)(),
|
|
47265
47816
|
running,
|
|
47266
47817
|
pluginCacheFsDeps(configRoot, directoryBytes),
|
|
47267
47818
|
{ withBytes: true, configRoot, includeStaging: surface !== "codex" }
|