@mutmutco/cli 4.4.17 → 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 +339 -68
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -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"]);
|
|
@@ -18889,6 +18934,12 @@ async function observePublishIfRequired(deps, ctx, meta, model, stage, releaseSh
|
|
|
18889
18934
|
deployStatus: aggregateWorkflowRuns(workflowRuns)
|
|
18890
18935
|
};
|
|
18891
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
|
+
}
|
|
18892
18943
|
function appendPublishDispatch(deploy, publish) {
|
|
18893
18944
|
if (!publish) return deploy;
|
|
18894
18945
|
return {
|
|
@@ -19053,19 +19104,29 @@ async function appendServedChannelPublish(deps, input, dispatch) {
|
|
|
19053
19104
|
};
|
|
19054
19105
|
}
|
|
19055
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
|
+
}
|
|
19056
19117
|
if (!probe.ok) {
|
|
19057
19118
|
const why = probe.error ?? `HTTP ${probe.status ?? "unknown"}`;
|
|
19058
19119
|
return {
|
|
19059
19120
|
...dispatch,
|
|
19060
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`,
|
|
19061
|
-
workflowRuns:
|
|
19122
|
+
workflowRuns: evidence("pending", `${channelUrl} unreadable (${why})`)
|
|
19062
19123
|
};
|
|
19063
19124
|
}
|
|
19064
19125
|
if (expected && probe.version !== expected) {
|
|
19065
19126
|
return {
|
|
19066
19127
|
...dispatch,
|
|
19067
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}\``,
|
|
19068
|
-
workflowRuns:
|
|
19129
|
+
workflowRuns: evidence("failure", `${channelUrl} reports ${probe.version}, expected ${expected}`),
|
|
19069
19130
|
deployStatus: "failure"
|
|
19070
19131
|
};
|
|
19071
19132
|
}
|
|
@@ -19073,15 +19134,88 @@ async function appendServedChannelPublish(deps, input, dispatch) {
|
|
|
19073
19134
|
return {
|
|
19074
19135
|
...dispatch,
|
|
19075
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`,
|
|
19076
|
-
workflowRuns:
|
|
19137
|
+
workflowRuns: evidence("pending", `${channelUrl} reports ${probe.version}; nothing to compare against`)
|
|
19077
19138
|
};
|
|
19078
19139
|
}
|
|
19079
19140
|
return {
|
|
19080
19141
|
...dispatch,
|
|
19081
19142
|
note: `${dispatch.note}; served channel moved \u2014 ${channelUrl} now reports ${probe.version}`,
|
|
19082
|
-
workflowRuns:
|
|
19143
|
+
workflowRuns: evidence("success", `${channelUrl} reports ${probe.version}`)
|
|
19083
19144
|
};
|
|
19084
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
|
+
}
|
|
19085
19219
|
var JERV_GATEWAY_RUN_URL_NOTE = "no run URL emitted \u2014 host health leg runs outside GitHub Actions";
|
|
19086
19220
|
var JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT = "scripts/jerv-gateway-release-deploy.sh";
|
|
19087
19221
|
async function runTaggedJervGatewayLocalWrapper(deps, tag, tagSha) {
|
|
@@ -19141,15 +19275,15 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
|
|
|
19141
19275
|
}
|
|
19142
19276
|
}
|
|
19143
19277
|
async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha, enumerateSha = false) {
|
|
19144
|
-
const
|
|
19145
|
-
|
|
19278
|
+
const parallel = targets.length > 1 ? { parallel: true } : {};
|
|
19279
|
+
const workflowRuns = await Promise.all(targets.map(async (target) => {
|
|
19146
19280
|
try {
|
|
19147
19281
|
const run = await correlateWorkflowRun(deps, { ...target, since, headSha, repo });
|
|
19148
|
-
|
|
19282
|
+
return { ...await watchWorkflowRun(deps, target.workflow, run, repo), ...parallel };
|
|
19149
19283
|
} catch {
|
|
19150
|
-
|
|
19284
|
+
return { workflow: target.workflow, conclusion: "failure", ...parallel };
|
|
19151
19285
|
}
|
|
19152
|
-
}
|
|
19286
|
+
}));
|
|
19153
19287
|
if (!enumerateSha) return workflowRuns;
|
|
19154
19288
|
const seen = new Set(workflowRuns.map((r) => r.runId).filter((id) => typeof id === "number"));
|
|
19155
19289
|
workflowRuns.push(...await discoverShaWorkflowRuns(deps, repo, headSha, seen));
|
|
@@ -19204,8 +19338,8 @@ async function vercelDeployDispatch(deps, ctx, ref, watch, headSha) {
|
|
|
19204
19338
|
return { note: `${note}; ${detail}`, ...read?.logUrl ? { runUrl: read.logUrl } : {}, workflowRuns: [row(read, deployStatus)], deployStatus };
|
|
19205
19339
|
}
|
|
19206
19340
|
function shipLegsGreen(deploy, publish) {
|
|
19207
|
-
const green = (
|
|
19208
|
-
return green(deploy
|
|
19341
|
+
const green = (leg) => leg?.superseded !== true && (leg?.state === "complete" || leg?.state === "skipped");
|
|
19342
|
+
return green(deploy) && green(publish);
|
|
19209
19343
|
}
|
|
19210
19344
|
async function announceShippedRelease(deps, args, model, legs, deferred) {
|
|
19211
19345
|
if (!deps.announce) return void 0;
|
|
@@ -21184,7 +21318,9 @@ var TRANSIENT_TRANSPORT_MARKERS = [
|
|
|
21184
21318
|
/socket hang ?up/i,
|
|
21185
21319
|
/remote end hung ?up unexpectedly/i,
|
|
21186
21320
|
/early EOF/i,
|
|
21187
|
-
/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
|
|
21188
21324
|
];
|
|
21189
21325
|
function classifyTransientTransportFailure(evidence) {
|
|
21190
21326
|
if (!evidence) return false;
|
|
@@ -21855,6 +21991,70 @@ async function healStaleAlignmentLeg(deps, cwd, expected) {
|
|
|
21855
21991
|
return prior.tag;
|
|
21856
21992
|
});
|
|
21857
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
|
+
}
|
|
21858
22058
|
function supersedeableDeployLegShape(rec) {
|
|
21859
22059
|
return rec.state === "failed" || rec.state === "pending" && rec.runId == null;
|
|
21860
22060
|
}
|
|
@@ -22482,6 +22682,7 @@ var defaultDeps2 = {
|
|
|
22482
22682
|
}
|
|
22483
22683
|
},
|
|
22484
22684
|
healAlignmentLeg: healStaleAlignmentLeg,
|
|
22685
|
+
healRunLegs: healUnfoldedRunLegs,
|
|
22485
22686
|
readSurfacesRaw: (cwd) => {
|
|
22486
22687
|
try {
|
|
22487
22688
|
return (0, import_node_fs23.readFileSync)((0, import_node_path23.join)(cwd, "surfaces.json"), "utf8");
|
|
@@ -22828,6 +23029,21 @@ async function runTrainDoctor(input) {
|
|
|
22828
23029
|
try {
|
|
22829
23030
|
let ledger = parseReleaseLedger(ledgerRaw);
|
|
22830
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
|
+
}
|
|
22831
23047
|
if (heal && derived.phaseStatus === "phases-pending" && derived.legs.every((l) => l.state !== "pending" || l.phase === "alignment")) {
|
|
22832
23048
|
const closedTag = await deps.healAlignmentLeg(train, cwd);
|
|
22833
23049
|
if (closedTag) {
|
|
@@ -22866,7 +23082,7 @@ async function runTrainDoctor(input) {
|
|
|
22866
23082
|
// #6429: each lane finishes its own run (#6068) — an rcand-lane ledger named as a release
|
|
22867
23083
|
// ledger, with `release --resume` as its remedy, sends the operator to a verb that refuses it.
|
|
22868
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)`,
|
|
22869
|
-
remedy: failed ? ledgerFailedRemedy(ledger) : `finish it first: ${ledgerLaneResumeCommand(ledger)} (alignment-only pending legs close themselves with \`train doctor --heal\` once the PR merges)${supersession}`
|
|
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}`
|
|
22870
23086
|
});
|
|
22871
23087
|
} else if (newestRemoteReleaseTag && newestRemoteReleaseTag !== ledger.tag) {
|
|
22872
23088
|
add({
|
|
@@ -23622,6 +23838,8 @@ async function recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, phase,
|
|
|
23622
23838
|
}
|
|
23623
23839
|
async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch, input, recover = recoverPhaseWithMaybeTransientRerun) {
|
|
23624
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;
|
|
23625
23843
|
let workflowRuns = dispatch.workflowRuns?.length ? [...dispatch.workflowRuns] : void 0;
|
|
23626
23844
|
const notes = [];
|
|
23627
23845
|
let fallbackRecovered = false;
|
|
@@ -23629,6 +23847,7 @@ async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch
|
|
|
23629
23847
|
if (failedRows.length > 0) {
|
|
23630
23848
|
for (const failed of failedRows) {
|
|
23631
23849
|
const rowPhase = failed.workflow.includes("publish") ? "publish" : phase;
|
|
23850
|
+
if (persisted?.phases[rowPhase]?.superseded === true) continue;
|
|
23632
23851
|
const recovery = await recover(deps, ledger, anchors, rowPhase, {
|
|
23633
23852
|
repo: input.repo,
|
|
23634
23853
|
sha: input.sha,
|
|
@@ -23803,7 +24022,10 @@ ${recovery.note}`), recoveryInput);
|
|
|
23803
24022
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
23804
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" });
|
|
23805
24024
|
let alignment = alignEarly ? await alignEarly() : void 0;
|
|
23806
|
-
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
|
+
);
|
|
23807
24029
|
const deployRunRepo = releaseRunRepoFor(deployModel, ctx.repo);
|
|
23808
24030
|
const failedReleaseRun = deployDispatch0.workflowRuns?.find((run) => run.conclusion === "failure");
|
|
23809
24031
|
const dispatchFailurePhase = failedReleaseRun?.workflow === "publish.yml" || deployModel === "registry-publish" ? "publish" : "deploy";
|
|
@@ -23825,7 +24047,6 @@ ${recovery.note}`), recoveryInput);
|
|
|
23825
24047
|
await recordPhase(ledger, deps, ledgerAnchors, "deploy", deployLeg, {
|
|
23826
24048
|
landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
|
|
23827
24049
|
});
|
|
23828
|
-
const publishDispatch0 = await observePublishIfRequired(deps, ctx, meta, deployModel, "main", releaseSha, watch);
|
|
23829
24050
|
const publishDispatch = publishDispatch0 ? await recoverFailedDispatchPhase(
|
|
23830
24051
|
deps,
|
|
23831
24052
|
ledger,
|
|
@@ -23841,6 +24062,14 @@ ${recovery.note}`), recoveryInput);
|
|
|
23841
24062
|
await recordPhase(ledger, deps, ledgerAnchors, "publish", publishLeg, {
|
|
23842
24063
|
landed: "the immutable tag, the green required-check wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
23843
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);
|
|
23844
24073
|
let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
23845
24074
|
dispatch = await appendMmiGetRedeploy(
|
|
23846
24075
|
deps,
|
|
@@ -23852,19 +24081,8 @@ ${recovery.note}`), recoveryInput);
|
|
|
23852
24081
|
{ repo: ctx.repo, publishStatus: publishLeg.state === "complete" ? "success" : "failure", watch },
|
|
23853
24082
|
dispatch
|
|
23854
24083
|
);
|
|
23855
|
-
|
|
23856
|
-
|
|
23857
|
-
{ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
|
|
23858
|
-
deployModel,
|
|
23859
|
-
{ deploy: deployLeg, publish: publishLeg },
|
|
23860
|
-
releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
|
|
23861
|
-
);
|
|
23862
|
-
if (watch && alignment) {
|
|
23863
|
-
alignment = {
|
|
23864
|
-
dev: await awaitAlignmentLanded(deps, ctx, alignment.dev, "development"),
|
|
23865
|
-
...alignment.rc ? { rc: await awaitAlignmentLanded(deps, ctx, alignment.rc, "rc") } : {}
|
|
23866
|
-
};
|
|
23867
|
-
}
|
|
24084
|
+
dispatch = await appendServedChannelProof(deps, { repo: ctx.repo, tag, shipped: shipLegsGreen(deployLeg, publishLeg) }, dispatch);
|
|
24085
|
+
alignment = await aligning;
|
|
23868
24086
|
const announceNote = await announcing;
|
|
23869
24087
|
return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors, ...alignment ? { alignment } : {} };
|
|
23870
24088
|
}
|
|
@@ -24180,6 +24398,8 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
24180
24398
|
dispatch2.deployStatus = historicalDeploy;
|
|
24181
24399
|
dispatch2.note = `historical ${deployModel} deploy/publish status: ${historicalDeploy}; ${companion.note}`;
|
|
24182
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;
|
|
24183
24403
|
if (persisted) {
|
|
24184
24404
|
if (persisted.phases.deploy.superseded !== true) {
|
|
24185
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" });
|
|
@@ -24191,7 +24411,8 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
24191
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" });
|
|
24192
24412
|
}
|
|
24193
24413
|
if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
|
|
24194
|
-
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(
|
|
24195
24416
|
deps,
|
|
24196
24417
|
{ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
|
|
24197
24418
|
deployModel,
|
|
@@ -24281,7 +24502,10 @@ ${recovery.note}`), recoveryInput);
|
|
|
24281
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" });
|
|
24282
24503
|
let devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
24283
24504
|
let rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
|
|
24284
|
-
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
|
+
);
|
|
24285
24509
|
const recoveredDeployDispatch = await recoverFailedDispatchPhase(
|
|
24286
24510
|
deps,
|
|
24287
24511
|
ledger,
|
|
@@ -24297,12 +24521,13 @@ ${recovery.note}`), recoveryInput);
|
|
|
24297
24521
|
);
|
|
24298
24522
|
const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch);
|
|
24299
24523
|
steps.push(`dispatched the ${deployModel} deploy path`);
|
|
24300
|
-
const deployLeg = phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
|
|
24301
|
-
|
|
24302
|
-
|
|
24303
|
-
|
|
24304
|
-
|
|
24305
|
-
|
|
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
|
+
}
|
|
24306
24531
|
const publishDispatch = publishDispatch0 ? await recoverFailedDispatchPhase(
|
|
24307
24532
|
deps,
|
|
24308
24533
|
ledger,
|
|
@@ -24311,11 +24536,13 @@ ${recovery.note}`), recoveryInput);
|
|
|
24311
24536
|
publishDispatch0,
|
|
24312
24537
|
{ repo: ctx.repo, sha: tagSha, watched: false, workflow: "publish.yml" }
|
|
24313
24538
|
) : null;
|
|
24314
|
-
const publishLeg = phaseEntry(publishPhaseInput(deployModel, deployDispatch, publishDispatch, meta.publishRequired === true, { releaseSha: tagSha, repo: ctx.repo }));
|
|
24315
|
-
|
|
24316
|
-
|
|
24317
|
-
|
|
24318
|
-
|
|
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
|
+
}
|
|
24319
24546
|
const dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
24320
24547
|
const announcing = announceShippedRelease(
|
|
24321
24548
|
deps,
|
|
@@ -24325,8 +24552,9 @@ ${recovery.note}`), recoveryInput);
|
|
|
24325
24552
|
releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
|
|
24326
24553
|
);
|
|
24327
24554
|
if (watch) {
|
|
24328
|
-
|
|
24329
|
-
|
|
24555
|
+
const landed = await awaitAlignmentsLanded(deps, ctx, { dev: devRollForward, rc: rcAlignment });
|
|
24556
|
+
devRollForward = landed.dev;
|
|
24557
|
+
rcAlignment = landed.rc;
|
|
24330
24558
|
}
|
|
24331
24559
|
const announceNote = await announcing;
|
|
24332
24560
|
steps.push(`development roll-forward: ${devRollForward.status}`);
|
|
@@ -24981,6 +25209,7 @@ var RULESET_TEMPLATE_SEED = "seed:mmi-product-required-checks.template.json";
|
|
|
24981
25209
|
var RULESET_REFERENCE_MATCH_LABEL = "committed ruleset reference matches the live ruleset (#3816)";
|
|
24982
25210
|
var GATE_GREEN_ON_BASE_LABEL = "gate is green on the default branch (#3819)";
|
|
24983
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)";
|
|
24984
25213
|
var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressable (#3880)";
|
|
24985
25214
|
var RELEASE_BRANCH_REACHABLE_LABEL = "rc/main required contexts are reachable from a release-base head (#5193)";
|
|
24986
25215
|
var RULESET_TRACK_SCOPE_LABEL = "product ruleset branch scope covers the release track (#6327)";
|
|
@@ -25413,6 +25642,17 @@ async function auditRepoCi(repo, deps) {
|
|
|
25413
25642
|
remediation: missingEmitters.length ? `Fix the pull_request workflows or ${contextAuthority.source} before reconciling` : void 0
|
|
25414
25643
|
});
|
|
25415
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
|
+
}
|
|
25416
25656
|
const prOnly = contextAuthority.coversReleaseBranches ? contextAuthority.contexts.filter((context) => TRAIN_PR_ONLY_CONTEXTS.has(context)) : [];
|
|
25417
25657
|
const tagBlind = contextAuthority.coversReleaseBranches ? contextsUnreachableOnTagPush(contextAuthority.contexts, await getBranchWorkflows() ?? [], TRAIN_PR_ONLY_CONTEXTS) : [];
|
|
25418
25658
|
checks.push({
|
|
@@ -43998,6 +44238,32 @@ function formatHotfixBatchPreflightRefusal(failures, fromSpecs) {
|
|
|
43998
44238
|
${lines2.join("\n")}
|
|
43999
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`;
|
|
44000
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
|
+
}
|
|
44001
44267
|
async function preflightHotfixBatchPicks(deps, sources, pickTolerated) {
|
|
44002
44268
|
if (sources.length === 0) return [];
|
|
44003
44269
|
const startBranch = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
@@ -44184,6 +44450,18 @@ async function runHotfixStart(deps, options, doctor = runTrainDoctor) {
|
|
|
44184
44450
|
}
|
|
44185
44451
|
const label = sources.map((s) => s.label).join(", ");
|
|
44186
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
|
+
}
|
|
44187
44465
|
const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
44188
44466
|
if (remoteBranch) {
|
|
44189
44467
|
await deps.run("git", ["checkout", branch]);
|
|
@@ -44248,7 +44526,9 @@ async function runHotfixStart(deps, options, doctor = runTrainDoctor) {
|
|
|
44248
44526
|
"--body",
|
|
44249
44527
|
`Hotfix ${tag}: cherry-pick of ${label} onto origin/main${bumpNote}.
|
|
44250
44528
|
|
|
44251
|
-
<!-- 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) -->` : ""}
|
|
44252
44532
|
|
|
44253
44533
|
Merge this PR (human-initiated), then run \`mmi-cli devops hotfix release ${tag}\`.`
|
|
44254
44534
|
]));
|
|
@@ -44638,9 +44918,9 @@ ${decision.note}`);
|
|
|
44638
44918
|
let deployNote;
|
|
44639
44919
|
let tenantPublishDispatch = null;
|
|
44640
44920
|
if (deployModel === "hub-serverless") {
|
|
44641
|
-
|
|
44642
|
-
|
|
44643
|
-
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));
|
|
44644
44924
|
}
|
|
44645
44925
|
deployNote = "watched release-triggered deploy.yml + publish.yml";
|
|
44646
44926
|
} else if (deployModel === "registry-publish") {
|
|
@@ -44649,17 +44929,9 @@ ${decision.note}`);
|
|
|
44649
44929
|
runs.push(run);
|
|
44650
44930
|
deployNote = "watched this repo's own release-triggered publish.yml (#2428 \u2014 no central dispatch)";
|
|
44651
44931
|
} else if (deployModel === "tenant-container" || deployModel === "solo-container") {
|
|
44652
|
-
const deploy0 = await
|
|
44653
|
-
deps,
|
|
44654
|
-
ctx,
|
|
44655
|
-
"main",
|
|
44656
|
-
"main",
|
|
44657
|
-
deployModel,
|
|
44658
|
-
true,
|
|
44659
|
-
(deps.now ?? Date.now)(),
|
|
44660
|
-
mergedSha,
|
|
44661
|
-
"report",
|
|
44662
|
-
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)
|
|
44663
44935
|
);
|
|
44664
44936
|
const deploy = await recoverFailedDispatchPhase(
|
|
44665
44937
|
deps,
|
|
@@ -44669,7 +44941,6 @@ ${decision.note}`);
|
|
|
44669
44941
|
deploy0,
|
|
44670
44942
|
{ repo: releaseRunRepoFor(deployModel, ctx.repo), sha: mergedSha, watched: true, workflow: "tenant-deploy.yml" }
|
|
44671
44943
|
);
|
|
44672
|
-
const publish0 = await observePublishIfRequired(deps, ctx, meta, deployModel, "main", mergedSha, true);
|
|
44673
44944
|
const publish = publish0 ? await recoverFailedDispatchPhase(
|
|
44674
44945
|
deps,
|
|
44675
44946
|
ledger,
|
|
@@ -45491,7 +45762,7 @@ function trainApplyDeps() {
|
|
|
45491
45762
|
}
|
|
45492
45763
|
function formatWorkflowRun(r) {
|
|
45493
45764
|
const evidenced = workflowRunWithEvidence(r);
|
|
45494
|
-
return `${evidenced.workflow} ${evidenced.runUrl ?? evidenced.runUrlNote} ${evidenced.conclusion.toUpperCase()}`;
|
|
45765
|
+
return `${evidenced.workflow} ${evidenced.runUrl ?? evidenced.runUrlNote} ${evidenced.conclusion.toUpperCase()}${r.parallel ? " (parallel)" : ""}`;
|
|
45495
45766
|
}
|
|
45496
45767
|
function renderDeployLine(d) {
|
|
45497
45768
|
const parts = [d.dispatch];
|
|
@@ -46136,7 +46407,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
46136
46407
|
const steps = trainPlan("hotfix");
|
|
46137
46408
|
console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli devops hotfix: dry-run plan", steps));
|
|
46138
46409
|
});
|
|
46139
|
-
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));
|
|
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));
|
|
46140
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) => {
|
|
46141
46412
|
if (o.retryDeploy) {
|
|
46142
46413
|
if (o.announceSummaryFile || o.carries) {
|
package/package.json
CHANGED