@mutmutco/cli 4.5.36 → 4.5.38
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 +296 -59
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -863,6 +863,9 @@ var init_house_map = __esm({
|
|
|
863
863
|
});
|
|
864
864
|
|
|
865
865
|
// src/stdin-inject.ts
|
|
866
|
+
function setInjectedStdin(payload) {
|
|
867
|
+
injectedStdin = payload;
|
|
868
|
+
}
|
|
866
869
|
function stdinHasPipedInput(statFd = () => (0, import_node_fs4.fstatSync)(0), getIsTTY = () => process.stdin.isTTY) {
|
|
867
870
|
try {
|
|
868
871
|
const stat4 = statFd();
|
|
@@ -922,6 +925,34 @@ var init_stdin_inject = __esm({
|
|
|
922
925
|
});
|
|
923
926
|
|
|
924
927
|
// src/cli-shared.ts
|
|
928
|
+
var cli_shared_exports = {};
|
|
929
|
+
__export(cli_shared_exports, {
|
|
930
|
+
DEFAULT_EXEC_TIMEOUT_MS: () => DEFAULT_EXEC_TIMEOUT_MS,
|
|
931
|
+
ExecDeadlineError: () => ExecDeadlineError,
|
|
932
|
+
GIT_TIMEOUT_MS: () => GIT_TIMEOUT_MS,
|
|
933
|
+
consoleIo: () => consoleIo,
|
|
934
|
+
execFileHard: () => execFileHard,
|
|
935
|
+
execFileP: () => execFileP,
|
|
936
|
+
fail: () => fail,
|
|
937
|
+
failGraceful: () => failGraceful,
|
|
938
|
+
failGracefulEnvelope: () => failGracefulEnvelope,
|
|
939
|
+
gitOut: () => gitOut,
|
|
940
|
+
githubLogin: () => githubLogin,
|
|
941
|
+
hubHeaders: () => hubHeaders,
|
|
942
|
+
installBrokenPipeGuard: () => installBrokenPipeGuard,
|
|
943
|
+
installProcessBackstop: () => installProcessBackstop,
|
|
944
|
+
isOrgRepoRoot: () => isOrgRepoRoot,
|
|
945
|
+
jsonParity: () => jsonParity,
|
|
946
|
+
loadConfig: () => loadConfig,
|
|
947
|
+
mutating: () => mutating,
|
|
948
|
+
planMutation: () => planMutation,
|
|
949
|
+
printLine: () => printLine,
|
|
950
|
+
rawFlag: () => rawFlag,
|
|
951
|
+
rawValue: () => rawValue,
|
|
952
|
+
readStdin: () => readStdin,
|
|
953
|
+
repoProject: () => repoProject,
|
|
954
|
+
setInjectedStdin: () => setInjectedStdin
|
|
955
|
+
});
|
|
925
956
|
function killProcessTree(pid) {
|
|
926
957
|
try {
|
|
927
958
|
if (process.platform === "win32") {
|
|
@@ -11654,11 +11685,11 @@ function writeMarketplacePinsOnDisk(path2, pins, succeeded, failedVerb, declineW
|
|
|
11654
11685
|
return `could NOT ${failedVerb} ${[...pins.keys()].join(", ")} \u2014 set it by hand`;
|
|
11655
11686
|
}
|
|
11656
11687
|
const verify = readKnownMarketplacesFile(path2);
|
|
11657
|
-
const
|
|
11688
|
+
const failed2 = [...pins].filter(([name, want]) => {
|
|
11658
11689
|
const got = readKnownMarketplace(verify, name);
|
|
11659
11690
|
return typeof want.autoUpdate === "boolean" && got.declared !== want.autoUpdate || want.ref !== void 0 && got.ref !== want.ref;
|
|
11660
11691
|
});
|
|
11661
|
-
return
|
|
11692
|
+
return failed2.length ? `${failedVerb} did NOT take for ${failed2.map(([n]) => n).join(", ")} \u2014 set it by hand` : succeeded(pins);
|
|
11662
11693
|
});
|
|
11663
11694
|
} catch (error) {
|
|
11664
11695
|
if (error instanceof Error && error.message.startsWith("shared-document lock timeout:")) return error.message;
|
|
@@ -16840,10 +16871,10 @@ var rollout_plan_default = {
|
|
|
16840
16871
|
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)."
|
|
16841
16872
|
},
|
|
16842
16873
|
baseline: {
|
|
16843
|
-
version: "4.5.
|
|
16844
|
-
tag: "v4.5.
|
|
16845
|
-
commit: "
|
|
16846
|
-
npm: "@mutmutco/cli@4.5.
|
|
16874
|
+
version: "4.5.38",
|
|
16875
|
+
tag: "v4.5.38",
|
|
16876
|
+
commit: "94bd7909821f",
|
|
16877
|
+
npm: "@mutmutco/cli@4.5.38"
|
|
16847
16878
|
},
|
|
16848
16879
|
exitCriterion: "fleet-n-of-n",
|
|
16849
16880
|
hubOnlyShortcut: "forbidden",
|
|
@@ -16860,14 +16891,14 @@ var rollout_plan_default = {
|
|
|
16860
16891
|
repo: "mutmutco/mmi-hub",
|
|
16861
16892
|
role: "canary",
|
|
16862
16893
|
schedule: "train",
|
|
16863
|
-
v3Target: "v4.5.
|
|
16894
|
+
v3Target: "v4.5.38"
|
|
16864
16895
|
}
|
|
16865
16896
|
],
|
|
16866
16897
|
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.",
|
|
16867
16898
|
rollback: {
|
|
16868
16899
|
independent: true,
|
|
16869
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
16870
|
-
v3Target: "v4.5.
|
|
16900
|
+
mechanism: "npm dist-tag latest -> 4.5.38 and redeploy the Hub Lambda from tag v4.5.38 (94bd7909821f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
16901
|
+
v3Target: "v4.5.38 (@mutmutco/cli@4.5.38, tag commit 94bd7909821f \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
16871
16902
|
}
|
|
16872
16903
|
},
|
|
16873
16904
|
{
|
|
@@ -19633,9 +19664,9 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
|
|
|
19633
19664
|
});
|
|
19634
19665
|
const satisfiedNote = autoSatisfied.size ? `, auto-satisfied (never runs on a tag, see #2404): ${[...autoSatisfied].join(", ")}` : "";
|
|
19635
19666
|
lastStatus = `${states.map(([c, s]) => `${c}=${s}`).join(", ")}${satisfiedNote}`;
|
|
19636
|
-
const
|
|
19637
|
-
if (
|
|
19638
|
-
throw new Error(`required train check failed: ${
|
|
19667
|
+
const failed2 = states.filter(([, s]) => s === "failed").map(([c]) => c);
|
|
19668
|
+
if (failed2.length > 0) {
|
|
19669
|
+
throw new Error(`required train check failed: ${failed2.join(", ")} (${lastStatus}${fleetNote})`);
|
|
19639
19670
|
}
|
|
19640
19671
|
if (states.every(([, s]) => s === "success")) {
|
|
19641
19672
|
return `required checks passed: ${pending.join(", ") || "(none pending)"}${satisfiedNote}${fleetNote}`;
|
|
@@ -19774,6 +19805,26 @@ function installerPageError(value) {
|
|
|
19774
19805
|
if (url.search || url.hash) return "installer.page must not carry a query string or fragment";
|
|
19775
19806
|
return null;
|
|
19776
19807
|
}
|
|
19808
|
+
var INSTALLER_VERIFY_HOSTS = ["mutmut.work", "dev.mutmut.work", "rc.mutmut.work"];
|
|
19809
|
+
var INSTALLER_VERIFY_STAGES = ["dev", "rc", "main"];
|
|
19810
|
+
function installerVerifyError(value) {
|
|
19811
|
+
const shape = `installer.verify must be {dev?, rc?, main?} with at least one https URL on ${INSTALLER_VERIFY_HOSTS.join(", ")}`;
|
|
19812
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return shape;
|
|
19813
|
+
const entries = Object.entries(value);
|
|
19814
|
+
if (entries.length === 0) return shape;
|
|
19815
|
+
for (const [stage, url] of entries) {
|
|
19816
|
+
if (!INSTALLER_VERIFY_STAGES.includes(stage)) return `installer.verify.${stage} is not a stage \u2014 expected dev, rc or main`;
|
|
19817
|
+
let parsed;
|
|
19818
|
+
try {
|
|
19819
|
+
parsed = typeof url === "string" && url.trim() === url ? new URL(url) : null;
|
|
19820
|
+
} catch {
|
|
19821
|
+
parsed = null;
|
|
19822
|
+
}
|
|
19823
|
+
if (!parsed || parsed.protocol !== "https:" || parsed.username || parsed.password || !INSTALLER_VERIFY_HOSTS.includes(parsed.hostname)) return shape;
|
|
19824
|
+
if (parsed.search || parsed.hash) return `installer.verify.${stage} must not carry a query string or fragment`;
|
|
19825
|
+
}
|
|
19826
|
+
return null;
|
|
19827
|
+
}
|
|
19777
19828
|
function installerVersionUrl(installer) {
|
|
19778
19829
|
if (typeof installer.gate === "string" && installer.gate) return `${installer.gate.replace(/\/+$/, "")}/release/version`;
|
|
19779
19830
|
return `https://${INSTALLER_FAMILY_HOSTS[installer.family]}/${installer.product}/release/version`;
|
|
@@ -19781,14 +19832,14 @@ function installerVersionUrl(installer) {
|
|
|
19781
19832
|
function installerDeclarationError(value, division) {
|
|
19782
19833
|
if (value === void 0 || value === null) return null;
|
|
19783
19834
|
if (typeof value !== "object" || Array.isArray(value)) {
|
|
19784
|
-
return "installer must be an object {product, family, login?, gate?}";
|
|
19835
|
+
return "installer must be an object {product, family, login?, gate?, page?, verify?}";
|
|
19785
19836
|
}
|
|
19786
19837
|
const record = (
|
|
19787
19838
|
/** @type {Record<string, unknown>} */
|
|
19788
19839
|
value
|
|
19789
19840
|
);
|
|
19790
19841
|
for (const key of Object.keys(record)) {
|
|
19791
|
-
if (!["product", "family", "login", "gate", "page"].includes(key)) return `installer.${key} is not a field \u2014 expected product, family and optionally login, gate and
|
|
19842
|
+
if (!["product", "family", "login", "gate", "page", "verify"].includes(key)) return `installer.${key} is not a field \u2014 expected product, family and optionally login, gate, page and verify`;
|
|
19792
19843
|
}
|
|
19793
19844
|
if (typeof record.product !== "string" || !PRODUCT_RE.test(record.product)) {
|
|
19794
19845
|
return "installer.product must be a lowercase kebab name (e.g. mm-strategy) \u2014 it is the /<product> path on the family host";
|
|
@@ -19812,6 +19863,10 @@ function installerDeclarationError(value, division) {
|
|
|
19812
19863
|
const pageError = installerPageError(record.page);
|
|
19813
19864
|
if (pageError) return pageError;
|
|
19814
19865
|
}
|
|
19866
|
+
if (record.verify !== void 0) {
|
|
19867
|
+
const verifyError = installerVerifyError(record.verify);
|
|
19868
|
+
if (verifyError) return verifyError;
|
|
19869
|
+
}
|
|
19815
19870
|
const expected = expectedInstallerLogin(
|
|
19816
19871
|
/** @type {'jerv' | 'mmi'} */
|
|
19817
19872
|
record.family,
|
|
@@ -19937,9 +19992,9 @@ async function readVercelDeployment(api, repo, sha, target, since) {
|
|
|
19937
19992
|
}
|
|
19938
19993
|
if (newest.size === 0) return void 0;
|
|
19939
19994
|
const reads = await Promise.all([...newest].map(async ([env, id]) => ({ env, id, ...await readOne(api, repo, id, since) })));
|
|
19940
|
-
const
|
|
19995
|
+
const failed2 = reads.find((r) => FAILED.has(r.state));
|
|
19941
19996
|
const unfinished = reads.find((r) => !isTerminalVercelState(r.state));
|
|
19942
|
-
const pick =
|
|
19997
|
+
const pick = failed2 ?? unfinished ?? reads[0];
|
|
19943
19998
|
return {
|
|
19944
19999
|
id: pick.id,
|
|
19945
20000
|
environment: reads.map((r) => r.env).join(", "),
|
|
@@ -20332,11 +20387,11 @@ async function appendServedChannelProof(deps, input, dispatch) {
|
|
|
20332
20387
|
const channel = servedChannelFor(input.repo, input.installer);
|
|
20333
20388
|
if (!channel) return dispatch;
|
|
20334
20389
|
const expected = input.tag.replace(/^v/, "");
|
|
20335
|
-
const withProof = (conclusion, evidence, note,
|
|
20390
|
+
const withProof = (conclusion, evidence, note, failed2 = false) => ({
|
|
20336
20391
|
...dispatch,
|
|
20337
20392
|
note: `${dispatch.note}; ${note}`,
|
|
20338
20393
|
workflowRuns: [...dispatch.workflowRuns ?? [], servedChannelRow(channel.product, conclusion, evidence)],
|
|
20339
|
-
...
|
|
20394
|
+
...failed2 ? { deployStatus: "failure" } : {}
|
|
20340
20395
|
});
|
|
20341
20396
|
if (!input.shipped) {
|
|
20342
20397
|
return withProof("pending", `${channel.url} not probed`, `served channel ${channel.url} not probed \u2014 deploy/publish are not proven green, so it is not expected to have moved`);
|
|
@@ -20611,8 +20666,8 @@ function shipLegsGreen(deploy, publish) {
|
|
|
20611
20666
|
async function announceShippedRelease(deps, args, model, legs, deferred) {
|
|
20612
20667
|
if (!deps.announce) return void 0;
|
|
20613
20668
|
if (trainObservesShipment(model) && !shipLegsGreen(legs.deploy, legs.publish)) {
|
|
20614
|
-
const
|
|
20615
|
-
return `not announced: deploy/publish ${
|
|
20669
|
+
const failed2 = legs.deploy.state === "failed" || legs.publish.state === "failed";
|
|
20670
|
+
return `not announced: deploy/publish ${failed2 ? "FAILED" : "not proven green"} \u2014 ${deferred}`;
|
|
20616
20671
|
}
|
|
20617
20672
|
return (await deps.announce(args)).note;
|
|
20618
20673
|
}
|
|
@@ -23378,8 +23433,8 @@ function ledgerFailedRemedy(ledger) {
|
|
|
23378
23433
|
const openLegs = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
|
|
23379
23434
|
const tails = formatFailedLegTails(ledger);
|
|
23380
23435
|
if (hotfixRetryDeployShape(ledger)) {
|
|
23381
|
-
const
|
|
23382
|
-
return `the published ${ledger.tag} hotfix can retry the failed ${joinLegNames(
|
|
23436
|
+
const failed2 = openLegs.filter((l) => l.state === "failed").map((l) => l.phase);
|
|
23437
|
+
return `the published ${ledger.tag} hotfix can retry the failed ${joinLegNames(failed2)} of the same tag: \`mmi-cli devops hotfix release ${ledger.tag} --retry-deploy\` (the deploy helper is idempotent; alignment re-attempts the fold-port PR) (#6985)${tails}`;
|
|
23383
23438
|
}
|
|
23384
23439
|
if (rcandRetryDeployShape(ledger)) {
|
|
23385
23440
|
return `fix the rc deploy failure, then retry the failed deploy of the promoted ${ledger.tag} candidate (same tag and SHA, never re-cut): \`mmi-cli devops rcand --retry-deploy --watch\` (#7053)${tails}`;
|
|
@@ -23547,8 +23602,8 @@ async function supersedeContentCausedDeployLeg(deps, cwd) {
|
|
|
23547
23602
|
`release --supersede-deploy: origin reports ${prior.tag} at ${liveSha.slice(0, 12) || "no tag"} but the ledger anchors ${prior.tagSha.slice(0, 12)} \u2014 an absent or divergent release is NOT supersedeable (release --abort --apply is the unpublished candidate's own exit); nothing was written (#6553)`
|
|
23548
23603
|
);
|
|
23549
23604
|
}
|
|
23550
|
-
const
|
|
23551
|
-
const note =
|
|
23605
|
+
const failed2 = prior.phases.deploy.state === "failed";
|
|
23606
|
+
const note = failed2 ? `superseded: content-caused deploy failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, #6553)` : `superseded: ${prior.tag} deploy outcome uncorrelatable from the release SHA (no failure recorded) \u2014 closed on the owner's evidence (master-approved closure, #6553, #7327)`;
|
|
23552
23607
|
await fence(() => atomicWriteJson(path2, {
|
|
23553
23608
|
...prior,
|
|
23554
23609
|
phases: {
|
|
@@ -23562,7 +23617,7 @@ async function supersedeContentCausedDeployLeg(deps, cwd) {
|
|
|
23562
23617
|
repo: prior.repo,
|
|
23563
23618
|
tag: prior.tag,
|
|
23564
23619
|
tagSha: prior.tagSha,
|
|
23565
|
-
note:
|
|
23620
|
+
note: failed2 ? `closed the ${prior.tag} deploy leg as superseded (skipped) \u2014 the ledger is phases-green; rerun the train doctor to confirm, then run the next patch train (hotfix or release) to carry the cause fix` : `closed the ${prior.tag} uncorrelatable deploy leg as superseded (skipped) \u2014 the ledger is phases-green; rerun the train doctor to confirm`
|
|
23566
23621
|
};
|
|
23567
23622
|
});
|
|
23568
23623
|
}
|
|
@@ -24537,17 +24592,17 @@ async function runTrainDoctor(input) {
|
|
|
24537
24592
|
if (derived.phaseStatus !== "phases-green") {
|
|
24538
24593
|
const openLegs = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
|
|
24539
24594
|
const open2 = openLegs.map((l) => `${l.phase}=${l.state}`).join(", ");
|
|
24540
|
-
const
|
|
24595
|
+
const failed2 = derived.phaseStatus === "phases-failed";
|
|
24541
24596
|
const deployOnlyDeadlock = openLegs.length > 0 && openLegs.every((l) => l.phase === "deploy" && supersedeableDeployLegShape(l));
|
|
24542
24597
|
const supersession = deployOnlyDeadlock ? `; when the deploy failure is content-caused on the already-published ${ledger.tag} (the immutable tag can never deploy green), the sanctioned exit is \`mmi-cli devops release --supersede-deploy --apply\` after explicit master approval \u2014 the next patch train carries the cause fix (#6553)` : "";
|
|
24543
24598
|
add({
|
|
24544
|
-
code:
|
|
24599
|
+
code: failed2 ? "ledger-failed" : "ledger-pending",
|
|
24545
24600
|
severity: "blocker",
|
|
24546
24601
|
source: "local",
|
|
24547
24602
|
// #6429: each lane finishes its own run (#6068) — an rcand-lane ledger named as a release
|
|
24548
24603
|
// ledger, with `release --resume` as its remedy, sends the operator to a verb that refuses it.
|
|
24549
24604
|
title: `a prior ${ledger.lane ?? "release"} ledger for ${ledger.tag} is ${derived.phaseStatus} (${open2}) \u2014 a new train must not stack on it (#5987)`,
|
|
24550
|
-
remedy:
|
|
24605
|
+
remedy: failed2 ? ledgerFailedRemedy(ledger) : `finish it first: ${ledgerLaneResumeCommand(ledger)} (alignment-only pending legs close themselves with \`train doctor --heal\` once the PR merges, and a deploy/publish leg whose run already concluded folds under the same heal)${supersession}`
|
|
24551
24606
|
});
|
|
24552
24607
|
} else if (newestRemoteReleaseTag && newestRemoteReleaseTag !== ledger.tag) {
|
|
24553
24608
|
add({
|
|
@@ -24594,21 +24649,21 @@ regenerate the projections (\`node scripts/check-skill-payload.mjs\`) and commit
|
|
|
24594
24649
|
const installed = [];
|
|
24595
24650
|
let pending = firstDir;
|
|
24596
24651
|
let outstanding = failure;
|
|
24597
|
-
let
|
|
24652
|
+
let failed2 = null;
|
|
24598
24653
|
while (pending && !installed.includes(pending)) {
|
|
24599
24654
|
const dir = pending;
|
|
24600
24655
|
try {
|
|
24601
24656
|
await train.run("npm", ["--prefix", dir, "ci"]);
|
|
24602
24657
|
} catch (e) {
|
|
24603
|
-
|
|
24658
|
+
failed2 = { dir, detail: `\`npm --prefix ${dir} ci\` failed: ${message(e)}` };
|
|
24604
24659
|
break;
|
|
24605
24660
|
}
|
|
24606
24661
|
installed.push(dir);
|
|
24607
24662
|
outstanding = await verifyDeps();
|
|
24608
24663
|
pending = outstanding ? /in (cli|updater)\/node_modules/u.exec(outstanding)?.[1] ?? null : null;
|
|
24609
24664
|
}
|
|
24610
|
-
if (
|
|
24611
|
-
add({ code: "hub-build-deps", severity: "blocker", source: "local", title:
|
|
24665
|
+
if (failed2) {
|
|
24666
|
+
add({ code: "hub-build-deps", severity: "blocker", source: "local", title: failed2.detail, remedy: `repair ${failed2.dir}/node_modules by hand, then rerun` });
|
|
24612
24667
|
} else if (outstanding) {
|
|
24613
24668
|
add({ code: "hub-build-deps", severity: "blocker", source: "local", title: `installing ${installed.join(", ")} from ${installed.length === 1 ? "its lockfile" : "their lockfiles"} did not resolve the build dependencies`, remedy: outstanding });
|
|
24614
24669
|
} else {
|
|
@@ -24709,6 +24764,133 @@ function formatTrainDoctor(v) {
|
|
|
24709
24764
|
return lines2.join("\n");
|
|
24710
24765
|
}
|
|
24711
24766
|
|
|
24767
|
+
// src/vercel-version-env.ts
|
|
24768
|
+
var SKIPPED = { status: "skipped", note: "" };
|
|
24769
|
+
function vercelVersionEnvAheadNote(key) {
|
|
24770
|
+
return `a failed branch push can leave ${key} ahead of what is live`;
|
|
24771
|
+
}
|
|
24772
|
+
function safeReason(reason, token) {
|
|
24773
|
+
const compact = reason.replace(/\s+/g, " ").trim().slice(0, 180);
|
|
24774
|
+
const scrubbed = token && compact.includes(token) ? compact.split(token).join("[redacted]") : compact;
|
|
24775
|
+
return scrubbed || "unknown error";
|
|
24776
|
+
}
|
|
24777
|
+
function failed(deps, key, tag, reason) {
|
|
24778
|
+
const note = `vercel version env: FAILED to set ${key}=${tag} (${reason}) \u2014 /healthz will show the commit, not the tag; fix: set it and redeploy once`;
|
|
24779
|
+
deps.warn?.(note);
|
|
24780
|
+
return { status: "failed", note };
|
|
24781
|
+
}
|
|
24782
|
+
function targetLabel(stage) {
|
|
24783
|
+
return stage === "main" ? "production" : "preview";
|
|
24784
|
+
}
|
|
24785
|
+
function envCollectionUrl(project2, team, extra) {
|
|
24786
|
+
const url = new URL(`https://api.vercel.com/v10/projects/${encodeURIComponent(project2)}/env`);
|
|
24787
|
+
if (team) url.searchParams.set("teamId", team);
|
|
24788
|
+
for (const [key, value] of Object.entries(extra ?? {})) url.searchParams.set(key, value);
|
|
24789
|
+
return url.toString();
|
|
24790
|
+
}
|
|
24791
|
+
function envItemUrl(project2, id, team) {
|
|
24792
|
+
const url = new URL(`https://api.vercel.com/v9/projects/${encodeURIComponent(project2)}/env/${encodeURIComponent(id)}`);
|
|
24793
|
+
if (team) url.searchParams.set("teamId", team);
|
|
24794
|
+
return url.toString();
|
|
24795
|
+
}
|
|
24796
|
+
function isStageEnv(row, stage, key) {
|
|
24797
|
+
if (row.key !== key) return false;
|
|
24798
|
+
const target = Array.isArray(row.target) ? row.target.filter((item) => typeof item === "string") : [];
|
|
24799
|
+
if (stage === "main") return target.length === 1 && target[0] === "production";
|
|
24800
|
+
return target.length === 1 && target[0] === "preview" && row.gitBranch === "rc";
|
|
24801
|
+
}
|
|
24802
|
+
function envsOf(body) {
|
|
24803
|
+
if (Array.isArray(body)) return body;
|
|
24804
|
+
if (body && typeof body === "object" && Array.isArray(body.envs)) {
|
|
24805
|
+
return body.envs;
|
|
24806
|
+
}
|
|
24807
|
+
return null;
|
|
24808
|
+
}
|
|
24809
|
+
async function readBody(res, token) {
|
|
24810
|
+
let raw = "";
|
|
24811
|
+
try {
|
|
24812
|
+
raw = await res.text();
|
|
24813
|
+
} catch (e) {
|
|
24814
|
+
return { ok: false, reason: safeReason(e instanceof Error ? e.message : String(e), token) };
|
|
24815
|
+
}
|
|
24816
|
+
if (!res.ok) return { ok: false, reason: `HTTP ${res.status}${raw ? `: ${safeReason(raw, token)}` : ""}` };
|
|
24817
|
+
if (!raw.trim()) return { ok: true, body: {} };
|
|
24818
|
+
try {
|
|
24819
|
+
return { ok: true, body: JSON.parse(raw) };
|
|
24820
|
+
} catch {
|
|
24821
|
+
return { ok: false, reason: "response was not JSON" };
|
|
24822
|
+
}
|
|
24823
|
+
}
|
|
24824
|
+
async function setVercelVersionEnv(deps, meta, stage, tag) {
|
|
24825
|
+
if (meta?.deployModel !== "vercel" || !meta.vercelVersionEnv) return SKIPPED;
|
|
24826
|
+
const { project: project2, key, team } = meta.vercelVersionEnv;
|
|
24827
|
+
if (!project2 || !key) return failed(deps, key || "(missing key)", tag, "vercelVersionEnv is missing project or key");
|
|
24828
|
+
let token;
|
|
24829
|
+
try {
|
|
24830
|
+
token = await deps.readToken();
|
|
24831
|
+
} catch (e) {
|
|
24832
|
+
return failed(deps, key, tag, safeReason(e instanceof Error ? e.message : String(e), null));
|
|
24833
|
+
}
|
|
24834
|
+
if (!token) return failed(deps, key, tag, "no _org/vercel/API_TOKEN");
|
|
24835
|
+
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
24836
|
+
let listed;
|
|
24837
|
+
try {
|
|
24838
|
+
const res = await deps.fetch(envCollectionUrl(project2, team, stage === "rc" ? { gitBranch: "rc" } : void 0), { method: "GET", headers });
|
|
24839
|
+
listed = await readBody(res, token);
|
|
24840
|
+
} catch (e) {
|
|
24841
|
+
return failed(deps, key, tag, safeReason(e instanceof Error ? e.message : String(e), token));
|
|
24842
|
+
}
|
|
24843
|
+
if (!listed.ok) return failed(deps, key, tag, listed.reason);
|
|
24844
|
+
const rows = envsOf(listed.body);
|
|
24845
|
+
if (!rows) return failed(deps, key, tag, "env list returned an unexpected shape");
|
|
24846
|
+
const existing = rows.find((row) => isStageEnv(row, stage, key) && typeof row.id === "string" && row.id.length > 0);
|
|
24847
|
+
const createBody = stage === "main" ? { key, value: tag, type: "plain", target: ["production"] } : { key, value: tag, type: "plain", target: ["preview"], gitBranch: "rc" };
|
|
24848
|
+
try {
|
|
24849
|
+
const res = existing ? await deps.fetch(envItemUrl(project2, existing.id, team), {
|
|
24850
|
+
method: "PATCH",
|
|
24851
|
+
headers,
|
|
24852
|
+
body: JSON.stringify({ value: tag })
|
|
24853
|
+
}) : await deps.fetch(envCollectionUrl(project2, team), {
|
|
24854
|
+
method: "POST",
|
|
24855
|
+
headers,
|
|
24856
|
+
body: JSON.stringify(createBody)
|
|
24857
|
+
});
|
|
24858
|
+
const written = await readBody(res, token);
|
|
24859
|
+
if (!written.ok) return failed(deps, key, tag, written.reason);
|
|
24860
|
+
} catch (e) {
|
|
24861
|
+
return failed(deps, key, tag, safeReason(e instanceof Error ? e.message : String(e), token));
|
|
24862
|
+
}
|
|
24863
|
+
const note = `vercel version env: ${key}=${tag} set on ${targetLabel(stage)}`;
|
|
24864
|
+
deps.warn?.(note);
|
|
24865
|
+
return { status: "set", note };
|
|
24866
|
+
}
|
|
24867
|
+
async function defaultVercelFetch(url, init) {
|
|
24868
|
+
const res = await fetch(url, { ...init, signal: AbortSignal.timeout(15e3) });
|
|
24869
|
+
return { ok: res.ok, status: res.status, text: () => res.text() };
|
|
24870
|
+
}
|
|
24871
|
+
async function readOrgVercelApiToken() {
|
|
24872
|
+
const { loadConfig: loadConfig2, hubHeaders: hubHeaders2, repoProject: repoProject2 } = await Promise.resolve().then(() => (init_cli_shared(), cli_shared_exports));
|
|
24873
|
+
const cfg = await loadConfig2();
|
|
24874
|
+
if (!cfg.sagaApiUrl) return null;
|
|
24875
|
+
const repo = `mutmutco/${(await repoProject2(cfg)).toLowerCase()}`;
|
|
24876
|
+
const res = await fetch(`${cfg.sagaApiUrl}/secrets/get`, {
|
|
24877
|
+
method: "POST",
|
|
24878
|
+
headers: await hubHeaders2({ "content-type": "application/json" }),
|
|
24879
|
+
body: JSON.stringify({ repo, key: "vercel/API_TOKEN", use: true, slug: "_org" }),
|
|
24880
|
+
signal: AbortSignal.timeout(8e3)
|
|
24881
|
+
});
|
|
24882
|
+
if (!res.ok) return null;
|
|
24883
|
+
const body = await res.json();
|
|
24884
|
+
return typeof body.value === "string" ? body.value : null;
|
|
24885
|
+
}
|
|
24886
|
+
function vercelVersionEnvDeps(deps) {
|
|
24887
|
+
return {
|
|
24888
|
+
fetch: deps.vercelFetch ?? defaultVercelFetch,
|
|
24889
|
+
readToken: deps.readVercelApiToken ?? readOrgVercelApiToken,
|
|
24890
|
+
warn: (message2) => deps.warn?.(message2)
|
|
24891
|
+
};
|
|
24892
|
+
}
|
|
24893
|
+
|
|
24712
24894
|
// src/train-apply-phases.ts
|
|
24713
24895
|
var TRAIN_BUMP_INTENTS = ["major", "minor", "patch"];
|
|
24714
24896
|
function readLocalGateWorkflows() {
|
|
@@ -25313,14 +25495,14 @@ async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch
|
|
|
25313
25495
|
let fallbackRecovered = false;
|
|
25314
25496
|
const failedRows = workflowRuns?.filter((run) => run.conclusion === "failure") ?? [];
|
|
25315
25497
|
if (failedRows.length > 0) {
|
|
25316
|
-
for (const
|
|
25317
|
-
const rowPhase =
|
|
25498
|
+
for (const failed2 of failedRows) {
|
|
25499
|
+
const rowPhase = failed2.workflow.includes("publish") ? "publish" : phase;
|
|
25318
25500
|
if (persisted?.phases[rowPhase]?.superseded === true) continue;
|
|
25319
25501
|
const recovery = await recover(deps, ledger, anchors, rowPhase, {
|
|
25320
25502
|
repo: input.repo,
|
|
25321
25503
|
sha: input.sha,
|
|
25322
|
-
failure: `watched ${rowPhase} run failed: ${
|
|
25323
|
-
runs: [{ ...
|
|
25504
|
+
failure: `watched ${rowPhase} run failed: ${failed2.workflow} (${dispatch.note})`,
|
|
25505
|
+
runs: [{ ...failed2, repo: input.repo }],
|
|
25324
25506
|
watched: input.watched
|
|
25325
25507
|
});
|
|
25326
25508
|
if (recovery.note) notes.push(recovery.note);
|
|
@@ -25386,6 +25568,27 @@ async function recordRcandDeployLegs(ledger, deps, anchors, deployModel, d, opts
|
|
|
25386
25568
|
function releaseAnnounceDeferral(model, summaryFile) {
|
|
25387
25569
|
return prodReleaseTargets(model).length > 0 || model === "vercel" ? `\`mmi-cli devops release --resume --watch${summaryFile ? ` --announce-summary-file ${summaryFile}` : ""}\` announces it once they are green` : "a central deploy is proven only under --watch, so this release stays unannounced";
|
|
25388
25570
|
}
|
|
25571
|
+
function annotateVersionEnvDispatch(dispatch, result) {
|
|
25572
|
+
if (!result.note) return dispatch;
|
|
25573
|
+
return { ...dispatch, note: `${dispatch.note}; ${result.note}` };
|
|
25574
|
+
}
|
|
25575
|
+
function annotateVersionEnvAnnounce(note, result) {
|
|
25576
|
+
if (!result.note) return note;
|
|
25577
|
+
return note ? `${note}; ${result.note}` : result.note;
|
|
25578
|
+
}
|
|
25579
|
+
async function pushAfterVercelVersionEnv(deps, meta, stage, tag, pushArgs) {
|
|
25580
|
+
const result = await setVercelVersionEnv(vercelVersionEnvDeps(deps), meta, stage, tag);
|
|
25581
|
+
try {
|
|
25582
|
+
await runGitPush(deps, pushArgs);
|
|
25583
|
+
} catch (e) {
|
|
25584
|
+
const base = e instanceof Error ? e.message : String(e);
|
|
25585
|
+
const parts = [base];
|
|
25586
|
+
if (result.note) parts.push(result.note);
|
|
25587
|
+
if (result.status === "set" && meta.vercelVersionEnv?.key) parts.push(vercelVersionEnvAheadNote(meta.vercelVersionEnv.key));
|
|
25588
|
+
throw new Error(parts.join(" \u2014 "));
|
|
25589
|
+
}
|
|
25590
|
+
return result;
|
|
25591
|
+
}
|
|
25389
25592
|
async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, startBranch, preFold, tagProbe, branchHints, ledger, alignEarly) {
|
|
25390
25593
|
const resumeCommand = options.dev ? "mmi-cli devops release --dev --apply" : "mmi-cli devops release --apply";
|
|
25391
25594
|
const ledgerAnchors = { repo: ctx.repo, tag, tagSha: releaseSha };
|
|
@@ -25482,9 +25685,10 @@ ${recovery.note}`), recoveryInput);
|
|
|
25482
25685
|
landed: "the immutable tag pushed at " + releaseSha.slice(0, 12)
|
|
25483
25686
|
});
|
|
25484
25687
|
if (trueMergeGateNote) checks = `${trueMergeGateNote}; ${checks}`;
|
|
25688
|
+
let versionEnv = { status: "skipped", note: "" };
|
|
25485
25689
|
try {
|
|
25486
25690
|
deps.warn?.("Release: Promoting the verified release");
|
|
25487
|
-
await
|
|
25691
|
+
versionEnv = await pushAfterVercelVersionEnv(deps, meta, "main", tag, ["push", "origin", "main"]);
|
|
25488
25692
|
} catch (e) {
|
|
25489
25693
|
await recordPhase(ledger, deps, ledgerAnchors, "promotion", phaseEntry({ state: "failed", sha: releaseSha, error: `origin/main push failed after ${tag} was pushed: ${e instanceof Error ? e.message : String(e)}` }), { landed: "the immutable tag pushed at " + releaseSha.slice(0, 12) });
|
|
25490
25694
|
throw partialTrainRecoveryError(e, recoveryInput);
|
|
@@ -25517,7 +25721,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
25517
25721
|
workflow: failedReleaseRun?.workflow ?? (isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow")
|
|
25518
25722
|
}
|
|
25519
25723
|
);
|
|
25520
|
-
const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch);
|
|
25724
|
+
const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch), versionEnv);
|
|
25521
25725
|
const deployLeg = phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip" ? { state: "skipped", sha: releaseSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha }));
|
|
25522
25726
|
await recordPhase(ledger, deps, ledgerAnchors, "deploy", deployLeg, {
|
|
25523
25727
|
landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
|
|
@@ -25551,7 +25755,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
25551
25755
|
appendPublishDispatch(deployDispatch, publishDispatch)
|
|
25552
25756
|
);
|
|
25553
25757
|
alignment = await aligning;
|
|
25554
|
-
const announceNote = await announcing;
|
|
25758
|
+
const announceNote = annotateVersionEnvAnnounce(await announcing, versionEnv);
|
|
25555
25759
|
return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors, ...alignment ? { alignment } : {} };
|
|
25556
25760
|
}
|
|
25557
25761
|
function resumeStateOf(status) {
|
|
@@ -25754,10 +25958,10 @@ ${spent.note}`);
|
|
|
25754
25958
|
await recordPhase(ledger, deps, anchors, "ordinaryCi", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified the required-check wall live on this resume" }), { strict, landed: "the immutable rc tag on origin at the resumed SHA" });
|
|
25755
25959
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
25756
25960
|
deps.warn?.("Release: Promoting the verified release candidate");
|
|
25757
|
-
await
|
|
25961
|
+
const versionEnv = await pushAfterVercelVersionEnv(deps, meta, "rc", tag, ["push", "origin", `${tagSha}:refs/heads/rc`]);
|
|
25758
25962
|
await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: tagSha, note: `resumed: ${tagSha.slice(0, 12)} pushed to origin/rc (tag preserved, never re-cut)` }), { strict, landed: "the immutable rc tag and the re-proven required-check wall" });
|
|
25759
25963
|
await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "skipped", sha: tagSha, note: "rcand mints no GitHub Release \u2014 the rc tag is the candidate" }), { strict, landed: "the immutable rc tag, the green wall, and origin/rc" });
|
|
25760
|
-
const dispatch0 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel, watch, autoRunSince, tagSha, "report");
|
|
25964
|
+
const dispatch0 = annotateVersionEnvDispatch(await dispatchDeploy(deps, ctx, "rc", "rc", deployModel, watch, autoRunSince, tagSha, "report"), versionEnv);
|
|
25761
25965
|
const dispatch = await recordRcandDeployLegs(ledger, deps, anchors, deployModel, dispatch0, { strict });
|
|
25762
25966
|
const ledgerReport = releaseLedgerReport(await ledger.load(anchors).catch(() => void 0), ledger.path);
|
|
25763
25967
|
return {
|
|
@@ -26037,7 +26241,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
26037
26241
|
}
|
|
26038
26242
|
await recordPhase(ledger, deps, anchors, "ordinaryCi", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified the required-check wall live on this resume" }), { strict: ledgerMode === "strict", landed: "the immutable tag on origin at the resumed SHA" });
|
|
26039
26243
|
deps.warn?.("Release: Promoting the verified release");
|
|
26040
|
-
await
|
|
26244
|
+
const versionEnv = await pushAfterVercelVersionEnv(deps, meta, "main", tag, ["push", "origin", `${tagSha}:refs/heads/main`]);
|
|
26041
26245
|
steps.push(`pushed ${tagSha.slice(0, 12)} to origin/main`);
|
|
26042
26246
|
await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: tagSha, note: `resumed: ${tagSha.slice(0, 12)} pushed to origin/main (tag preserved, never re-cut)` }), { strict: ledgerMode === "strict", landed: "the immutable tag and the re-proven required-check wall" });
|
|
26043
26247
|
deps.warn?.("Release: Creating the GitHub Release");
|
|
@@ -26067,7 +26271,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
26067
26271
|
workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow"
|
|
26068
26272
|
}
|
|
26069
26273
|
);
|
|
26070
|
-
const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch);
|
|
26274
|
+
const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch), versionEnv);
|
|
26071
26275
|
steps.push(`dispatched the ${deployModel} deploy path`);
|
|
26072
26276
|
const deployLeg = persisted?.phases.deploy.superseded === true ? persisted.phases.deploy : phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip" ? { state: "skipped", sha: tagSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
|
|
26073
26277
|
if (persisted?.phases.deploy.superseded !== true) {
|
|
@@ -26108,7 +26312,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
26108
26312
|
devRollForward = landed.dev;
|
|
26109
26313
|
rcAlignment = landed.rc;
|
|
26110
26314
|
}
|
|
26111
|
-
const announceNote = await announcing;
|
|
26315
|
+
const announceNote = annotateVersionEnvAnnounce(await announcing, versionEnv);
|
|
26112
26316
|
steps.push(`development roll-forward: ${devRollForward.status}`);
|
|
26113
26317
|
if (rcAlignment) steps.push(`rc alignment: ${rcAlignment.status}`);
|
|
26114
26318
|
const followUp = deriveTrainFollowUpStatus({
|
|
@@ -26448,15 +26652,16 @@ ${spent.note}`), rcandRecovery(tag2));
|
|
|
26448
26652
|
await recordPhase(ledger, deps, anchors, "ordinaryCi", phaseEntry({ state: "complete", sha: rcSha, note: checks2 }), { landed: "the immutable rc tag pushed at " + rcSha.slice(0, 12) });
|
|
26449
26653
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
26450
26654
|
deps.warn?.("Release: Promoting the verified release candidate");
|
|
26655
|
+
let versionEnv = { status: "skipped", note: "" };
|
|
26451
26656
|
try {
|
|
26452
|
-
await
|
|
26657
|
+
versionEnv = await pushAfterVercelVersionEnv(deps, meta, "rc", tag2, ["push", "origin", "rc"]);
|
|
26453
26658
|
} catch (e) {
|
|
26454
26659
|
await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "failed", sha: rcSha, error: `origin/rc push failed after ${tag2} was pushed: ${e instanceof Error ? e.message : String(e)}` }), { landed: "the immutable rc tag pushed at " + rcSha.slice(0, 12) });
|
|
26455
26660
|
throw partialTrainRecoveryError(e, rcandRecovery(tag2));
|
|
26456
26661
|
}
|
|
26457
26662
|
await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: rcSha, note: `immutable ${tag2} pushed at ${rcSha.slice(0, 12)}; origin/rc fast-forwarded to it` }), { landed: "the immutable rc tag and the green required-check wall" });
|
|
26458
26663
|
await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "skipped", sha: rcSha, note: "rcand mints no GitHub Release \u2014 the rc tag is the candidate" }), { landed: "the immutable rc tag, the green wall, and origin/rc" });
|
|
26459
|
-
const d0 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha, "report");
|
|
26664
|
+
const d0 = annotateVersionEnvDispatch(await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha, "report"), versionEnv);
|
|
26460
26665
|
const d2 = await recordRcandDeployLegs(ledger, deps, anchors, deployModel2, d0);
|
|
26461
26666
|
const rcLedgerReport = releaseLedgerReport(await ledger?.load(anchors).catch(() => void 0), ledger?.path ?? "(no ledger)");
|
|
26462
26667
|
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, bumpIntent, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, workflowRuns: d2.workflowRuns, deployStatus: d2.deployStatus, ...rcLedgerReport ? { ledger: rcLedgerReport } : {} };
|
|
@@ -28643,13 +28848,13 @@ function buildPluginCachePlan(home, running, deps, opts = {}) {
|
|
|
28643
28848
|
}
|
|
28644
28849
|
function applyPluginCachePlan(plan, remove2, stagingGuard) {
|
|
28645
28850
|
const removed = [];
|
|
28646
|
-
const
|
|
28851
|
+
const failed2 = [];
|
|
28647
28852
|
for (const version of plan.prune) {
|
|
28648
28853
|
try {
|
|
28649
28854
|
remove2(`${plan.cacheRoot}/${version}`);
|
|
28650
28855
|
removed.push(version);
|
|
28651
28856
|
} catch (e) {
|
|
28652
|
-
|
|
28857
|
+
failed2.push({ version, error: e.message });
|
|
28653
28858
|
}
|
|
28654
28859
|
}
|
|
28655
28860
|
const removedStaging = [];
|
|
@@ -28682,7 +28887,7 @@ function applyPluginCachePlan(plan, remove2, stagingGuard) {
|
|
|
28682
28887
|
}
|
|
28683
28888
|
return {
|
|
28684
28889
|
removed,
|
|
28685
|
-
failed,
|
|
28890
|
+
failed: failed2,
|
|
28686
28891
|
bytes: plan.bytes,
|
|
28687
28892
|
removedStaging,
|
|
28688
28893
|
failedStaging,
|
|
@@ -29694,7 +29899,7 @@ init_hub_auth();
|
|
|
29694
29899
|
init_github_client();
|
|
29695
29900
|
|
|
29696
29901
|
// src/project-set.ts
|
|
29697
|
-
var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy"];
|
|
29902
|
+
var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy", "vercelVersionEnv"];
|
|
29698
29903
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
29699
29904
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
29700
29905
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -30048,6 +30253,32 @@ function parseNotifyOnDeployVar(raw) {
|
|
|
30048
30253
|
}
|
|
30049
30254
|
return parsed;
|
|
30050
30255
|
}
|
|
30256
|
+
var VERCEL_VERSION_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
30257
|
+
var VERCEL_VERSION_ENV_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
30258
|
+
function parseVercelVersionEnvVar(raw) {
|
|
30259
|
+
let parsed;
|
|
30260
|
+
try {
|
|
30261
|
+
parsed = JSON.parse(raw);
|
|
30262
|
+
} catch {
|
|
30263
|
+
throw new Error('org project set: vercelVersionEnv must be JSON, e.g. {"project":"mm-work","key":"MMW_VERSION","team":"team_xxx"}');
|
|
30264
|
+
}
|
|
30265
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
30266
|
+
throw new Error("org project set: vercelVersionEnv must be a JSON object { project, key, team? }");
|
|
30267
|
+
}
|
|
30268
|
+
const row = parsed;
|
|
30269
|
+
const unknown = Object.keys(row).filter((key) => key !== "project" && key !== "key" && key !== "team");
|
|
30270
|
+
if (unknown.length > 0) throw new Error(`org project set: vercelVersionEnv has unknown field(s): ${unknown.join(", ")}`);
|
|
30271
|
+
if (typeof row.project !== "string" || !VERCEL_VERSION_ENV_ID_RE.test(row.project)) {
|
|
30272
|
+
throw new Error("org project set: vercelVersionEnv.project must be a Vercel project id or name");
|
|
30273
|
+
}
|
|
30274
|
+
if (typeof row.key !== "string" || !VERCEL_VERSION_ENV_NAME_RE.test(row.key)) {
|
|
30275
|
+
throw new Error("org project set: vercelVersionEnv.key must be an env name (e.g. MMW_VERSION)");
|
|
30276
|
+
}
|
|
30277
|
+
if (row.team !== void 0 && (typeof row.team !== "string" || !VERCEL_VERSION_ENV_ID_RE.test(row.team))) {
|
|
30278
|
+
throw new Error("org project set: vercelVersionEnv.team must be a Vercel team id or slug");
|
|
30279
|
+
}
|
|
30280
|
+
return { project: row.project, key: row.key, ...typeof row.team === "string" ? { team: row.team } : {} };
|
|
30281
|
+
}
|
|
30051
30282
|
function parseCiExemptReasonVar(raw) {
|
|
30052
30283
|
const trimmed = raw.trim();
|
|
30053
30284
|
if (!trimmed) throw new Error("org project set: ciExemptReason must be a non-empty reason (or use --unset ciExemptReason to clear it)");
|
|
@@ -30189,7 +30420,8 @@ var SETTABLE_VAR_KEYS = [
|
|
|
30189
30420
|
"secrets",
|
|
30190
30421
|
"seedCanary",
|
|
30191
30422
|
"installer",
|
|
30192
|
-
"notifyOnDeploy"
|
|
30423
|
+
"notifyOnDeploy",
|
|
30424
|
+
"vercelVersionEnv"
|
|
30193
30425
|
];
|
|
30194
30426
|
var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
30195
30427
|
var SETTABLE_VAR_HINTS = {
|
|
@@ -30219,6 +30451,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
30219
30451
|
requiredChecks: 'JSON array, e.g. ["gate"] or [] for no-ci',
|
|
30220
30452
|
requiredCheckBranches: "non-empty JSON branch-name array; exact product-ruleset scope (#5210)",
|
|
30221
30453
|
notifyOnDeploy: 'JSON stage array, e.g. ["dev"] \u2014 successful tenant-deploy sends <stage>-deployed to the project repo (#7358)',
|
|
30454
|
+
vercelVersionEnv: "JSON {project,key,team?} \u2014 train sets this Vercel env to the release tag before the main/rc push (#7362)",
|
|
30222
30455
|
ciExemptReason: "non-empty string \u2014 declares this deployable repo has no CI surface (#4265)",
|
|
30223
30456
|
gate: "JSON {runtime,cmd,affectedCmd,workdir,cacheDepPath,pyVersion}"
|
|
30224
30457
|
};
|
|
@@ -30364,6 +30597,8 @@ function buildProjectSetPatch(input) {
|
|
|
30364
30597
|
patch[key] = parseRequiredCheckBranchesVar(raw);
|
|
30365
30598
|
} else if (key === "notifyOnDeploy") {
|
|
30366
30599
|
patch[key] = parseNotifyOnDeployVar(raw);
|
|
30600
|
+
} else if (key === "vercelVersionEnv") {
|
|
30601
|
+
patch[key] = parseVercelVersionEnvVar(raw);
|
|
30367
30602
|
} else if (key === "ciExemptReason") {
|
|
30368
30603
|
patch[key] = parseCiExemptReasonVar(raw);
|
|
30369
30604
|
} else if (key === "gate") {
|
|
@@ -35014,7 +35249,7 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
35014
35249
|
throw new Error(`working tree must be clean before hotfix start${describeBlockingPaths(status)}`);
|
|
35015
35250
|
}
|
|
35016
35251
|
await deps.run("git", ["fetch", "origin", "--tags"]);
|
|
35017
|
-
const { deployModel } = await hotfixPreflight(deps, ctx, "start");
|
|
35252
|
+
const { deployModel, meta } = await hotfixPreflight(deps, ctx, "start");
|
|
35018
35253
|
const bumpNotes = [];
|
|
35019
35254
|
let resolved = await resolveHotfixVersion(deps);
|
|
35020
35255
|
for (; ; ) {
|
|
@@ -35136,6 +35371,8 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
35136
35371
|
await assertHotfixFoldBomStamp(deps, tag);
|
|
35137
35372
|
await deps.run("git", ["push", "-u", "origin", branch]);
|
|
35138
35373
|
}
|
|
35374
|
+
const versionEnv = await setVercelVersionEnv(vercelVersionEnvDeps(deps), meta, "main", tag);
|
|
35375
|
+
if (versionEnv.note) notes.push(versionEnv.note);
|
|
35139
35376
|
const bumpNote = deployModel === "hub-serverless" ? " with the locked distribution bump" : foldPaths.length > 0 ? " with the package version bump" : "";
|
|
35140
35377
|
const prUrl = clean3(await deps.run("gh", [
|
|
35141
35378
|
"pr",
|
|
@@ -37387,12 +37624,12 @@ async function resolveBoardAdvanceForPr(prNumber, repoOption) {
|
|
|
37387
37624
|
}
|
|
37388
37625
|
function evaluatePrMergeHousekeeping(report, context, force = false) {
|
|
37389
37626
|
const scratch = report.scratch;
|
|
37390
|
-
const
|
|
37627
|
+
const failed2 = scratch?.status === "failed";
|
|
37391
37628
|
const prunable = scratch?.safeAuto ?? 0;
|
|
37392
37629
|
const unprunable = scratch?.skipped ?? 0;
|
|
37393
|
-
if (!(
|
|
37630
|
+
if (!(failed2 || prunable > 0 || unprunable > 0)) return { blocked: false };
|
|
37394
37631
|
if (force) return { blocked: false };
|
|
37395
|
-
const detail =
|
|
37632
|
+
const detail = failed2 ? `scratch housekeeping failed${scratch?.error ? `: ${scratch.error}` : ""}` : `${prunable} prunable + ${unprunable} un-prunable scratch item(s) remain after cleanup`;
|
|
37396
37633
|
return {
|
|
37397
37634
|
blocked: true,
|
|
37398
37635
|
reason: `${context} housekeeping blocked merge: ${detail}. Run host-owned scratch cleanup, then retry \u2014 or re-run \`mmi-cli ${context} --force\` to acknowledge and land anyway. (Kept advisory plans/ scratch never blocks: it is gitignored and can never reach origin.)`
|
|
@@ -45137,15 +45374,15 @@ function renderReport(checks, opts) {
|
|
|
45137
45374
|
}
|
|
45138
45375
|
function renderTally(checks) {
|
|
45139
45376
|
const total = checks.length;
|
|
45140
|
-
const
|
|
45377
|
+
const failed2 = checks.filter((c) => !c.ok).length;
|
|
45141
45378
|
const unverified = checks.filter((c) => c.ok && c.verified === false).length;
|
|
45142
45379
|
const warned = checks.filter((c) => c.ok && c.verified !== false && c.warn).length;
|
|
45143
|
-
const healthy = total -
|
|
45144
|
-
if (
|
|
45380
|
+
const healthy = total - failed2 - unverified - warned;
|
|
45381
|
+
if (failed2 === 0 && unverified === 0 && warned === 0) return `\u2713 all ${total} checks healthy`;
|
|
45145
45382
|
const parts = [`\u2713 ${healthy} verified healthy`];
|
|
45146
45383
|
if (warned > 0) parts.push(`\u26A0 ${warned} warn`);
|
|
45147
45384
|
if (unverified > 0) parts.push(`? ${unverified} unverified`);
|
|
45148
|
-
if (
|
|
45385
|
+
if (failed2 > 0) parts.push(`\u2717 ${failed2} failed`);
|
|
45149
45386
|
return `${parts.join(" \xB7 ")} \u2014 ${total} checks`;
|
|
45150
45387
|
}
|
|
45151
45388
|
function doctorReportExitCode(checks) {
|
package/package.json
CHANGED