@mutmutco/cli 4.5.0 → 4.5.2
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 +455 -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.2",
|
|
16002
|
+
tag: "v4.5.2",
|
|
16003
|
+
commit: "32bfbb00fcb9",
|
|
16004
|
+
npm: "@mutmutco/cli@4.5.2"
|
|
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.2"
|
|
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.2 and redeploy the Hub Lambda from tag v4.5.2 (32bfbb00fcb9); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
16028
|
+
v3Target: "v4.5.2 (@mutmutco/cli@4.5.2, tag commit 32bfbb00fcb9 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
16007
16029
|
}
|
|
16008
16030
|
},
|
|
16009
16031
|
{
|
|
@@ -18779,6 +18801,64 @@ 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
|
+
if (typeof installer.gate === "string" && installer.gate) return `${installer.gate.replace(/\/+$/, "")}/release/version`;
|
|
18818
|
+
return `https://${INSTALLER_FAMILY_HOSTS[installer.family]}/${installer.product}/release/version`;
|
|
18819
|
+
}
|
|
18820
|
+
function installerDeclarationError(value, division) {
|
|
18821
|
+
if (value === void 0 || value === null) return null;
|
|
18822
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
18823
|
+
return "installer must be an object {product, family, login?, gate?}";
|
|
18824
|
+
}
|
|
18825
|
+
const record = (
|
|
18826
|
+
/** @type {Record<string, unknown>} */
|
|
18827
|
+
value
|
|
18828
|
+
);
|
|
18829
|
+
for (const key of Object.keys(record)) {
|
|
18830
|
+
if (!["product", "family", "login", "gate"].includes(key)) return `installer.${key} is not a field \u2014 expected product, family and optionally login and gate`;
|
|
18831
|
+
}
|
|
18832
|
+
if (typeof record.product !== "string" || !PRODUCT_RE.test(record.product)) {
|
|
18833
|
+
return "installer.product must be a lowercase kebab name (e.g. mm-strategy) \u2014 it is the /<product> path on the family host";
|
|
18834
|
+
}
|
|
18835
|
+
if (!INSTALLER_FAMILIES.includes(
|
|
18836
|
+
/** @type {string} */
|
|
18837
|
+
record.family
|
|
18838
|
+
)) {
|
|
18839
|
+
return `installer.family must be one of: ${INSTALLER_FAMILIES.join(", ")} (jerv \u2192 ${INSTALLER_FAMILY_HOSTS.jerv}, mmi \u2192 ${INSTALLER_FAMILY_HOSTS.mmi})`;
|
|
18840
|
+
}
|
|
18841
|
+
if (record.gate !== void 0 && (typeof record.gate !== "string" || !/^https:\/\/[a-z0-9.-]+\.[a-z]{2,}(\/[A-Za-z0-9._\/-]*)?$/.test(record.gate))) {
|
|
18842
|
+
return "installer.gate must be the https origin of the product's own sign-in server (e.g. https://strategy.mutmut.work)";
|
|
18843
|
+
}
|
|
18844
|
+
if (record.login !== void 0 && !INSTALLER_LOGINS.includes(
|
|
18845
|
+
/** @type {string} */
|
|
18846
|
+
record.login
|
|
18847
|
+
)) {
|
|
18848
|
+
return `installer.login must be one of: ${INSTALLER_LOGINS.join(", ")}`;
|
|
18849
|
+
}
|
|
18850
|
+
const expected = expectedInstallerLogin(
|
|
18851
|
+
/** @type {'jerv' | 'mmi'} */
|
|
18852
|
+
record.family,
|
|
18853
|
+
division
|
|
18854
|
+
);
|
|
18855
|
+
if (record.login !== void 0 && record.login !== expected) {
|
|
18856
|
+
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";
|
|
18857
|
+
return `installer.login ${record.login} breaks the sign-in rule: ${who} (owner ruling 2026-09-23)`;
|
|
18858
|
+
}
|
|
18859
|
+
return null;
|
|
18860
|
+
}
|
|
18861
|
+
|
|
18782
18862
|
// src/tenant-control-parse.ts
|
|
18783
18863
|
var OUTPUT_BEGIN = "mmi-control-output-begin";
|
|
18784
18864
|
var OUTPUT_END = "mmi-control-output-end";
|
|
@@ -18961,7 +19041,7 @@ function mmiGetRedeployCommand(repo) {
|
|
|
18961
19041
|
}
|
|
18962
19042
|
function readLocalLauncherVersion(root = process.cwd()) {
|
|
18963
19043
|
try {
|
|
18964
|
-
const raw = (0, import_node_fs19.readFileSync)((0, import_node_path17.join)(root, "
|
|
19044
|
+
const raw = (0, import_node_fs19.readFileSync)((0, import_node_path17.join)(root, "cli", "package.json"), "utf8");
|
|
18965
19045
|
const version = JSON.parse(raw).version;
|
|
18966
19046
|
return typeof version === "string" && version.trim() ? version.trim() : null;
|
|
18967
19047
|
} catch {
|
|
@@ -18990,17 +19070,54 @@ async function fetchServedChannelVersion(url, fetchImpl = fetch) {
|
|
|
18990
19070
|
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
18991
19071
|
}
|
|
18992
19072
|
}
|
|
19073
|
+
async function findPriorGreenDispatch(deps, repo, workflow, sha, titleTokens) {
|
|
19074
|
+
if (!/^[0-9a-f]{40}$/i.test(sha)) return void 0;
|
|
19075
|
+
try {
|
|
19076
|
+
const rows = JSON.parse(await deps.run("gh", [
|
|
19077
|
+
"run",
|
|
19078
|
+
"list",
|
|
19079
|
+
"--repo",
|
|
19080
|
+
repo,
|
|
19081
|
+
"--workflow",
|
|
19082
|
+
workflow,
|
|
19083
|
+
"--commit",
|
|
19084
|
+
sha,
|
|
19085
|
+
"--event",
|
|
19086
|
+
"workflow_dispatch",
|
|
19087
|
+
"--limit",
|
|
19088
|
+
"20",
|
|
19089
|
+
"--json",
|
|
19090
|
+
"databaseId,url,status,conclusion,displayTitle,headSha"
|
|
19091
|
+
]));
|
|
19092
|
+
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)));
|
|
19093
|
+
return hit ? { runId: hit.databaseId, ...hit.url ? { runUrl: hit.url } : {} } : void 0;
|
|
19094
|
+
} catch {
|
|
19095
|
+
return void 0;
|
|
19096
|
+
}
|
|
19097
|
+
}
|
|
19098
|
+
function planeFailureRow(workflow, evidence) {
|
|
19099
|
+
return { workflow, runUrlNote: evidence, conclusion: "failure" };
|
|
19100
|
+
}
|
|
18993
19101
|
async function appendMmiGetRedeploy(deps, input, dispatch) {
|
|
18994
19102
|
if (!isHubControlRepo(input.repo)) return dispatch;
|
|
18995
19103
|
const remedy = mmiGetRedeployCommand(input.repo);
|
|
18996
|
-
const withNote = (note, deployStatus) => ({
|
|
19104
|
+
const withNote = (note, deployStatus, row) => ({
|
|
18997
19105
|
...dispatch,
|
|
18998
19106
|
note: `${dispatch.note}; ${note}`,
|
|
18999
|
-
...deployStatus ? { deployStatus } : {}
|
|
19107
|
+
...deployStatus ? { deployStatus } : {},
|
|
19108
|
+
...row ? { workflowRuns: [...dispatch.workflowRuns ?? [], row] } : {}
|
|
19000
19109
|
});
|
|
19001
19110
|
if (input.publishStatus !== "success") {
|
|
19002
19111
|
return withNote(`mmi-get box NOT redeployed (publish did not succeed) \u2014 after the publish is green, run \`${remedy}\``);
|
|
19003
19112
|
}
|
|
19113
|
+
const prior = input.releaseSha ? await findPriorGreenDispatch(deps, input.repo, "tenant-deploy.yml", input.releaseSha, [HUB_GET_SLUG, HUB_GET_STAGE]) : void 0;
|
|
19114
|
+
if (prior) {
|
|
19115
|
+
return {
|
|
19116
|
+
...dispatch,
|
|
19117
|
+
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`,
|
|
19118
|
+
workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "tenant-deploy.yml", runId: prior.runId, ...prior.runUrl ? { runUrl: prior.runUrl } : {}, conclusion: "success" }]
|
|
19119
|
+
};
|
|
19120
|
+
}
|
|
19004
19121
|
const since = (deps.now ?? Date.now)();
|
|
19005
19122
|
try {
|
|
19006
19123
|
await deps.run("gh", [
|
|
@@ -19022,12 +19139,12 @@ async function appendMmiGetRedeploy(deps, input, dispatch) {
|
|
|
19022
19139
|
]);
|
|
19023
19140
|
} catch (e) {
|
|
19024
19141
|
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");
|
|
19142
|
+
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
19143
|
}
|
|
19027
19144
|
const correlated = await correlateTenantRun(deps, since, [HUB_GET_SLUG, HUB_GET_STAGE]);
|
|
19028
19145
|
if (correlated.read === "failed" || correlated.read === "absent") {
|
|
19029
19146
|
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");
|
|
19147
|
+
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
19148
|
}
|
|
19032
19149
|
const { runId, runUrl } = correlated;
|
|
19033
19150
|
const runRow = { workflow: "tenant-deploy.yml", ...runId ? { runId } : {}, ...runUrl ? { runUrl } : {} };
|
|
@@ -19057,35 +19174,44 @@ async function appendServedChannelPublish(deps, input, dispatch) {
|
|
|
19057
19174
|
if (!isHubControlRepo(input.repo)) return dispatch;
|
|
19058
19175
|
const channelUrl = input.channelUrl ?? readServedChannelUrl();
|
|
19059
19176
|
const remedy = getPublishDispatchCommand(input.repo);
|
|
19060
|
-
const withNote = (note, deployStatus) => ({
|
|
19177
|
+
const withNote = (note, deployStatus, row) => ({
|
|
19061
19178
|
...dispatch,
|
|
19062
19179
|
note: `${dispatch.note}; ${note}`,
|
|
19063
|
-
...deployStatus ? { deployStatus } : {}
|
|
19180
|
+
...deployStatus ? { deployStatus } : {},
|
|
19181
|
+
...row ? { workflowRuns: [...dispatch.workflowRuns ?? [], row] } : {}
|
|
19064
19182
|
});
|
|
19065
19183
|
if (input.publishStatus !== "success") {
|
|
19066
19184
|
return withNote(`served channel NOT moved (publish did not succeed) \u2014 after the publish is green, run \`${remedy}\``);
|
|
19067
19185
|
}
|
|
19068
|
-
const
|
|
19069
|
-
|
|
19070
|
-
|
|
19071
|
-
|
|
19072
|
-
|
|
19073
|
-
|
|
19074
|
-
}
|
|
19075
|
-
|
|
19076
|
-
|
|
19077
|
-
|
|
19078
|
-
|
|
19079
|
-
|
|
19080
|
-
|
|
19081
|
-
|
|
19082
|
-
|
|
19083
|
-
|
|
19084
|
-
|
|
19085
|
-
|
|
19086
|
-
};
|
|
19186
|
+
const prior = input.releaseSha ? await findPriorGreenDispatch(deps, input.repo, "get-publish.yml", input.releaseSha, [HUB_SERVED_PRODUCT]) : void 0;
|
|
19187
|
+
let runRow;
|
|
19188
|
+
let runStatus;
|
|
19189
|
+
if (prior) {
|
|
19190
|
+
runRow = { workflow: "get-publish.yml", runId: prior.runId, ...prior.runUrl ? { runUrl: prior.runUrl } : {} };
|
|
19191
|
+
runStatus = "success";
|
|
19192
|
+
} else {
|
|
19193
|
+
const since = (deps.now ?? Date.now)();
|
|
19194
|
+
try {
|
|
19195
|
+
await deps.run("gh", ["workflow", "run", "get-publish.yml", "--repo", input.repo, "--ref", "main", "-f", `product=${HUB_SERVED_PRODUCT}`]);
|
|
19196
|
+
} catch (e) {
|
|
19197
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
19198
|
+
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}`));
|
|
19199
|
+
}
|
|
19200
|
+
const correlated = await correlateGetPublishRun(deps, since, HUB_SERVED_PRODUCT);
|
|
19201
|
+
if (correlated.read === "failed") {
|
|
19202
|
+
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`));
|
|
19203
|
+
}
|
|
19204
|
+
const { runId, runUrl } = correlated;
|
|
19205
|
+
runRow = { workflow: "get-publish.yml", ...runId ? { runId } : {}, ...runUrl ? { runUrl } : {} };
|
|
19206
|
+
if (!input.watch) {
|
|
19207
|
+
return {
|
|
19208
|
+
...dispatch,
|
|
19209
|
+
note: `${dispatch.note}; dispatched get-publish.yml (product=${HUB_SERVED_PRODUCT}) \u2014 NOT watched, so the served channel is not yet proven moved`,
|
|
19210
|
+
workflowRuns: [...dispatch.workflowRuns ?? [], { ...runRow, conclusion: "pending" }]
|
|
19211
|
+
};
|
|
19212
|
+
}
|
|
19213
|
+
runStatus = await watchTenantRun(deps, runId, input.repo);
|
|
19087
19214
|
}
|
|
19088
|
-
const runStatus = await watchTenantRun(deps, runId, input.repo);
|
|
19089
19215
|
if (runStatus !== "success") {
|
|
19090
19216
|
return {
|
|
19091
19217
|
...dispatch,
|
|
@@ -19170,13 +19296,22 @@ var SERVED_CHANNELS = {
|
|
|
19170
19296
|
remedy: JERV_GATEWAY_REMEDY
|
|
19171
19297
|
}
|
|
19172
19298
|
};
|
|
19173
|
-
function servedChannelFor(repo) {
|
|
19174
|
-
|
|
19299
|
+
function servedChannelFor(repo, installer) {
|
|
19300
|
+
const known = SERVED_CHANNELS[repo.toLowerCase()];
|
|
19301
|
+
if (!installer) return known;
|
|
19302
|
+
if (isHubControlRepo(repo)) return void 0;
|
|
19303
|
+
const url = installerVersionUrl(installer);
|
|
19304
|
+
return {
|
|
19305
|
+
product: installer.product,
|
|
19306
|
+
url,
|
|
19307
|
+
mover: known?.mover ?? `this repo's release (${installer.family === "jerv" ? "get.jerv.ai" : "get.mutmut.work"} serves /${installer.product})`,
|
|
19308
|
+
remedy: known?.remedy ?? `re-run the release's served-channel publish for ${installer.product} (\`mmi-cli devops release --resume\`)`
|
|
19309
|
+
};
|
|
19175
19310
|
}
|
|
19176
19311
|
var SERVED_CHANNEL_PROBE_ATTEMPTS = 3;
|
|
19177
19312
|
var SERVED_CHANNEL_PROBE_BACKOFF_MS = 1e4;
|
|
19178
19313
|
async function appendServedChannelProof(deps, input, dispatch) {
|
|
19179
|
-
const channel = servedChannelFor(input.repo);
|
|
19314
|
+
const channel = servedChannelFor(input.repo, input.installer);
|
|
19180
19315
|
if (!channel) return dispatch;
|
|
19181
19316
|
const expected = input.tag.replace(/^v/, "");
|
|
19182
19317
|
const withProof = (conclusion, evidence, note, failed = false) => ({
|
|
@@ -19216,6 +19351,15 @@ async function appendServedChannelProof(deps, input, dispatch) {
|
|
|
19216
19351
|
}
|
|
19217
19352
|
return withProof("success", `${channel.url} reports ${probe.version}`, `served channel moved \u2014 ${channel.url} reports ${probe.version}`);
|
|
19218
19353
|
}
|
|
19354
|
+
async function appendServedChannelLegs(deps, input, dispatch) {
|
|
19355
|
+
const publishStatus = input.publish?.state === "complete" ? "success" : "failure";
|
|
19356
|
+
let out = await appendMmiGetRedeploy(deps, { repo: input.repo, publishStatus, watch: input.watch, releaseSha: input.releaseSha }, dispatch);
|
|
19357
|
+
out = await appendServedChannelPublish(deps, { repo: input.repo, publishStatus, watch: input.watch, releaseSha: input.releaseSha }, out);
|
|
19358
|
+
return appendServedChannelProof(deps, { repo: input.repo, tag: input.tag, shipped: shipLegsGreen(input.deploy, input.publish), installer: input.installer }, out);
|
|
19359
|
+
}
|
|
19360
|
+
function servedChannelRowsAfter(before, after) {
|
|
19361
|
+
return (after.workflowRuns ?? []).slice((before.workflowRuns ?? []).length);
|
|
19362
|
+
}
|
|
19219
19363
|
var JERV_GATEWAY_RUN_URL_NOTE = "no run URL emitted \u2014 host health leg runs outside GitHub Actions";
|
|
19220
19364
|
var JERV_GATEWAY_RELEASE_DEPLOY_SCRIPT = "scripts/jerv-gateway-release-deploy.sh";
|
|
19221
19365
|
async function runTaggedJervGatewayLocalWrapper(deps, tag, tagSha) {
|
|
@@ -20852,6 +20996,20 @@ var import_node_crypto5 = require("node:crypto");
|
|
|
20852
20996
|
// src/file-lock.ts
|
|
20853
20997
|
var import_promises2 = require("node:fs/promises");
|
|
20854
20998
|
var import_node_path21 = require("node:path");
|
|
20999
|
+
function pidIsDead(pid) {
|
|
21000
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
21001
|
+
if (pid === process.pid) return false;
|
|
21002
|
+
try {
|
|
21003
|
+
process.kill(pid, 0);
|
|
21004
|
+
return false;
|
|
21005
|
+
} catch (e) {
|
|
21006
|
+
return e.code === "ESRCH";
|
|
21007
|
+
}
|
|
21008
|
+
}
|
|
21009
|
+
function lockTokenPid(token) {
|
|
21010
|
+
const m = /^(\d+)-/.exec(token);
|
|
21011
|
+
return m ? Number(m[1]) : void 0;
|
|
21012
|
+
}
|
|
20855
21013
|
var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
20856
21014
|
var IMMEDIATE_RETRY_BUDGET = 3;
|
|
20857
21015
|
var FileLockBusyError = class extends Error {
|
|
@@ -20867,9 +21025,24 @@ function resolveFileLockOpts(opts = {}) {
|
|
|
20867
21025
|
staleMs: opts.staleMs ?? 3e4,
|
|
20868
21026
|
retryMs: opts.retryMs ?? 50,
|
|
20869
21027
|
maxWaitMs: opts.maxWaitMs ?? 5e3,
|
|
20870
|
-
label: opts.label ?? "file lock"
|
|
21028
|
+
label: opts.label ?? "file lock",
|
|
21029
|
+
deadHolderSteal: opts.deadHolderSteal ?? false,
|
|
21030
|
+
holder: opts.holder ?? ""
|
|
20871
21031
|
};
|
|
20872
21032
|
}
|
|
21033
|
+
async function lockIsStealable(lockPath, opts) {
|
|
21034
|
+
const age = Date.now() - (await (0, import_promises2.stat)(lockPath)).mtimeMs;
|
|
21035
|
+
if (age > opts.staleMs) return true;
|
|
21036
|
+
if (!opts.deadHolderSteal) return false;
|
|
21037
|
+
let token;
|
|
21038
|
+
try {
|
|
21039
|
+
token = await (0, import_promises2.readFile)(lockPath, "utf8");
|
|
21040
|
+
} catch {
|
|
21041
|
+
return false;
|
|
21042
|
+
}
|
|
21043
|
+
const pid = lockTokenPid(token);
|
|
21044
|
+
return pid !== void 0 && pidIsDead(pid);
|
|
21045
|
+
}
|
|
20873
21046
|
async function acquireFileLock(lockPath, opts, deadline) {
|
|
20874
21047
|
let immediateRetries = 0;
|
|
20875
21048
|
for (; ; ) {
|
|
@@ -20880,12 +21053,12 @@ async function acquireFileLock(lockPath, opts, deadline) {
|
|
|
20880
21053
|
const code = e.code;
|
|
20881
21054
|
if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
|
|
20882
21055
|
const retryNow = async () => {
|
|
21056
|
+
if (++immediateRetries <= IMMEDIATE_RETRY_BUDGET) return;
|
|
20883
21057
|
if (Date.now() >= deadline) throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
|
|
20884
|
-
|
|
21058
|
+
await sleep(opts.retryMs);
|
|
20885
21059
|
};
|
|
20886
21060
|
try {
|
|
20887
|
-
|
|
20888
|
-
if (age > opts.staleMs) {
|
|
21061
|
+
if (await lockIsStealable(lockPath, opts)) {
|
|
20889
21062
|
try {
|
|
20890
21063
|
await (0, import_promises2.unlink)(lockPath);
|
|
20891
21064
|
await retryNow();
|
|
@@ -20911,7 +21084,8 @@ async function acquireFileLock(lockPath, opts, deadline) {
|
|
|
20911
21084
|
await sleep(opts.retryMs);
|
|
20912
21085
|
continue;
|
|
20913
21086
|
}
|
|
20914
|
-
const
|
|
21087
|
+
const stamp = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
21088
|
+
const token = opts.holder ? `${stamp} ${opts.holder.replace(/\s+/g, " ").trim()}` : stamp;
|
|
20915
21089
|
try {
|
|
20916
21090
|
await handle.writeFile(token);
|
|
20917
21091
|
} catch (e) {
|
|
@@ -20935,11 +21109,14 @@ async function releaseFileLock(lockPath, guard) {
|
|
|
20935
21109
|
}
|
|
20936
21110
|
}
|
|
20937
21111
|
async function withFileLock(lockPath, opts, fn) {
|
|
21112
|
+
return withFencedFileLock(lockPath, opts, () => fn());
|
|
21113
|
+
}
|
|
21114
|
+
async function withFencedFileLock(lockPath, opts, fn) {
|
|
20938
21115
|
const resolved = resolveFileLockOpts(opts);
|
|
20939
21116
|
await (0, import_promises2.mkdir)((0, import_node_path21.dirname)(lockPath), { recursive: true }).catch(() => void 0);
|
|
20940
21117
|
const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
|
|
20941
21118
|
try {
|
|
20942
|
-
return await fn();
|
|
21119
|
+
return await fn(() => fileLockHeldBy(lockPath, guard.token));
|
|
20943
21120
|
} finally {
|
|
20944
21121
|
await releaseFileLock(lockPath, guard);
|
|
20945
21122
|
}
|
|
@@ -21177,8 +21354,114 @@ function atomicWriteJson(path2, ledger) {
|
|
|
21177
21354
|
`, "utf8");
|
|
21178
21355
|
(0, import_node_fs22.renameSync)(tmp, path2);
|
|
21179
21356
|
}
|
|
21357
|
+
var LEDGER_LOCK_OPTS = { label: "release ledger", staleMs: 18e4, deadHolderSteal: true };
|
|
21358
|
+
async function withLedgerLock(path2, fn, opts = {}) {
|
|
21359
|
+
return withFencedFileLock(`${path2}.lock`, { ...LEDGER_LOCK_OPTS, ...opts.maxWaitMs != null ? { maxWaitMs: opts.maxWaitMs } : {} }, (stillHeld) => fn(async (write) => {
|
|
21360
|
+
if (!await stillHeld()) {
|
|
21361
|
+
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)`);
|
|
21362
|
+
}
|
|
21363
|
+
return write();
|
|
21364
|
+
}));
|
|
21365
|
+
}
|
|
21366
|
+
function checkoutRootFrom(cwd) {
|
|
21367
|
+
let dir = (0, import_node_path22.resolve)(cwd);
|
|
21368
|
+
for (; ; ) {
|
|
21369
|
+
if (resolveGitDir(dir)) return dir;
|
|
21370
|
+
const parent = (0, import_node_path22.dirname)(dir);
|
|
21371
|
+
if (parent === dir) throw new ReleaseLedgerError("no-checkout", `train writer lock: no git checkout found above ${cwd}`);
|
|
21372
|
+
dir = parent;
|
|
21373
|
+
}
|
|
21374
|
+
}
|
|
21375
|
+
function trainWriterLockPath(cwd) {
|
|
21376
|
+
return (0, import_node_path22.join)((0, import_node_path22.dirname)(releaseLedgerPath(checkoutRootFrom(cwd))), "train.lock");
|
|
21377
|
+
}
|
|
21378
|
+
var TrainWriterBusyError = class extends Error {
|
|
21379
|
+
lockPath;
|
|
21380
|
+
holder;
|
|
21381
|
+
constructor(lockPath, holder, verb) {
|
|
21382
|
+
super(
|
|
21383
|
+
`${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)`
|
|
21384
|
+
);
|
|
21385
|
+
this.name = "TrainWriterBusyError";
|
|
21386
|
+
this.lockPath = lockPath;
|
|
21387
|
+
this.holder = holder;
|
|
21388
|
+
}
|
|
21389
|
+
};
|
|
21390
|
+
var TRAIN_WRITER_STALE_MS = 12 * 60 * 6e4;
|
|
21391
|
+
function describeTrainWriterToken(token) {
|
|
21392
|
+
const pid = lockTokenPid(token);
|
|
21393
|
+
const [stamp, ...rest] = token.trim().split(" ");
|
|
21394
|
+
const since = Number((stamp ?? "").split("-")[1]);
|
|
21395
|
+
const when = Number.isFinite(since) && since > 0 ? `, since ${new Date(since).toISOString()}` : "";
|
|
21396
|
+
const what = rest.join(" ").trim();
|
|
21397
|
+
return `${what || "a train command"}${pid ? `, pid ${pid}` : ""}${when}`;
|
|
21398
|
+
}
|
|
21399
|
+
function liveTrainWriterHolder(cwd) {
|
|
21400
|
+
let token;
|
|
21401
|
+
try {
|
|
21402
|
+
token = (0, import_node_fs22.readFileSync)(trainWriterLockPath(cwd), "utf8");
|
|
21403
|
+
} catch {
|
|
21404
|
+
return void 0;
|
|
21405
|
+
}
|
|
21406
|
+
const pid = lockTokenPid(token);
|
|
21407
|
+
if (pid === void 0 || pid === process.pid || pidIsDead(pid)) return void 0;
|
|
21408
|
+
return describeTrainWriterToken(token);
|
|
21409
|
+
}
|
|
21410
|
+
var heldTrainWriter;
|
|
21411
|
+
async function acquireTrainWriterLock(cwd, verb) {
|
|
21412
|
+
let lockPath;
|
|
21413
|
+
try {
|
|
21414
|
+
lockPath = trainWriterLockPath(cwd);
|
|
21415
|
+
} catch {
|
|
21416
|
+
return async () => void 0;
|
|
21417
|
+
}
|
|
21418
|
+
if (heldTrainWriter && heldTrainWriter.lockPath === lockPath) {
|
|
21419
|
+
heldTrainWriter.depth += 1;
|
|
21420
|
+
return async () => {
|
|
21421
|
+
if (heldTrainWriter) heldTrainWriter.depth -= 1;
|
|
21422
|
+
};
|
|
21423
|
+
}
|
|
21424
|
+
(0, import_node_fs22.mkdirSync)((0, import_node_path22.dirname)(lockPath), { recursive: true });
|
|
21425
|
+
const opts = resolveFileLockOpts({ label: "train writer", staleMs: TRAIN_WRITER_STALE_MS, deadHolderSteal: true, maxWaitMs: 0, holder: verb });
|
|
21426
|
+
let guard;
|
|
21427
|
+
try {
|
|
21428
|
+
guard = await acquireFileLock(lockPath, opts, Date.now());
|
|
21429
|
+
} catch (e) {
|
|
21430
|
+
if (!(e instanceof FileLockBusyError)) throw e;
|
|
21431
|
+
let token = "";
|
|
21432
|
+
try {
|
|
21433
|
+
token = (0, import_node_fs22.readFileSync)(lockPath, "utf8");
|
|
21434
|
+
} catch {
|
|
21435
|
+
token = "";
|
|
21436
|
+
}
|
|
21437
|
+
throw new TrainWriterBusyError(lockPath, token ? describeTrainWriterToken(token) : "holder unreadable", verb);
|
|
21438
|
+
}
|
|
21439
|
+
const held = {
|
|
21440
|
+
lockPath,
|
|
21441
|
+
depth: 1,
|
|
21442
|
+
release: async () => {
|
|
21443
|
+
heldTrainWriter = void 0;
|
|
21444
|
+
await releaseFileLock(lockPath, guard);
|
|
21445
|
+
}
|
|
21446
|
+
};
|
|
21447
|
+
heldTrainWriter = held;
|
|
21448
|
+
return async () => {
|
|
21449
|
+
held.depth -= 1;
|
|
21450
|
+
if (held.depth <= 0 && heldTrainWriter === held) await held.release();
|
|
21451
|
+
};
|
|
21452
|
+
}
|
|
21453
|
+
async function withTrainWriter(deps, verb, fn) {
|
|
21454
|
+
const seam = "trainWriter" in deps ? deps.trainWriter : void 0;
|
|
21455
|
+
if (!seam) return fn();
|
|
21456
|
+
const release = await seam(verb, process.cwd());
|
|
21457
|
+
try {
|
|
21458
|
+
return await fn();
|
|
21459
|
+
} finally {
|
|
21460
|
+
await release();
|
|
21461
|
+
}
|
|
21462
|
+
}
|
|
21180
21463
|
async function withReleaseLedger(path2, expected, fn, opts = {}) {
|
|
21181
|
-
return
|
|
21464
|
+
return withLedgerLock(path2, async (fence) => {
|
|
21182
21465
|
let current;
|
|
21183
21466
|
let raw;
|
|
21184
21467
|
try {
|
|
@@ -21193,9 +21476,9 @@ async function withReleaseLedger(path2, expected, fn, opts = {}) {
|
|
|
21193
21476
|
if (expected) validateLedgerAnchors(current, expected);
|
|
21194
21477
|
}
|
|
21195
21478
|
const { ledger, value } = await fn(current);
|
|
21196
|
-
if (ledger) atomicWriteJson(path2, ledger);
|
|
21479
|
+
if (ledger) await fence(() => atomicWriteJson(path2, ledger));
|
|
21197
21480
|
return value;
|
|
21198
|
-
});
|
|
21481
|
+
}, { maxWaitMs: opts.maxWaitMs ?? 5e3 });
|
|
21199
21482
|
}
|
|
21200
21483
|
function openReleaseLedgerWriter(cwd, opts = {}) {
|
|
21201
21484
|
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -21810,7 +22093,7 @@ async function reverifyPersistedRunPhases(deps, path2, anchors, input) {
|
|
|
21810
22093
|
return result;
|
|
21811
22094
|
}
|
|
21812
22095
|
async function clearLedgerFile(path2, anchors, maxWaitMs) {
|
|
21813
|
-
return
|
|
22096
|
+
return withLedgerLock(path2, async (fence) => {
|
|
21814
22097
|
let raw;
|
|
21815
22098
|
try {
|
|
21816
22099
|
raw = (0, import_node_fs22.readFileSync)(path2, "utf8");
|
|
@@ -21829,9 +22112,9 @@ async function clearLedgerFile(path2, anchors, maxWaitMs) {
|
|
|
21829
22112
|
} catch (e) {
|
|
21830
22113
|
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
22114
|
}
|
|
21832
|
-
(0, import_node_fs22.unlinkSync)(path2);
|
|
22115
|
+
await fence(() => (0, import_node_fs22.unlinkSync)(path2));
|
|
21833
22116
|
return { cleared: true, note: `cleared the release ledger at ${path2} for the live-proven aborted ${anchors.tag}` };
|
|
21834
|
-
});
|
|
22117
|
+
}, { maxWaitMs });
|
|
21835
22118
|
}
|
|
21836
22119
|
async function clearReleaseLedger(cwd, anchors, opts = {}) {
|
|
21837
22120
|
let path2;
|
|
@@ -21971,7 +22254,7 @@ async function alignmentLegClosedLive(deps, prior) {
|
|
|
21971
22254
|
}
|
|
21972
22255
|
async function healStaleAlignmentLeg(deps, cwd, expected) {
|
|
21973
22256
|
const path2 = releaseLedgerPath(cwd);
|
|
21974
|
-
return
|
|
22257
|
+
return withLedgerLock(path2, async (fence) => {
|
|
21975
22258
|
let prior;
|
|
21976
22259
|
try {
|
|
21977
22260
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -21987,7 +22270,7 @@ async function healStaleAlignmentLeg(deps, cwd, expected) {
|
|
|
21987
22270
|
}
|
|
21988
22271
|
const closed = await alignmentLegClosedLive(deps, prior);
|
|
21989
22272
|
if (!closed) return void 0;
|
|
21990
|
-
atomicWriteJson(path2, { ...prior, phases: { ...prior.phases, alignment: closed }, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
22273
|
+
await fence(() => atomicWriteJson(path2, { ...prior, phases: { ...prior.phases, alignment: closed }, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
21991
22274
|
return prior.tag;
|
|
21992
22275
|
});
|
|
21993
22276
|
}
|
|
@@ -22060,7 +22343,7 @@ function supersedeableDeployLegShape(rec) {
|
|
|
22060
22343
|
}
|
|
22061
22344
|
async function supersedeContentCausedDeployLeg(deps, cwd) {
|
|
22062
22345
|
const path2 = releaseLedgerPath(cwd);
|
|
22063
|
-
return
|
|
22346
|
+
return withLedgerLock(path2, async (fence) => {
|
|
22064
22347
|
let prior;
|
|
22065
22348
|
try {
|
|
22066
22349
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -22101,14 +22384,14 @@ async function supersedeContentCausedDeployLeg(deps, cwd) {
|
|
|
22101
22384
|
);
|
|
22102
22385
|
}
|
|
22103
22386
|
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, {
|
|
22387
|
+
await fence(() => atomicWriteJson(path2, {
|
|
22105
22388
|
...prior,
|
|
22106
22389
|
phases: {
|
|
22107
22390
|
...phases,
|
|
22108
22391
|
deploy: { state: "skipped", sha: prior.tagSha, superseded: true, note, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
22109
22392
|
},
|
|
22110
22393
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22111
|
-
});
|
|
22394
|
+
}));
|
|
22112
22395
|
return {
|
|
22113
22396
|
command: "release-supersede-deploy",
|
|
22114
22397
|
repo: prior.repo,
|
|
@@ -22124,7 +22407,7 @@ function supersedeablePublishLegShape(rec) {
|
|
|
22124
22407
|
function deferredGatewayDeployLegShape(rec) {
|
|
22125
22408
|
return rec.state === "pending" && rec.runId == null && rec.workflow === "jerv-gateway";
|
|
22126
22409
|
}
|
|
22127
|
-
async function proveOriginPromotionOrArchived(deps, prior, path2, label, ref) {
|
|
22410
|
+
async function proveOriginPromotionOrArchived(deps, prior, path2, label, ref, fence) {
|
|
22128
22411
|
let liveSha = "";
|
|
22129
22412
|
try {
|
|
22130
22413
|
liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
|
|
@@ -22163,12 +22446,12 @@ async function proveOriginPromotionOrArchived(deps, prior, path2, label, ref) {
|
|
|
22163
22446
|
if ((0, import_node_fs22.existsSync)(archivePath)) {
|
|
22164
22447
|
throw new Error(`${label}: archive already exists at ${archivePath} \u2014 preserving both ledger records; resolve the collision by hand; nothing was written (${ref})`);
|
|
22165
22448
|
}
|
|
22166
|
-
(0, import_node_fs22.renameSync)(path2, archivePath);
|
|
22449
|
+
await fence(() => (0, import_node_fs22.renameSync)(path2, archivePath));
|
|
22167
22450
|
return "deleted";
|
|
22168
22451
|
}
|
|
22169
22452
|
async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
22170
22453
|
const path2 = releaseLedgerPath(cwd);
|
|
22171
|
-
return
|
|
22454
|
+
return withLedgerLock(path2, async (fence) => {
|
|
22172
22455
|
let prior;
|
|
22173
22456
|
try {
|
|
22174
22457
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -22199,7 +22482,7 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
22199
22482
|
`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
22483
|
);
|
|
22201
22484
|
}
|
|
22202
|
-
if (await proveOriginPromotionOrArchived(deps, prior, path2, "release --supersede-publish", "#6638") === "deleted") {
|
|
22485
|
+
if (await proveOriginPromotionOrArchived(deps, prior, path2, "release --supersede-publish", "#6638", fence) === "deleted") {
|
|
22203
22486
|
return {
|
|
22204
22487
|
command: "release-supersede-publish",
|
|
22205
22488
|
repo: prior.repo,
|
|
@@ -22228,14 +22511,14 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
22228
22511
|
gatewayDeploy = { workflow: "jerv-gateway", state: "complete", note: dispatch.note };
|
|
22229
22512
|
}
|
|
22230
22513
|
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, {
|
|
22514
|
+
await fence(() => atomicWriteJson(path2, {
|
|
22232
22515
|
...prior,
|
|
22233
22516
|
phases: {
|
|
22234
22517
|
...phases,
|
|
22235
22518
|
publish: { state: "skipped", sha: prior.tagSha, superseded: true, note, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
22236
22519
|
},
|
|
22237
22520
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22238
|
-
});
|
|
22521
|
+
}));
|
|
22239
22522
|
return {
|
|
22240
22523
|
command: "release-supersede-publish",
|
|
22241
22524
|
repo: prior.repo,
|
|
@@ -22250,7 +22533,7 @@ async function supersedeContentCausedDeployAndPublishLegs(deps, cwd) {
|
|
|
22250
22533
|
const label = "release --supersede-deploy --supersede-publish";
|
|
22251
22534
|
const ref = "#6752";
|
|
22252
22535
|
const path2 = releaseLedgerPath(cwd);
|
|
22253
|
-
return
|
|
22536
|
+
return withLedgerLock(path2, async (fence) => {
|
|
22254
22537
|
let prior;
|
|
22255
22538
|
try {
|
|
22256
22539
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -22298,7 +22581,7 @@ async function supersedeContentCausedDeployAndPublishLegs(deps, cwd) {
|
|
|
22298
22581
|
`${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
22582
|
);
|
|
22300
22583
|
}
|
|
22301
|
-
if (await proveOriginPromotionOrArchived(deps, prior, path2, label, ref) === "deleted") {
|
|
22584
|
+
if (await proveOriginPromotionOrArchived(deps, prior, path2, label, ref, fence) === "deleted") {
|
|
22302
22585
|
return {
|
|
22303
22586
|
command: "release-supersede-deploy-and-publish",
|
|
22304
22587
|
repo: prior.repo,
|
|
@@ -22344,7 +22627,7 @@ async function supersedeContentCausedDeployAndPublishLegs(deps, cwd) {
|
|
|
22344
22627
|
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
22628
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22346
22629
|
};
|
|
22347
|
-
atomicWriteJson(path2, { ...prior, phases, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
22630
|
+
await fence(() => atomicWriteJson(path2, { ...prior, phases, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
22348
22631
|
return {
|
|
22349
22632
|
command: "release-supersede-deploy-and-publish",
|
|
22350
22633
|
repo: prior.repo,
|
|
@@ -22375,7 +22658,7 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
|
|
|
22375
22658
|
return { ok: true };
|
|
22376
22659
|
}
|
|
22377
22660
|
await reverifyPriorRunLegs(deps, path2, targetTag);
|
|
22378
|
-
return
|
|
22661
|
+
return withLedgerLock(path2, async (fence) => {
|
|
22379
22662
|
let raw;
|
|
22380
22663
|
try {
|
|
22381
22664
|
raw = (0, import_node_fs22.readFileSync)(path2, "utf8");
|
|
@@ -22395,7 +22678,7 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
|
|
|
22395
22678
|
const closed = await alignmentLegClosedLive(deps, prior);
|
|
22396
22679
|
if (closed) {
|
|
22397
22680
|
prior = { ...prior, phases: { ...prior.phases, alignment: closed }, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
22398
|
-
atomicWriteJson(path2, prior);
|
|
22681
|
+
await fence(() => atomicWriteJson(path2, prior));
|
|
22399
22682
|
derived = deriveReleasePhaseStatus(prior);
|
|
22400
22683
|
}
|
|
22401
22684
|
}
|
|
@@ -22418,13 +22701,13 @@ async function beginReleaseLedger(deps, cwd, targetTag) {
|
|
|
22418
22701
|
}
|
|
22419
22702
|
const archivePath = (0, import_node_path22.join)((0, import_node_path22.dirname)(path2), `phases.archive-${prior.tag}.json`);
|
|
22420
22703
|
try {
|
|
22421
|
-
(0, import_node_fs22.unlinkSync)(archivePath);
|
|
22704
|
+
await fence(() => (0, import_node_fs22.unlinkSync)(archivePath));
|
|
22422
22705
|
} catch (e) {
|
|
22423
22706
|
if (e.code !== "ENOENT") {
|
|
22424
22707
|
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
22708
|
}
|
|
22426
22709
|
}
|
|
22427
|
-
(0, import_node_fs22.renameSync)(path2, archivePath);
|
|
22710
|
+
await fence(() => (0, import_node_fs22.renameSync)(path2, archivePath));
|
|
22428
22711
|
return { ok: true, note: `archived the completed release ledger ${prior.tag} to ${archivePath} (live-proven at ${prior.tagSha.slice(0, 12)})` };
|
|
22429
22712
|
});
|
|
22430
22713
|
}
|
|
@@ -22449,7 +22732,7 @@ async function defaultRetryDeploy(deps, prior) {
|
|
|
22449
22732
|
}
|
|
22450
22733
|
async function retryHotfixDeployLegs(deps, cwd, opts = {}) {
|
|
22451
22734
|
const path2 = releaseLedgerPath(cwd);
|
|
22452
|
-
return
|
|
22735
|
+
return withLedgerLock(path2, async (fence) => {
|
|
22453
22736
|
let prior;
|
|
22454
22737
|
try {
|
|
22455
22738
|
prior = parseReleaseLedger((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
@@ -22530,7 +22813,7 @@ async function retryHotfixDeployLegs(deps, cwd, opts = {}) {
|
|
|
22530
22813
|
);
|
|
22531
22814
|
}
|
|
22532
22815
|
}
|
|
22533
|
-
atomicWriteJson(path2, { ...prior, phases, updatedAt: now().toISOString() });
|
|
22816
|
+
await fence(() => atomicWriteJson(path2, { ...prior, phases, updatedAt: now().toISOString() }));
|
|
22534
22817
|
const stillFailed = retried.filter((phase) => phases[phase].state === "failed");
|
|
22535
22818
|
const status = stillFailed.length === 0 ? "success" : "failed";
|
|
22536
22819
|
return {
|
|
@@ -22683,6 +22966,7 @@ var defaultDeps2 = {
|
|
|
22683
22966
|
},
|
|
22684
22967
|
healAlignmentLeg: healStaleAlignmentLeg,
|
|
22685
22968
|
healRunLegs: healUnfoldedRunLegs,
|
|
22969
|
+
trainWriterHolder: liveTrainWriterHolder,
|
|
22686
22970
|
readSurfacesRaw: (cwd) => {
|
|
22687
22971
|
try {
|
|
22688
22972
|
return (0, import_node_fs23.readFileSync)((0, import_node_path23.join)(cwd, "surfaces.json"), "utf8");
|
|
@@ -23025,12 +23309,17 @@ async function runTrainDoctor(input) {
|
|
|
23025
23309
|
break;
|
|
23026
23310
|
}
|
|
23027
23311
|
}
|
|
23312
|
+
const runningTrain = deps.trainWriterHolder(cwd);
|
|
23313
|
+
if (runningTrain) {
|
|
23314
|
+
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)" });
|
|
23315
|
+
}
|
|
23316
|
+
const healLedger = heal && !runningTrain;
|
|
23028
23317
|
if (ledgerRaw !== void 0 && ledgerPath) {
|
|
23029
23318
|
try {
|
|
23030
23319
|
let ledger = parseReleaseLedger(ledgerRaw);
|
|
23031
23320
|
let derived = deriveReleasePhaseStatus(ledger);
|
|
23032
23321
|
const deployModel = typeof meta?.deployModel === "string" && isDeployModel(meta.deployModel) ? meta.deployModel : void 0;
|
|
23033
|
-
if (
|
|
23322
|
+
if (healLedger && deployModel && derived.legs.some((l) => (l.phase === "deploy" || l.phase === "publish") && l.state === "pending")) {
|
|
23034
23323
|
const run = await deps.healRunLegs(train, cwd, deployModel);
|
|
23035
23324
|
if (run) {
|
|
23036
23325
|
add({
|
|
@@ -23044,7 +23333,7 @@ async function runTrainDoctor(input) {
|
|
|
23044
23333
|
derived = deriveReleasePhaseStatus(ledger);
|
|
23045
23334
|
}
|
|
23046
23335
|
}
|
|
23047
|
-
if (
|
|
23336
|
+
if (healLedger && derived.phaseStatus === "phases-pending" && derived.legs.every((l) => l.state !== "pending" || l.phase === "alignment")) {
|
|
23048
23337
|
const closedTag = await deps.healAlignmentLeg(train, cwd);
|
|
23049
23338
|
if (closedTag) {
|
|
23050
23339
|
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 +24359,11 @@ ${recovery.note}`), recoveryInput);
|
|
|
24070
24359
|
releaseAnnounceDeferral(deployModel, options.announceSummaryFile)
|
|
24071
24360
|
);
|
|
24072
24361
|
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(
|
|
24362
|
+
const dispatch = await appendServedChannelLegs(
|
|
24080
24363
|
deps,
|
|
24081
|
-
{ repo: ctx.repo,
|
|
24082
|
-
|
|
24364
|
+
{ repo: ctx.repo, tag, releaseSha, watch, deploy: deployLeg, publish: publishLeg, installer: meta.installer },
|
|
24365
|
+
appendPublishDispatch(deployDispatch, publishDispatch)
|
|
24083
24366
|
);
|
|
24084
|
-
dispatch = await appendServedChannelProof(deps, { repo: ctx.repo, tag, shipped: shipLegsGreen(deployLeg, publishLeg) }, dispatch);
|
|
24085
24367
|
alignment = await aligning;
|
|
24086
24368
|
const announceNote = await announcing;
|
|
24087
24369
|
return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors, ...alignment ? { alignment } : {} };
|
|
@@ -24150,6 +24432,9 @@ async function probeAncestor(deps, ancestor, descendant, label) {
|
|
|
24150
24432
|
}
|
|
24151
24433
|
}
|
|
24152
24434
|
async function runRcandResume(deps, options = {}) {
|
|
24435
|
+
return withTrainWriter(deps, "rcand --resume", () => runRcandResumeLocked(deps, options));
|
|
24436
|
+
}
|
|
24437
|
+
async function runRcandResumeLocked(deps, options) {
|
|
24153
24438
|
const watch = options.watch ?? false;
|
|
24154
24439
|
const ctx = await buildTrainApplyContext(deps);
|
|
24155
24440
|
await requireCleanTree(deps);
|
|
@@ -24410,6 +24695,12 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
24410
24695
|
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
24696
|
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
24697
|
}
|
|
24698
|
+
const shippedDispatch = await appendServedChannelLegs(
|
|
24699
|
+
deps,
|
|
24700
|
+
{ repo: ctx.repo, tag, releaseSha: tagSha, watch, deploy: phaseInputs.deploy, publish: phaseInputs.publish, installer: meta.installer },
|
|
24701
|
+
dispatch2
|
|
24702
|
+
);
|
|
24703
|
+
Object.assign(dispatch2, shippedDispatch);
|
|
24413
24704
|
if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
|
|
24414
24705
|
const settledBySupersede = persisted?.phases.deploy.superseded === true || persisted?.phases.publish.superseded === true;
|
|
24415
24706
|
const announceNote2 = persisted && !shippedBefore && !settledBySupersede && (prodReleaseTargets(deployModel).length > 0 || companion !== null) ? await announceShippedRelease(
|
|
@@ -24543,7 +24834,11 @@ ${recovery.note}`), recoveryInput);
|
|
|
24543
24834
|
landed: "the immutable tag, the re-proven wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
24544
24835
|
});
|
|
24545
24836
|
}
|
|
24546
|
-
const dispatch =
|
|
24837
|
+
const dispatch = await appendServedChannelLegs(
|
|
24838
|
+
deps,
|
|
24839
|
+
{ repo: ctx.repo, tag, releaseSha: tagSha, watch, deploy: deployLeg, publish: publishLeg, installer: meta.installer },
|
|
24840
|
+
appendPublishDispatch(deployDispatch, publishDispatch)
|
|
24841
|
+
);
|
|
24547
24842
|
const announcing = announceShippedRelease(
|
|
24548
24843
|
deps,
|
|
24549
24844
|
{ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
|
|
@@ -24600,6 +24895,9 @@ async function withCheckoutRestoredAfterReleaseResume(deps, startBranch, operati
|
|
|
24600
24895
|
return withCheckoutRestoredAfterRelease(deps, result, startBranch);
|
|
24601
24896
|
}
|
|
24602
24897
|
async function runReleaseResume(deps, options = {}) {
|
|
24898
|
+
return withTrainWriter(deps, "release --resume", () => runReleaseResumeLocked(deps, options));
|
|
24899
|
+
}
|
|
24900
|
+
async function runReleaseResumeLocked(deps, options) {
|
|
24603
24901
|
const ctx = await buildTrainApplyContext(deps);
|
|
24604
24902
|
await requireCleanTree(deps);
|
|
24605
24903
|
await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
|
|
@@ -24615,6 +24913,10 @@ async function runReleaseResume(deps, options = {}) {
|
|
|
24615
24913
|
);
|
|
24616
24914
|
}
|
|
24617
24915
|
async function runReleaseAbort(deps, options = {}) {
|
|
24916
|
+
if (!options.approved) return runReleaseAbortLocked(deps, options);
|
|
24917
|
+
return withTrainWriter(deps, "release --abort", () => runReleaseAbortLocked(deps, options));
|
|
24918
|
+
}
|
|
24919
|
+
async function runReleaseAbortLocked(deps, options) {
|
|
24618
24920
|
if (!options.approved) {
|
|
24619
24921
|
throw new Error("release --abort requires --apply after explicit approval; nothing was written");
|
|
24620
24922
|
}
|
|
@@ -24689,7 +24991,7 @@ async function runReleaseSupersedeDeploy(deps, options = {}) {
|
|
|
24689
24991
|
throw new Error("release --supersede-deploy requires --apply after explicit master approval; nothing was written");
|
|
24690
24992
|
}
|
|
24691
24993
|
try {
|
|
24692
|
-
return await supersedeContentCausedDeployLeg(deps, process.cwd());
|
|
24994
|
+
return await withTrainWriter(deps, "release --supersede-deploy", () => supersedeContentCausedDeployLeg(deps, process.cwd()));
|
|
24693
24995
|
} catch (e) {
|
|
24694
24996
|
const message2 = e instanceof Error ? e.message : String(e);
|
|
24695
24997
|
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
@@ -24700,7 +25002,7 @@ async function runReleaseSupersedePublish(deps, options = {}) {
|
|
|
24700
25002
|
throw new Error("release --supersede-publish requires --apply after explicit master approval; nothing was written");
|
|
24701
25003
|
}
|
|
24702
25004
|
try {
|
|
24703
|
-
return await supersedeContentCausedPublishLeg(deps, process.cwd());
|
|
25005
|
+
return await withTrainWriter(deps, "release --supersede-publish", () => supersedeContentCausedPublishLeg(deps, process.cwd()));
|
|
24704
25006
|
} catch (e) {
|
|
24705
25007
|
const message2 = e instanceof Error ? e.message : String(e);
|
|
24706
25008
|
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
@@ -24711,13 +25013,17 @@ async function runReleaseSupersedeDeployAndPublish(deps, options = {}) {
|
|
|
24711
25013
|
throw new Error("release --supersede-deploy --supersede-publish requires --apply after explicit master approval; nothing was written");
|
|
24712
25014
|
}
|
|
24713
25015
|
try {
|
|
24714
|
-
return await supersedeContentCausedDeployAndPublishLegs(deps, process.cwd());
|
|
25016
|
+
return await withTrainWriter(deps, "release --supersede-deploy --supersede-publish", () => supersedeContentCausedDeployAndPublishLegs(deps, process.cwd()));
|
|
24715
25017
|
} catch (e) {
|
|
24716
25018
|
const message2 = e instanceof Error ? e.message : String(e);
|
|
24717
25019
|
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
24718
25020
|
}
|
|
24719
25021
|
}
|
|
24720
25022
|
async function runReleasePublishRetry(deps, runId, options = {}) {
|
|
25023
|
+
if (!options.approved) return runReleasePublishRetryLocked(deps, runId, options);
|
|
25024
|
+
return withTrainWriter(deps, "release --retry-publish", () => runReleasePublishRetryLocked(deps, runId, options));
|
|
25025
|
+
}
|
|
25026
|
+
async function runReleasePublishRetryLocked(deps, runId, options) {
|
|
24721
25027
|
if (!options.approved) {
|
|
24722
25028
|
throw new Error("release --retry-publish requires --apply after explicit approval; nothing was written");
|
|
24723
25029
|
}
|
|
@@ -25162,6 +25468,9 @@ async function resolveHotfixVersion(deps, env = process.env) {
|
|
|
25162
25468
|
return { tag: `v${version}`, version, baseTag };
|
|
25163
25469
|
}
|
|
25164
25470
|
async function runTrainApply(command, deps, options = {}) {
|
|
25471
|
+
return withTrainWriter(deps, `${command} --apply`, () => runTrainApplyLocked(command, deps, options));
|
|
25472
|
+
}
|
|
25473
|
+
async function runTrainApplyLocked(command, deps, options = {}) {
|
|
25165
25474
|
const watch = options.watch ?? false;
|
|
25166
25475
|
await runTrainDoctor({ lane: command === "rcand" ? "rcand" : "release", dev: command === "release" && options.dev === true, heal: true, train: deps, refuse: true });
|
|
25167
25476
|
const ctx = await buildTrainApplyContext(deps);
|
|
@@ -30588,7 +30897,7 @@ function registerDeployCommands(program3) {
|
|
|
30588
30897
|
init_cli_shared();
|
|
30589
30898
|
|
|
30590
30899
|
// 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"];
|
|
30900
|
+
var UNSET_KEYS = ["installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary"];
|
|
30592
30901
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
30593
30902
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
30594
30903
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -30846,6 +31155,17 @@ function parseRuntimeVaultOnlyVar(raw) {
|
|
|
30846
31155
|
if (raw === "false") return false;
|
|
30847
31156
|
throw new Error("org project set: runtimeVaultOnly must be true or false");
|
|
30848
31157
|
}
|
|
31158
|
+
function parseInstallerVar(raw) {
|
|
31159
|
+
let value;
|
|
31160
|
+
try {
|
|
31161
|
+
value = JSON.parse(raw);
|
|
31162
|
+
} catch {
|
|
31163
|
+
throw new Error('org project set: installer must be JSON, e.g. {"product":"mm-strategy","family":"mmi"}');
|
|
31164
|
+
}
|
|
31165
|
+
const error = installerDeclarationError(value, "mmi");
|
|
31166
|
+
if (error && !error.includes("breaks the sign-in rule")) throw new Error(`org project set: ${error}`);
|
|
31167
|
+
return value;
|
|
31168
|
+
}
|
|
30849
31169
|
function parseSeedCanaryVar(raw) {
|
|
30850
31170
|
if (raw === "true") return true;
|
|
30851
31171
|
if (raw === "false") return false;
|
|
@@ -31016,7 +31336,8 @@ var SETTABLE_VAR_KEYS = [
|
|
|
31016
31336
|
"ciExemptReason",
|
|
31017
31337
|
"gate",
|
|
31018
31338
|
"secrets",
|
|
31019
|
-
"seedCanary"
|
|
31339
|
+
"seedCanary",
|
|
31340
|
+
"installer"
|
|
31020
31341
|
];
|
|
31021
31342
|
var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
31022
31343
|
var SETTABLE_VAR_HINTS = {
|
|
@@ -31029,6 +31350,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
31029
31350
|
consumesDesignSystem: '"fofu"',
|
|
31030
31351
|
runtimeVaultOnly: "true|false",
|
|
31031
31352
|
seedCanary: "true|false",
|
|
31353
|
+
installer: "JSON {product,family:jerv|mmi,login?:github|google,gate?:https origin of its own sign-in server} \u2014 the repo ships a one-line installer on get.jerv.ai or get.mutmut.work",
|
|
31032
31354
|
repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
|
|
31033
31355
|
oauth: "JSON {subdomains,domains,callbackPath,extraCallbackPaths}",
|
|
31034
31356
|
requiredGcpApis: "comma-string",
|
|
@@ -31164,6 +31486,8 @@ function buildProjectSetPatch(input) {
|
|
|
31164
31486
|
patch[key] = parseRuntimeVaultOnlyVar(raw);
|
|
31165
31487
|
} else if (key === "seedCanary") {
|
|
31166
31488
|
patch[key] = parseSeedCanaryVar(raw);
|
|
31489
|
+
} else if (key === "installer") {
|
|
31490
|
+
patch[key] = parseInstallerVar(raw);
|
|
31167
31491
|
} else if (key === "consumesDesignSystem") {
|
|
31168
31492
|
patch[key] = parseConsumesDesignSystemVar(raw);
|
|
31169
31493
|
} else if (key === "publishDir") {
|
|
@@ -44403,6 +44727,9 @@ async function hotfixReleaseExists(deps, ctx, tag) {
|
|
|
44403
44727
|
}
|
|
44404
44728
|
}
|
|
44405
44729
|
async function runHotfixStart(deps, options, doctor = runTrainDoctor) {
|
|
44730
|
+
return withTrainWriter(deps, "hotfix start", () => runHotfixStartLocked(deps, options, doctor));
|
|
44731
|
+
}
|
|
44732
|
+
async function runHotfixStartLocked(deps, options, doctor) {
|
|
44406
44733
|
await doctor({ lane: "hotfix", heal: true, train: deps, refuse: true });
|
|
44407
44734
|
const ctx = await buildTrainApplyContext(deps);
|
|
44408
44735
|
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
@@ -44700,6 +45027,24 @@ async function appendHotfixGatewayRun(deps, repo, tag, sha, runs, note) {
|
|
|
44700
45027
|
}
|
|
44701
45028
|
return gateway.note;
|
|
44702
45029
|
}
|
|
45030
|
+
async function appendHotfixServedChannel(deps, input, runs, note) {
|
|
45031
|
+
const before = hotfixDispatchFromRuns(runs, note);
|
|
45032
|
+
const after = await appendServedChannelLegs(
|
|
45033
|
+
deps,
|
|
45034
|
+
{ repo: input.repo, tag: input.tag, releaseSha: input.sha, watch: true, deploy: input.deploy, publish: input.publish, installer: input.installer },
|
|
45035
|
+
before
|
|
45036
|
+
);
|
|
45037
|
+
for (const row of servedChannelRowsAfter(before, after)) {
|
|
45038
|
+
runs.push({
|
|
45039
|
+
workflow: row.workflow,
|
|
45040
|
+
...row.runId != null ? { runId: row.runId } : {},
|
|
45041
|
+
...row.runUrl ? { url: row.runUrl } : {},
|
|
45042
|
+
...row.runUrlNote ? { runUrlNote: row.runUrlNote } : {},
|
|
45043
|
+
conclusion: row.conclusion
|
|
45044
|
+
});
|
|
45045
|
+
}
|
|
45046
|
+
return after.note;
|
|
45047
|
+
}
|
|
44703
45048
|
function hotfixPhaseInputsFromRuns(deployModel, runs, note, publishDispatch, publishRequired, opts) {
|
|
44704
45049
|
const dispatch = hotfixDispatchFromRuns(runs, note);
|
|
44705
45050
|
return {
|
|
@@ -44822,6 +45167,9 @@ async function assertConsumerBomLineage(deps, mergedSha, tag) {
|
|
|
44822
45167
|
}
|
|
44823
45168
|
}
|
|
44824
45169
|
async function runHotfixRelease(deps, versionInput, options = {}, doctor = runTrainDoctor) {
|
|
45170
|
+
return withTrainWriter(deps, `hotfix release ${versionInput}`, () => runHotfixReleaseLocked(deps, versionInput, options, doctor));
|
|
45171
|
+
}
|
|
45172
|
+
async function runHotfixReleaseLocked(deps, versionInput, options, doctor) {
|
|
44825
45173
|
await doctor({ lane: "hotfix", heal: true, train: deps, refuse: true });
|
|
44826
45174
|
const ctx = await buildTrainApplyContext(deps);
|
|
44827
45175
|
const { tag, version } = normalizeHotfixVersion(versionInput);
|
|
@@ -44968,6 +45316,7 @@ ${decision.note}`);
|
|
|
44968
45316
|
);
|
|
44969
45317
|
await recordHotfixPhase(ledger, anchors, "deploy", ledgerInputs.deploy, "the immutable tag, the green wall, origin/main, and the GitHub Release");
|
|
44970
45318
|
await recordHotfixPhase(ledger, anchors, "publish", ledgerInputs.publish, "the immutable tag, the green wall, origin/main, the GitHub Release, and the deploy path");
|
|
45319
|
+
deployNote = await appendHotfixServedChannel(deps, { repo: ctx.repo, tag, sha: mergedSha, deploy: ledgerInputs.deploy, publish: ledgerInputs.publish, installer: meta.installer }, runs, deployNote);
|
|
44971
45320
|
const announceNote = releaseExists ? void 0 : await announceShippedRelease(
|
|
44972
45321
|
deps,
|
|
44973
45322
|
{ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile },
|
|
@@ -45036,6 +45385,9 @@ ${decision.note}`);
|
|
|
45036
45385
|
return { ...result, ...localSync ? { localSync } : {} };
|
|
45037
45386
|
}
|
|
45038
45387
|
async function runHotfixRetryDeploy(deps, versionInput) {
|
|
45388
|
+
return withTrainWriter(deps, `hotfix release ${versionInput} --retry-deploy`, () => runHotfixRetryDeployLocked(deps, versionInput));
|
|
45389
|
+
}
|
|
45390
|
+
async function runHotfixRetryDeployLocked(deps, versionInput) {
|
|
45039
45391
|
const ctx = await buildTrainApplyContext(deps);
|
|
45040
45392
|
const { tag } = normalizeHotfixVersion(versionInput);
|
|
45041
45393
|
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
@@ -45651,6 +46003,8 @@ async function announceSummaryRefusal(path2) {
|
|
|
45651
46003
|
}
|
|
45652
46004
|
function trainApplyDeps() {
|
|
45653
46005
|
return {
|
|
46006
|
+
// #7027: every mutating train verb holds this checkout's train writer lock for its whole run.
|
|
46007
|
+
trainWriter: (verb, cwd) => acquireTrainWriterLock(cwd, `mmi-cli devops ${verb}`),
|
|
45654
46008
|
run: async (file, args) => {
|
|
45655
46009
|
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
46010
|
try {
|
|
@@ -47866,10 +48220,9 @@ function resolveHouseShim(argv) {
|
|
|
47866
48220
|
const flatPath = flatTokens.join(" ");
|
|
47867
48221
|
const flatHouse = houseForPath(flatPath);
|
|
47868
48222
|
if (!flatHouse || flatHouse === "core") return;
|
|
47869
|
-
|
|
47870
|
-
|
|
47871
|
-
|
|
47872
|
-
);
|
|
48223
|
+
process.stderr.write(`mmi-cli: '${flatPath}' is the old flat form \u2014 ran it as \`mmi-cli ${canonicalPathFor(registeredPathPrefix(flatPath))}\` (use that spelling)
|
|
48224
|
+
`);
|
|
48225
|
+
return;
|
|
47873
48226
|
}
|
|
47874
48227
|
const remainder = argv.slice(3);
|
|
47875
48228
|
const first = remainder[0];
|
|
@@ -47891,6 +48244,14 @@ function resolveHouseShim(argv) {
|
|
|
47891
48244
|
if (actual && isDeclaredHouseAlias(houseToken, lookupPath)) {
|
|
47892
48245
|
process.stderr.write(
|
|
47893
48246
|
`mmi-cli: '${lookupPath}' lives in house '${actual}', not '${houseToken}' \u2014 routed to \`mmi-cli ${canonicalPathFor(lookupPath)}\`
|
|
48247
|
+
`
|
|
48248
|
+
);
|
|
48249
|
+
argv.splice(2, 1);
|
|
48250
|
+
return;
|
|
48251
|
+
}
|
|
48252
|
+
if (actual && actual !== "core" && registeredPathPrefix(lookupPath) === lookupPath) {
|
|
48253
|
+
process.stderr.write(
|
|
48254
|
+
`mmi-cli: '${lookupPath}' lives in house '${actual}', not '${houseToken}' \u2014 ran it as \`mmi-cli ${canonicalPathFor(lookupPath)}\`
|
|
47894
48255
|
`
|
|
47895
48256
|
);
|
|
47896
48257
|
argv.splice(2, 1);
|
|
@@ -47919,8 +48280,8 @@ function printHouseRootHelp(house) {
|
|
|
47919
48280
|
}
|
|
47920
48281
|
lines2.push(
|
|
47921
48282
|
"",
|
|
47922
|
-
`Canonical: \`mmi-cli ${house} <command> \u2026\`. The flat \`mmi-cli <command> \u2026\`
|
|
47923
|
-
|
|
48283
|
+
`Canonical: \`mmi-cli ${house} <command> \u2026\`. The old flat \`mmi-cli <command> \u2026\` form still runs`,
|
|
48284
|
+
"with a notice naming the canonical spelling (#7034), so an out-of-date plugin keeps working."
|
|
47924
48285
|
);
|
|
47925
48286
|
consoleIo.log(lines2.join("\n"));
|
|
47926
48287
|
}
|
package/package.json
CHANGED