@mutmutco/cli 4.5.0 → 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 +451 -94
- 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?",
|
|
@@ -15017,6 +15017,26 @@ async function installAppFoldDeps(deps) {
|
|
|
15017
15017
|
}
|
|
15018
15018
|
await deps.run("npm", lockListing.trim() ? ["ci"] : ["install"]);
|
|
15019
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
|
+
}
|
|
15020
15040
|
async function restoreMainNpmPackIdentities(deps, version) {
|
|
15021
15041
|
const bomPath = "distribution-bom.json";
|
|
15022
15042
|
let mainListing;
|
|
@@ -15171,6 +15191,8 @@ async function foldReleaseVersion(deps, model, tag, foldPaths, sourceCommit = "H
|
|
|
15171
15191
|
}
|
|
15172
15192
|
} else {
|
|
15173
15193
|
await installAppFoldDeps(deps);
|
|
15194
|
+
const refresh = await refreshSharedInstallerDeps(deps);
|
|
15195
|
+
if (refresh) foldNoteExtra = `; ${refresh}`;
|
|
15174
15196
|
await deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]);
|
|
15175
15197
|
const patch = Number(version.split(".")[2] ?? "0");
|
|
15176
15198
|
if (model === "registry-publish" && Number.isFinite(patch) && patch > 0) {
|
|
@@ -15976,10 +15998,10 @@ var rollout_plan_default = {
|
|
|
15976
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)."
|
|
15977
15999
|
},
|
|
15978
16000
|
baseline: {
|
|
15979
|
-
version: "4.5.
|
|
15980
|
-
tag: "v4.5.
|
|
15981
|
-
commit: "
|
|
15982
|
-
npm: "@mutmutco/cli@4.5.
|
|
16001
|
+
version: "4.5.1",
|
|
16002
|
+
tag: "v4.5.1",
|
|
16003
|
+
commit: "489af0245908",
|
|
16004
|
+
npm: "@mutmutco/cli@4.5.1"
|
|
15983
16005
|
},
|
|
15984
16006
|
exitCriterion: "fleet-n-of-n",
|
|
15985
16007
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15996,14 +16018,14 @@ var rollout_plan_default = {
|
|
|
15996
16018
|
repo: "mutmutco/mmi-hub",
|
|
15997
16019
|
role: "canary",
|
|
15998
16020
|
schedule: "train",
|
|
15999
|
-
v3Target: "v4.5.
|
|
16021
|
+
v3Target: "v4.5.1"
|
|
16000
16022
|
}
|
|
16001
16023
|
],
|
|
16002
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.",
|
|
16003
16025
|
rollback: {
|
|
16004
16026
|
independent: true,
|
|
16005
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
16006
|
-
v3Target: "v4.5.
|
|
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)"
|
|
16007
16029
|
}
|
|
16008
16030
|
},
|
|
16009
16031
|
{
|
|
@@ -18779,6 +18801,60 @@ async function resolveRcResumeTag(deps, base, sha, probed) {
|
|
|
18779
18801
|
return { tag: newest, note };
|
|
18780
18802
|
}
|
|
18781
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
|
+
|
|
18782
18858
|
// src/tenant-control-parse.ts
|
|
18783
18859
|
var OUTPUT_BEGIN = "mmi-control-output-begin";
|
|
18784
18860
|
var OUTPUT_END = "mmi-control-output-end";
|
|
@@ -18961,7 +19037,7 @@ function mmiGetRedeployCommand(repo) {
|
|
|
18961
19037
|
}
|
|
18962
19038
|
function readLocalLauncherVersion(root = process.cwd()) {
|
|
18963
19039
|
try {
|
|
18964
|
-
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");
|
|
18965
19041
|
const version = JSON.parse(raw).version;
|
|
18966
19042
|
return typeof version === "string" && version.trim() ? version.trim() : null;
|
|
18967
19043
|
} catch {
|
|
@@ -18990,17 +19066,54 @@ async function fetchServedChannelVersion(url, fetchImpl = fetch) {
|
|
|
18990
19066
|
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
18991
19067
|
}
|
|
18992
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
|
+
}
|
|
18993
19097
|
async function appendMmiGetRedeploy(deps, input, dispatch) {
|
|
18994
19098
|
if (!isHubControlRepo(input.repo)) return dispatch;
|
|
18995
19099
|
const remedy = mmiGetRedeployCommand(input.repo);
|
|
18996
|
-
const withNote = (note, deployStatus) => ({
|
|
19100
|
+
const withNote = (note, deployStatus, row) => ({
|
|
18997
19101
|
...dispatch,
|
|
18998
19102
|
note: `${dispatch.note}; ${note}`,
|
|
18999
|
-
...deployStatus ? { deployStatus } : {}
|
|
19103
|
+
...deployStatus ? { deployStatus } : {},
|
|
19104
|
+
...row ? { workflowRuns: [...dispatch.workflowRuns ?? [], row] } : {}
|
|
19000
19105
|
});
|
|
19001
19106
|
if (input.publishStatus !== "success") {
|
|
19002
19107
|
return withNote(`mmi-get box NOT redeployed (publish did not succeed) \u2014 after the publish is green, run \`${remedy}\``);
|
|
19003
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
|
+
}
|
|
19004
19117
|
const since = (deps.now ?? Date.now)();
|
|
19005
19118
|
try {
|
|
19006
19119
|
await deps.run("gh", [
|
|
@@ -19022,12 +19135,12 @@ async function appendMmiGetRedeploy(deps, input, dispatch) {
|
|
|
19022
19135
|
]);
|
|
19023
19136
|
} catch (e) {
|
|
19024
19137
|
const msg = e instanceof Error ? e.message : String(e);
|
|
19025
|
-
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}`));
|
|
19026
19139
|
}
|
|
19027
19140
|
const correlated = await correlateTenantRun(deps, since, [HUB_GET_SLUG, HUB_GET_STAGE]);
|
|
19028
19141
|
if (correlated.read === "failed" || correlated.read === "absent") {
|
|
19029
19142
|
const why = correlated.read === "failed" ? `its run could not be READ: ${correlated.error}` : "no matching run appeared";
|
|
19030
|
-
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}`));
|
|
19031
19144
|
}
|
|
19032
19145
|
const { runId, runUrl } = correlated;
|
|
19033
19146
|
const runRow = { workflow: "tenant-deploy.yml", ...runId ? { runId } : {}, ...runUrl ? { runUrl } : {} };
|
|
@@ -19057,35 +19170,44 @@ async function appendServedChannelPublish(deps, input, dispatch) {
|
|
|
19057
19170
|
if (!isHubControlRepo(input.repo)) return dispatch;
|
|
19058
19171
|
const channelUrl = input.channelUrl ?? readServedChannelUrl();
|
|
19059
19172
|
const remedy = getPublishDispatchCommand(input.repo);
|
|
19060
|
-
const withNote = (note, deployStatus) => ({
|
|
19173
|
+
const withNote = (note, deployStatus, row) => ({
|
|
19061
19174
|
...dispatch,
|
|
19062
19175
|
note: `${dispatch.note}; ${note}`,
|
|
19063
|
-
...deployStatus ? { deployStatus } : {}
|
|
19176
|
+
...deployStatus ? { deployStatus } : {},
|
|
19177
|
+
...row ? { workflowRuns: [...dispatch.workflowRuns ?? [], row] } : {}
|
|
19064
19178
|
});
|
|
19065
19179
|
if (input.publishStatus !== "success") {
|
|
19066
19180
|
return withNote(`served channel NOT moved (publish did not succeed) \u2014 after the publish is green, run \`${remedy}\``);
|
|
19067
19181
|
}
|
|
19068
|
-
const
|
|
19069
|
-
|
|
19070
|
-
|
|
19071
|
-
|
|
19072
|
-
|
|
19073
|
-
|
|
19074
|
-
}
|
|
19075
|
-
|
|
19076
|
-
|
|
19077
|
-
|
|
19078
|
-
|
|
19079
|
-
|
|
19080
|
-
|
|
19081
|
-
|
|
19082
|
-
|
|
19083
|
-
|
|
19084
|
-
|
|
19085
|
-
|
|
19086
|
-
};
|
|
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);
|
|
19087
19210
|
}
|
|
19088
|
-
const runStatus = await watchTenantRun(deps, runId, input.repo);
|
|
19089
19211
|
if (runStatus !== "success") {
|
|
19090
19212
|
return {
|
|
19091
19213
|
...dispatch,
|
|
@@ -19170,13 +19292,22 @@ var SERVED_CHANNELS = {
|
|
|
19170
19292
|
remedy: JERV_GATEWAY_REMEDY
|
|
19171
19293
|
}
|
|
19172
19294
|
};
|
|
19173
|
-
function servedChannelFor(repo) {
|
|
19174
|
-
|
|
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\`)`
|
|
19305
|
+
};
|
|
19175
19306
|
}
|
|
19176
19307
|
var SERVED_CHANNEL_PROBE_ATTEMPTS = 3;
|
|
19177
19308
|
var SERVED_CHANNEL_PROBE_BACKOFF_MS = 1e4;
|
|
19178
19309
|
async function appendServedChannelProof(deps, input, dispatch) {
|
|
19179
|
-
const channel = servedChannelFor(input.repo);
|
|
19310
|
+
const channel = servedChannelFor(input.repo, input.installer);
|
|
19180
19311
|
if (!channel) return dispatch;
|
|
19181
19312
|
const expected = input.tag.replace(/^v/, "");
|
|
19182
19313
|
const withProof = (conclusion, evidence, note, failed = false) => ({
|
|
@@ -19216,6 +19347,15 @@ async function appendServedChannelProof(deps, input, dispatch) {
|
|
|
19216
19347
|
}
|
|
19217
19348
|
return withProof("success", `${channel.url} reports ${probe.version}`, `served channel moved \u2014 ${channel.url} reports ${probe.version}`);
|
|
19218
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
|
+
}
|
|
19219
19359
|
var JERV_GATEWAY_RUN_URL_NOTE = "no run URL emitted \u2014 host health leg runs outside GitHub Actions";
|
|
19220
19360
|
var JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT = "scripts/jerv-gateway-release-deploy.sh";
|
|
19221
19361
|
async function runTaggedJervGatewayLocalWrapper(deps, tag, tagSha) {
|
|
@@ -20852,6 +20992,20 @@ var import_node_crypto5 = require("node:crypto");
|
|
|
20852
20992
|
// src/file-lock.ts
|
|
20853
20993
|
var import_promises2 = require("node:fs/promises");
|
|
20854
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
|
+
}
|
|
20855
21009
|
var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
20856
21010
|
var IMMEDIATE_RETRY_BUDGET = 3;
|
|
20857
21011
|
var FileLockBusyError = class extends Error {
|
|
@@ -20867,9 +21021,24 @@ function resolveFileLockOpts(opts = {}) {
|
|
|
20867
21021
|
staleMs: opts.staleMs ?? 3e4,
|
|
20868
21022
|
retryMs: opts.retryMs ?? 50,
|
|
20869
21023
|
maxWaitMs: opts.maxWaitMs ?? 5e3,
|
|
20870
|
-
label: opts.label ?? "file lock"
|
|
21024
|
+
label: opts.label ?? "file lock",
|
|
21025
|
+
deadHolderSteal: opts.deadHolderSteal ?? false,
|
|
21026
|
+
holder: opts.holder ?? ""
|
|
20871
21027
|
};
|
|
20872
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
|
+
}
|
|
20873
21042
|
async function acquireFileLock(lockPath, opts, deadline) {
|
|
20874
21043
|
let immediateRetries = 0;
|
|
20875
21044
|
for (; ; ) {
|
|
@@ -20880,12 +21049,12 @@ async function acquireFileLock(lockPath, opts, deadline) {
|
|
|
20880
21049
|
const code = e.code;
|
|
20881
21050
|
if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
|
|
20882
21051
|
const retryNow = async () => {
|
|
21052
|
+
if (++immediateRetries <= IMMEDIATE_RETRY_BUDGET) return;
|
|
20883
21053
|
if (Date.now() >= deadline) throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
|
|
20884
|
-
|
|
21054
|
+
await sleep(opts.retryMs);
|
|
20885
21055
|
};
|
|
20886
21056
|
try {
|
|
20887
|
-
|
|
20888
|
-
if (age > opts.staleMs) {
|
|
21057
|
+
if (await lockIsStealable(lockPath, opts)) {
|
|
20889
21058
|
try {
|
|
20890
21059
|
await (0, import_promises2.unlink)(lockPath);
|
|
20891
21060
|
await retryNow();
|
|
@@ -20911,7 +21080,8 @@ async function acquireFileLock(lockPath, opts, deadline) {
|
|
|
20911
21080
|
await sleep(opts.retryMs);
|
|
20912
21081
|
continue;
|
|
20913
21082
|
}
|
|
20914
|
-
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;
|
|
20915
21085
|
try {
|
|
20916
21086
|
await handle.writeFile(token);
|
|
20917
21087
|
} catch (e) {
|
|
@@ -20935,11 +21105,14 @@ async function releaseFileLock(lockPath, guard) {
|
|
|
20935
21105
|
}
|
|
20936
21106
|
}
|
|
20937
21107
|
async function withFileLock(lockPath, opts, fn) {
|
|
21108
|
+
return withFencedFileLock(lockPath, opts, () => fn());
|
|
21109
|
+
}
|
|
21110
|
+
async function withFencedFileLock(lockPath, opts, fn) {
|
|
20938
21111
|
const resolved = resolveFileLockOpts(opts);
|
|
20939
21112
|
await (0, import_promises2.mkdir)((0, import_node_path21.dirname)(lockPath), { recursive: true }).catch(() => void 0);
|
|
20940
21113
|
const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
|
|
20941
21114
|
try {
|
|
20942
|
-
return await fn();
|
|
21115
|
+
return await fn(() => fileLockHeldBy(lockPath, guard.token));
|
|
20943
21116
|
} finally {
|
|
20944
21117
|
await releaseFileLock(lockPath, guard);
|
|
20945
21118
|
}
|
|
@@ -21177,8 +21350,114 @@ function atomicWriteJson(path2, ledger) {
|
|
|
21177
21350
|
`, "utf8");
|
|
21178
21351
|
(0, import_node_fs22.renameSync)(tmp, path2);
|
|
21179
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
|
+
}
|
|
21180
21459
|
async function withReleaseLedger(path2, expected, fn, opts = {}) {
|
|
21181
|
-
return
|
|
21460
|
+
return withLedgerLock(path2, async (fence) => {
|
|
21182
21461
|
let current;
|
|
21183
21462
|
let raw;
|
|
21184
21463
|
try {
|
|
@@ -21193,9 +21472,9 @@ async function withReleaseLedger(path2, expected, fn, opts = {}) {
|
|
|
21193
21472
|
if (expected) validateLedgerAnchors(current, expected);
|
|
21194
21473
|
}
|
|
21195
21474
|
const { ledger, value } = await fn(current);
|
|
21196
|
-
if (ledger) atomicWriteJson(path2, ledger);
|
|
21475
|
+
if (ledger) await fence(() => atomicWriteJson(path2, ledger));
|
|
21197
21476
|
return value;
|
|
21198
|
-
});
|
|
21477
|
+
}, { maxWaitMs: opts.maxWaitMs ?? 5e3 });
|
|
21199
21478
|
}
|
|
21200
21479
|
function openReleaseLedgerWriter(cwd, opts = {}) {
|
|
21201
21480
|
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -21810,7 +22089,7 @@ async function reverifyPersistedRunPhases(deps, path2, anchors, input) {
|
|
|
21810
22089
|
return result;
|
|
21811
22090
|
}
|
|
21812
22091
|
async function clearLedgerFile(path2, anchors, maxWaitMs) {
|
|
21813
|
-
return
|
|
22092
|
+
return withLedgerLock(path2, async (fence) => {
|
|
21814
22093
|
let raw;
|
|
21815
22094
|
try {
|
|
21816
22095
|
raw = (0, import_node_fs22.readFileSync)(path2, "utf8");
|
|
@@ -21829,9 +22108,9 @@ async function clearLedgerFile(path2, anchors, maxWaitMs) {
|
|
|
21829
22108
|
} catch (e) {
|
|
21830
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)})` };
|
|
21831
22110
|
}
|
|
21832
|
-
(0, import_node_fs22.unlinkSync)(path2);
|
|
22111
|
+
await fence(() => (0, import_node_fs22.unlinkSync)(path2));
|
|
21833
22112
|
return { cleared: true, note: `cleared the release ledger at ${path2} for the live-proven aborted ${anchors.tag}` };
|
|
21834
|
-
});
|
|
22113
|
+
}, { maxWaitMs });
|
|
21835
22114
|
}
|
|
21836
22115
|
async function clearReleaseLedger(cwd, anchors, opts = {}) {
|
|
21837
22116
|
let path2;
|
|
@@ -21971,7 +22250,7 @@ async function alignmentLegClosedLive(deps, prior) {
|
|
|
21971
22250
|
}
|
|
21972
22251
|
async function healStaleAlignmentLeg(deps, cwd, expected) {
|
|
21973
22252
|
const path2 = releaseLedgerPath(cwd);
|
|
21974
|
-
return
|
|
22253
|
+
return withLedgerLock(path2, async (fence) => {
|
|
21975
22254
|
let prior;
|
|
21976
22255
|
try {
|
|
21977
22256
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -21987,7 +22266,7 @@ async function healStaleAlignmentLeg(deps, cwd, expected) {
|
|
|
21987
22266
|
}
|
|
21988
22267
|
const closed = await alignmentLegClosedLive(deps, prior);
|
|
21989
22268
|
if (!closed) return void 0;
|
|
21990
|
-
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() }));
|
|
21991
22270
|
return prior.tag;
|
|
21992
22271
|
});
|
|
21993
22272
|
}
|
|
@@ -22060,7 +22339,7 @@ function supersedeableDeployLegShape(rec) {
|
|
|
22060
22339
|
}
|
|
22061
22340
|
async function supersedeContentCausedDeployLeg(deps, cwd) {
|
|
22062
22341
|
const path2 = releaseLedgerPath(cwd);
|
|
22063
|
-
return
|
|
22342
|
+
return withLedgerLock(path2, async (fence) => {
|
|
22064
22343
|
let prior;
|
|
22065
22344
|
try {
|
|
22066
22345
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -22101,14 +22380,14 @@ async function supersedeContentCausedDeployLeg(deps, cwd) {
|
|
|
22101
22380
|
);
|
|
22102
22381
|
}
|
|
22103
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)`;
|
|
22104
|
-
atomicWriteJson(path2, {
|
|
22383
|
+
await fence(() => atomicWriteJson(path2, {
|
|
22105
22384
|
...prior,
|
|
22106
22385
|
phases: {
|
|
22107
22386
|
...phases,
|
|
22108
22387
|
deploy: { state: "skipped", sha: prior.tagSha, superseded: true, note, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
22109
22388
|
},
|
|
22110
22389
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22111
|
-
});
|
|
22390
|
+
}));
|
|
22112
22391
|
return {
|
|
22113
22392
|
command: "release-supersede-deploy",
|
|
22114
22393
|
repo: prior.repo,
|
|
@@ -22124,7 +22403,7 @@ function supersedeablePublishLegShape(rec) {
|
|
|
22124
22403
|
function deferredGatewayDeployLegShape(rec) {
|
|
22125
22404
|
return rec.state === "pending" && rec.runId == null && rec.workflow === "jerv-gateway";
|
|
22126
22405
|
}
|
|
22127
|
-
async function proveOriginPromotionOrArchived(deps, prior, path2, label, ref) {
|
|
22406
|
+
async function proveOriginPromotionOrArchived(deps, prior, path2, label, ref, fence) {
|
|
22128
22407
|
let liveSha = "";
|
|
22129
22408
|
try {
|
|
22130
22409
|
liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
|
|
@@ -22163,12 +22442,12 @@ async function proveOriginPromotionOrArchived(deps, prior, path2, label, ref) {
|
|
|
22163
22442
|
if ((0, import_node_fs22.existsSync)(archivePath)) {
|
|
22164
22443
|
throw new Error(`${label}: archive already exists at ${archivePath} \u2014 preserving both ledger records; resolve the collision by hand; nothing was written (${ref})`);
|
|
22165
22444
|
}
|
|
22166
|
-
(0, import_node_fs22.renameSync)(path2, archivePath);
|
|
22445
|
+
await fence(() => (0, import_node_fs22.renameSync)(path2, archivePath));
|
|
22167
22446
|
return "deleted";
|
|
22168
22447
|
}
|
|
22169
22448
|
async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
22170
22449
|
const path2 = releaseLedgerPath(cwd);
|
|
22171
|
-
return
|
|
22450
|
+
return withLedgerLock(path2, async (fence) => {
|
|
22172
22451
|
let prior;
|
|
22173
22452
|
try {
|
|
22174
22453
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -22199,7 +22478,7 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
22199
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)`
|
|
22200
22479
|
);
|
|
22201
22480
|
}
|
|
22202
|
-
if (await proveOriginPromotionOrArchived(deps, prior, path2, "release --supersede-publish", "#6638") === "deleted") {
|
|
22481
|
+
if (await proveOriginPromotionOrArchived(deps, prior, path2, "release --supersede-publish", "#6638", fence) === "deleted") {
|
|
22203
22482
|
return {
|
|
22204
22483
|
command: "release-supersede-publish",
|
|
22205
22484
|
repo: prior.repo,
|
|
@@ -22228,14 +22507,14 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
22228
22507
|
gatewayDeploy = { workflow: "jerv-gateway", state: "complete", note: dispatch.note };
|
|
22229
22508
|
}
|
|
22230
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)`;
|
|
22231
|
-
atomicWriteJson(path2, {
|
|
22510
|
+
await fence(() => atomicWriteJson(path2, {
|
|
22232
22511
|
...prior,
|
|
22233
22512
|
phases: {
|
|
22234
22513
|
...phases,
|
|
22235
22514
|
publish: { state: "skipped", sha: prior.tagSha, superseded: true, note, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
22236
22515
|
},
|
|
22237
22516
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22238
|
-
});
|
|
22517
|
+
}));
|
|
22239
22518
|
return {
|
|
22240
22519
|
command: "release-supersede-publish",
|
|
22241
22520
|
repo: prior.repo,
|
|
@@ -22250,7 +22529,7 @@ async function supersedeContentCausedDeployAndPublishLegs(deps, cwd) {
|
|
|
22250
22529
|
const label = "release --supersede-deploy --supersede-publish";
|
|
22251
22530
|
const ref = "#6752";
|
|
22252
22531
|
const path2 = releaseLedgerPath(cwd);
|
|
22253
|
-
return
|
|
22532
|
+
return withLedgerLock(path2, async (fence) => {
|
|
22254
22533
|
let prior;
|
|
22255
22534
|
try {
|
|
22256
22535
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -22298,7 +22577,7 @@ async function supersedeContentCausedDeployAndPublishLegs(deps, cwd) {
|
|
|
22298
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})`
|
|
22299
22578
|
);
|
|
22300
22579
|
}
|
|
22301
|
-
if (await proveOriginPromotionOrArchived(deps, prior, path2, label, ref) === "deleted") {
|
|
22580
|
+
if (await proveOriginPromotionOrArchived(deps, prior, path2, label, ref, fence) === "deleted") {
|
|
22302
22581
|
return {
|
|
22303
22582
|
command: "release-supersede-deploy-and-publish",
|
|
22304
22583
|
repo: prior.repo,
|
|
@@ -22344,7 +22623,7 @@ async function supersedeContentCausedDeployAndPublishLegs(deps, cwd) {
|
|
|
22344
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})`,
|
|
22345
22624
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22346
22625
|
};
|
|
22347
|
-
atomicWriteJson(path2, { ...prior, phases, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
22626
|
+
await fence(() => atomicWriteJson(path2, { ...prior, phases, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
22348
22627
|
return {
|
|
22349
22628
|
command: "release-supersede-deploy-and-publish",
|
|
22350
22629
|
repo: prior.repo,
|
|
@@ -22375,7 +22654,7 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
|
|
|
22375
22654
|
return { ok: true };
|
|
22376
22655
|
}
|
|
22377
22656
|
await reverifyPriorRunLegs(deps, path2, targetTag);
|
|
22378
|
-
return
|
|
22657
|
+
return withLedgerLock(path2, async (fence) => {
|
|
22379
22658
|
let raw;
|
|
22380
22659
|
try {
|
|
22381
22660
|
raw = (0, import_node_fs22.readFileSync)(path2, "utf8");
|
|
@@ -22395,7 +22674,7 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
|
|
|
22395
22674
|
const closed = await alignmentLegClosedLive(deps, prior);
|
|
22396
22675
|
if (closed) {
|
|
22397
22676
|
prior = { ...prior, phases: { ...prior.phases, alignment: closed }, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
22398
|
-
atomicWriteJson(path2, prior);
|
|
22677
|
+
await fence(() => atomicWriteJson(path2, prior));
|
|
22399
22678
|
derived = deriveReleasePhaseStatus(prior);
|
|
22400
22679
|
}
|
|
22401
22680
|
}
|
|
@@ -22418,13 +22697,13 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
|
|
|
22418
22697
|
}
|
|
22419
22698
|
const archivePath = (0, import_node_path22.join)((0, import_node_path22.dirname)(path2), `phases.archive-${prior.tag}.json`);
|
|
22420
22699
|
try {
|
|
22421
|
-
(0, import_node_fs22.unlinkSync)(archivePath);
|
|
22700
|
+
await fence(() => (0, import_node_fs22.unlinkSync)(archivePath));
|
|
22422
22701
|
} catch (e) {
|
|
22423
22702
|
if (e.code !== "ENOENT") {
|
|
22424
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)` };
|
|
22425
22704
|
}
|
|
22426
22705
|
}
|
|
22427
|
-
(0, import_node_fs22.renameSync)(path2, archivePath);
|
|
22706
|
+
await fence(() => (0, import_node_fs22.renameSync)(path2, archivePath));
|
|
22428
22707
|
return { ok: true, note: `archived the completed release ledger ${prior.tag} to ${archivePath} (live-proven at ${prior.tagSha.slice(0, 12)})` };
|
|
22429
22708
|
});
|
|
22430
22709
|
}
|
|
@@ -22449,7 +22728,7 @@ async function defaultRetryDeploy(deps, prior) {
|
|
|
22449
22728
|
}
|
|
22450
22729
|
async function retryHotfixDeployLegs(deps, cwd, opts = {}) {
|
|
22451
22730
|
const path2 = releaseLedgerPath(cwd);
|
|
22452
|
-
return
|
|
22731
|
+
return withLedgerLock(path2, async (fence) => {
|
|
22453
22732
|
let prior;
|
|
22454
22733
|
try {
|
|
22455
22734
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -22530,7 +22809,7 @@ async function retryHotfixDeployLegs(deps, cwd, opts = {}) {
|
|
|
22530
22809
|
);
|
|
22531
22810
|
}
|
|
22532
22811
|
}
|
|
22533
|
-
atomicWriteJson(path2, { ...prior, phases, updatedAt: now().toISOString() });
|
|
22812
|
+
await fence(() => atomicWriteJson(path2, { ...prior, phases, updatedAt: now().toISOString() }));
|
|
22534
22813
|
const stillFailed = retried.filter((phase) => phases[phase].state === "failed");
|
|
22535
22814
|
const status = stillFailed.length === 0 ? "success" : "failed";
|
|
22536
22815
|
return {
|
|
@@ -22683,6 +22962,7 @@ var defaultDeps2 = {
|
|
|
22683
22962
|
},
|
|
22684
22963
|
healAlignmentLeg: healStaleAlignmentLeg,
|
|
22685
22964
|
healRunLegs: healUnfoldedRunLegs,
|
|
22965
|
+
trainWriterHolder: liveTrainWriterHolder,
|
|
22686
22966
|
readSurfacesRaw: (cwd) => {
|
|
22687
22967
|
try {
|
|
22688
22968
|
return (0, import_node_fs23.readFileSync)((0, import_node_path23.join)(cwd, "surfaces.json"), "utf8");
|
|
@@ -23025,12 +23305,17 @@ async function runTrainDoctor(input) {
|
|
|
23025
23305
|
break;
|
|
23026
23306
|
}
|
|
23027
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;
|
|
23028
23313
|
if (ledgerRaw !== void 0 && ledgerPath) {
|
|
23029
23314
|
try {
|
|
23030
23315
|
let ledger = parseReleaseLedger(ledgerRaw);
|
|
23031
23316
|
let derived = deriveReleasePhaseStatus(ledger);
|
|
23032
23317
|
const deployModel = typeof meta?.deployModel === "string" && isDeployModel(meta.deployModel) ? meta.deployModel : void 0;
|
|
23033
|
-
if (
|
|
23318
|
+
if (healLedger && deployModel && derived.legs.some((l) => (l.phase === "deploy" || l.phase === "publish") && l.state === "pending")) {
|
|
23034
23319
|
const run = await deps.healRunLegs(train, cwd, deployModel);
|
|
23035
23320
|
if (run) {
|
|
23036
23321
|
add({
|
|
@@ -23044,7 +23329,7 @@ async function runTrainDoctor(input) {
|
|
|
23044
23329
|
derived = deriveReleasePhaseStatus(ledger);
|
|
23045
23330
|
}
|
|
23046
23331
|
}
|
|
23047
|
-
if (
|
|
23332
|
+
if (healLedger && derived.phaseStatus === "phases-pending" && derived.legs.every((l) => l.state !== "pending" || l.phase === "alignment")) {
|
|
23048
23333
|
const closedTag = await deps.healAlignmentLeg(train, cwd);
|
|
23049
23334
|
if (closedTag) {
|
|
23050
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" });
|
|
@@ -24070,18 +24355,11 @@ ${recovery.note}`), recoveryInput);
|
|
|
24070
24355
|
releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
|
|
24071
24356
|
);
|
|
24072
24357
|
const aligning = watch && alignment ? awaitAlignmentsLanded(deps, ctx, alignment) : Promise.resolve(alignment);
|
|
24073
|
-
|
|
24074
|
-
dispatch = await appendMmiGetRedeploy(
|
|
24075
|
-
deps,
|
|
24076
|
-
{ repo: ctx.repo, publishStatus: publishLeg.state === "complete" ? "success" : "failure", watch },
|
|
24077
|
-
dispatch
|
|
24078
|
-
);
|
|
24079
|
-
dispatch = await appendServedChannelPublish(
|
|
24358
|
+
const dispatch = await appendServedChannelLegs(
|
|
24080
24359
|
deps,
|
|
24081
|
-
{ repo: ctx.repo,
|
|
24082
|
-
|
|
24360
|
+
{ repo: ctx.repo, tag, releaseSha, watch, deploy: deployLeg, publish: publishLeg, installer: meta.installer },
|
|
24361
|
+
appendPublishDispatch(deployDispatch, publishDispatch)
|
|
24083
24362
|
);
|
|
24084
|
-
dispatch = await appendServedChannelProof(deps, { repo: ctx.repo, tag, shipped: shipLegsGreen(deployLeg, publishLeg) }, dispatch);
|
|
24085
24363
|
alignment = await aligning;
|
|
24086
24364
|
const announceNote = await announcing;
|
|
24087
24365
|
return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors, ...alignment ? { alignment } : {} };
|
|
@@ -24150,6 +24428,9 @@ async function probeAncestor(deps, ancestor, descendant, label) {
|
|
|
24150
24428
|
}
|
|
24151
24429
|
}
|
|
24152
24430
|
async function runRcandResume(deps, options = {}) {
|
|
24431
|
+
return withTrainWriter(deps, "rcand --resume", () => runRcandResumeLocked(deps, options));
|
|
24432
|
+
}
|
|
24433
|
+
async function runRcandResumeLocked(deps, options) {
|
|
24153
24434
|
const watch = options.watch ?? false;
|
|
24154
24435
|
const ctx = await buildTrainApplyContext(deps);
|
|
24155
24436
|
await requireCleanTree(deps);
|
|
@@ -24410,6 +24691,12 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
24410
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)" });
|
|
24411
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" });
|
|
24412
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);
|
|
24413
24700
|
if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
|
|
24414
24701
|
const settledBySupersede = persisted?.phases.deploy.superseded === true || persisted?.phases.publish.superseded === true;
|
|
24415
24702
|
const announceNote2 = persisted && !shippedBefore && !settledBySupersede && (prodReleaseTargets(deployModel).length > 0 || companion !== null) ? await announceShippedRelease(
|
|
@@ -24543,7 +24830,11 @@ ${recovery.note}`), recoveryInput);
|
|
|
24543
24830
|
landed: "the immutable tag, the re-proven wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
24544
24831
|
});
|
|
24545
24832
|
}
|
|
24546
|
-
const dispatch =
|
|
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
|
+
);
|
|
24547
24838
|
const announcing = announceShippedRelease(
|
|
24548
24839
|
deps,
|
|
24549
24840
|
{ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
|
|
@@ -24600,6 +24891,9 @@ async function withCheckoutRestoredAfterReleaseResume(deps, startBranch, operati
|
|
|
24600
24891
|
return withCheckoutRestoredAfterRelease(deps, result, startBranch);
|
|
24601
24892
|
}
|
|
24602
24893
|
async function runReleaseResume(deps, options = {}) {
|
|
24894
|
+
return withTrainWriter(deps, "release --resume", () => runReleaseResumeLocked(deps, options));
|
|
24895
|
+
}
|
|
24896
|
+
async function runReleaseResumeLocked(deps, options) {
|
|
24603
24897
|
const ctx = await buildTrainApplyContext(deps);
|
|
24604
24898
|
await requireCleanTree(deps);
|
|
24605
24899
|
await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
|
|
@@ -24615,6 +24909,10 @@ async function runReleaseResume(deps, options = {}) {
|
|
|
24615
24909
|
);
|
|
24616
24910
|
}
|
|
24617
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) {
|
|
24618
24916
|
if (!options.approved) {
|
|
24619
24917
|
throw new Error("release --abort requires --apply after explicit approval; nothing was written");
|
|
24620
24918
|
}
|
|
@@ -24689,7 +24987,7 @@ async function runReleaseSupersedeDeploy(deps, options = {}) {
|
|
|
24689
24987
|
throw new Error("release --supersede-deploy requires --apply after explicit master approval; nothing was written");
|
|
24690
24988
|
}
|
|
24691
24989
|
try {
|
|
24692
|
-
return await supersedeContentCausedDeployLeg(deps, process.cwd());
|
|
24990
|
+
return await withTrainWriter(deps, "release --supersede-deploy", () => supersedeContentCausedDeployLeg(deps, process.cwd()));
|
|
24693
24991
|
} catch (e) {
|
|
24694
24992
|
const message2 = e instanceof Error ? e.message : String(e);
|
|
24695
24993
|
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
@@ -24700,7 +24998,7 @@ async function runReleaseSupersedePublish(deps, options = {}) {
|
|
|
24700
24998
|
throw new Error("release --supersede-publish requires --apply after explicit master approval; nothing was written");
|
|
24701
24999
|
}
|
|
24702
25000
|
try {
|
|
24703
|
-
return await supersedeContentCausedPublishLeg(deps, process.cwd());
|
|
25001
|
+
return await withTrainWriter(deps, "release --supersede-publish", () => supersedeContentCausedPublishLeg(deps, process.cwd()));
|
|
24704
25002
|
} catch (e) {
|
|
24705
25003
|
const message2 = e instanceof Error ? e.message : String(e);
|
|
24706
25004
|
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
@@ -24711,13 +25009,17 @@ async function runReleaseSupersedeDeployAndPublish(deps, options = {}) {
|
|
|
24711
25009
|
throw new Error("release --supersede-deploy --supersede-publish requires --apply after explicit master approval; nothing was written");
|
|
24712
25010
|
}
|
|
24713
25011
|
try {
|
|
24714
|
-
return await supersedeContentCausedDeployAndPublishLegs(deps, process.cwd());
|
|
25012
|
+
return await withTrainWriter(deps, "release --supersede-deploy --supersede-publish", () => supersedeContentCausedDeployAndPublishLegs(deps, process.cwd()));
|
|
24715
25013
|
} catch (e) {
|
|
24716
25014
|
const message2 = e instanceof Error ? e.message : String(e);
|
|
24717
25015
|
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
24718
25016
|
}
|
|
24719
25017
|
}
|
|
24720
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) {
|
|
24721
25023
|
if (!options.approved) {
|
|
24722
25024
|
throw new Error("release --retry-publish requires --apply after explicit approval; nothing was written");
|
|
24723
25025
|
}
|
|
@@ -25162,6 +25464,9 @@ async function resolveHotfixVersion(deps, env = process.env) {
|
|
|
25162
25464
|
return { tag: `v${version}`, version, baseTag };
|
|
25163
25465
|
}
|
|
25164
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 = {}) {
|
|
25165
25470
|
const watch = options.watch ?? false;
|
|
25166
25471
|
await runTrainDoctor({ lane: command === "rcand" ? "rcand" : "release", dev: command === "release" && options.dev === true, heal: true, train: deps, refuse: true });
|
|
25167
25472
|
const ctx = await buildTrainApplyContext(deps);
|
|
@@ -30588,7 +30893,7 @@ function registerDeployCommands(program3) {
|
|
|
30588
30893
|
init_cli_shared();
|
|
30589
30894
|
|
|
30590
30895
|
// src/project-set.ts
|
|
30591
|
-
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"];
|
|
30592
30897
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
30593
30898
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
30594
30899
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -30846,6 +31151,17 @@ function parseRuntimeVaultOnlyVar(raw) {
|
|
|
30846
31151
|
if (raw === "false") return false;
|
|
30847
31152
|
throw new Error("org project set: runtimeVaultOnly must be true or false");
|
|
30848
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
|
+
}
|
|
30849
31165
|
function parseSeedCanaryVar(raw) {
|
|
30850
31166
|
if (raw === "true") return true;
|
|
30851
31167
|
if (raw === "false") return false;
|
|
@@ -31016,7 +31332,8 @@ var SETTABLE_VAR_KEYS = [
|
|
|
31016
31332
|
"ciExemptReason",
|
|
31017
31333
|
"gate",
|
|
31018
31334
|
"secrets",
|
|
31019
|
-
"seedCanary"
|
|
31335
|
+
"seedCanary",
|
|
31336
|
+
"installer"
|
|
31020
31337
|
];
|
|
31021
31338
|
var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
31022
31339
|
var SETTABLE_VAR_HINTS = {
|
|
@@ -31029,6 +31346,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
31029
31346
|
consumesDesignSystem: '"fofu"',
|
|
31030
31347
|
runtimeVaultOnly: "true|false",
|
|
31031
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",
|
|
31032
31350
|
repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
|
|
31033
31351
|
oauth: "JSON {subdomains,domains,callbackPath,extraCallbackPaths}",
|
|
31034
31352
|
requiredGcpApis: "comma-string",
|
|
@@ -31164,6 +31482,8 @@ function buildProjectSetPatch(input) {
|
|
|
31164
31482
|
patch[key] = parseRuntimeVaultOnlyVar(raw);
|
|
31165
31483
|
} else if (key === "seedCanary") {
|
|
31166
31484
|
patch[key] = parseSeedCanaryVar(raw);
|
|
31485
|
+
} else if (key === "installer") {
|
|
31486
|
+
patch[key] = parseInstallerVar(raw);
|
|
31167
31487
|
} else if (key === "consumesDesignSystem") {
|
|
31168
31488
|
patch[key] = parseConsumesDesignSystemVar(raw);
|
|
31169
31489
|
} else if (key === "publishDir") {
|
|
@@ -44403,6 +44723,9 @@ async function hotfixReleaseExists(deps, ctx, tag) {
|
|
|
44403
44723
|
}
|
|
44404
44724
|
}
|
|
44405
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) {
|
|
44406
44729
|
await doctor({ lane: "hotfix", heal: true, train: deps, refuse: true });
|
|
44407
44730
|
const ctx = await buildTrainApplyContext(deps);
|
|
44408
44731
|
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
@@ -44700,6 +45023,24 @@ async function appendHotfixGatewayRun(deps, repo, tag, sha, runs, note) {
|
|
|
44700
45023
|
}
|
|
44701
45024
|
return gateway.note;
|
|
44702
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
|
+
}
|
|
44703
45044
|
function hotfixPhaseInputsFromRuns(deployModel, runs, note, publishDispatch, publishRequired, opts) {
|
|
44704
45045
|
const dispatch = hotfixDispatchFromRuns(runs, note);
|
|
44705
45046
|
return {
|
|
@@ -44822,6 +45163,9 @@ async function assertConsumerBomLineage(deps, mergedSha, tag) {
|
|
|
44822
45163
|
}
|
|
44823
45164
|
}
|
|
44824
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) {
|
|
44825
45169
|
await doctor({ lane: "hotfix", heal: true, train: deps, refuse: true });
|
|
44826
45170
|
const ctx = await buildTrainApplyContext(deps);
|
|
44827
45171
|
const { tag, version } = normalizeHotfixVersion(versionInput);
|
|
@@ -44968,6 +45312,7 @@ ${decision.note}`);
|
|
|
44968
45312
|
);
|
|
44969
45313
|
await recordHotfixPhase(ledger, anchors, "deploy", ledgerInputs.deploy, "the immutable tag, the green wall, origin/main, and the GitHub Release");
|
|
44970
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);
|
|
44971
45316
|
const announceNote = releaseExists ? void 0 : await announceShippedRelease(
|
|
44972
45317
|
deps,
|
|
44973
45318
|
{ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
|
|
@@ -45036,6 +45381,9 @@ ${decision.note}`);
|
|
|
45036
45381
|
return { ...result, ...localSync ? { localSync } : {} };
|
|
45037
45382
|
}
|
|
45038
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) {
|
|
45039
45387
|
const ctx = await buildTrainApplyContext(deps);
|
|
45040
45388
|
const { tag } = normalizeHotfixVersion(versionInput);
|
|
45041
45389
|
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
@@ -45651,6 +45999,8 @@ async function announceSummaryRefusal(path2) {
|
|
|
45651
45999
|
}
|
|
45652
46000
|
function trainApplyDeps() {
|
|
45653
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}`),
|
|
45654
46004
|
run: async (file, args) => {
|
|
45655
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;
|
|
45656
46006
|
try {
|
|
@@ -47866,10 +48216,9 @@ function resolveHouseShim(argv) {
|
|
|
47866
48216
|
const flatPath = flatTokens.join(" ");
|
|
47867
48217
|
const flatHouse = houseForPath(flatPath);
|
|
47868
48218
|
if (!flatHouse || flatHouse === "core") return;
|
|
47869
|
-
|
|
47870
|
-
|
|
47871
|
-
|
|
47872
|
-
);
|
|
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;
|
|
47873
48222
|
}
|
|
47874
48223
|
const remainder = argv.slice(3);
|
|
47875
48224
|
const first = remainder[0];
|
|
@@ -47891,6 +48240,14 @@ function resolveHouseShim(argv) {
|
|
|
47891
48240
|
if (actual && isDeclaredHouseAlias(houseToken, lookupPath)) {
|
|
47892
48241
|
process.stderr.write(
|
|
47893
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)}\`
|
|
47894
48251
|
`
|
|
47895
48252
|
);
|
|
47896
48253
|
argv.splice(2, 1);
|
|
@@ -47919,8 +48276,8 @@ function printHouseRootHelp(house) {
|
|
|
47919
48276
|
}
|
|
47920
48277
|
lines2.push(
|
|
47921
48278
|
"",
|
|
47922
|
-
`Canonical: \`mmi-cli ${house} <command> \u2026\`. The flat \`mmi-cli <command> \u2026\`
|
|
47923
|
-
|
|
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."
|
|
47924
48281
|
);
|
|
47925
48282
|
consoleIo.log(lines2.join("\n"));
|
|
47926
48283
|
}
|
package/package.json
CHANGED