@mutmutco/cli 4.4.17 → 4.5.1
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 +773 -145
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -757,7 +757,7 @@ var init_house_map = __esm({
|
|
|
757
757
|
"use strict";
|
|
758
758
|
HOUSE_ROOTS = ["oracle", "harbour", "devops", "vault", "learning"];
|
|
759
759
|
COMPAT_SHIM_DEATH_WAVE = "Wave 3";
|
|
760
|
-
FLAT_ALIAS_CUT_NOTE = `flat Wave 0 alias \u2014
|
|
760
|
+
FLAT_ALIAS_CUT_NOTE = `flat Wave 0 alias \u2014 no longer advertised since ${COMPAT_SHIM_DEATH_WAVE} (#4316, #4438); it still runs with a notice naming the canonical house-prefixed path (#7034)`;
|
|
761
761
|
HOUSE_QUESTIONS = {
|
|
762
762
|
oracle: "Is it live?",
|
|
763
763
|
harbour: "Is it bounded?",
|
|
@@ -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) {
|
|
@@ -14979,6 +15017,26 @@ async function installAppFoldDeps(deps) {
|
|
|
14979
15017
|
}
|
|
14980
15018
|
await deps.run("npm", lockListing.trim() ? ["ci"] : ["install"]);
|
|
14981
15019
|
}
|
|
15020
|
+
var SHARED_INSTALLER_PACKAGES = ["@mutmutco/installer-face", "@mutmutco/installer-launcher", "@mutmutco/installer-gate"];
|
|
15021
|
+
async function refreshSharedInstallerDeps(deps) {
|
|
15022
|
+
let declared;
|
|
15023
|
+
try {
|
|
15024
|
+
const lock = JSON.parse(await deps.run("git", ["show", "HEAD:package-lock.json"]));
|
|
15025
|
+
const present = Object.keys(lock.packages ?? {});
|
|
15026
|
+
declared = SHARED_INSTALLER_PACKAGES.filter((name) => present.some((key) => key === `node_modules/${name}` || key.endsWith(`/node_modules/${name}`)));
|
|
15027
|
+
} catch {
|
|
15028
|
+
return null;
|
|
15029
|
+
}
|
|
15030
|
+
if (declared.length === 0) return null;
|
|
15031
|
+
try {
|
|
15032
|
+
await deps.run("npm", ["update", ...declared, "--package-lock-only", "--no-audit", "--no-fund"]);
|
|
15033
|
+
} catch (e) {
|
|
15034
|
+
const why = e instanceof Error ? e.message.split("\n")[0] : String(e);
|
|
15035
|
+
deps.warn?.(`shared installer refresh skipped (${why}) \u2014 shipping the locked versions`);
|
|
15036
|
+
return `shared installer refresh skipped (${why}); the locked ${declared.join(", ")} still satisfy their ranges`;
|
|
15037
|
+
}
|
|
15038
|
+
return `shared installer deps refreshed to the newest their ranges allow (${declared.join(", ")})`;
|
|
15039
|
+
}
|
|
14982
15040
|
async function restoreMainNpmPackIdentities(deps, version) {
|
|
14983
15041
|
const bomPath = "distribution-bom.json";
|
|
14984
15042
|
let mainListing;
|
|
@@ -15133,6 +15191,8 @@ async function foldReleaseVersion(deps, model, tag, foldPaths, sourceCommit = "H
|
|
|
15133
15191
|
}
|
|
15134
15192
|
} else {
|
|
15135
15193
|
await installAppFoldDeps(deps);
|
|
15194
|
+
const refresh = await refreshSharedInstallerDeps(deps);
|
|
15195
|
+
if (refresh) foldNoteExtra = `; ${refresh}`;
|
|
15136
15196
|
await deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]);
|
|
15137
15197
|
const patch = Number(version.split(".")[2] ?? "0");
|
|
15138
15198
|
if (model === "registry-publish" && Number.isFinite(patch) && patch > 0) {
|
|
@@ -15938,10 +15998,10 @@ var rollout_plan_default = {
|
|
|
15938
15998
|
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
15999
|
},
|
|
15940
16000
|
baseline: {
|
|
15941
|
-
version: "4.
|
|
15942
|
-
tag: "v4.
|
|
15943
|
-
commit: "
|
|
15944
|
-
npm: "@mutmutco/cli@4.
|
|
16001
|
+
version: "4.5.1",
|
|
16002
|
+
tag: "v4.5.1",
|
|
16003
|
+
commit: "489af0245908",
|
|
16004
|
+
npm: "@mutmutco/cli@4.5.1"
|
|
15945
16005
|
},
|
|
15946
16006
|
exitCriterion: "fleet-n-of-n",
|
|
15947
16007
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15958,14 +16018,14 @@ var rollout_plan_default = {
|
|
|
15958
16018
|
repo: "mutmutco/mmi-hub",
|
|
15959
16019
|
role: "canary",
|
|
15960
16020
|
schedule: "train",
|
|
15961
|
-
v3Target: "v4.
|
|
16021
|
+
v3Target: "v4.5.1"
|
|
15962
16022
|
}
|
|
15963
16023
|
],
|
|
15964
16024
|
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
16025
|
rollback: {
|
|
15966
16026
|
independent: true,
|
|
15967
|
-
mechanism: "npm dist-tag latest -> 4.
|
|
15968
|
-
v3Target: "v4.
|
|
16027
|
+
mechanism: "npm dist-tag latest -> 4.5.1 and redeploy the Hub Lambda from tag v4.5.1 (489af0245908); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
16028
|
+
v3Target: "v4.5.1 (@mutmutco/cli@4.5.1, tag commit 489af0245908 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15969
16029
|
}
|
|
15970
16030
|
},
|
|
15971
16031
|
{
|
|
@@ -18487,6 +18547,13 @@ async function awaitAlignmentLanded(deps, ctx, leg, base, opts = {}) {
|
|
|
18487
18547
|
await sleep2(ALIGNMENT_LAND_POLL_MS);
|
|
18488
18548
|
}
|
|
18489
18549
|
}
|
|
18550
|
+
async function awaitAlignmentsLanded(deps, ctx, legs) {
|
|
18551
|
+
const [dev, rc] = await Promise.all([
|
|
18552
|
+
awaitAlignmentLanded(deps, ctx, legs.dev, "development"),
|
|
18553
|
+
legs.rc ? awaitAlignmentLanded(deps, ctx, legs.rc, "rc") : void 0
|
|
18554
|
+
]);
|
|
18555
|
+
return { dev, ...rc ? { rc } : {} };
|
|
18556
|
+
}
|
|
18490
18557
|
async function alignRcForward(deps, ctx, tag) {
|
|
18491
18558
|
deps.warn?.("Release: Bringing the release candidate up to date");
|
|
18492
18559
|
await runGitRemoteRead(deps, ["fetch", "origin", "main", "rc"]);
|
|
@@ -18734,6 +18801,60 @@ async function resolveRcResumeTag(deps, base, sha, probed) {
|
|
|
18734
18801
|
return { tag: newest, note };
|
|
18735
18802
|
}
|
|
18736
18803
|
|
|
18804
|
+
// ../infra/installer-families.mjs
|
|
18805
|
+
var INSTALLER_FAMILIES = ["jerv", "mmi"];
|
|
18806
|
+
var INSTALLER_FAMILY_HOSTS = {
|
|
18807
|
+
jerv: "get.jerv.ai",
|
|
18808
|
+
mmi: "get.mutmut.work"
|
|
18809
|
+
};
|
|
18810
|
+
var INSTALLER_LOGINS = ["github", "google"];
|
|
18811
|
+
var PRODUCT_RE = /^[a-z][a-z0-9-]{0,39}$/;
|
|
18812
|
+
function expectedInstallerLogin(family, division) {
|
|
18813
|
+
if (family === "jerv") return "github";
|
|
18814
|
+
return String(division ?? "").trim().toLowerCase() === "mmi" ? "github" : "google";
|
|
18815
|
+
}
|
|
18816
|
+
function installerVersionUrl(installer) {
|
|
18817
|
+
return `https://${INSTALLER_FAMILY_HOSTS[installer.family]}/${installer.product}/release/version`;
|
|
18818
|
+
}
|
|
18819
|
+
function installerDeclarationError(value, division) {
|
|
18820
|
+
if (value === void 0 || value === null) return null;
|
|
18821
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
18822
|
+
return "installer must be an object {product, family, login?}";
|
|
18823
|
+
}
|
|
18824
|
+
const record = (
|
|
18825
|
+
/** @type {Record<string, unknown>} */
|
|
18826
|
+
value
|
|
18827
|
+
);
|
|
18828
|
+
for (const key of Object.keys(record)) {
|
|
18829
|
+
if (!["product", "family", "login"].includes(key)) return `installer.${key} is not a field \u2014 expected product, family and optionally login`;
|
|
18830
|
+
}
|
|
18831
|
+
if (typeof record.product !== "string" || !PRODUCT_RE.test(record.product)) {
|
|
18832
|
+
return "installer.product must be a lowercase kebab name (e.g. mm-strategy) \u2014 it is the /<product> path on the family host";
|
|
18833
|
+
}
|
|
18834
|
+
if (!INSTALLER_FAMILIES.includes(
|
|
18835
|
+
/** @type {string} */
|
|
18836
|
+
record.family
|
|
18837
|
+
)) {
|
|
18838
|
+
return `installer.family must be one of: ${INSTALLER_FAMILIES.join(", ")} (jerv \u2192 ${INSTALLER_FAMILY_HOSTS.jerv}, mmi \u2192 ${INSTALLER_FAMILY_HOSTS.mmi})`;
|
|
18839
|
+
}
|
|
18840
|
+
if (record.login !== void 0 && !INSTALLER_LOGINS.includes(
|
|
18841
|
+
/** @type {string} */
|
|
18842
|
+
record.login
|
|
18843
|
+
)) {
|
|
18844
|
+
return `installer.login must be one of: ${INSTALLER_LOGINS.join(", ")}`;
|
|
18845
|
+
}
|
|
18846
|
+
const expected = expectedInstallerLogin(
|
|
18847
|
+
/** @type {'jerv' | 'mmi'} */
|
|
18848
|
+
record.family,
|
|
18849
|
+
division
|
|
18850
|
+
);
|
|
18851
|
+
if (record.login !== void 0 && record.login !== expected) {
|
|
18852
|
+
const who = record.family === "jerv" ? "Jerv products sign in with GitHub" : String(division ?? "").trim().toLowerCase() === "mmi" ? "MMI products sign in with GitHub" : "MM and other Mutatis Mutandis products sign in with Google";
|
|
18853
|
+
return `installer.login ${record.login} breaks the sign-in rule: ${who} (owner ruling 2026-09-23)`;
|
|
18854
|
+
}
|
|
18855
|
+
return null;
|
|
18856
|
+
}
|
|
18857
|
+
|
|
18737
18858
|
// src/tenant-control-parse.ts
|
|
18738
18859
|
var OUTPUT_BEGIN = "mmi-control-output-begin";
|
|
18739
18860
|
var OUTPUT_END = "mmi-control-output-end";
|
|
@@ -18889,6 +19010,12 @@ async function observePublishIfRequired(deps, ctx, meta, model, stage, releaseSh
|
|
|
18889
19010
|
deployStatus: aggregateWorkflowRuns(workflowRuns)
|
|
18890
19011
|
};
|
|
18891
19012
|
}
|
|
19013
|
+
async function joinDeployAndPublish(deploy, publish) {
|
|
19014
|
+
const [d, p] = await Promise.all([deploy, publish]);
|
|
19015
|
+
if (!p) return [d, p];
|
|
19016
|
+
const mark = (x) => x.workflowRuns ? { ...x, workflowRuns: x.workflowRuns.map((r) => ({ ...r, parallel: true })) } : x;
|
|
19017
|
+
return [mark(d), mark(p)];
|
|
19018
|
+
}
|
|
18892
19019
|
function appendPublishDispatch(deploy, publish) {
|
|
18893
19020
|
if (!publish) return deploy;
|
|
18894
19021
|
return {
|
|
@@ -18910,7 +19037,7 @@ function mmiGetRedeployCommand(repo) {
|
|
|
18910
19037
|
}
|
|
18911
19038
|
function readLocalLauncherVersion(root = process.cwd()) {
|
|
18912
19039
|
try {
|
|
18913
|
-
const raw = (0, import_node_fs19.readFileSync)((0, import_node_path17.join)(root, "
|
|
19040
|
+
const raw = (0, import_node_fs19.readFileSync)((0, import_node_path17.join)(root, "cli", "package.json"), "utf8");
|
|
18914
19041
|
const version = JSON.parse(raw).version;
|
|
18915
19042
|
return typeof version === "string" && version.trim() ? version.trim() : null;
|
|
18916
19043
|
} catch {
|
|
@@ -18939,17 +19066,54 @@ async function fetchServedChannelVersion(url, fetchImpl = fetch) {
|
|
|
18939
19066
|
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
18940
19067
|
}
|
|
18941
19068
|
}
|
|
19069
|
+
async function findPriorGreenDispatch(deps, repo, workflow, sha, titleTokens) {
|
|
19070
|
+
if (!/^[0-9a-f]{40}$/i.test(sha)) return void 0;
|
|
19071
|
+
try {
|
|
19072
|
+
const rows = JSON.parse(await deps.run("gh", [
|
|
19073
|
+
"run",
|
|
19074
|
+
"list",
|
|
19075
|
+
"--repo",
|
|
19076
|
+
repo,
|
|
19077
|
+
"--workflow",
|
|
19078
|
+
workflow,
|
|
19079
|
+
"--commit",
|
|
19080
|
+
sha,
|
|
19081
|
+
"--event",
|
|
19082
|
+
"workflow_dispatch",
|
|
19083
|
+
"--limit",
|
|
19084
|
+
"20",
|
|
19085
|
+
"--json",
|
|
19086
|
+
"databaseId,url,status,conclusion,displayTitle,headSha"
|
|
19087
|
+
]));
|
|
19088
|
+
const hit = rows.find((r) => typeof r.databaseId === "number" && r.headSha?.toLowerCase() === sha.toLowerCase() && r.status === "completed" && r.conclusion === "success" && titleTokens.every((t) => (r.displayTitle ?? "").split(/[^\w.-]+/).includes(t)));
|
|
19089
|
+
return hit ? { runId: hit.databaseId, ...hit.url ? { runUrl: hit.url } : {} } : void 0;
|
|
19090
|
+
} catch {
|
|
19091
|
+
return void 0;
|
|
19092
|
+
}
|
|
19093
|
+
}
|
|
19094
|
+
function planeFailureRow(workflow, evidence) {
|
|
19095
|
+
return { workflow, runUrlNote: evidence, conclusion: "failure" };
|
|
19096
|
+
}
|
|
18942
19097
|
async function appendMmiGetRedeploy(deps, input, dispatch) {
|
|
18943
19098
|
if (!isHubControlRepo(input.repo)) return dispatch;
|
|
18944
19099
|
const remedy = mmiGetRedeployCommand(input.repo);
|
|
18945
|
-
const withNote = (note, deployStatus) => ({
|
|
19100
|
+
const withNote = (note, deployStatus, row) => ({
|
|
18946
19101
|
...dispatch,
|
|
18947
19102
|
note: `${dispatch.note}; ${note}`,
|
|
18948
|
-
...deployStatus ? { deployStatus } : {}
|
|
19103
|
+
...deployStatus ? { deployStatus } : {},
|
|
19104
|
+
...row ? { workflowRuns: [...dispatch.workflowRuns ?? [], row] } : {}
|
|
18949
19105
|
});
|
|
18950
19106
|
if (input.publishStatus !== "success") {
|
|
18951
19107
|
return withNote(`mmi-get box NOT redeployed (publish did not succeed) \u2014 after the publish is green, run \`${remedy}\``);
|
|
18952
19108
|
}
|
|
19109
|
+
const prior = input.releaseSha ? await findPriorGreenDispatch(deps, input.repo, "tenant-deploy.yml", input.releaseSha, [HUB_GET_SLUG, HUB_GET_STAGE]) : void 0;
|
|
19110
|
+
if (prior) {
|
|
19111
|
+
return {
|
|
19112
|
+
...dispatch,
|
|
19113
|
+
note: `${dispatch.note}; mmi-get box already redeployed for this release (tenant-deploy.yml run ${prior.runId} at ${input.releaseSha.slice(0, 12)}) \u2014 not dispatched again`,
|
|
19114
|
+
workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "tenant-deploy.yml", runId: prior.runId, ...prior.runUrl ? { runUrl: prior.runUrl } : {}, conclusion: "success" }]
|
|
19115
|
+
};
|
|
19116
|
+
}
|
|
18953
19117
|
const since = (deps.now ?? Date.now)();
|
|
18954
19118
|
try {
|
|
18955
19119
|
await deps.run("gh", [
|
|
@@ -18971,12 +19135,12 @@ async function appendMmiGetRedeploy(deps, input, dispatch) {
|
|
|
18971
19135
|
]);
|
|
18972
19136
|
} catch (e) {
|
|
18973
19137
|
const msg = e instanceof Error ? e.message : String(e);
|
|
18974
|
-
return withNote(`tenant-deploy dispatch FAILED: ${msg}. The release itself landed, but the MMI-GET BOX WAS NOT REDEPLOYED \u2014 recover with \`${remedy}\``, "failure");
|
|
19138
|
+
return withNote(`tenant-deploy dispatch FAILED: ${msg}. The release itself landed, but the MMI-GET BOX WAS NOT REDEPLOYED \u2014 recover with \`${remedy}\``, "failure", planeFailureRow("tenant-deploy.yml", `dispatch refused: ${msg}`));
|
|
18975
19139
|
}
|
|
18976
19140
|
const correlated = await correlateTenantRun(deps, since, [HUB_GET_SLUG, HUB_GET_STAGE]);
|
|
18977
19141
|
if (correlated.read === "failed" || correlated.read === "absent") {
|
|
18978
19142
|
const why = correlated.read === "failed" ? `its run could not be READ: ${correlated.error}` : "no matching run appeared";
|
|
18979
|
-
return withNote(`tenant-deploy dispatched (slug=${HUB_GET_SLUG}, stage=${HUB_GET_STAGE}) but ${why}. Check the Actions tab; if it never ran, recover with \`${remedy}\``, "failure");
|
|
19143
|
+
return withNote(`tenant-deploy dispatched (slug=${HUB_GET_SLUG}, stage=${HUB_GET_STAGE}) but ${why}. Check the Actions tab; if it never ran, recover with \`${remedy}\``, "failure", planeFailureRow("tenant-deploy.yml", `dispatched, but ${why}`));
|
|
18980
19144
|
}
|
|
18981
19145
|
const { runId, runUrl } = correlated;
|
|
18982
19146
|
const runRow = { workflow: "tenant-deploy.yml", ...runId ? { runId } : {}, ...runUrl ? { runUrl } : {} };
|
|
@@ -19006,35 +19170,44 @@ async function appendServedChannelPublish(deps, input, dispatch) {
|
|
|
19006
19170
|
if (!isHubControlRepo(input.repo)) return dispatch;
|
|
19007
19171
|
const channelUrl = input.channelUrl ?? readServedChannelUrl();
|
|
19008
19172
|
const remedy = getPublishDispatchCommand(input.repo);
|
|
19009
|
-
const withNote = (note, deployStatus) => ({
|
|
19173
|
+
const withNote = (note, deployStatus, row) => ({
|
|
19010
19174
|
...dispatch,
|
|
19011
19175
|
note: `${dispatch.note}; ${note}`,
|
|
19012
|
-
...deployStatus ? { deployStatus } : {}
|
|
19176
|
+
...deployStatus ? { deployStatus } : {},
|
|
19177
|
+
...row ? { workflowRuns: [...dispatch.workflowRuns ?? [], row] } : {}
|
|
19013
19178
|
});
|
|
19014
19179
|
if (input.publishStatus !== "success") {
|
|
19015
19180
|
return withNote(`served channel NOT moved (publish did not succeed) \u2014 after the publish is green, run \`${remedy}\``);
|
|
19016
19181
|
}
|
|
19017
|
-
const
|
|
19018
|
-
|
|
19019
|
-
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
|
|
19023
|
-
}
|
|
19024
|
-
|
|
19025
|
-
|
|
19026
|
-
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
|
|
19033
|
-
|
|
19034
|
-
|
|
19035
|
-
};
|
|
19182
|
+
const prior = input.releaseSha ? await findPriorGreenDispatch(deps, input.repo, "get-publish.yml", input.releaseSha, [HUB_SERVED_PRODUCT]) : void 0;
|
|
19183
|
+
let runRow;
|
|
19184
|
+
let runStatus;
|
|
19185
|
+
if (prior) {
|
|
19186
|
+
runRow = { workflow: "get-publish.yml", runId: prior.runId, ...prior.runUrl ? { runUrl: prior.runUrl } : {} };
|
|
19187
|
+
runStatus = "success";
|
|
19188
|
+
} else {
|
|
19189
|
+
const since = (deps.now ?? Date.now)();
|
|
19190
|
+
try {
|
|
19191
|
+
await deps.run("gh", ["workflow", "run", "get-publish.yml", "--repo", input.repo, "--ref", "main", "-f", `product=${HUB_SERVED_PRODUCT}`]);
|
|
19192
|
+
} catch (e) {
|
|
19193
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
19194
|
+
return withNote(`get-publish dispatch FAILED: ${msg}. The release itself landed, but the SERVED CHANNEL IS UNCHANGED \u2014 recover with \`${remedy}\``, "failure", planeFailureRow("get-publish.yml", `dispatch refused: ${msg}`));
|
|
19195
|
+
}
|
|
19196
|
+
const correlated = await correlateGetPublishRun(deps, since, HUB_SERVED_PRODUCT);
|
|
19197
|
+
if (correlated.read === "failed") {
|
|
19198
|
+
return withNote(`get-publish dispatched but its run could not be READ: ${correlated.error}. Check the Actions tab; if it never ran, recover with \`${remedy}\``, "failure", planeFailureRow("get-publish.yml", `dispatched, but its run could not be read`));
|
|
19199
|
+
}
|
|
19200
|
+
const { runId, runUrl } = correlated;
|
|
19201
|
+
runRow = { workflow: "get-publish.yml", ...runId ? { runId } : {}, ...runUrl ? { runUrl } : {} };
|
|
19202
|
+
if (!input.watch) {
|
|
19203
|
+
return {
|
|
19204
|
+
...dispatch,
|
|
19205
|
+
note: `${dispatch.note}; dispatched get-publish.yml (product=${HUB_SERVED_PRODUCT}) \u2014 NOT watched, so the served channel is not yet proven moved`,
|
|
19206
|
+
workflowRuns: [...dispatch.workflowRuns ?? [], { ...runRow, conclusion: "pending" }]
|
|
19207
|
+
};
|
|
19208
|
+
}
|
|
19209
|
+
runStatus = await watchTenantRun(deps, runId, input.repo);
|
|
19036
19210
|
}
|
|
19037
|
-
const runStatus = await watchTenantRun(deps, runId, input.repo);
|
|
19038
19211
|
if (runStatus !== "success") {
|
|
19039
19212
|
return {
|
|
19040
19213
|
...dispatch,
|
|
@@ -19053,19 +19226,29 @@ async function appendServedChannelPublish(deps, input, dispatch) {
|
|
|
19053
19226
|
};
|
|
19054
19227
|
}
|
|
19055
19228
|
const probe = await (deps.readServedChannelVersion ?? fetchServedChannelVersion)(channelUrl);
|
|
19229
|
+
const evidence = (conclusion, text) => [...runs, servedChannelRow(HUB_SERVED_PRODUCT, conclusion, text)];
|
|
19230
|
+
if (!probe.ok && staleServedProcess(probe)) {
|
|
19231
|
+
const redeploy = mmiGetRedeployCommand(input.repo);
|
|
19232
|
+
return {
|
|
19233
|
+
...dispatch,
|
|
19234
|
+
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}\``,
|
|
19235
|
+
workflowRuns: evidence("failure", `${channelUrl} answered HTTP ${probe.status} (stale served process)`),
|
|
19236
|
+
deployStatus: "failure"
|
|
19237
|
+
};
|
|
19238
|
+
}
|
|
19056
19239
|
if (!probe.ok) {
|
|
19057
19240
|
const why = probe.error ?? `HTTP ${probe.status ?? "unknown"}`;
|
|
19058
19241
|
return {
|
|
19059
19242
|
...dispatch,
|
|
19060
19243
|
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:
|
|
19244
|
+
workflowRuns: evidence("pending", `${channelUrl} unreadable (${why})`)
|
|
19062
19245
|
};
|
|
19063
19246
|
}
|
|
19064
19247
|
if (expected && probe.version !== expected) {
|
|
19065
19248
|
return {
|
|
19066
19249
|
...dispatch,
|
|
19067
19250
|
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:
|
|
19251
|
+
workflowRuns: evidence("failure", `${channelUrl} reports ${probe.version}, expected ${expected}`),
|
|
19069
19252
|
deployStatus: "failure"
|
|
19070
19253
|
};
|
|
19071
19254
|
}
|
|
@@ -19073,15 +19256,106 @@ async function appendServedChannelPublish(deps, input, dispatch) {
|
|
|
19073
19256
|
return {
|
|
19074
19257
|
...dispatch,
|
|
19075
19258
|
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:
|
|
19259
|
+
workflowRuns: evidence("pending", `${channelUrl} reports ${probe.version}; nothing to compare against`)
|
|
19077
19260
|
};
|
|
19078
19261
|
}
|
|
19079
19262
|
return {
|
|
19080
19263
|
...dispatch,
|
|
19081
19264
|
note: `${dispatch.note}; served channel moved \u2014 ${channelUrl} now reports ${probe.version}`,
|
|
19082
|
-
workflowRuns:
|
|
19265
|
+
workflowRuns: evidence("success", `${channelUrl} reports ${probe.version}`)
|
|
19266
|
+
};
|
|
19267
|
+
}
|
|
19268
|
+
function staleServedProcess(probe) {
|
|
19269
|
+
return probe.status === 401 || probe.status === 403 || probe.status === 404;
|
|
19270
|
+
}
|
|
19271
|
+
function servedChannelRow(product, conclusion, evidence) {
|
|
19272
|
+
return { workflow: `served-channel(${product})`, runUrlNote: evidence, conclusion };
|
|
19273
|
+
}
|
|
19274
|
+
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>`";
|
|
19275
|
+
var SERVED_CHANNELS = {
|
|
19276
|
+
"mutmutco/mm-strategy": {
|
|
19277
|
+
product: "mm-strategy",
|
|
19278
|
+
url: "https://strategy.mutmut.work/release/version",
|
|
19279
|
+
mover: "the MM-Strategy container deploy (the running server's own version is the pointer)",
|
|
19280
|
+
remedy: "`mmi-cli devops runtime tenant redeploy mutmutco/MM-Strategy main`"
|
|
19281
|
+
},
|
|
19282
|
+
"mutmutco/jerv-hub": {
|
|
19283
|
+
product: "jerv",
|
|
19284
|
+
url: "https://get.jerv.ai/jerv/release/version",
|
|
19285
|
+
mover: "the Jerv Gateway deploy (`jerv-gate fetch-release --product jerv`)",
|
|
19286
|
+
remedy: JERV_GATEWAY_REMEDY
|
|
19287
|
+
},
|
|
19288
|
+
"mutmutco/jerv-jervcode": {
|
|
19289
|
+
product: "jervcode",
|
|
19290
|
+
url: "https://get.jerv.ai/jervcode/release/version",
|
|
19291
|
+
mover: "the Jerv Gateway deploy, which fetches JervCode's latest release \u2014 a JervCode release alone does not move it",
|
|
19292
|
+
remedy: JERV_GATEWAY_REMEDY
|
|
19293
|
+
}
|
|
19294
|
+
};
|
|
19295
|
+
function servedChannelFor(repo, installer) {
|
|
19296
|
+
const known = SERVED_CHANNELS[repo.toLowerCase()];
|
|
19297
|
+
if (!installer) return known;
|
|
19298
|
+
if (isHubControlRepo(repo)) return void 0;
|
|
19299
|
+
const url = installerVersionUrl(installer);
|
|
19300
|
+
return {
|
|
19301
|
+
product: installer.product,
|
|
19302
|
+
url,
|
|
19303
|
+
mover: known?.mover ?? `this repo's release (${installer.family === "jerv" ? "get.jerv.ai" : "get.mutmut.work"} serves /${installer.product})`,
|
|
19304
|
+
remedy: known?.remedy ?? `re-run the release's served-channel publish for ${installer.product} (\`mmi-cli devops release --resume\`)`
|
|
19083
19305
|
};
|
|
19084
19306
|
}
|
|
19307
|
+
var SERVED_CHANNEL_PROBE_ATTEMPTS = 3;
|
|
19308
|
+
var SERVED_CHANNEL_PROBE_BACKOFF_MS = 1e4;
|
|
19309
|
+
async function appendServedChannelProof(deps, input, dispatch) {
|
|
19310
|
+
const channel = servedChannelFor(input.repo, input.installer);
|
|
19311
|
+
if (!channel) return dispatch;
|
|
19312
|
+
const expected = input.tag.replace(/^v/, "");
|
|
19313
|
+
const withProof = (conclusion, evidence, note, failed = false) => ({
|
|
19314
|
+
...dispatch,
|
|
19315
|
+
note: `${dispatch.note}; ${note}`,
|
|
19316
|
+
workflowRuns: [...dispatch.workflowRuns ?? [], servedChannelRow(channel.product, conclusion, evidence)],
|
|
19317
|
+
...failed ? { deployStatus: "failure" } : {}
|
|
19318
|
+
});
|
|
19319
|
+
if (!input.shipped) {
|
|
19320
|
+
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`);
|
|
19321
|
+
}
|
|
19322
|
+
const read = deps.readServedChannelVersion ?? fetchServedChannelVersion;
|
|
19323
|
+
let probe = await read(channel.url);
|
|
19324
|
+
for (let attempt = 1; attempt < SERVED_CHANNEL_PROBE_ATTEMPTS && probe.ok && probe.version !== expected; attempt++) {
|
|
19325
|
+
await (deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))))(SERVED_CHANNEL_PROBE_BACKOFF_MS * attempt);
|
|
19326
|
+
probe = await read(channel.url);
|
|
19327
|
+
}
|
|
19328
|
+
if (!probe.ok && staleServedProcess(probe)) {
|
|
19329
|
+
return withProof(
|
|
19330
|
+
"failure",
|
|
19331
|
+
`${channel.url} answered HTTP ${probe.status} (stale served process)`,
|
|
19332
|
+
`${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}`,
|
|
19333
|
+
true
|
|
19334
|
+
);
|
|
19335
|
+
}
|
|
19336
|
+
if (!probe.ok) {
|
|
19337
|
+
const why = probe.error ?? `HTTP ${probe.status ?? "unknown"}`;
|
|
19338
|
+
return withProof("pending", `${channel.url} unreadable (${why})`, `served channel ${channel.url} could NOT be read (${why}) \u2014 the move is unverified`);
|
|
19339
|
+
}
|
|
19340
|
+
if (probe.version !== expected) {
|
|
19341
|
+
return withProof(
|
|
19342
|
+
"failure",
|
|
19343
|
+
`${channel.url} reports ${probe.version}, expected ${expected}`,
|
|
19344
|
+
`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}`,
|
|
19345
|
+
true
|
|
19346
|
+
);
|
|
19347
|
+
}
|
|
19348
|
+
return withProof("success", `${channel.url} reports ${probe.version}`, `served channel moved \u2014 ${channel.url} reports ${probe.version}`);
|
|
19349
|
+
}
|
|
19350
|
+
async function appendServedChannelLegs(deps, input, dispatch) {
|
|
19351
|
+
const publishStatus = input.publish?.state === "complete" ? "success" : "failure";
|
|
19352
|
+
let out = await appendMmiGetRedeploy(deps, { repo: input.repo, publishStatus, watch: input.watch, releaseSha: input.releaseSha }, dispatch);
|
|
19353
|
+
out = await appendServedChannelPublish(deps, { repo: input.repo, publishStatus, watch: input.watch, releaseSha: input.releaseSha }, out);
|
|
19354
|
+
return appendServedChannelProof(deps, { repo: input.repo, tag: input.tag, shipped: shipLegsGreen(input.deploy, input.publish), installer: input.installer }, out);
|
|
19355
|
+
}
|
|
19356
|
+
function servedChannelRowsAfter(before, after) {
|
|
19357
|
+
return (after.workflowRuns ?? []).slice((before.workflowRuns ?? []).length);
|
|
19358
|
+
}
|
|
19085
19359
|
var JERV_GATEWAY_RUN_URL_NOTE = "no run URL emitted \u2014 host health leg runs outside GitHub Actions";
|
|
19086
19360
|
var JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT = "scripts/jerv-gateway-release-deploy.sh";
|
|
19087
19361
|
async function runTaggedJervGatewayLocalWrapper(deps, tag, tagSha) {
|
|
@@ -19141,15 +19415,15 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
|
|
|
19141
19415
|
}
|
|
19142
19416
|
}
|
|
19143
19417
|
async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha, enumerateSha = false) {
|
|
19144
|
-
const
|
|
19145
|
-
|
|
19418
|
+
const parallel = targets.length > 1 ? { parallel: true } : {};
|
|
19419
|
+
const workflowRuns = await Promise.all(targets.map(async (target) => {
|
|
19146
19420
|
try {
|
|
19147
19421
|
const run = await correlateWorkflowRun(deps, { ...target, since, headSha, repo });
|
|
19148
|
-
|
|
19422
|
+
return { ...await watchWorkflowRun(deps, target.workflow, run, repo), ...parallel };
|
|
19149
19423
|
} catch {
|
|
19150
|
-
|
|
19424
|
+
return { workflow: target.workflow, conclusion: "failure", ...parallel };
|
|
19151
19425
|
}
|
|
19152
|
-
}
|
|
19426
|
+
}));
|
|
19153
19427
|
if (!enumerateSha) return workflowRuns;
|
|
19154
19428
|
const seen = new Set(workflowRuns.map((r) => r.runId).filter((id) => typeof id === "number"));
|
|
19155
19429
|
workflowRuns.push(...await discoverShaWorkflowRuns(deps, repo, headSha, seen));
|
|
@@ -19204,8 +19478,8 @@ async function vercelDeployDispatch(deps, ctx, ref, watch, headSha) {
|
|
|
19204
19478
|
return { note: `${note}; ${detail}`, ...read?.logUrl ? { runUrl: read.logUrl } : {}, workflowRuns: [row(read, deployStatus)], deployStatus };
|
|
19205
19479
|
}
|
|
19206
19480
|
function shipLegsGreen(deploy, publish) {
|
|
19207
|
-
const green = (
|
|
19208
|
-
return green(deploy
|
|
19481
|
+
const green = (leg) => leg?.superseded !== true && (leg?.state === "complete" || leg?.state === "skipped");
|
|
19482
|
+
return green(deploy) && green(publish);
|
|
19209
19483
|
}
|
|
19210
19484
|
async function announceShippedRelease(deps, args, model, legs, deferred) {
|
|
19211
19485
|
if (!deps.announce) return void 0;
|
|
@@ -20718,6 +20992,20 @@ var import_node_crypto5 = require("node:crypto");
|
|
|
20718
20992
|
// src/file-lock.ts
|
|
20719
20993
|
var import_promises2 = require("node:fs/promises");
|
|
20720
20994
|
var import_node_path21 = require("node:path");
|
|
20995
|
+
function pidIsDead(pid) {
|
|
20996
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
20997
|
+
if (pid === process.pid) return false;
|
|
20998
|
+
try {
|
|
20999
|
+
process.kill(pid, 0);
|
|
21000
|
+
return false;
|
|
21001
|
+
} catch (e) {
|
|
21002
|
+
return e.code === "ESRCH";
|
|
21003
|
+
}
|
|
21004
|
+
}
|
|
21005
|
+
function lockTokenPid(token) {
|
|
21006
|
+
const m = /^(\d+)-/.exec(token);
|
|
21007
|
+
return m ? Number(m[1]) : void 0;
|
|
21008
|
+
}
|
|
20721
21009
|
var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
20722
21010
|
var IMMEDIATE_RETRY_BUDGET = 3;
|
|
20723
21011
|
var FileLockBusyError = class extends Error {
|
|
@@ -20733,9 +21021,24 @@ function resolveFileLockOpts(opts = {}) {
|
|
|
20733
21021
|
staleMs: opts.staleMs ?? 3e4,
|
|
20734
21022
|
retryMs: opts.retryMs ?? 50,
|
|
20735
21023
|
maxWaitMs: opts.maxWaitMs ?? 5e3,
|
|
20736
|
-
label: opts.label ?? "file lock"
|
|
21024
|
+
label: opts.label ?? "file lock",
|
|
21025
|
+
deadHolderSteal: opts.deadHolderSteal ?? false,
|
|
21026
|
+
holder: opts.holder ?? ""
|
|
20737
21027
|
};
|
|
20738
21028
|
}
|
|
21029
|
+
async function lockIsStealable(lockPath, opts) {
|
|
21030
|
+
const age = Date.now() - (await (0, import_promises2.stat)(lockPath)).mtimeMs;
|
|
21031
|
+
if (age > opts.staleMs) return true;
|
|
21032
|
+
if (!opts.deadHolderSteal) return false;
|
|
21033
|
+
let token;
|
|
21034
|
+
try {
|
|
21035
|
+
token = await (0, import_promises2.readFile)(lockPath, "utf8");
|
|
21036
|
+
} catch {
|
|
21037
|
+
return false;
|
|
21038
|
+
}
|
|
21039
|
+
const pid = lockTokenPid(token);
|
|
21040
|
+
return pid !== void 0 && pidIsDead(pid);
|
|
21041
|
+
}
|
|
20739
21042
|
async function acquireFileLock(lockPath, opts, deadline) {
|
|
20740
21043
|
let immediateRetries = 0;
|
|
20741
21044
|
for (; ; ) {
|
|
@@ -20746,12 +21049,12 @@ async function acquireFileLock(lockPath, opts, deadline) {
|
|
|
20746
21049
|
const code = e.code;
|
|
20747
21050
|
if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
|
|
20748
21051
|
const retryNow = async () => {
|
|
21052
|
+
if (++immediateRetries <= IMMEDIATE_RETRY_BUDGET) return;
|
|
20749
21053
|
if (Date.now() >= deadline) throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
|
|
20750
|
-
|
|
21054
|
+
await sleep(opts.retryMs);
|
|
20751
21055
|
};
|
|
20752
21056
|
try {
|
|
20753
|
-
|
|
20754
|
-
if (age > opts.staleMs) {
|
|
21057
|
+
if (await lockIsStealable(lockPath, opts)) {
|
|
20755
21058
|
try {
|
|
20756
21059
|
await (0, import_promises2.unlink)(lockPath);
|
|
20757
21060
|
await retryNow();
|
|
@@ -20777,7 +21080,8 @@ async function acquireFileLock(lockPath, opts, deadline) {
|
|
|
20777
21080
|
await sleep(opts.retryMs);
|
|
20778
21081
|
continue;
|
|
20779
21082
|
}
|
|
20780
|
-
const
|
|
21083
|
+
const stamp = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
21084
|
+
const token = opts.holder ? `${stamp} ${opts.holder.replace(/\s+/g, " ").trim()}` : stamp;
|
|
20781
21085
|
try {
|
|
20782
21086
|
await handle.writeFile(token);
|
|
20783
21087
|
} catch (e) {
|
|
@@ -20801,11 +21105,14 @@ async function releaseFileLock(lockPath, guard) {
|
|
|
20801
21105
|
}
|
|
20802
21106
|
}
|
|
20803
21107
|
async function withFileLock(lockPath, opts, fn) {
|
|
21108
|
+
return withFencedFileLock(lockPath, opts, () => fn());
|
|
21109
|
+
}
|
|
21110
|
+
async function withFencedFileLock(lockPath, opts, fn) {
|
|
20804
21111
|
const resolved = resolveFileLockOpts(opts);
|
|
20805
21112
|
await (0, import_promises2.mkdir)((0, import_node_path21.dirname)(lockPath), { recursive: true }).catch(() => void 0);
|
|
20806
21113
|
const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
|
|
20807
21114
|
try {
|
|
20808
|
-
return await fn();
|
|
21115
|
+
return await fn(() => fileLockHeldBy(lockPath, guard.token));
|
|
20809
21116
|
} finally {
|
|
20810
21117
|
await releaseFileLock(lockPath, guard);
|
|
20811
21118
|
}
|
|
@@ -21043,8 +21350,114 @@ function atomicWriteJson(path2, ledger) {
|
|
|
21043
21350
|
`, "utf8");
|
|
21044
21351
|
(0, import_node_fs22.renameSync)(tmp, path2);
|
|
21045
21352
|
}
|
|
21353
|
+
var LEDGER_LOCK_OPTS = { label: "release ledger", staleMs: 18e4, deadHolderSteal: true };
|
|
21354
|
+
async function withLedgerLock(path2, fn, opts = {}) {
|
|
21355
|
+
return withFencedFileLock(`${path2}.lock`, { ...LEDGER_LOCK_OPTS, ...opts.maxWaitMs != null ? { maxWaitMs: opts.maxWaitMs } : {} }, (stillHeld) => fn(async (write) => {
|
|
21356
|
+
if (!await stillHeld()) {
|
|
21357
|
+
throw new ReleaseLedgerError("lock-lost", `release ledger at ${path2}: this writer's lock was stolen while it worked \u2014 nothing was written over the successor (#7027)`);
|
|
21358
|
+
}
|
|
21359
|
+
return write();
|
|
21360
|
+
}));
|
|
21361
|
+
}
|
|
21362
|
+
function checkoutRootFrom(cwd) {
|
|
21363
|
+
let dir = (0, import_node_path22.resolve)(cwd);
|
|
21364
|
+
for (; ; ) {
|
|
21365
|
+
if (resolveGitDir(dir)) return dir;
|
|
21366
|
+
const parent = (0, import_node_path22.dirname)(dir);
|
|
21367
|
+
if (parent === dir) throw new ReleaseLedgerError("no-checkout", `train writer lock: no git checkout found above ${cwd}`);
|
|
21368
|
+
dir = parent;
|
|
21369
|
+
}
|
|
21370
|
+
}
|
|
21371
|
+
function trainWriterLockPath(cwd) {
|
|
21372
|
+
return (0, import_node_path22.join)((0, import_node_path22.dirname)(releaseLedgerPath(checkoutRootFrom(cwd))), "train.lock");
|
|
21373
|
+
}
|
|
21374
|
+
var TrainWriterBusyError = class extends Error {
|
|
21375
|
+
lockPath;
|
|
21376
|
+
holder;
|
|
21377
|
+
constructor(lockPath, holder, verb) {
|
|
21378
|
+
super(
|
|
21379
|
+
`${verb} refused before any mutation: another train is running on this checkout (${holder}). One train at a time writes the release ledger \u2014 wait for it to finish, or, if that process is gone, rerun: a dead holder's lock is reclaimed automatically (lock: ${lockPath}) (#7027)`
|
|
21380
|
+
);
|
|
21381
|
+
this.name = "TrainWriterBusyError";
|
|
21382
|
+
this.lockPath = lockPath;
|
|
21383
|
+
this.holder = holder;
|
|
21384
|
+
}
|
|
21385
|
+
};
|
|
21386
|
+
var TRAIN_WRITER_STALE_MS = 12 * 60 * 6e4;
|
|
21387
|
+
function describeTrainWriterToken(token) {
|
|
21388
|
+
const pid = lockTokenPid(token);
|
|
21389
|
+
const [stamp, ...rest] = token.trim().split(" ");
|
|
21390
|
+
const since = Number((stamp ?? "").split("-")[1]);
|
|
21391
|
+
const when = Number.isFinite(since) && since > 0 ? `, since ${new Date(since).toISOString()}` : "";
|
|
21392
|
+
const what = rest.join(" ").trim();
|
|
21393
|
+
return `${what || "a train command"}${pid ? `, pid ${pid}` : ""}${when}`;
|
|
21394
|
+
}
|
|
21395
|
+
function liveTrainWriterHolder(cwd) {
|
|
21396
|
+
let token;
|
|
21397
|
+
try {
|
|
21398
|
+
token = (0, import_node_fs22.readFileSync)(trainWriterLockPath(cwd), "utf8");
|
|
21399
|
+
} catch {
|
|
21400
|
+
return void 0;
|
|
21401
|
+
}
|
|
21402
|
+
const pid = lockTokenPid(token);
|
|
21403
|
+
if (pid === void 0 || pid === process.pid || pidIsDead(pid)) return void 0;
|
|
21404
|
+
return describeTrainWriterToken(token);
|
|
21405
|
+
}
|
|
21406
|
+
var heldTrainWriter;
|
|
21407
|
+
async function acquireTrainWriterLock(cwd, verb) {
|
|
21408
|
+
let lockPath;
|
|
21409
|
+
try {
|
|
21410
|
+
lockPath = trainWriterLockPath(cwd);
|
|
21411
|
+
} catch {
|
|
21412
|
+
return async () => void 0;
|
|
21413
|
+
}
|
|
21414
|
+
if (heldTrainWriter && heldTrainWriter.lockPath === lockPath) {
|
|
21415
|
+
heldTrainWriter.depth += 1;
|
|
21416
|
+
return async () => {
|
|
21417
|
+
if (heldTrainWriter) heldTrainWriter.depth -= 1;
|
|
21418
|
+
};
|
|
21419
|
+
}
|
|
21420
|
+
(0, import_node_fs22.mkdirSync)((0, import_node_path22.dirname)(lockPath), { recursive: true });
|
|
21421
|
+
const opts = resolveFileLockOpts({ label: "train writer", staleMs: TRAIN_WRITER_STALE_MS, deadHolderSteal: true, maxWaitMs: 0, holder: verb });
|
|
21422
|
+
let guard;
|
|
21423
|
+
try {
|
|
21424
|
+
guard = await acquireFileLock(lockPath, opts, Date.now());
|
|
21425
|
+
} catch (e) {
|
|
21426
|
+
if (!(e instanceof FileLockBusyError)) throw e;
|
|
21427
|
+
let token = "";
|
|
21428
|
+
try {
|
|
21429
|
+
token = (0, import_node_fs22.readFileSync)(lockPath, "utf8");
|
|
21430
|
+
} catch {
|
|
21431
|
+
token = "";
|
|
21432
|
+
}
|
|
21433
|
+
throw new TrainWriterBusyError(lockPath, token ? describeTrainWriterToken(token) : "holder unreadable", verb);
|
|
21434
|
+
}
|
|
21435
|
+
const held = {
|
|
21436
|
+
lockPath,
|
|
21437
|
+
depth: 1,
|
|
21438
|
+
release: async () => {
|
|
21439
|
+
heldTrainWriter = void 0;
|
|
21440
|
+
await releaseFileLock(lockPath, guard);
|
|
21441
|
+
}
|
|
21442
|
+
};
|
|
21443
|
+
heldTrainWriter = held;
|
|
21444
|
+
return async () => {
|
|
21445
|
+
held.depth -= 1;
|
|
21446
|
+
if (held.depth <= 0 && heldTrainWriter === held) await held.release();
|
|
21447
|
+
};
|
|
21448
|
+
}
|
|
21449
|
+
async function withTrainWriter(deps, verb, fn) {
|
|
21450
|
+
const seam = "trainWriter" in deps ? deps.trainWriter : void 0;
|
|
21451
|
+
if (!seam) return fn();
|
|
21452
|
+
const release = await seam(verb, process.cwd());
|
|
21453
|
+
try {
|
|
21454
|
+
return await fn();
|
|
21455
|
+
} finally {
|
|
21456
|
+
await release();
|
|
21457
|
+
}
|
|
21458
|
+
}
|
|
21046
21459
|
async function withReleaseLedger(path2, expected, fn, opts = {}) {
|
|
21047
|
-
return
|
|
21460
|
+
return withLedgerLock(path2, async (fence) => {
|
|
21048
21461
|
let current;
|
|
21049
21462
|
let raw;
|
|
21050
21463
|
try {
|
|
@@ -21059,9 +21472,9 @@ async function withReleaseLedger(path2, expected, fn, opts = {}) {
|
|
|
21059
21472
|
if (expected) validateLedgerAnchors(current, expected);
|
|
21060
21473
|
}
|
|
21061
21474
|
const { ledger, value } = await fn(current);
|
|
21062
|
-
if (ledger) atomicWriteJson(path2, ledger);
|
|
21475
|
+
if (ledger) await fence(() => atomicWriteJson(path2, ledger));
|
|
21063
21476
|
return value;
|
|
21064
|
-
});
|
|
21477
|
+
}, { maxWaitMs: opts.maxWaitMs ?? 5e3 });
|
|
21065
21478
|
}
|
|
21066
21479
|
function openReleaseLedgerWriter(cwd, opts = {}) {
|
|
21067
21480
|
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -21184,7 +21597,9 @@ var TRANSIENT_TRANSPORT_MARKERS = [
|
|
|
21184
21597
|
/socket hang ?up/i,
|
|
21185
21598
|
/remote end hung ?up unexpectedly/i,
|
|
21186
21599
|
/early EOF/i,
|
|
21187
|
-
/tls handshake timeout/i
|
|
21600
|
+
/tls handshake timeout/i,
|
|
21601
|
+
/kex_exchange_identification: connection closed by remote host/i,
|
|
21602
|
+
/connection closed by remote host/i
|
|
21188
21603
|
];
|
|
21189
21604
|
function classifyTransientTransportFailure(evidence) {
|
|
21190
21605
|
if (!evidence) return false;
|
|
@@ -21674,7 +22089,7 @@ async function reverifyPersistedRunPhases(deps, path2, anchors, input) {
|
|
|
21674
22089
|
return result;
|
|
21675
22090
|
}
|
|
21676
22091
|
async function clearLedgerFile(path2, anchors, maxWaitMs) {
|
|
21677
|
-
return
|
|
22092
|
+
return withLedgerLock(path2, async (fence) => {
|
|
21678
22093
|
let raw;
|
|
21679
22094
|
try {
|
|
21680
22095
|
raw = (0, import_node_fs22.readFileSync)(path2, "utf8");
|
|
@@ -21693,9 +22108,9 @@ async function clearLedgerFile(path2, anchors, maxWaitMs) {
|
|
|
21693
22108
|
} catch (e) {
|
|
21694
22109
|
return { cleared: false, note: `release ledger at ${path2} belongs to ${ledger.tag} @ ${ledger.tagSha.slice(0, 12)} \u2014 not this release; left in place (${e instanceof Error ? e.message : String(e)})` };
|
|
21695
22110
|
}
|
|
21696
|
-
(0, import_node_fs22.unlinkSync)(path2);
|
|
22111
|
+
await fence(() => (0, import_node_fs22.unlinkSync)(path2));
|
|
21697
22112
|
return { cleared: true, note: `cleared the release ledger at ${path2} for the live-proven aborted ${anchors.tag}` };
|
|
21698
|
-
});
|
|
22113
|
+
}, { maxWaitMs });
|
|
21699
22114
|
}
|
|
21700
22115
|
async function clearReleaseLedger(cwd, anchors, opts = {}) {
|
|
21701
22116
|
let path2;
|
|
@@ -21835,7 +22250,7 @@ async function alignmentLegClosedLive(deps, prior) {
|
|
|
21835
22250
|
}
|
|
21836
22251
|
async function healStaleAlignmentLeg(deps, cwd, expected) {
|
|
21837
22252
|
const path2 = releaseLedgerPath(cwd);
|
|
21838
|
-
return
|
|
22253
|
+
return withLedgerLock(path2, async (fence) => {
|
|
21839
22254
|
let prior;
|
|
21840
22255
|
try {
|
|
21841
22256
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -21851,16 +22266,80 @@ async function healStaleAlignmentLeg(deps, cwd, expected) {
|
|
|
21851
22266
|
}
|
|
21852
22267
|
const closed = await alignmentLegClosedLive(deps, prior);
|
|
21853
22268
|
if (!closed) return void 0;
|
|
21854
|
-
atomicWriteJson(path2, { ...prior, phases: { ...prior.phases, alignment: closed }, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
22269
|
+
await fence(() => atomicWriteJson(path2, { ...prior, phases: { ...prior.phases, alignment: closed }, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
21855
22270
|
return prior.tag;
|
|
21856
22271
|
});
|
|
21857
22272
|
}
|
|
22273
|
+
async function healUnfoldedRunLegs(deps, cwd, model) {
|
|
22274
|
+
const path2 = releaseLedgerPath(cwd);
|
|
22275
|
+
let prior;
|
|
22276
|
+
try {
|
|
22277
|
+
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
22278
|
+
} catch {
|
|
22279
|
+
return void 0;
|
|
22280
|
+
}
|
|
22281
|
+
const legs = ["deploy", "publish"];
|
|
22282
|
+
const openLeg = (rec) => rec.state === "pending" && rec.superseded !== true;
|
|
22283
|
+
if (!legs.some((phase) => openLeg(prior.phases[phase]))) return void 0;
|
|
22284
|
+
const anchors = { repo: prior.repo, tag: prior.tag, tagSha: prior.tagSha };
|
|
22285
|
+
const folded = [];
|
|
22286
|
+
let current = prior;
|
|
22287
|
+
if (legs.some((phase) => openLeg(prior.phases[phase]) && prior.phases[phase].runId != null)) {
|
|
22288
|
+
let reverified;
|
|
22289
|
+
try {
|
|
22290
|
+
reverified = await reverifyPersistedRunPhases(deps, path2, anchors, { sha: prior.tagSha, repo: prior.repo });
|
|
22291
|
+
} catch {
|
|
22292
|
+
return void 0;
|
|
22293
|
+
}
|
|
22294
|
+
if (!reverified.ledger) return void 0;
|
|
22295
|
+
current = reverified.ledger;
|
|
22296
|
+
for (const phase of legs) {
|
|
22297
|
+
const before = prior.phases[phase];
|
|
22298
|
+
const after = current.phases[phase];
|
|
22299
|
+
if (openLeg(before) && before.runId != null && after.state !== "pending") folded.push({ phase, state: after.state, runId: after.runId });
|
|
22300
|
+
}
|
|
22301
|
+
}
|
|
22302
|
+
const targets = prodReleaseTargets(model);
|
|
22303
|
+
const runless = legs.filter((phase) => openLeg(current.phases[phase]) && current.phases[phase].runId == null);
|
|
22304
|
+
if (runless.length > 0 && targets.length > 0) {
|
|
22305
|
+
let rows;
|
|
22306
|
+
try {
|
|
22307
|
+
rows = await listOwnWorkflowRuns(deps, releaseRunRepoFor(model, prior.repo), targets, prior.tagSha);
|
|
22308
|
+
} catch {
|
|
22309
|
+
rows = [];
|
|
22310
|
+
}
|
|
22311
|
+
const inputs = phaseInputsFromRunRows(model, rows, prior.tagSha, { repo: prior.repo });
|
|
22312
|
+
const foldable = runless.filter((phase) => inputs[phase].runId != null && inputs[phase].state !== "pending");
|
|
22313
|
+
if (foldable.length > 0) {
|
|
22314
|
+
const written = await withReleaseLedger(path2, anchors, (ledger) => {
|
|
22315
|
+
if (!ledger) return { value: [] };
|
|
22316
|
+
const phases = { ...ledger.phases };
|
|
22317
|
+
const done = [];
|
|
22318
|
+
for (const phase of foldable) {
|
|
22319
|
+
const live = phases[phase];
|
|
22320
|
+
if (!openLeg(live) || live.runId != null) continue;
|
|
22321
|
+
const entry = compactEntry(inputs[phase]);
|
|
22322
|
+
phases[phase] = {
|
|
22323
|
+
...entry,
|
|
22324
|
+
note: `${entry.note ?? `${phase} run on the release SHA`} \u2014 folded by train doctor --heal from the concluded run (#7003)`,
|
|
22325
|
+
retries: live.retries,
|
|
22326
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22327
|
+
};
|
|
22328
|
+
done.push({ phase, state: entry.state, runId: entry.runId });
|
|
22329
|
+
}
|
|
22330
|
+
return done.length ? { ledger: { ...ledger, phases, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, value: done } : { value: done };
|
|
22331
|
+
}).catch(() => []);
|
|
22332
|
+
folded.push(...written);
|
|
22333
|
+
}
|
|
22334
|
+
}
|
|
22335
|
+
return folded.length ? { tag: prior.tag, folded } : void 0;
|
|
22336
|
+
}
|
|
21858
22337
|
function supersedeableDeployLegShape(rec) {
|
|
21859
22338
|
return rec.state === "failed" || rec.state === "pending" && rec.runId == null;
|
|
21860
22339
|
}
|
|
21861
22340
|
async function supersedeContentCausedDeployLeg(deps, cwd) {
|
|
21862
22341
|
const path2 = releaseLedgerPath(cwd);
|
|
21863
|
-
return
|
|
22342
|
+
return withLedgerLock(path2, async (fence) => {
|
|
21864
22343
|
let prior;
|
|
21865
22344
|
try {
|
|
21866
22345
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -21901,14 +22380,14 @@ async function supersedeContentCausedDeployLeg(deps, cwd) {
|
|
|
21901
22380
|
);
|
|
21902
22381
|
}
|
|
21903
22382
|
const note = `superseded: content-caused deploy failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, #6553)`;
|
|
21904
|
-
atomicWriteJson(path2, {
|
|
22383
|
+
await fence(() => atomicWriteJson(path2, {
|
|
21905
22384
|
...prior,
|
|
21906
22385
|
phases: {
|
|
21907
22386
|
...phases,
|
|
21908
22387
|
deploy: { state: "skipped", sha: prior.tagSha, superseded: true, note, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
21909
22388
|
},
|
|
21910
22389
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21911
|
-
});
|
|
22390
|
+
}));
|
|
21912
22391
|
return {
|
|
21913
22392
|
command: "release-supersede-deploy",
|
|
21914
22393
|
repo: prior.repo,
|
|
@@ -21924,7 +22403,7 @@ function supersedeablePublishLegShape(rec) {
|
|
|
21924
22403
|
function deferredGatewayDeployLegShape(rec) {
|
|
21925
22404
|
return rec.state === "pending" && rec.runId == null && rec.workflow === "jerv-gateway";
|
|
21926
22405
|
}
|
|
21927
|
-
async function proveOriginPromotionOrArchived(deps, prior, path2, label, ref) {
|
|
22406
|
+
async function proveOriginPromotionOrArchived(deps, prior, path2, label, ref, fence) {
|
|
21928
22407
|
let liveSha = "";
|
|
21929
22408
|
try {
|
|
21930
22409
|
liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
|
|
@@ -21963,12 +22442,12 @@ async function proveOriginPromotionOrArchived(deps, prior, path2, label, ref) {
|
|
|
21963
22442
|
if ((0, import_node_fs22.existsSync)(archivePath)) {
|
|
21964
22443
|
throw new Error(`${label}: archive already exists at ${archivePath} \u2014 preserving both ledger records; resolve the collision by hand; nothing was written (${ref})`);
|
|
21965
22444
|
}
|
|
21966
|
-
(0, import_node_fs22.renameSync)(path2, archivePath);
|
|
22445
|
+
await fence(() => (0, import_node_fs22.renameSync)(path2, archivePath));
|
|
21967
22446
|
return "deleted";
|
|
21968
22447
|
}
|
|
21969
22448
|
async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
21970
22449
|
const path2 = releaseLedgerPath(cwd);
|
|
21971
|
-
return
|
|
22450
|
+
return withLedgerLock(path2, async (fence) => {
|
|
21972
22451
|
let prior;
|
|
21973
22452
|
try {
|
|
21974
22453
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -21999,7 +22478,7 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
21999
22478
|
`release --supersede-publish: ${prior.tag} has unresolved legs besides publish (${unresolved.map((p) => `${p}=${phases[p].state}`).join(", ")}) \u2014 the supersession closes only the publish leg of an otherwise-complete release; resolve those first, nothing was written (#6638)`
|
|
22000
22479
|
);
|
|
22001
22480
|
}
|
|
22002
|
-
if (await proveOriginPromotionOrArchived(deps, prior, path2, "release --supersede-publish", "#6638") === "deleted") {
|
|
22481
|
+
if (await proveOriginPromotionOrArchived(deps, prior, path2, "release --supersede-publish", "#6638", fence) === "deleted") {
|
|
22003
22482
|
return {
|
|
22004
22483
|
command: "release-supersede-publish",
|
|
22005
22484
|
repo: prior.repo,
|
|
@@ -22028,14 +22507,14 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
22028
22507
|
gatewayDeploy = { workflow: "jerv-gateway", state: "complete", note: dispatch.note };
|
|
22029
22508
|
}
|
|
22030
22509
|
const note = `superseded: content-caused publish failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, #6638)`;
|
|
22031
|
-
atomicWriteJson(path2, {
|
|
22510
|
+
await fence(() => atomicWriteJson(path2, {
|
|
22032
22511
|
...prior,
|
|
22033
22512
|
phases: {
|
|
22034
22513
|
...phases,
|
|
22035
22514
|
publish: { state: "skipped", sha: prior.tagSha, superseded: true, note, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
22036
22515
|
},
|
|
22037
22516
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22038
|
-
});
|
|
22517
|
+
}));
|
|
22039
22518
|
return {
|
|
22040
22519
|
command: "release-supersede-publish",
|
|
22041
22520
|
repo: prior.repo,
|
|
@@ -22050,7 +22529,7 @@ async function supersedeContentCausedDeployAndPublishLegs(deps, cwd) {
|
|
|
22050
22529
|
const label = "release --supersede-deploy --supersede-publish";
|
|
22051
22530
|
const ref = "#6752";
|
|
22052
22531
|
const path2 = releaseLedgerPath(cwd);
|
|
22053
|
-
return
|
|
22532
|
+
return withLedgerLock(path2, async (fence) => {
|
|
22054
22533
|
let prior;
|
|
22055
22534
|
try {
|
|
22056
22535
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -22098,7 +22577,7 @@ async function supersedeContentCausedDeployAndPublishLegs(deps, cwd) {
|
|
|
22098
22577
|
`${label}: ${prior.tag} has unresolved legs besides deploy and publish (${unresolved.map((p) => `${p}=${phases[p].state}`).join(", ")}) \u2014 the combined closure closes only the two content-caused legs of an otherwise-complete release; resolve those first, nothing was written (${ref})`
|
|
22099
22578
|
);
|
|
22100
22579
|
}
|
|
22101
|
-
if (await proveOriginPromotionOrArchived(deps, prior, path2, label, ref) === "deleted") {
|
|
22580
|
+
if (await proveOriginPromotionOrArchived(deps, prior, path2, label, ref, fence) === "deleted") {
|
|
22102
22581
|
return {
|
|
22103
22582
|
command: "release-supersede-deploy-and-publish",
|
|
22104
22583
|
repo: prior.repo,
|
|
@@ -22144,7 +22623,7 @@ async function supersedeContentCausedDeployAndPublishLegs(deps, cwd) {
|
|
|
22144
22623
|
note: `superseded: content-caused publish failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, ${ref})`,
|
|
22145
22624
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22146
22625
|
};
|
|
22147
|
-
atomicWriteJson(path2, { ...prior, phases, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
22626
|
+
await fence(() => atomicWriteJson(path2, { ...prior, phases, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
22148
22627
|
return {
|
|
22149
22628
|
command: "release-supersede-deploy-and-publish",
|
|
22150
22629
|
repo: prior.repo,
|
|
@@ -22175,7 +22654,7 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
|
|
|
22175
22654
|
return { ok: true };
|
|
22176
22655
|
}
|
|
22177
22656
|
await reverifyPriorRunLegs(deps, path2, targetTag);
|
|
22178
|
-
return
|
|
22657
|
+
return withLedgerLock(path2, async (fence) => {
|
|
22179
22658
|
let raw;
|
|
22180
22659
|
try {
|
|
22181
22660
|
raw = (0, import_node_fs22.readFileSync)(path2, "utf8");
|
|
@@ -22195,7 +22674,7 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
|
|
|
22195
22674
|
const closed = await alignmentLegClosedLive(deps, prior);
|
|
22196
22675
|
if (closed) {
|
|
22197
22676
|
prior = { ...prior, phases: { ...prior.phases, alignment: closed }, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
22198
|
-
atomicWriteJson(path2, prior);
|
|
22677
|
+
await fence(() => atomicWriteJson(path2, prior));
|
|
22199
22678
|
derived = deriveReleasePhaseStatus(prior);
|
|
22200
22679
|
}
|
|
22201
22680
|
}
|
|
@@ -22218,13 +22697,13 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
|
|
|
22218
22697
|
}
|
|
22219
22698
|
const archivePath = (0, import_node_path22.join)((0, import_node_path22.dirname)(path2), `phases.archive-${prior.tag}.json`);
|
|
22220
22699
|
try {
|
|
22221
|
-
(0, import_node_fs22.unlinkSync)(archivePath);
|
|
22700
|
+
await fence(() => (0, import_node_fs22.unlinkSync)(archivePath));
|
|
22222
22701
|
} catch (e) {
|
|
22223
22702
|
if (e.code !== "ENOENT") {
|
|
22224
22703
|
return { ok: false, error: `could not clear the stale archive at ${archivePath} (${e instanceof Error ? e.message : String(e)}) \u2014 the ledger for ${prior.tag} stays active (#5987)` };
|
|
22225
22704
|
}
|
|
22226
22705
|
}
|
|
22227
|
-
(0, import_node_fs22.renameSync)(path2, archivePath);
|
|
22706
|
+
await fence(() => (0, import_node_fs22.renameSync)(path2, archivePath));
|
|
22228
22707
|
return { ok: true, note: `archived the completed release ledger ${prior.tag} to ${archivePath} (live-proven at ${prior.tagSha.slice(0, 12)})` };
|
|
22229
22708
|
});
|
|
22230
22709
|
}
|
|
@@ -22249,7 +22728,7 @@ async function defaultRetryDeploy(deps, prior) {
|
|
|
22249
22728
|
}
|
|
22250
22729
|
async function retryHotfixDeployLegs(deps, cwd, opts = {}) {
|
|
22251
22730
|
const path2 = releaseLedgerPath(cwd);
|
|
22252
|
-
return
|
|
22731
|
+
return withLedgerLock(path2, async (fence) => {
|
|
22253
22732
|
let prior;
|
|
22254
22733
|
try {
|
|
22255
22734
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -22330,7 +22809,7 @@ async function retryHotfixDeployLegs(deps, cwd, opts = {}) {
|
|
|
22330
22809
|
);
|
|
22331
22810
|
}
|
|
22332
22811
|
}
|
|
22333
|
-
atomicWriteJson(path2, { ...prior, phases, updatedAt: now().toISOString() });
|
|
22812
|
+
await fence(() => atomicWriteJson(path2, { ...prior, phases, updatedAt: now().toISOString() }));
|
|
22334
22813
|
const stillFailed = retried.filter((phase) => phases[phase].state === "failed");
|
|
22335
22814
|
const status = stillFailed.length === 0 ? "success" : "failed";
|
|
22336
22815
|
return {
|
|
@@ -22482,6 +22961,8 @@ var defaultDeps2 = {
|
|
|
22482
22961
|
}
|
|
22483
22962
|
},
|
|
22484
22963
|
healAlignmentLeg: healStaleAlignmentLeg,
|
|
22964
|
+
healRunLegs: healUnfoldedRunLegs,
|
|
22965
|
+
trainWriterHolder: liveTrainWriterHolder,
|
|
22485
22966
|
readSurfacesRaw: (cwd) => {
|
|
22486
22967
|
try {
|
|
22487
22968
|
return (0, import_node_fs23.readFileSync)((0, import_node_path23.join)(cwd, "surfaces.json"), "utf8");
|
|
@@ -22824,11 +23305,31 @@ async function runTrainDoctor(input) {
|
|
|
22824
23305
|
break;
|
|
22825
23306
|
}
|
|
22826
23307
|
}
|
|
23308
|
+
const runningTrain = deps.trainWriterHolder(cwd);
|
|
23309
|
+
if (runningTrain) {
|
|
23310
|
+
add({ code: "train-running", severity: "blocker", source: "local", title: `another train is running on this checkout: ${runningTrain}`, remedy: "wait for it to finish, then rerun; if that process is gone, just rerun \u2014 a dead holder's lock is reclaimed automatically (#7027)" });
|
|
23311
|
+
}
|
|
23312
|
+
const healLedger = heal && !runningTrain;
|
|
22827
23313
|
if (ledgerRaw !== void 0 && ledgerPath) {
|
|
22828
23314
|
try {
|
|
22829
23315
|
let ledger = parseReleaseLedger(ledgerRaw);
|
|
22830
23316
|
let derived = deriveReleasePhaseStatus(ledger);
|
|
22831
|
-
|
|
23317
|
+
const deployModel = typeof meta?.deployModel === "string" && isDeployModel(meta.deployModel) ? meta.deployModel : void 0;
|
|
23318
|
+
if (healLedger && deployModel && derived.legs.some((l) => (l.phase === "deploy" || l.phase === "publish") && l.state === "pending")) {
|
|
23319
|
+
const run = await deps.healRunLegs(train, cwd, deployModel);
|
|
23320
|
+
if (run) {
|
|
23321
|
+
add({
|
|
23322
|
+
code: "ledger-unfolded",
|
|
23323
|
+
severity: "healed",
|
|
23324
|
+
source: "origin",
|
|
23325
|
+
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)`,
|
|
23326
|
+
remedy: "nothing \u2014 the leg now records what GitHub already concluded"
|
|
23327
|
+
});
|
|
23328
|
+
ledger = parseReleaseLedger(deps.readLedgerRaw(ledgerPath) ?? ledgerRaw);
|
|
23329
|
+
derived = deriveReleasePhaseStatus(ledger);
|
|
23330
|
+
}
|
|
23331
|
+
}
|
|
23332
|
+
if (healLedger && derived.phaseStatus === "phases-pending" && derived.legs.every((l) => l.state !== "pending" || l.phase === "alignment")) {
|
|
22832
23333
|
const closedTag = await deps.healAlignmentLeg(train, cwd);
|
|
22833
23334
|
if (closedTag) {
|
|
22834
23335
|
add({ code: "ledger-alignment-stale", severity: "healed", source: "origin", title: `closed the alignment-pending leg of ${closedTag}: its alignment PR(s) read MERGED on a fresh origin read (#6061)`, remedy: "nothing \u2014 the ledger is phases-green" });
|
|
@@ -22866,7 +23367,7 @@ async function runTrainDoctor(input) {
|
|
|
22866
23367
|
// #6429: each lane finishes its own run (#6068) — an rcand-lane ledger named as a release
|
|
22867
23368
|
// ledger, with `release --resume` as its remedy, sends the operator to a verb that refuses it.
|
|
22868
23369
|
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}`
|
|
23370
|
+
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
23371
|
});
|
|
22871
23372
|
} else if (newestRemoteReleaseTag && newestRemoteReleaseTag !== ledger.tag) {
|
|
22872
23373
|
add({
|
|
@@ -23622,6 +24123,8 @@ async function recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, phase,
|
|
|
23622
24123
|
}
|
|
23623
24124
|
async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch, input, recover = recoverPhaseWithMaybeTransientRerun) {
|
|
23624
24125
|
if (dispatch.deployStatus !== "failure") return dispatch;
|
|
24126
|
+
const persisted = await ledger?.load(anchors).catch(() => void 0);
|
|
24127
|
+
if (persisted?.phases[phase]?.superseded === true) return dispatch;
|
|
23625
24128
|
let workflowRuns = dispatch.workflowRuns?.length ? [...dispatch.workflowRuns] : void 0;
|
|
23626
24129
|
const notes = [];
|
|
23627
24130
|
let fallbackRecovered = false;
|
|
@@ -23629,6 +24132,7 @@ async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch
|
|
|
23629
24132
|
if (failedRows.length > 0) {
|
|
23630
24133
|
for (const failed of failedRows) {
|
|
23631
24134
|
const rowPhase = failed.workflow.includes("publish") ? "publish" : phase;
|
|
24135
|
+
if (persisted?.phases[rowPhase]?.superseded === true) continue;
|
|
23632
24136
|
const recovery = await recover(deps, ledger, anchors, rowPhase, {
|
|
23633
24137
|
repo: input.repo,
|
|
23634
24138
|
sha: input.sha,
|
|
@@ -23803,7 +24307,10 @@ ${recovery.note}`), recoveryInput);
|
|
|
23803
24307
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
23804
24308
|
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
24309
|
let alignment = alignEarly ? await alignEarly() : void 0;
|
|
23806
|
-
const deployDispatch0 = await
|
|
24310
|
+
const [deployDispatch0, publishDispatch0] = await joinDeployAndPublish(
|
|
24311
|
+
dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir),
|
|
24312
|
+
observePublishIfRequired(deps, ctx, meta, deployModel, "main", releaseSha, watch)
|
|
24313
|
+
);
|
|
23807
24314
|
const deployRunRepo = releaseRunRepoFor(deployModel, ctx.repo);
|
|
23808
24315
|
const failedReleaseRun = deployDispatch0.workflowRuns?.find((run) => run.conclusion === "failure");
|
|
23809
24316
|
const dispatchFailurePhase = failedReleaseRun?.workflow === "publish.yml" || deployModel === "registry-publish" ? "publish" : "deploy";
|
|
@@ -23825,7 +24332,6 @@ ${recovery.note}`), recoveryInput);
|
|
|
23825
24332
|
await recordPhase(ledger, deps, ledgerAnchors, "deploy", deployLeg, {
|
|
23826
24333
|
landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
|
|
23827
24334
|
});
|
|
23828
|
-
const publishDispatch0 = await observePublishIfRequired(deps, ctx, meta, deployModel, "main", releaseSha, watch);
|
|
23829
24335
|
const publishDispatch = publishDispatch0 ? await recoverFailedDispatchPhase(
|
|
23830
24336
|
deps,
|
|
23831
24337
|
ledger,
|
|
@@ -23841,17 +24347,6 @@ ${recovery.note}`), recoveryInput);
|
|
|
23841
24347
|
await recordPhase(ledger, deps, ledgerAnchors, "publish", publishLeg, {
|
|
23842
24348
|
landed: "the immutable tag, the green required-check wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
23843
24349
|
});
|
|
23844
|
-
let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
23845
|
-
dispatch = await appendMmiGetRedeploy(
|
|
23846
|
-
deps,
|
|
23847
|
-
{ repo: ctx.repo, publishStatus: publishLeg.state === "complete" ? "success" : "failure", watch },
|
|
23848
|
-
dispatch
|
|
23849
|
-
);
|
|
23850
|
-
dispatch = await appendServedChannelPublish(
|
|
23851
|
-
deps,
|
|
23852
|
-
{ repo: ctx.repo, publishStatus: publishLeg.state === "complete" ? "success" : "failure", watch },
|
|
23853
|
-
dispatch
|
|
23854
|
-
);
|
|
23855
24350
|
const announcing = announceShippedRelease(
|
|
23856
24351
|
deps,
|
|
23857
24352
|
{ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
|
|
@@ -23859,12 +24354,13 @@ ${recovery.note}`), recoveryInput);
|
|
|
23859
24354
|
{ deploy: deployLeg, publish: publishLeg },
|
|
23860
24355
|
releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
|
|
23861
24356
|
);
|
|
23862
|
-
|
|
23863
|
-
|
|
23864
|
-
|
|
23865
|
-
|
|
23866
|
-
|
|
23867
|
-
|
|
24357
|
+
const aligning = watch && alignment ? awaitAlignmentsLanded(deps, ctx, alignment) : Promise.resolve(alignment);
|
|
24358
|
+
const dispatch = await appendServedChannelLegs(
|
|
24359
|
+
deps,
|
|
24360
|
+
{ repo: ctx.repo, tag, releaseSha, watch, deploy: deployLeg, publish: publishLeg, installer: meta.installer },
|
|
24361
|
+
appendPublishDispatch(deployDispatch, publishDispatch)
|
|
24362
|
+
);
|
|
24363
|
+
alignment = await aligning;
|
|
23868
24364
|
const announceNote = await announcing;
|
|
23869
24365
|
return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors, ...alignment ? { alignment } : {} };
|
|
23870
24366
|
}
|
|
@@ -23932,6 +24428,9 @@ async function probeAncestor(deps, ancestor, descendant, label) {
|
|
|
23932
24428
|
}
|
|
23933
24429
|
}
|
|
23934
24430
|
async function runRcandResume(deps, options = {}) {
|
|
24431
|
+
return withTrainWriter(deps, "rcand --resume", () => runRcandResumeLocked(deps, options));
|
|
24432
|
+
}
|
|
24433
|
+
async function runRcandResumeLocked(deps, options) {
|
|
23935
24434
|
const watch = options.watch ?? false;
|
|
23936
24435
|
const ctx = await buildTrainApplyContext(deps);
|
|
23937
24436
|
await requireCleanTree(deps);
|
|
@@ -24180,6 +24679,8 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
24180
24679
|
dispatch2.deployStatus = historicalDeploy;
|
|
24181
24680
|
dispatch2.note = `historical ${deployModel} deploy/publish status: ${historicalDeploy}; ${companion.note}`;
|
|
24182
24681
|
}
|
|
24682
|
+
if (persisted?.phases.deploy.superseded === true) phaseInputs.deploy = persisted.phases.deploy;
|
|
24683
|
+
if (persisted?.phases.publish.superseded === true) phaseInputs.publish = persisted.phases.publish;
|
|
24183
24684
|
if (persisted) {
|
|
24184
24685
|
if (persisted.phases.deploy.superseded !== true) {
|
|
24185
24686
|
await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(phaseInputs.deploy), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
|
|
@@ -24190,8 +24691,15 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
24190
24691
|
await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified live: the GitHub Release exists at the immutable tag SHA" }), { strict: true, landed: "the promotion (verified live)" });
|
|
24191
24692
|
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
24693
|
}
|
|
24694
|
+
const shippedDispatch = await appendServedChannelLegs(
|
|
24695
|
+
deps,
|
|
24696
|
+
{ repo: ctx.repo, tag, releaseSha: tagSha, watch, deploy: phaseInputs.deploy, publish: phaseInputs.publish, installer: meta.installer },
|
|
24697
|
+
dispatch2
|
|
24698
|
+
);
|
|
24699
|
+
Object.assign(dispatch2, shippedDispatch);
|
|
24193
24700
|
if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
|
|
24194
|
-
const
|
|
24701
|
+
const settledBySupersede = persisted?.phases.deploy.superseded === true || persisted?.phases.publish.superseded === true;
|
|
24702
|
+
const announceNote2 = persisted && !shippedBefore && !settledBySupersede && (prodReleaseTargets(deployModel).length > 0 || companion !== null) ? await announceShippedRelease(
|
|
24195
24703
|
deps,
|
|
24196
24704
|
{ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
|
|
24197
24705
|
deployModel,
|
|
@@ -24281,7 +24789,10 @@ ${recovery.note}`), recoveryInput);
|
|
|
24281
24789
|
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
24790
|
let devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
24283
24791
|
let rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
|
|
24284
|
-
const deployDispatch0 = await
|
|
24792
|
+
const [deployDispatch0, publishDispatch0] = await joinDeployAndPublish(
|
|
24793
|
+
dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir),
|
|
24794
|
+
observePublishIfRequired(deps, ctx, meta, deployModel, "main", tagSha, watch)
|
|
24795
|
+
);
|
|
24285
24796
|
const recoveredDeployDispatch = await recoverFailedDispatchPhase(
|
|
24286
24797
|
deps,
|
|
24287
24798
|
ledger,
|
|
@@ -24297,12 +24808,13 @@ ${recovery.note}`), recoveryInput);
|
|
|
24297
24808
|
);
|
|
24298
24809
|
const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch);
|
|
24299
24810
|
steps.push(`dispatched the ${deployModel} deploy path`);
|
|
24300
|
-
const deployLeg = phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
|
|
24301
|
-
|
|
24302
|
-
|
|
24303
|
-
|
|
24304
|
-
|
|
24305
|
-
|
|
24811
|
+
const deployLeg = persisted?.phases.deploy.superseded === true ? persisted.phases.deploy : phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
|
|
24812
|
+
if (persisted?.phases.deploy.superseded !== true) {
|
|
24813
|
+
await recordPhase(ledger, deps, anchors, "deploy", deployLeg, {
|
|
24814
|
+
strict: ledgerMode === "strict",
|
|
24815
|
+
landed: "the immutable tag, the re-proven wall, the origin/main push, and the verified GitHub Release"
|
|
24816
|
+
});
|
|
24817
|
+
}
|
|
24306
24818
|
const publishDispatch = publishDispatch0 ? await recoverFailedDispatchPhase(
|
|
24307
24819
|
deps,
|
|
24308
24820
|
ledger,
|
|
@@ -24311,12 +24823,18 @@ ${recovery.note}`), recoveryInput);
|
|
|
24311
24823
|
publishDispatch0,
|
|
24312
24824
|
{ repo: ctx.repo, sha: tagSha, watched: false, workflow: "publish.yml" }
|
|
24313
24825
|
) : null;
|
|
24314
|
-
const publishLeg = phaseEntry(publishPhaseInput(deployModel, deployDispatch, publishDispatch, meta.publishRequired === true, { releaseSha: tagSha, repo: ctx.repo }));
|
|
24315
|
-
|
|
24316
|
-
|
|
24317
|
-
|
|
24318
|
-
|
|
24319
|
-
|
|
24826
|
+
const publishLeg = persisted?.phases.publish.superseded === true ? persisted.phases.publish : phaseEntry(publishPhaseInput(deployModel, deployDispatch, publishDispatch, meta.publishRequired === true, { releaseSha: tagSha, repo: ctx.repo }));
|
|
24827
|
+
if (persisted?.phases.publish.superseded !== true) {
|
|
24828
|
+
await recordPhase(ledger, deps, anchors, "publish", publishLeg, {
|
|
24829
|
+
strict: ledgerMode === "strict",
|
|
24830
|
+
landed: "the immutable tag, the re-proven wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
24831
|
+
});
|
|
24832
|
+
}
|
|
24833
|
+
const dispatch = await appendServedChannelLegs(
|
|
24834
|
+
deps,
|
|
24835
|
+
{ repo: ctx.repo, tag, releaseSha: tagSha, watch, deploy: deployLeg, publish: publishLeg, installer: meta.installer },
|
|
24836
|
+
appendPublishDispatch(deployDispatch, publishDispatch)
|
|
24837
|
+
);
|
|
24320
24838
|
const announcing = announceShippedRelease(
|
|
24321
24839
|
deps,
|
|
24322
24840
|
{ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
|
|
@@ -24325,8 +24843,9 @@ ${recovery.note}`), recoveryInput);
|
|
|
24325
24843
|
releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
|
|
24326
24844
|
);
|
|
24327
24845
|
if (watch) {
|
|
24328
|
-
|
|
24329
|
-
|
|
24846
|
+
const landed = await awaitAlignmentsLanded(deps, ctx, { dev: devRollForward, rc: rcAlignment });
|
|
24847
|
+
devRollForward = landed.dev;
|
|
24848
|
+
rcAlignment = landed.rc;
|
|
24330
24849
|
}
|
|
24331
24850
|
const announceNote = await announcing;
|
|
24332
24851
|
steps.push(`development roll-forward: ${devRollForward.status}`);
|
|
@@ -24372,6 +24891,9 @@ async function withCheckoutRestoredAfterReleaseResume(deps, startBranch, operati
|
|
|
24372
24891
|
return withCheckoutRestoredAfterRelease(deps, result, startBranch);
|
|
24373
24892
|
}
|
|
24374
24893
|
async function runReleaseResume(deps, options = {}) {
|
|
24894
|
+
return withTrainWriter(deps, "release --resume", () => runReleaseResumeLocked(deps, options));
|
|
24895
|
+
}
|
|
24896
|
+
async function runReleaseResumeLocked(deps, options) {
|
|
24375
24897
|
const ctx = await buildTrainApplyContext(deps);
|
|
24376
24898
|
await requireCleanTree(deps);
|
|
24377
24899
|
await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
|
|
@@ -24387,6 +24909,10 @@ async function runReleaseResume(deps, options = {}) {
|
|
|
24387
24909
|
);
|
|
24388
24910
|
}
|
|
24389
24911
|
async function runReleaseAbort(deps, options = {}) {
|
|
24912
|
+
if (!options.approved) return runReleaseAbortLocked(deps, options);
|
|
24913
|
+
return withTrainWriter(deps, "release --abort", () => runReleaseAbortLocked(deps, options));
|
|
24914
|
+
}
|
|
24915
|
+
async function runReleaseAbortLocked(deps, options) {
|
|
24390
24916
|
if (!options.approved) {
|
|
24391
24917
|
throw new Error("release --abort requires --apply after explicit approval; nothing was written");
|
|
24392
24918
|
}
|
|
@@ -24461,7 +24987,7 @@ async function runReleaseSupersedeDeploy(deps, options = {}) {
|
|
|
24461
24987
|
throw new Error("release --supersede-deploy requires --apply after explicit master approval; nothing was written");
|
|
24462
24988
|
}
|
|
24463
24989
|
try {
|
|
24464
|
-
return await supersedeContentCausedDeployLeg(deps, process.cwd());
|
|
24990
|
+
return await withTrainWriter(deps, "release --supersede-deploy", () => supersedeContentCausedDeployLeg(deps, process.cwd()));
|
|
24465
24991
|
} catch (e) {
|
|
24466
24992
|
const message2 = e instanceof Error ? e.message : String(e);
|
|
24467
24993
|
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
@@ -24472,7 +24998,7 @@ async function runReleaseSupersedePublish(deps, options = {}) {
|
|
|
24472
24998
|
throw new Error("release --supersede-publish requires --apply after explicit master approval; nothing was written");
|
|
24473
24999
|
}
|
|
24474
25000
|
try {
|
|
24475
|
-
return await supersedeContentCausedPublishLeg(deps, process.cwd());
|
|
25001
|
+
return await withTrainWriter(deps, "release --supersede-publish", () => supersedeContentCausedPublishLeg(deps, process.cwd()));
|
|
24476
25002
|
} catch (e) {
|
|
24477
25003
|
const message2 = e instanceof Error ? e.message : String(e);
|
|
24478
25004
|
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
@@ -24483,13 +25009,17 @@ async function runReleaseSupersedeDeployAndPublish(deps, options = {}) {
|
|
|
24483
25009
|
throw new Error("release --supersede-deploy --supersede-publish requires --apply after explicit master approval; nothing was written");
|
|
24484
25010
|
}
|
|
24485
25011
|
try {
|
|
24486
|
-
return await supersedeContentCausedDeployAndPublishLegs(deps, process.cwd());
|
|
25012
|
+
return await withTrainWriter(deps, "release --supersede-deploy --supersede-publish", () => supersedeContentCausedDeployAndPublishLegs(deps, process.cwd()));
|
|
24487
25013
|
} catch (e) {
|
|
24488
25014
|
const message2 = e instanceof Error ? e.message : String(e);
|
|
24489
25015
|
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
24490
25016
|
}
|
|
24491
25017
|
}
|
|
24492
25018
|
async function runReleasePublishRetry(deps, runId, options = {}) {
|
|
25019
|
+
if (!options.approved) return runReleasePublishRetryLocked(deps, runId, options);
|
|
25020
|
+
return withTrainWriter(deps, "release --retry-publish", () => runReleasePublishRetryLocked(deps, runId, options));
|
|
25021
|
+
}
|
|
25022
|
+
async function runReleasePublishRetryLocked(deps, runId, options) {
|
|
24493
25023
|
if (!options.approved) {
|
|
24494
25024
|
throw new Error("release --retry-publish requires --apply after explicit approval; nothing was written");
|
|
24495
25025
|
}
|
|
@@ -24934,6 +25464,9 @@ async function resolveHotfixVersion(deps, env = process.env) {
|
|
|
24934
25464
|
return { tag: `v${version}`, version, baseTag };
|
|
24935
25465
|
}
|
|
24936
25466
|
async function runTrainApply(command, deps, options = {}) {
|
|
25467
|
+
return withTrainWriter(deps, `${command} --apply`, () => runTrainApplyLocked(command, deps, options));
|
|
25468
|
+
}
|
|
25469
|
+
async function runTrainApplyLocked(command, deps, options = {}) {
|
|
24937
25470
|
const watch = options.watch ?? false;
|
|
24938
25471
|
await runTrainDoctor({ lane: command === "rcand" ? "rcand" : "release", dev: command === "release" && options.dev === true, heal: true, train: deps, refuse: true });
|
|
24939
25472
|
const ctx = await buildTrainApplyContext(deps);
|
|
@@ -24981,6 +25514,7 @@ var RULESET_TEMPLATE_SEED = "seed:mmi-product-required-checks.template.json";
|
|
|
24981
25514
|
var RULESET_REFERENCE_MATCH_LABEL = "committed ruleset reference matches the live ruleset (#3816)";
|
|
24982
25515
|
var GATE_GREEN_ON_BASE_LABEL = "gate is green on the default branch (#3819)";
|
|
24983
25516
|
var REQUIRED_CONTEXTS_EMITTED_LABEL = "required check contexts match PR workflows";
|
|
25517
|
+
var UNBLOCKING_GATE_JOBS_LABEL = "every gate job can block a merge (#7012)";
|
|
24984
25518
|
var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressable (#3880)";
|
|
24985
25519
|
var RELEASE_BRANCH_REACHABLE_LABEL = "rc/main required contexts are reachable from a release-base head (#5193)";
|
|
24986
25520
|
var RULESET_TRACK_SCOPE_LABEL = "product ruleset branch scope covers the release track (#6327)";
|
|
@@ -25413,6 +25947,17 @@ async function auditRepoCi(repo, deps) {
|
|
|
25413
25947
|
remediation: missingEmitters.length ? `Fix the pull_request workflows or ${contextAuthority.source} before reconciling` : void 0
|
|
25414
25948
|
});
|
|
25415
25949
|
}
|
|
25950
|
+
if (hasRequiredChecks) {
|
|
25951
|
+
const gateFiles = new Set(gateWorkflowFiles(prWorkflowPaths));
|
|
25952
|
+
const gateWorkflows = (await getBranchWorkflows() ?? []).filter((wf) => gateFiles.has(wf.path.slice(wf.path.lastIndexOf("/") + 1)));
|
|
25953
|
+
const unblocking = unblockingGateJobs(gateWorkflows, new Set(liveContexts)).filter((job) => !AGENT_PR_BOOKKEEPING_CONTEXTS.has(job));
|
|
25954
|
+
checks.push({
|
|
25955
|
+
ok: unblocking.length === 0,
|
|
25956
|
+
label: UNBLOCKING_GATE_JOBS_LABEL,
|
|
25957
|
+
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,
|
|
25958
|
+
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
|
|
25959
|
+
});
|
|
25960
|
+
}
|
|
25416
25961
|
const prOnly = contextAuthority.coversReleaseBranches ? contextAuthority.contexts.filter((context) => TRAIN_PR_ONLY_CONTEXTS.has(context)) : [];
|
|
25417
25962
|
const tagBlind = contextAuthority.coversReleaseBranches ? contextsUnreachableOnTagPush(contextAuthority.contexts, await getBranchWorkflows() ?? [], TRAIN_PR_ONLY_CONTEXTS) : [];
|
|
25418
25963
|
checks.push({
|
|
@@ -30348,7 +30893,7 @@ function registerDeployCommands(program3) {
|
|
|
30348
30893
|
init_cli_shared();
|
|
30349
30894
|
|
|
30350
30895
|
// src/project-set.ts
|
|
30351
|
-
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary"];
|
|
30896
|
+
var UNSET_KEYS = ["installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary"];
|
|
30352
30897
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
30353
30898
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
30354
30899
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -30606,6 +31151,17 @@ function parseRuntimeVaultOnlyVar(raw) {
|
|
|
30606
31151
|
if (raw === "false") return false;
|
|
30607
31152
|
throw new Error("org project set: runtimeVaultOnly must be true or false");
|
|
30608
31153
|
}
|
|
31154
|
+
function parseInstallerVar(raw) {
|
|
31155
|
+
let value;
|
|
31156
|
+
try {
|
|
31157
|
+
value = JSON.parse(raw);
|
|
31158
|
+
} catch {
|
|
31159
|
+
throw new Error('org project set: installer must be JSON, e.g. {"product":"mm-strategy","family":"mmi"}');
|
|
31160
|
+
}
|
|
31161
|
+
const error = installerDeclarationError(value, "mmi");
|
|
31162
|
+
if (error && !error.includes("breaks the sign-in rule")) throw new Error(`org project set: ${error}`);
|
|
31163
|
+
return value;
|
|
31164
|
+
}
|
|
30609
31165
|
function parseSeedCanaryVar(raw) {
|
|
30610
31166
|
if (raw === "true") return true;
|
|
30611
31167
|
if (raw === "false") return false;
|
|
@@ -30776,7 +31332,8 @@ var SETTABLE_VAR_KEYS = [
|
|
|
30776
31332
|
"ciExemptReason",
|
|
30777
31333
|
"gate",
|
|
30778
31334
|
"secrets",
|
|
30779
|
-
"seedCanary"
|
|
31335
|
+
"seedCanary",
|
|
31336
|
+
"installer"
|
|
30780
31337
|
];
|
|
30781
31338
|
var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
30782
31339
|
var SETTABLE_VAR_HINTS = {
|
|
@@ -30789,6 +31346,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
30789
31346
|
consumesDesignSystem: '"fofu"',
|
|
30790
31347
|
runtimeVaultOnly: "true|false",
|
|
30791
31348
|
seedCanary: "true|false",
|
|
31349
|
+
installer: "JSON {product,family:jerv|mmi,login?:github|google} \u2014 the repo ships a one-line installer on get.jerv.ai or get.mutmut.work",
|
|
30792
31350
|
repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
|
|
30793
31351
|
oauth: "JSON {subdomains,domains,callbackPath,extraCallbackPaths}",
|
|
30794
31352
|
requiredGcpApis: "comma-string",
|
|
@@ -30924,6 +31482,8 @@ function buildProjectSetPatch(input) {
|
|
|
30924
31482
|
patch[key] = parseRuntimeVaultOnlyVar(raw);
|
|
30925
31483
|
} else if (key === "seedCanary") {
|
|
30926
31484
|
patch[key] = parseSeedCanaryVar(raw);
|
|
31485
|
+
} else if (key === "installer") {
|
|
31486
|
+
patch[key] = parseInstallerVar(raw);
|
|
30927
31487
|
} else if (key === "consumesDesignSystem") {
|
|
30928
31488
|
patch[key] = parseConsumesDesignSystemVar(raw);
|
|
30929
31489
|
} else if (key === "publishDir") {
|
|
@@ -43998,6 +44558,32 @@ function formatHotfixBatchPreflightRefusal(failures, fromSpecs) {
|
|
|
43998
44558
|
${lines2.join("\n")}
|
|
43999
44559
|
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
44560
|
}
|
|
44561
|
+
var CHERRY_PICKED_FROM = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
44562
|
+
async function findHotfixPickGaps(deps, sources, tolerated) {
|
|
44563
|
+
if (sources.length === 0) return [];
|
|
44564
|
+
const namedShas = new Set(sources.map((s) => s.sha.toLowerCase()));
|
|
44565
|
+
const carried = /* @__PURE__ */ new Set();
|
|
44566
|
+
const mainOnly = await deps.run("git", ["log", "--no-merges", "--format=%B", "origin/development..origin/main"]);
|
|
44567
|
+
for (const m of mainOnly.matchAll(CHERRY_PICKED_FROM)) carried.add(m[1].toLowerCase());
|
|
44568
|
+
const gaps = [];
|
|
44569
|
+
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);
|
|
44570
|
+
for (const line of devOnly) {
|
|
44571
|
+
const [sha, ...subjectParts] = line.split(" ");
|
|
44572
|
+
if (!sha) continue;
|
|
44573
|
+
const lower = sha.toLowerCase();
|
|
44574
|
+
if (namedShas.has(lower) || carried.has(lower)) continue;
|
|
44575
|
+
const paths = (await deps.run("git", ["show", "--no-color", "--name-only", "--pretty=format:", sha])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
44576
|
+
if (paths.length === 0 || paths.every((p) => pathIsTolerated(p, tolerated))) continue;
|
|
44577
|
+
gaps.push({ sha, subject: subjectParts.join(" ") });
|
|
44578
|
+
}
|
|
44579
|
+
return gaps;
|
|
44580
|
+
}
|
|
44581
|
+
function formatHotfixPickGapRefusal(gaps, tag, fromSpecs) {
|
|
44582
|
+
const lines2 = gaps.map((g) => ` - ${g.sha.slice(0, 7)} ${g.subject}`);
|
|
44583
|
+
return `hotfix start refused: ${tag} would omit ${gaps.length} merged development fix(es) not on origin/main:
|
|
44584
|
+
${lines2.join("\n")}
|
|
44585
|
+
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.`;
|
|
44586
|
+
}
|
|
44001
44587
|
async function preflightHotfixBatchPicks(deps, sources, pickTolerated) {
|
|
44002
44588
|
if (sources.length === 0) return [];
|
|
44003
44589
|
const startBranch = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
@@ -44137,6 +44723,9 @@ async function hotfixReleaseExists(deps, ctx, tag) {
|
|
|
44137
44723
|
}
|
|
44138
44724
|
}
|
|
44139
44725
|
async function runHotfixStart(deps, options, doctor = runTrainDoctor) {
|
|
44726
|
+
return withTrainWriter(deps, "hotfix start", () => runHotfixStartLocked(deps, options, doctor));
|
|
44727
|
+
}
|
|
44728
|
+
async function runHotfixStartLocked(deps, options, doctor) {
|
|
44140
44729
|
await doctor({ lane: "hotfix", heal: true, train: deps, refuse: true });
|
|
44141
44730
|
const ctx = await buildTrainApplyContext(deps);
|
|
44142
44731
|
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
@@ -44184,6 +44773,18 @@ async function runHotfixStart(deps, options, doctor = runTrainDoctor) {
|
|
|
44184
44773
|
}
|
|
44185
44774
|
const label = sources.map((s) => s.label).join(", ");
|
|
44186
44775
|
const { foldPaths, pickTolerated } = await resolveHotfixPickTolerated(deps, deployModel);
|
|
44776
|
+
const ackedShas = (options.ack ?? []).map((a) => a.trim().toLowerCase()).filter(Boolean);
|
|
44777
|
+
const pickGaps = await findHotfixPickGaps(deps, sources, pickTolerated);
|
|
44778
|
+
const unacked = pickGaps.filter((g) => {
|
|
44779
|
+
const sha = g.sha.toLowerCase();
|
|
44780
|
+
return !ackedShas.some((a) => sha.startsWith(a) || a.startsWith(sha));
|
|
44781
|
+
});
|
|
44782
|
+
if (unacked.length > 0) {
|
|
44783
|
+
throw new Error(formatHotfixPickGapRefusal(unacked, tag, specs));
|
|
44784
|
+
}
|
|
44785
|
+
if (pickGaps.length > 0) {
|
|
44786
|
+
notes.push(`pick-completeness ack: excluded ${pickGaps.map((g) => `${g.sha.slice(0, 7)} ${g.subject}`).join("; ")} by --ack (#6994)`);
|
|
44787
|
+
}
|
|
44187
44788
|
const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
44188
44789
|
if (remoteBranch) {
|
|
44189
44790
|
await deps.run("git", ["checkout", branch]);
|
|
@@ -44248,7 +44849,9 @@ async function runHotfixStart(deps, options, doctor = runTrainDoctor) {
|
|
|
44248
44849
|
"--body",
|
|
44249
44850
|
`Hotfix ${tag}: cherry-pick of ${label} onto origin/main${bumpNote}.
|
|
44250
44851
|
|
|
44251
|
-
<!-- mmi-hotfix-carries: ${sources.map((s) => s.sha).join(",")}
|
|
44852
|
+
<!-- mmi-hotfix-carries: ${sources.map((s) => s.sha).join(",")} -->${pickGaps.length > 0 ? `
|
|
44853
|
+
|
|
44854
|
+
<!-- mmi-hotfix-acked-excluded: ${pickGaps.map((g) => g.sha).join(",")} (#6994 pick-completeness) -->` : ""}
|
|
44252
44855
|
|
|
44253
44856
|
Merge this PR (human-initiated), then run \`mmi-cli devops hotfix release ${tag}\`.`
|
|
44254
44857
|
]));
|
|
@@ -44420,6 +45023,24 @@ async function appendHotfixGatewayRun(deps, repo, tag, sha, runs, note) {
|
|
|
44420
45023
|
}
|
|
44421
45024
|
return gateway.note;
|
|
44422
45025
|
}
|
|
45026
|
+
async function appendHotfixServedChannel(deps, input, runs, note) {
|
|
45027
|
+
const before = hotfixDispatchFromRuns(runs, note);
|
|
45028
|
+
const after = await appendServedChannelLegs(
|
|
45029
|
+
deps,
|
|
45030
|
+
{ repo: input.repo, tag: input.tag, releaseSha: input.sha, watch: true, deploy: input.deploy, publish: input.publish, installer: input.installer },
|
|
45031
|
+
before
|
|
45032
|
+
);
|
|
45033
|
+
for (const row of servedChannelRowsAfter(before, after)) {
|
|
45034
|
+
runs.push({
|
|
45035
|
+
workflow: row.workflow,
|
|
45036
|
+
...row.runId != null ? { runId: row.runId } : {},
|
|
45037
|
+
...row.runUrl ? { url: row.runUrl } : {},
|
|
45038
|
+
...row.runUrlNote ? { runUrlNote: row.runUrlNote } : {},
|
|
45039
|
+
conclusion: row.conclusion
|
|
45040
|
+
});
|
|
45041
|
+
}
|
|
45042
|
+
return after.note;
|
|
45043
|
+
}
|
|
44423
45044
|
function hotfixPhaseInputsFromRuns(deployModel, runs, note, publishDispatch, publishRequired, opts) {
|
|
44424
45045
|
const dispatch = hotfixDispatchFromRuns(runs, note);
|
|
44425
45046
|
return {
|
|
@@ -44542,6 +45163,9 @@ async function assertConsumerBomLineage(deps, mergedSha, tag) {
|
|
|
44542
45163
|
}
|
|
44543
45164
|
}
|
|
44544
45165
|
async function runHotfixRelease(deps, versionInput, options = {}, doctor = runTrainDoctor) {
|
|
45166
|
+
return withTrainWriter(deps, `hotfix release ${versionInput}`, () => runHotfixReleaseLocked(deps, versionInput, options, doctor));
|
|
45167
|
+
}
|
|
45168
|
+
async function runHotfixReleaseLocked(deps, versionInput, options, doctor) {
|
|
44545
45169
|
await doctor({ lane: "hotfix", heal: true, train: deps, refuse: true });
|
|
44546
45170
|
const ctx = await buildTrainApplyContext(deps);
|
|
44547
45171
|
const { tag, version } = normalizeHotfixVersion(versionInput);
|
|
@@ -44638,9 +45262,9 @@ ${decision.note}`);
|
|
|
44638
45262
|
let deployNote;
|
|
44639
45263
|
let tenantPublishDispatch = null;
|
|
44640
45264
|
if (deployModel === "hub-serverless") {
|
|
44641
|
-
|
|
44642
|
-
|
|
44643
|
-
runs.push(await recoverHotfixReleaseRun(deps, ledger, anchors, ctx, deployModel, workflow === "publish.yml" ? "publish" : "deploy", watched));
|
|
45265
|
+
const watchedRuns = await Promise.all(HOTFIX_RELEASE_WORKFLOWS.map((workflow) => watchReleaseRun(deps, ctx, workflow, mergedSha)));
|
|
45266
|
+
for (const watched of watchedRuns) {
|
|
45267
|
+
runs.push(await recoverHotfixReleaseRun(deps, ledger, anchors, ctx, deployModel, watched.workflow === "publish.yml" ? "publish" : "deploy", watched));
|
|
44644
45268
|
}
|
|
44645
45269
|
deployNote = "watched release-triggered deploy.yml + publish.yml";
|
|
44646
45270
|
} else if (deployModel === "registry-publish") {
|
|
@@ -44649,17 +45273,9 @@ ${decision.note}`);
|
|
|
44649
45273
|
runs.push(run);
|
|
44650
45274
|
deployNote = "watched this repo's own release-triggered publish.yml (#2428 \u2014 no central dispatch)";
|
|
44651
45275
|
} 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
|
|
45276
|
+
const [deploy0, publish0] = await joinDeployAndPublish(
|
|
45277
|
+
dispatchDeploy(deps, ctx, "main", "main", deployModel, true, (deps.now ?? Date.now)(), mergedSha, "report", meta.publishDir),
|
|
45278
|
+
observePublishIfRequired(deps, ctx, meta, deployModel, "main", mergedSha, true)
|
|
44663
45279
|
);
|
|
44664
45280
|
const deploy = await recoverFailedDispatchPhase(
|
|
44665
45281
|
deps,
|
|
@@ -44669,7 +45285,6 @@ ${decision.note}`);
|
|
|
44669
45285
|
deploy0,
|
|
44670
45286
|
{ repo: releaseRunRepoFor(deployModel, ctx.repo), sha: mergedSha, watched: true, workflow: "tenant-deploy.yml" }
|
|
44671
45287
|
);
|
|
44672
|
-
const publish0 = await observePublishIfRequired(deps, ctx, meta, deployModel, "main", mergedSha, true);
|
|
44673
45288
|
const publish = publish0 ? await recoverFailedDispatchPhase(
|
|
44674
45289
|
deps,
|
|
44675
45290
|
ledger,
|
|
@@ -44697,6 +45312,7 @@ ${decision.note}`);
|
|
|
44697
45312
|
);
|
|
44698
45313
|
await recordHotfixPhase(ledger, anchors, "deploy", ledgerInputs.deploy, "the immutable tag, the green wall, origin/main, and the GitHub Release");
|
|
44699
45314
|
await recordHotfixPhase(ledger, anchors, "publish", ledgerInputs.publish, "the immutable tag, the green wall, origin/main, the GitHub Release, and the deploy path");
|
|
45315
|
+
deployNote = await appendHotfixServedChannel(deps, { repo: ctx.repo, tag, sha: mergedSha, deploy: ledgerInputs.deploy, publish: ledgerInputs.publish, installer: meta.installer }, runs, deployNote);
|
|
44700
45316
|
const announceNote = releaseExists ? void 0 : await announceShippedRelease(
|
|
44701
45317
|
deps,
|
|
44702
45318
|
{ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
|
|
@@ -44765,6 +45381,9 @@ ${decision.note}`);
|
|
|
44765
45381
|
return { ...result, ...localSync ? { localSync } : {} };
|
|
44766
45382
|
}
|
|
44767
45383
|
async function runHotfixRetryDeploy(deps, versionInput) {
|
|
45384
|
+
return withTrainWriter(deps, `hotfix release ${versionInput} --retry-deploy`, () => runHotfixRetryDeployLocked(deps, versionInput));
|
|
45385
|
+
}
|
|
45386
|
+
async function runHotfixRetryDeployLocked(deps, versionInput) {
|
|
44768
45387
|
const ctx = await buildTrainApplyContext(deps);
|
|
44769
45388
|
const { tag } = normalizeHotfixVersion(versionInput);
|
|
44770
45389
|
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
@@ -45380,6 +45999,8 @@ async function announceSummaryRefusal(path2) {
|
|
|
45380
45999
|
}
|
|
45381
46000
|
function trainApplyDeps() {
|
|
45382
46001
|
return {
|
|
46002
|
+
// #7027: every mutating train verb holds this checkout's train writer lock for its whole run.
|
|
46003
|
+
trainWriter: (verb, cwd) => acquireTrainWriterLock(cwd, `mmi-cli devops ${verb}`),
|
|
45383
46004
|
run: async (file, args) => {
|
|
45384
46005
|
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;
|
|
45385
46006
|
try {
|
|
@@ -45491,7 +46112,7 @@ function trainApplyDeps() {
|
|
|
45491
46112
|
}
|
|
45492
46113
|
function formatWorkflowRun(r) {
|
|
45493
46114
|
const evidenced = workflowRunWithEvidence(r);
|
|
45494
|
-
return `${evidenced.workflow} ${evidenced.runUrl ?? evidenced.runUrlNote} ${evidenced.conclusion.toUpperCase()}`;
|
|
46115
|
+
return `${evidenced.workflow} ${evidenced.runUrl ?? evidenced.runUrlNote} ${evidenced.conclusion.toUpperCase()}${r.parallel ? " (parallel)" : ""}`;
|
|
45495
46116
|
}
|
|
45496
46117
|
function renderDeployLine(d) {
|
|
45497
46118
|
const parts = [d.dispatch];
|
|
@@ -46136,7 +46757,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
46136
46757
|
const steps = trainPlan("hotfix");
|
|
46137
46758
|
console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli devops hotfix: dry-run plan", steps));
|
|
46138
46759
|
});
|
|
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));
|
|
46760
|
+
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
46761
|
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
46762
|
if (o.retryDeploy) {
|
|
46142
46763
|
if (o.announceSummaryFile || o.carries) {
|
|
@@ -47595,10 +48216,9 @@ function resolveHouseShim(argv) {
|
|
|
47595
48216
|
const flatPath = flatTokens.join(" ");
|
|
47596
48217
|
const flatHouse = houseForPath(flatPath);
|
|
47597
48218
|
if (!flatHouse || flatHouse === "core") return;
|
|
47598
|
-
|
|
47599
|
-
|
|
47600
|
-
|
|
47601
|
-
);
|
|
48219
|
+
process.stderr.write(`mmi-cli: '${flatPath}' is the old flat form \u2014 ran it as \`mmi-cli ${canonicalPathFor(registeredPathPrefix(flatPath))}\` (use that spelling)
|
|
48220
|
+
`);
|
|
48221
|
+
return;
|
|
47602
48222
|
}
|
|
47603
48223
|
const remainder = argv.slice(3);
|
|
47604
48224
|
const first = remainder[0];
|
|
@@ -47620,6 +48240,14 @@ function resolveHouseShim(argv) {
|
|
|
47620
48240
|
if (actual && isDeclaredHouseAlias(houseToken, lookupPath)) {
|
|
47621
48241
|
process.stderr.write(
|
|
47622
48242
|
`mmi-cli: '${lookupPath}' lives in house '${actual}', not '${houseToken}' \u2014 routed to \`mmi-cli ${canonicalPathFor(lookupPath)}\`
|
|
48243
|
+
`
|
|
48244
|
+
);
|
|
48245
|
+
argv.splice(2, 1);
|
|
48246
|
+
return;
|
|
48247
|
+
}
|
|
48248
|
+
if (actual && actual !== "core" && registeredPathPrefix(lookupPath) === lookupPath) {
|
|
48249
|
+
process.stderr.write(
|
|
48250
|
+
`mmi-cli: '${lookupPath}' lives in house '${actual}', not '${houseToken}' \u2014 ran it as \`mmi-cli ${canonicalPathFor(lookupPath)}\`
|
|
47623
48251
|
`
|
|
47624
48252
|
);
|
|
47625
48253
|
argv.splice(2, 1);
|
|
@@ -47648,8 +48276,8 @@ function printHouseRootHelp(house) {
|
|
|
47648
48276
|
}
|
|
47649
48277
|
lines2.push(
|
|
47650
48278
|
"",
|
|
47651
|
-
`Canonical: \`mmi-cli ${house} <command> \u2026\`. The flat \`mmi-cli <command> \u2026\`
|
|
47652
|
-
|
|
48279
|
+
`Canonical: \`mmi-cli ${house} <command> \u2026\`. The old flat \`mmi-cli <command> \u2026\` form still runs`,
|
|
48280
|
+
"with a notice naming the canonical spelling (#7034), so an out-of-date plugin keeps working."
|
|
47653
48281
|
);
|
|
47654
48282
|
consoleIo.log(lines2.join("\n"));
|
|
47655
48283
|
}
|