@mutmutco/cli 4.5.35 → 4.5.37
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 +543 -149
- 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") {
|
|
@@ -7295,11 +7326,11 @@ function decorateAwsModel(entries, registry2, verdicts) {
|
|
|
7295
7326
|
});
|
|
7296
7327
|
}
|
|
7297
7328
|
var DECLARED_ENTRIES = [
|
|
7298
|
-
{ name: "mmi-backup (mm-central, mm-work, mmi-oguz
|
|
7329
|
+
{ name: "mmi-backup (mm-central, mm-work, mmi-oguz)", cadence: "17 2 * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup \u2192 /opt/mmi-control/nightly-backup.sh; every Docker volume is tarred and every running Postgres container is pg_dumpall'd beside it (#7147; source scripts/nightly-backup.sh); zuber (46.225.58.31) is gone: deleted 2026-09-18 by the owner-approved ZuberShade decommission (MMC-ZuberShade#1838, snapshot kept); Hetzner has since reassigned that IP to another machine (#7351)" },
|
|
7299
7330
|
// #7148: a backup nobody restored is a hope. Monthly, restore the latest artifact of every
|
|
7300
7331
|
// archived stage (including a stage whose containers are down) into scratch Postgres and compare
|
|
7301
7332
|
// row counts, and read-verify every storage archive against its listing; per-stage PASS/FAIL.
|
|
7302
|
-
{ name: "mmi-backup-restore-check (mm-central, mm-work, mmi-oguz
|
|
7333
|
+
{ name: "mmi-backup-restore-check (mm-central, mm-work, mmi-oguz)", cadence: "0 3 1 * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup-restore-check \u2192 /opt/mmi-control/nightly-backup-restore-check.sh; monthly restore proof for every archived stage (#7148; source scripts/nightly-backup-restore-check.sh); zuber (46.225.58.31) is gone: deleted 2026-09-18 by the owner-approved ZuberShade decommission (MMC-ZuberShade#1838, snapshot kept); Hetzner has since reassigned that IP to another machine (#7351)" },
|
|
7303
7334
|
// #3370/#6270: the one runner hygiene layer (runner-hygiene.yml was folded in and deleted). A workflow ran ON the runner and needed checkout +
|
|
7304
7335
|
// setup-node before it can prune, so at 100% it dies at checkout and never prunes — the cleanup is
|
|
7305
7336
|
// unavailable exactly when it is needed. This one is root cron on the box: no checkout, no node, and
|
|
@@ -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;
|
|
@@ -15715,9 +15746,9 @@ function trainFollowUpExitCode(status) {
|
|
|
15715
15746
|
}
|
|
15716
15747
|
function trainFollowUpNote(status) {
|
|
15717
15748
|
if (status === "complete") return void 0;
|
|
15718
|
-
return `follow-up ${status}: the exit code is never the verdict, read workflowRuns \u2014 see docs/Guides/train-troubleshooting.md#follow-up-pending`;
|
|
15749
|
+
return `follow-up ${status}: the exit code is never the verdict, read workflowRuns \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#follow-up-pending`;
|
|
15719
15750
|
}
|
|
15720
|
-
var TRAIN_DEPLOY_FAILED_NOTE = "promotion stands; retry the deploy, do not re-tag \u2014 see docs/Guides/train-troubleshooting.md#deploy-failed";
|
|
15751
|
+
var TRAIN_DEPLOY_FAILED_NOTE = "promotion stands; retry the deploy, do not re-tag \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#deploy-failed";
|
|
15721
15752
|
function missingRunUrlNote(runId) {
|
|
15722
15753
|
return runId != null ? `no run URL emitted \u2014 GitHub Actions returned run ${runId} without its URL` : "no run URL emitted \u2014 run absent or unreadable";
|
|
15723
15754
|
}
|
|
@@ -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.37",
|
|
16875
|
+
tag: "v4.5.37",
|
|
16876
|
+
commit: "75086128c3ba",
|
|
16877
|
+
npm: "@mutmutco/cli@4.5.37"
|
|
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.37"
|
|
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.37 and redeploy the Hub Lambda from tag v4.5.37 (75086128c3ba); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
16901
|
+
v3Target: "v4.5.37 (@mutmutco/cli@4.5.37, tag commit 75086128c3ba \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}`;
|
|
@@ -19826,7 +19857,7 @@ function installerDeclarationError(value, division) {
|
|
|
19826
19857
|
|
|
19827
19858
|
// src/train-suite.ts
|
|
19828
19859
|
var SUITE_GUARD_CODE = "suite-member-solo";
|
|
19829
|
-
var SUITE_GUARD_ANCHOR = `docs/Guides/train-troubleshooting.md#${SUITE_GUARD_CODE}`;
|
|
19860
|
+
var SUITE_GUARD_ANCHOR = `https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#${SUITE_GUARD_CODE}`;
|
|
19830
19861
|
function suiteLeadRepo(repo, suite) {
|
|
19831
19862
|
const owner = repo.split("/")[0];
|
|
19832
19863
|
return `${owner}/${suite === "mm-work" ? "MM-Work" : suite}`;
|
|
@@ -19937,9 +19968,9 @@ async function readVercelDeployment(api, repo, sha, target, since) {
|
|
|
19937
19968
|
}
|
|
19938
19969
|
if (newest.size === 0) return void 0;
|
|
19939
19970
|
const reads = await Promise.all([...newest].map(async ([env, id]) => ({ env, id, ...await readOne(api, repo, id, since) })));
|
|
19940
|
-
const
|
|
19971
|
+
const failed2 = reads.find((r) => FAILED.has(r.state));
|
|
19941
19972
|
const unfinished = reads.find((r) => !isTerminalVercelState(r.state));
|
|
19942
|
-
const pick =
|
|
19973
|
+
const pick = failed2 ?? unfinished ?? reads[0];
|
|
19943
19974
|
return {
|
|
19944
19975
|
id: pick.id,
|
|
19945
19976
|
environment: reads.map((r) => r.env).join(", "),
|
|
@@ -20332,11 +20363,11 @@ async function appendServedChannelProof(deps, input, dispatch) {
|
|
|
20332
20363
|
const channel = servedChannelFor(input.repo, input.installer);
|
|
20333
20364
|
if (!channel) return dispatch;
|
|
20334
20365
|
const expected = input.tag.replace(/^v/, "");
|
|
20335
|
-
const withProof = (conclusion, evidence, note,
|
|
20366
|
+
const withProof = (conclusion, evidence, note, failed2 = false) => ({
|
|
20336
20367
|
...dispatch,
|
|
20337
20368
|
note: `${dispatch.note}; ${note}`,
|
|
20338
20369
|
workflowRuns: [...dispatch.workflowRuns ?? [], servedChannelRow(channel.product, conclusion, evidence)],
|
|
20339
|
-
...
|
|
20370
|
+
...failed2 ? { deployStatus: "failure" } : {}
|
|
20340
20371
|
});
|
|
20341
20372
|
if (!input.shipped) {
|
|
20342
20373
|
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`);
|
|
@@ -20521,7 +20552,7 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
|
|
|
20521
20552
|
...dispatch,
|
|
20522
20553
|
// #6095: the helper's receipt, health or rollback failed after a proven publish — the release resumes, never re-tags.
|
|
20523
20554
|
// #6985: keep the FULL failing output in the note so the ledger can persist its tail (the first-line 240-char clip hid the real cause).
|
|
20524
|
-
note: `${dispatch.note}; Jerv Gateway deploy FAILED (${full}) \u2014 see docs/Guides/train-troubleshooting.md#jerv-gateway-deploy`,
|
|
20555
|
+
note: `${dispatch.note}; Jerv Gateway deploy FAILED (${full}) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#jerv-gateway-deploy`,
|
|
20525
20556
|
workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "jerv-gateway", runUrlNote: JERV_GATEWAY_RUN_URL_NOTE, conclusion: "failure" }],
|
|
20526
20557
|
deployStatus: "failure"
|
|
20527
20558
|
};
|
|
@@ -20611,8 +20642,8 @@ function shipLegsGreen(deploy, publish) {
|
|
|
20611
20642
|
async function announceShippedRelease(deps, args, model, legs, deferred) {
|
|
20612
20643
|
if (!deps.announce) return void 0;
|
|
20613
20644
|
if (trainObservesShipment(model) && !shipLegsGreen(legs.deploy, legs.publish)) {
|
|
20614
|
-
const
|
|
20615
|
-
return `not announced: deploy/publish ${
|
|
20645
|
+
const failed2 = legs.deploy.state === "failed" || legs.publish.state === "failed";
|
|
20646
|
+
return `not announced: deploy/publish ${failed2 ? "FAILED" : "not proven green"} \u2014 ${deferred}`;
|
|
20616
20647
|
}
|
|
20617
20648
|
return (await deps.announce(args)).note;
|
|
20618
20649
|
}
|
|
@@ -20742,7 +20773,7 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
|
|
|
20742
20773
|
const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
20743
20774
|
let note = `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage})`;
|
|
20744
20775
|
if (deployStatus === "failure") {
|
|
20745
|
-
note = `${note}; run FAILED \u2014 if its log shows exit 78 \`compose is fileless but DEPLOY#${stage}.noEnvFile is not true\`, set-deploy + reconcile before the redeploy \u2014 see docs/Guides/train-troubleshooting.md#deploy-exit-78-fileless`;
|
|
20776
|
+
note = `${note}; run FAILED \u2014 if its log shows exit 78 \`compose is fileless but DEPLOY#${stage}.noEnvFile is not true\`, set-deploy + reconcile before the redeploy \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#deploy-exit-78-fileless`;
|
|
20746
20777
|
}
|
|
20747
20778
|
return { note, runId, runUrl, deployStatus };
|
|
20748
20779
|
}
|
|
@@ -21057,7 +21088,7 @@ function isCanaryAccessBlockText(text) {
|
|
|
21057
21088
|
}
|
|
21058
21089
|
function actionsCanaryUndispatchedRefusal(detail) {
|
|
21059
21090
|
return new Error(
|
|
21060
|
-
`release refused: the hosted-job canary (#5604) was not dispatched, so a hosted job start is UNPROVEN \u2014 this is NOT a billing verdict. ${detail} The Hub dispatches ${CANARY_WORKFLOW} with its App token for a train-authorized caller (#6319): refresh the Hub session (\`gh auth login\`) and retry, and confirm \`mmi-cli oracle org access role <owner/repo>\` reports train authority. Nothing was tagged or pushed \u2014 see docs/Guides/train-troubleshooting.md#actions-job-start-canary`
|
|
21091
|
+
`release refused: the hosted-job canary (#5604) was not dispatched, so a hosted job start is UNPROVEN \u2014 this is NOT a billing verdict. ${detail} The Hub dispatches ${CANARY_WORKFLOW} with its App token for a train-authorized caller (#6319): refresh the Hub session (\`gh auth login\`) and retry, and confirm \`mmi-cli oracle org access role <owner/repo>\` reports train authority. Nothing was tagged or pushed \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#actions-job-start-canary`
|
|
21061
21092
|
);
|
|
21062
21093
|
}
|
|
21063
21094
|
function actionsBillingRefusal(detail) {
|
|
@@ -21221,11 +21252,11 @@ async function assertActionsJobsCanStart(deps, targetRepo2) {
|
|
|
21221
21252
|
return;
|
|
21222
21253
|
}
|
|
21223
21254
|
if (last === "billing-blocked" || last === "never-started") {
|
|
21224
|
-
throw actionsBillingRefusal(`${url}: hosted canary ${last} \u2014 see docs/Guides/train-troubleshooting.md#actions-job-start-canary`);
|
|
21255
|
+
throw actionsBillingRefusal(`${url}: hosted canary ${last} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#actions-job-start-canary`);
|
|
21225
21256
|
}
|
|
21226
21257
|
}
|
|
21227
21258
|
throw actionsBillingRefusal(
|
|
21228
|
-
`${url}: hosted canary stayed ${last} after ${CANARY_POLLS} polls \u2014 cannot prove a hosted job can start \u2014 see docs/Guides/train-troubleshooting.md#actions-job-start-canary`
|
|
21259
|
+
`${url}: hosted canary stayed ${last} after ${CANARY_POLLS} polls \u2014 cannot prove a hosted job can start \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#actions-job-start-canary`
|
|
21229
21260
|
);
|
|
21230
21261
|
}
|
|
21231
21262
|
|
|
@@ -23378,8 +23409,8 @@ function ledgerFailedRemedy(ledger) {
|
|
|
23378
23409
|
const openLegs = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
|
|
23379
23410
|
const tails = formatFailedLegTails(ledger);
|
|
23380
23411
|
if (hotfixRetryDeployShape(ledger)) {
|
|
23381
|
-
const
|
|
23382
|
-
return `the published ${ledger.tag} hotfix can retry the failed ${joinLegNames(
|
|
23412
|
+
const failed2 = openLegs.filter((l) => l.state === "failed").map((l) => l.phase);
|
|
23413
|
+
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
23414
|
}
|
|
23384
23415
|
if (rcandRetryDeployShape(ledger)) {
|
|
23385
23416
|
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 +23578,8 @@ async function supersedeContentCausedDeployLeg(deps, cwd) {
|
|
|
23547
23578
|
`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
23579
|
);
|
|
23549
23580
|
}
|
|
23550
|
-
const
|
|
23551
|
-
const note =
|
|
23581
|
+
const failed2 = prior.phases.deploy.state === "failed";
|
|
23582
|
+
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
23583
|
await fence(() => atomicWriteJson(path2, {
|
|
23553
23584
|
...prior,
|
|
23554
23585
|
phases: {
|
|
@@ -23562,7 +23593,7 @@ async function supersedeContentCausedDeployLeg(deps, cwd) {
|
|
|
23562
23593
|
repo: prior.repo,
|
|
23563
23594
|
tag: prior.tag,
|
|
23564
23595
|
tagSha: prior.tagSha,
|
|
23565
|
-
note:
|
|
23596
|
+
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
23597
|
};
|
|
23567
23598
|
});
|
|
23568
23599
|
}
|
|
@@ -23641,7 +23672,7 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
23641
23672
|
}
|
|
23642
23673
|
let gatewayDeploy;
|
|
23643
23674
|
const deferredGateway = isJervHubRepo(prior.repo) && deferredGatewayDeployLegShape(phases.deploy);
|
|
23644
|
-
const unresolved = ["promotion", "ordinaryCi", "githubRelease", "deploy", "alignment"].filter((phase) => !(phase === "deploy" && deferredGateway)).filter((phase) => phases[phase].state === "pending" || phases[phase].state === "failed");
|
|
23675
|
+
const unresolved = ["promotion", "ordinaryCi", "githubRelease", "deploy", "alignment"].filter((phase) => !(phase === "deploy" && deferredGateway)).filter((phase) => !(phase === "alignment" && phases.alignment.state === "pending")).filter((phase) => phases[phase].state === "pending" || phases[phase].state === "failed");
|
|
23645
23676
|
if (unresolved.length > 0) {
|
|
23646
23677
|
throw new Error(
|
|
23647
23678
|
`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)`
|
|
@@ -23689,7 +23720,7 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
23689
23720
|
repo: prior.repo,
|
|
23690
23721
|
tag: prior.tag,
|
|
23691
23722
|
tagSha: prior.tagSha,
|
|
23692
|
-
note: `closed the ${prior.tag} publish 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`,
|
|
23723
|
+
note: `closed the ${prior.tag} publish 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${phases.alignment.state === "pending" ? "; alignment still pending (fold-port PR open \u2014 closes live at the next pre-flight, #6061)" : ""}`,
|
|
23693
23724
|
...gatewayDeploy ? { gatewayDeploy } : {}
|
|
23694
23725
|
};
|
|
23695
23726
|
});
|
|
@@ -24537,17 +24568,17 @@ async function runTrainDoctor(input) {
|
|
|
24537
24568
|
if (derived.phaseStatus !== "phases-green") {
|
|
24538
24569
|
const openLegs = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
|
|
24539
24570
|
const open2 = openLegs.map((l) => `${l.phase}=${l.state}`).join(", ");
|
|
24540
|
-
const
|
|
24571
|
+
const failed2 = derived.phaseStatus === "phases-failed";
|
|
24541
24572
|
const deployOnlyDeadlock = openLegs.length > 0 && openLegs.every((l) => l.phase === "deploy" && supersedeableDeployLegShape(l));
|
|
24542
24573
|
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
24574
|
add({
|
|
24544
|
-
code:
|
|
24575
|
+
code: failed2 ? "ledger-failed" : "ledger-pending",
|
|
24545
24576
|
severity: "blocker",
|
|
24546
24577
|
source: "local",
|
|
24547
24578
|
// #6429: each lane finishes its own run (#6068) — an rcand-lane ledger named as a release
|
|
24548
24579
|
// ledger, with `release --resume` as its remedy, sends the operator to a verb that refuses it.
|
|
24549
24580
|
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:
|
|
24581
|
+
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
24582
|
});
|
|
24552
24583
|
} else if (newestRemoteReleaseTag && newestRemoteReleaseTag !== ledger.tag) {
|
|
24553
24584
|
add({
|
|
@@ -24594,21 +24625,21 @@ regenerate the projections (\`node scripts/check-skill-payload.mjs\`) and commit
|
|
|
24594
24625
|
const installed = [];
|
|
24595
24626
|
let pending = firstDir;
|
|
24596
24627
|
let outstanding = failure;
|
|
24597
|
-
let
|
|
24628
|
+
let failed2 = null;
|
|
24598
24629
|
while (pending && !installed.includes(pending)) {
|
|
24599
24630
|
const dir = pending;
|
|
24600
24631
|
try {
|
|
24601
24632
|
await train.run("npm", ["--prefix", dir, "ci"]);
|
|
24602
24633
|
} catch (e) {
|
|
24603
|
-
|
|
24634
|
+
failed2 = { dir, detail: `\`npm --prefix ${dir} ci\` failed: ${message(e)}` };
|
|
24604
24635
|
break;
|
|
24605
24636
|
}
|
|
24606
24637
|
installed.push(dir);
|
|
24607
24638
|
outstanding = await verifyDeps();
|
|
24608
24639
|
pending = outstanding ? /in (cli|updater)\/node_modules/u.exec(outstanding)?.[1] ?? null : null;
|
|
24609
24640
|
}
|
|
24610
|
-
if (
|
|
24611
|
-
add({ code: "hub-build-deps", severity: "blocker", source: "local", title:
|
|
24641
|
+
if (failed2) {
|
|
24642
|
+
add({ code: "hub-build-deps", severity: "blocker", source: "local", title: failed2.detail, remedy: `repair ${failed2.dir}/node_modules by hand, then rerun` });
|
|
24612
24643
|
} else if (outstanding) {
|
|
24613
24644
|
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
24645
|
} else {
|
|
@@ -24709,6 +24740,133 @@ function formatTrainDoctor(v) {
|
|
|
24709
24740
|
return lines2.join("\n");
|
|
24710
24741
|
}
|
|
24711
24742
|
|
|
24743
|
+
// src/vercel-version-env.ts
|
|
24744
|
+
var SKIPPED = { status: "skipped", note: "" };
|
|
24745
|
+
function vercelVersionEnvAheadNote(key) {
|
|
24746
|
+
return `a failed branch push can leave ${key} ahead of what is live`;
|
|
24747
|
+
}
|
|
24748
|
+
function safeReason(reason, token) {
|
|
24749
|
+
const compact = reason.replace(/\s+/g, " ").trim().slice(0, 180);
|
|
24750
|
+
const scrubbed = token && compact.includes(token) ? compact.split(token).join("[redacted]") : compact;
|
|
24751
|
+
return scrubbed || "unknown error";
|
|
24752
|
+
}
|
|
24753
|
+
function failed(deps, key, tag, reason) {
|
|
24754
|
+
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`;
|
|
24755
|
+
deps.warn?.(note);
|
|
24756
|
+
return { status: "failed", note };
|
|
24757
|
+
}
|
|
24758
|
+
function targetLabel(stage) {
|
|
24759
|
+
return stage === "main" ? "production" : "preview";
|
|
24760
|
+
}
|
|
24761
|
+
function envCollectionUrl(project2, team, extra) {
|
|
24762
|
+
const url = new URL(`https://api.vercel.com/v10/projects/${encodeURIComponent(project2)}/env`);
|
|
24763
|
+
if (team) url.searchParams.set("teamId", team);
|
|
24764
|
+
for (const [key, value] of Object.entries(extra ?? {})) url.searchParams.set(key, value);
|
|
24765
|
+
return url.toString();
|
|
24766
|
+
}
|
|
24767
|
+
function envItemUrl(project2, id, team) {
|
|
24768
|
+
const url = new URL(`https://api.vercel.com/v9/projects/${encodeURIComponent(project2)}/env/${encodeURIComponent(id)}`);
|
|
24769
|
+
if (team) url.searchParams.set("teamId", team);
|
|
24770
|
+
return url.toString();
|
|
24771
|
+
}
|
|
24772
|
+
function isStageEnv(row, stage, key) {
|
|
24773
|
+
if (row.key !== key) return false;
|
|
24774
|
+
const target = Array.isArray(row.target) ? row.target.filter((item) => typeof item === "string") : [];
|
|
24775
|
+
if (stage === "main") return target.length === 1 && target[0] === "production";
|
|
24776
|
+
return target.length === 1 && target[0] === "preview" && row.gitBranch === "rc";
|
|
24777
|
+
}
|
|
24778
|
+
function envsOf(body) {
|
|
24779
|
+
if (Array.isArray(body)) return body;
|
|
24780
|
+
if (body && typeof body === "object" && Array.isArray(body.envs)) {
|
|
24781
|
+
return body.envs;
|
|
24782
|
+
}
|
|
24783
|
+
return null;
|
|
24784
|
+
}
|
|
24785
|
+
async function readBody(res, token) {
|
|
24786
|
+
let raw = "";
|
|
24787
|
+
try {
|
|
24788
|
+
raw = await res.text();
|
|
24789
|
+
} catch (e) {
|
|
24790
|
+
return { ok: false, reason: safeReason(e instanceof Error ? e.message : String(e), token) };
|
|
24791
|
+
}
|
|
24792
|
+
if (!res.ok) return { ok: false, reason: `HTTP ${res.status}${raw ? `: ${safeReason(raw, token)}` : ""}` };
|
|
24793
|
+
if (!raw.trim()) return { ok: true, body: {} };
|
|
24794
|
+
try {
|
|
24795
|
+
return { ok: true, body: JSON.parse(raw) };
|
|
24796
|
+
} catch {
|
|
24797
|
+
return { ok: false, reason: "response was not JSON" };
|
|
24798
|
+
}
|
|
24799
|
+
}
|
|
24800
|
+
async function setVercelVersionEnv(deps, meta, stage, tag) {
|
|
24801
|
+
if (meta?.deployModel !== "vercel" || !meta.vercelVersionEnv) return SKIPPED;
|
|
24802
|
+
const { project: project2, key, team } = meta.vercelVersionEnv;
|
|
24803
|
+
if (!project2 || !key) return failed(deps, key || "(missing key)", tag, "vercelVersionEnv is missing project or key");
|
|
24804
|
+
let token;
|
|
24805
|
+
try {
|
|
24806
|
+
token = await deps.readToken();
|
|
24807
|
+
} catch (e) {
|
|
24808
|
+
return failed(deps, key, tag, safeReason(e instanceof Error ? e.message : String(e), null));
|
|
24809
|
+
}
|
|
24810
|
+
if (!token) return failed(deps, key, tag, "no _org/vercel/API_TOKEN");
|
|
24811
|
+
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
24812
|
+
let listed;
|
|
24813
|
+
try {
|
|
24814
|
+
const res = await deps.fetch(envCollectionUrl(project2, team, stage === "rc" ? { gitBranch: "rc" } : void 0), { method: "GET", headers });
|
|
24815
|
+
listed = await readBody(res, token);
|
|
24816
|
+
} catch (e) {
|
|
24817
|
+
return failed(deps, key, tag, safeReason(e instanceof Error ? e.message : String(e), token));
|
|
24818
|
+
}
|
|
24819
|
+
if (!listed.ok) return failed(deps, key, tag, listed.reason);
|
|
24820
|
+
const rows = envsOf(listed.body);
|
|
24821
|
+
if (!rows) return failed(deps, key, tag, "env list returned an unexpected shape");
|
|
24822
|
+
const existing = rows.find((row) => isStageEnv(row, stage, key) && typeof row.id === "string" && row.id.length > 0);
|
|
24823
|
+
const createBody = stage === "main" ? { key, value: tag, type: "plain", target: ["production"] } : { key, value: tag, type: "plain", target: ["preview"], gitBranch: "rc" };
|
|
24824
|
+
try {
|
|
24825
|
+
const res = existing ? await deps.fetch(envItemUrl(project2, existing.id, team), {
|
|
24826
|
+
method: "PATCH",
|
|
24827
|
+
headers,
|
|
24828
|
+
body: JSON.stringify({ value: tag })
|
|
24829
|
+
}) : await deps.fetch(envCollectionUrl(project2, team), {
|
|
24830
|
+
method: "POST",
|
|
24831
|
+
headers,
|
|
24832
|
+
body: JSON.stringify(createBody)
|
|
24833
|
+
});
|
|
24834
|
+
const written = await readBody(res, token);
|
|
24835
|
+
if (!written.ok) return failed(deps, key, tag, written.reason);
|
|
24836
|
+
} catch (e) {
|
|
24837
|
+
return failed(deps, key, tag, safeReason(e instanceof Error ? e.message : String(e), token));
|
|
24838
|
+
}
|
|
24839
|
+
const note = `vercel version env: ${key}=${tag} set on ${targetLabel(stage)}`;
|
|
24840
|
+
deps.warn?.(note);
|
|
24841
|
+
return { status: "set", note };
|
|
24842
|
+
}
|
|
24843
|
+
async function defaultVercelFetch(url, init) {
|
|
24844
|
+
const res = await fetch(url, { ...init, signal: AbortSignal.timeout(15e3) });
|
|
24845
|
+
return { ok: res.ok, status: res.status, text: () => res.text() };
|
|
24846
|
+
}
|
|
24847
|
+
async function readOrgVercelApiToken() {
|
|
24848
|
+
const { loadConfig: loadConfig2, hubHeaders: hubHeaders2, repoProject: repoProject2 } = await Promise.resolve().then(() => (init_cli_shared(), cli_shared_exports));
|
|
24849
|
+
const cfg = await loadConfig2();
|
|
24850
|
+
if (!cfg.sagaApiUrl) return null;
|
|
24851
|
+
const repo = `mutmutco/${(await repoProject2(cfg)).toLowerCase()}`;
|
|
24852
|
+
const res = await fetch(`${cfg.sagaApiUrl}/secrets/get`, {
|
|
24853
|
+
method: "POST",
|
|
24854
|
+
headers: await hubHeaders2({ "content-type": "application/json" }),
|
|
24855
|
+
body: JSON.stringify({ repo, key: "vercel/API_TOKEN", use: true, slug: "_org" }),
|
|
24856
|
+
signal: AbortSignal.timeout(8e3)
|
|
24857
|
+
});
|
|
24858
|
+
if (!res.ok) return null;
|
|
24859
|
+
const body = await res.json();
|
|
24860
|
+
return typeof body.value === "string" ? body.value : null;
|
|
24861
|
+
}
|
|
24862
|
+
function vercelVersionEnvDeps(deps) {
|
|
24863
|
+
return {
|
|
24864
|
+
fetch: deps.vercelFetch ?? defaultVercelFetch,
|
|
24865
|
+
readToken: deps.readVercelApiToken ?? readOrgVercelApiToken,
|
|
24866
|
+
warn: (message2) => deps.warn?.(message2)
|
|
24867
|
+
};
|
|
24868
|
+
}
|
|
24869
|
+
|
|
24712
24870
|
// src/train-apply-phases.ts
|
|
24713
24871
|
var TRAIN_BUMP_INTENTS = ["major", "minor", "patch"];
|
|
24714
24872
|
function readLocalGateWorkflows() {
|
|
@@ -24793,7 +24951,7 @@ async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix
|
|
|
24793
24951
|
const predicted = await predictMergeConflicts(deps, ours, remoteRef);
|
|
24794
24952
|
const predictedBlocking = predicted.filter((f) => !pathIsTolerated(f, tolerated));
|
|
24795
24953
|
if (predictedBlocking.length > 0) {
|
|
24796
|
-
throw new Error(`${blockingPrefix}: ${predictedBlocking.join(", ")} \u2014 no merge was started. ${realignMessage} \u2014 see docs/Guides/train-troubleshooting.md#merge-conflict`);
|
|
24954
|
+
throw new Error(`${blockingPrefix}: ${predictedBlocking.join(", ")} \u2014 no merge was started. ${realignMessage} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#merge-conflict`);
|
|
24797
24955
|
}
|
|
24798
24956
|
return { foldPaths, tolerated, predicted };
|
|
24799
24957
|
}
|
|
@@ -25022,7 +25180,7 @@ async function recoverFailedFold(deps, cause, startBranch, preFoldMainSha, resum
|
|
|
25022
25180
|
const causeMessage = anchor ? `${rawCause} \u2014 see ${anchor}` : rawCause;
|
|
25023
25181
|
const probe = await probeFoldFailureState(deps);
|
|
25024
25182
|
if (probe.mergeInProgress) {
|
|
25025
|
-
const conflictCause = `${rawCause} \u2014 see docs/Guides/train-troubleshooting.md#merge-conflict`;
|
|
25183
|
+
const conflictCause = `${rawCause} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#merge-conflict`;
|
|
25026
25184
|
try {
|
|
25027
25185
|
await deps.run("git", ["merge", "--abort"]);
|
|
25028
25186
|
} catch (e) {
|
|
@@ -25313,14 +25471,14 @@ async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch
|
|
|
25313
25471
|
let fallbackRecovered = false;
|
|
25314
25472
|
const failedRows = workflowRuns?.filter((run) => run.conclusion === "failure") ?? [];
|
|
25315
25473
|
if (failedRows.length > 0) {
|
|
25316
|
-
for (const
|
|
25317
|
-
const rowPhase =
|
|
25474
|
+
for (const failed2 of failedRows) {
|
|
25475
|
+
const rowPhase = failed2.workflow.includes("publish") ? "publish" : phase;
|
|
25318
25476
|
if (persisted?.phases[rowPhase]?.superseded === true) continue;
|
|
25319
25477
|
const recovery = await recover(deps, ledger, anchors, rowPhase, {
|
|
25320
25478
|
repo: input.repo,
|
|
25321
25479
|
sha: input.sha,
|
|
25322
|
-
failure: `watched ${rowPhase} run failed: ${
|
|
25323
|
-
runs: [{ ...
|
|
25480
|
+
failure: `watched ${rowPhase} run failed: ${failed2.workflow} (${dispatch.note})`,
|
|
25481
|
+
runs: [{ ...failed2, repo: input.repo }],
|
|
25324
25482
|
watched: input.watched
|
|
25325
25483
|
});
|
|
25326
25484
|
if (recovery.note) notes.push(recovery.note);
|
|
@@ -25386,6 +25544,27 @@ async function recordRcandDeployLegs(ledger, deps, anchors, deployModel, d, opts
|
|
|
25386
25544
|
function releaseAnnounceDeferral(model, summaryFile) {
|
|
25387
25545
|
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
25546
|
}
|
|
25547
|
+
function annotateVersionEnvDispatch(dispatch, result) {
|
|
25548
|
+
if (!result.note) return dispatch;
|
|
25549
|
+
return { ...dispatch, note: `${dispatch.note}; ${result.note}` };
|
|
25550
|
+
}
|
|
25551
|
+
function annotateVersionEnvAnnounce(note, result) {
|
|
25552
|
+
if (!result.note) return note;
|
|
25553
|
+
return note ? `${note}; ${result.note}` : result.note;
|
|
25554
|
+
}
|
|
25555
|
+
async function pushAfterVercelVersionEnv(deps, meta, stage, tag, pushArgs) {
|
|
25556
|
+
const result = await setVercelVersionEnv(vercelVersionEnvDeps(deps), meta, stage, tag);
|
|
25557
|
+
try {
|
|
25558
|
+
await runGitPush(deps, pushArgs);
|
|
25559
|
+
} catch (e) {
|
|
25560
|
+
const base = e instanceof Error ? e.message : String(e);
|
|
25561
|
+
const parts = [base];
|
|
25562
|
+
if (result.note) parts.push(result.note);
|
|
25563
|
+
if (result.status === "set" && meta.vercelVersionEnv?.key) parts.push(vercelVersionEnvAheadNote(meta.vercelVersionEnv.key));
|
|
25564
|
+
throw new Error(parts.join(" \u2014 "));
|
|
25565
|
+
}
|
|
25566
|
+
return result;
|
|
25567
|
+
}
|
|
25389
25568
|
async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, startBranch, preFold, tagProbe, branchHints, ledger, alignEarly) {
|
|
25390
25569
|
const resumeCommand = options.dev ? "mmi-cli devops release --dev --apply" : "mmi-cli devops release --apply";
|
|
25391
25570
|
const ledgerAnchors = { repo: ctx.repo, tag, tagSha: releaseSha };
|
|
@@ -25482,9 +25661,10 @@ ${recovery.note}`), recoveryInput);
|
|
|
25482
25661
|
landed: "the immutable tag pushed at " + releaseSha.slice(0, 12)
|
|
25483
25662
|
});
|
|
25484
25663
|
if (trueMergeGateNote) checks = `${trueMergeGateNote}; ${checks}`;
|
|
25664
|
+
let versionEnv = { status: "skipped", note: "" };
|
|
25485
25665
|
try {
|
|
25486
25666
|
deps.warn?.("Release: Promoting the verified release");
|
|
25487
|
-
await
|
|
25667
|
+
versionEnv = await pushAfterVercelVersionEnv(deps, meta, "main", tag, ["push", "origin", "main"]);
|
|
25488
25668
|
} catch (e) {
|
|
25489
25669
|
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
25670
|
throw partialTrainRecoveryError(e, recoveryInput);
|
|
@@ -25517,7 +25697,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
25517
25697
|
workflow: failedReleaseRun?.workflow ?? (isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow")
|
|
25518
25698
|
}
|
|
25519
25699
|
);
|
|
25520
|
-
const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch);
|
|
25700
|
+
const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch), versionEnv);
|
|
25521
25701
|
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
25702
|
await recordPhase(ledger, deps, ledgerAnchors, "deploy", deployLeg, {
|
|
25523
25703
|
landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
|
|
@@ -25551,7 +25731,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
25551
25731
|
appendPublishDispatch(deployDispatch, publishDispatch)
|
|
25552
25732
|
);
|
|
25553
25733
|
alignment = await aligning;
|
|
25554
|
-
const announceNote = await announcing;
|
|
25734
|
+
const announceNote = annotateVersionEnvAnnounce(await announcing, versionEnv);
|
|
25555
25735
|
return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors, ...alignment ? { alignment } : {} };
|
|
25556
25736
|
}
|
|
25557
25737
|
function resumeStateOf(status) {
|
|
@@ -25754,10 +25934,10 @@ ${spent.note}`);
|
|
|
25754
25934
|
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
25935
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
25756
25936
|
deps.warn?.("Release: Promoting the verified release candidate");
|
|
25757
|
-
await
|
|
25937
|
+
const versionEnv = await pushAfterVercelVersionEnv(deps, meta, "rc", tag, ["push", "origin", `${tagSha}:refs/heads/rc`]);
|
|
25758
25938
|
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
25939
|
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");
|
|
25940
|
+
const dispatch0 = annotateVersionEnvDispatch(await dispatchDeploy(deps, ctx, "rc", "rc", deployModel, watch, autoRunSince, tagSha, "report"), versionEnv);
|
|
25761
25941
|
const dispatch = await recordRcandDeployLegs(ledger, deps, anchors, deployModel, dispatch0, { strict });
|
|
25762
25942
|
const ledgerReport = releaseLedgerReport(await ledger.load(anchors).catch(() => void 0), ledger.path);
|
|
25763
25943
|
return {
|
|
@@ -26037,7 +26217,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
26037
26217
|
}
|
|
26038
26218
|
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
26219
|
deps.warn?.("Release: Promoting the verified release");
|
|
26040
|
-
await
|
|
26220
|
+
const versionEnv = await pushAfterVercelVersionEnv(deps, meta, "main", tag, ["push", "origin", `${tagSha}:refs/heads/main`]);
|
|
26041
26221
|
steps.push(`pushed ${tagSha.slice(0, 12)} to origin/main`);
|
|
26042
26222
|
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
26223
|
deps.warn?.("Release: Creating the GitHub Release");
|
|
@@ -26067,7 +26247,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
26067
26247
|
workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow"
|
|
26068
26248
|
}
|
|
26069
26249
|
);
|
|
26070
|
-
const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch);
|
|
26250
|
+
const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch), versionEnv);
|
|
26071
26251
|
steps.push(`dispatched the ${deployModel} deploy path`);
|
|
26072
26252
|
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
26253
|
if (persisted?.phases.deploy.superseded !== true) {
|
|
@@ -26108,7 +26288,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
26108
26288
|
devRollForward = landed.dev;
|
|
26109
26289
|
rcAlignment = landed.rc;
|
|
26110
26290
|
}
|
|
26111
|
-
const announceNote = await announcing;
|
|
26291
|
+
const announceNote = annotateVersionEnvAnnounce(await announcing, versionEnv);
|
|
26112
26292
|
steps.push(`development roll-forward: ${devRollForward.status}`);
|
|
26113
26293
|
if (rcAlignment) steps.push(`rc alignment: ${rcAlignment.status}`);
|
|
26114
26294
|
const followUp = deriveTrainFollowUpStatus({
|
|
@@ -26448,15 +26628,16 @@ ${spent.note}`), rcandRecovery(tag2));
|
|
|
26448
26628
|
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
26629
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
26450
26630
|
deps.warn?.("Release: Promoting the verified release candidate");
|
|
26631
|
+
let versionEnv = { status: "skipped", note: "" };
|
|
26451
26632
|
try {
|
|
26452
|
-
await
|
|
26633
|
+
versionEnv = await pushAfterVercelVersionEnv(deps, meta, "rc", tag2, ["push", "origin", "rc"]);
|
|
26453
26634
|
} catch (e) {
|
|
26454
26635
|
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
26636
|
throw partialTrainRecoveryError(e, rcandRecovery(tag2));
|
|
26456
26637
|
}
|
|
26457
26638
|
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
26639
|
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");
|
|
26640
|
+
const d0 = annotateVersionEnvDispatch(await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha, "report"), versionEnv);
|
|
26460
26641
|
const d2 = await recordRcandDeployLegs(ledger, deps, anchors, deployModel2, d0);
|
|
26461
26642
|
const rcLedgerReport = releaseLedgerReport(await ledger?.load(anchors).catch(() => void 0), ledger?.path ?? "(no ledger)");
|
|
26462
26643
|
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 } : {} };
|
|
@@ -26593,7 +26774,7 @@ ${spent.note}`), rcandRecovery(tag2));
|
|
|
26593
26774
|
if (!coverage.ok) {
|
|
26594
26775
|
const list = coverage.uncovered.map((c) => `${c.sha.slice(0, 8)} ${c.subject}`).join("; ");
|
|
26595
26776
|
throw new Error(
|
|
26596
|
-
`hotfix-coverage: ${coverage.uncovered.length} main-only commit(s) not proven in origin/rc \u2014 the candidate would revert a prod hotfix: ${list}. Re-cut /rcand from development, or have the authorized human verify the content is in the candidate and rerun release with --ack <sha>[,<sha>\u2026] \u2014 see docs/Guides/train-troubleshooting.md#hotfix-coverage`
|
|
26777
|
+
`hotfix-coverage: ${coverage.uncovered.length} main-only commit(s) not proven in origin/rc \u2014 the candidate would revert a prod hotfix: ${list}. Re-cut /rcand from development, or have the authorized human verify the content is in the candidate and rerun release with --ack <sha>[,<sha>\u2026] \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-coverage`
|
|
26597
26778
|
// #6095: the guard names its lesson
|
|
26598
26779
|
);
|
|
26599
26780
|
}
|
|
@@ -26673,7 +26854,7 @@ async function resolveTrainBumpIntent(deps, env = process.env, latestRelease) {
|
|
|
26673
26854
|
const explicitIntent = (env.MMI_BUMP_INTENT ?? "").trim();
|
|
26674
26855
|
if (explicitIntent) {
|
|
26675
26856
|
if (!TRAIN_BUMP_INTENTS.includes(explicitIntent)) {
|
|
26676
|
-
throw new Error("MMI_BUMP_INTENT must be one of major|minor|patch \u2014 see docs/Guides/train-troubleshooting.md#bump-intent-invalid");
|
|
26857
|
+
throw new Error("MMI_BUMP_INTENT must be one of major|minor|patch \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#bump-intent-invalid");
|
|
26677
26858
|
}
|
|
26678
26859
|
return explicitIntent;
|
|
26679
26860
|
}
|
|
@@ -26718,12 +26899,12 @@ async function resolveHotfixVersion(deps, env = process.env) {
|
|
|
26718
26899
|
}
|
|
26719
26900
|
const nonPatch = declared.find((d) => d.intent !== "patch");
|
|
26720
26901
|
if (nonPatch) {
|
|
26721
|
-
throw new Error(`hotfix is PATCH-only \u2014 ${nonPatch.name}=${nonPatch.value} declares a ${nonPatch.intent} bump over ${baseTag}; unset it (or run the release train for a ${nonPatch.intent} release) \u2014 see docs/Guides/train-troubleshooting.md#bump-intent-invalid`);
|
|
26902
|
+
throw new Error(`hotfix is PATCH-only \u2014 ${nonPatch.name}=${nonPatch.value} declares a ${nonPatch.intent} bump over ${baseTag}; unset it (or run the release train for a ${nonPatch.intent} release) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#bump-intent-invalid`);
|
|
26722
26903
|
}
|
|
26723
26904
|
const explicit = explicitVersion.replace(/^v/, "");
|
|
26724
26905
|
const nextPatch = `${latest.major}.${latest.minor}.${latest.patch + 1}`;
|
|
26725
26906
|
if (explicitIntent && explicit && explicit !== nextPatch) {
|
|
26726
|
-
throw new Error(`hotfix version declarations conflict \u2014 MMI_BUMP_INTENT=patch targets ${nextPatch} from ${baseTag}, but MMI_RELEASE_VERSION=${explicitVersion} targets ${explicit}; set one declaration or make both target ${nextPatch} \u2014 see docs/Guides/train-troubleshooting.md#bump-intent-invalid`);
|
|
26907
|
+
throw new Error(`hotfix version declarations conflict \u2014 MMI_BUMP_INTENT=patch targets ${nextPatch} from ${baseTag}, but MMI_RELEASE_VERSION=${explicitVersion} targets ${explicit}; set one declaration or make both target ${nextPatch} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#bump-intent-invalid`);
|
|
26727
26908
|
}
|
|
26728
26909
|
const version = explicit || nextPatch;
|
|
26729
26910
|
return { tag: `v${version}`, version, baseTag };
|
|
@@ -28643,13 +28824,13 @@ function buildPluginCachePlan(home, running, deps, opts = {}) {
|
|
|
28643
28824
|
}
|
|
28644
28825
|
function applyPluginCachePlan(plan, remove2, stagingGuard) {
|
|
28645
28826
|
const removed = [];
|
|
28646
|
-
const
|
|
28827
|
+
const failed2 = [];
|
|
28647
28828
|
for (const version of plan.prune) {
|
|
28648
28829
|
try {
|
|
28649
28830
|
remove2(`${plan.cacheRoot}/${version}`);
|
|
28650
28831
|
removed.push(version);
|
|
28651
28832
|
} catch (e) {
|
|
28652
|
-
|
|
28833
|
+
failed2.push({ version, error: e.message });
|
|
28653
28834
|
}
|
|
28654
28835
|
}
|
|
28655
28836
|
const removedStaging = [];
|
|
@@ -28682,7 +28863,7 @@ function applyPluginCachePlan(plan, remove2, stagingGuard) {
|
|
|
28682
28863
|
}
|
|
28683
28864
|
return {
|
|
28684
28865
|
removed,
|
|
28685
|
-
failed,
|
|
28866
|
+
failed: failed2,
|
|
28686
28867
|
bytes: plan.bytes,
|
|
28687
28868
|
removedStaging,
|
|
28688
28869
|
failedStaging,
|
|
@@ -29694,7 +29875,7 @@ init_hub_auth();
|
|
|
29694
29875
|
init_github_client();
|
|
29695
29876
|
|
|
29696
29877
|
// 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"];
|
|
29878
|
+
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
29879
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
29699
29880
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
29700
29881
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -30033,6 +30214,47 @@ function parseRequiredCheckBranchesVar(raw) {
|
|
|
30033
30214
|
}
|
|
30034
30215
|
return REQUIRED_CHECK_BRANCHES.filter((branch) => parsed.includes(branch));
|
|
30035
30216
|
}
|
|
30217
|
+
function parseNotifyOnDeployVar(raw) {
|
|
30218
|
+
let parsed;
|
|
30219
|
+
try {
|
|
30220
|
+
parsed = JSON.parse(raw);
|
|
30221
|
+
} catch {
|
|
30222
|
+
throw new Error('org project set: notifyOnDeploy must be a JSON array, e.g. ["dev"]');
|
|
30223
|
+
}
|
|
30224
|
+
if (!Array.isArray(parsed) || parsed.some((s) => s !== "dev" && s !== "rc" && s !== "main")) {
|
|
30225
|
+
throw new Error('org project set: notifyOnDeploy must be a JSON array of stages: dev, rc, main (e.g. ["dev"])');
|
|
30226
|
+
}
|
|
30227
|
+
if (new Set(parsed).size !== parsed.length) {
|
|
30228
|
+
throw new Error("org project set: notifyOnDeploy must not contain duplicate stages");
|
|
30229
|
+
}
|
|
30230
|
+
return parsed;
|
|
30231
|
+
}
|
|
30232
|
+
var VERCEL_VERSION_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
30233
|
+
var VERCEL_VERSION_ENV_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
30234
|
+
function parseVercelVersionEnvVar(raw) {
|
|
30235
|
+
let parsed;
|
|
30236
|
+
try {
|
|
30237
|
+
parsed = JSON.parse(raw);
|
|
30238
|
+
} catch {
|
|
30239
|
+
throw new Error('org project set: vercelVersionEnv must be JSON, e.g. {"project":"mm-work","key":"MMW_VERSION","team":"team_xxx"}');
|
|
30240
|
+
}
|
|
30241
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
30242
|
+
throw new Error("org project set: vercelVersionEnv must be a JSON object { project, key, team? }");
|
|
30243
|
+
}
|
|
30244
|
+
const row = parsed;
|
|
30245
|
+
const unknown = Object.keys(row).filter((key) => key !== "project" && key !== "key" && key !== "team");
|
|
30246
|
+
if (unknown.length > 0) throw new Error(`org project set: vercelVersionEnv has unknown field(s): ${unknown.join(", ")}`);
|
|
30247
|
+
if (typeof row.project !== "string" || !VERCEL_VERSION_ENV_ID_RE.test(row.project)) {
|
|
30248
|
+
throw new Error("org project set: vercelVersionEnv.project must be a Vercel project id or name");
|
|
30249
|
+
}
|
|
30250
|
+
if (typeof row.key !== "string" || !VERCEL_VERSION_ENV_NAME_RE.test(row.key)) {
|
|
30251
|
+
throw new Error("org project set: vercelVersionEnv.key must be an env name (e.g. MMW_VERSION)");
|
|
30252
|
+
}
|
|
30253
|
+
if (row.team !== void 0 && (typeof row.team !== "string" || !VERCEL_VERSION_ENV_ID_RE.test(row.team))) {
|
|
30254
|
+
throw new Error("org project set: vercelVersionEnv.team must be a Vercel team id or slug");
|
|
30255
|
+
}
|
|
30256
|
+
return { project: row.project, key: row.key, ...typeof row.team === "string" ? { team: row.team } : {} };
|
|
30257
|
+
}
|
|
30036
30258
|
function parseCiExemptReasonVar(raw) {
|
|
30037
30259
|
const trimmed = raw.trim();
|
|
30038
30260
|
if (!trimmed) throw new Error("org project set: ciExemptReason must be a non-empty reason (or use --unset ciExemptReason to clear it)");
|
|
@@ -30173,7 +30395,9 @@ var SETTABLE_VAR_KEYS = [
|
|
|
30173
30395
|
"gate",
|
|
30174
30396
|
"secrets",
|
|
30175
30397
|
"seedCanary",
|
|
30176
|
-
"installer"
|
|
30398
|
+
"installer",
|
|
30399
|
+
"notifyOnDeploy",
|
|
30400
|
+
"vercelVersionEnv"
|
|
30177
30401
|
];
|
|
30178
30402
|
var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
30179
30403
|
var SETTABLE_VAR_HINTS = {
|
|
@@ -30202,6 +30426,8 @@ var SETTABLE_VAR_HINTS = {
|
|
|
30202
30426
|
ci: "none \u2014 declare intentional no-ci",
|
|
30203
30427
|
requiredChecks: 'JSON array, e.g. ["gate"] or [] for no-ci',
|
|
30204
30428
|
requiredCheckBranches: "non-empty JSON branch-name array; exact product-ruleset scope (#5210)",
|
|
30429
|
+
notifyOnDeploy: 'JSON stage array, e.g. ["dev"] \u2014 successful tenant-deploy sends <stage>-deployed to the project repo (#7358)',
|
|
30430
|
+
vercelVersionEnv: "JSON {project,key,team?} \u2014 train sets this Vercel env to the release tag before the main/rc push (#7362)",
|
|
30205
30431
|
ciExemptReason: "non-empty string \u2014 declares this deployable repo has no CI surface (#4265)",
|
|
30206
30432
|
gate: "JSON {runtime,cmd,affectedCmd,workdir,cacheDepPath,pyVersion}"
|
|
30207
30433
|
};
|
|
@@ -30345,6 +30571,10 @@ function buildProjectSetPatch(input) {
|
|
|
30345
30571
|
patch[key] = parseRequiredChecksVar(raw);
|
|
30346
30572
|
} else if (key === "requiredCheckBranches") {
|
|
30347
30573
|
patch[key] = parseRequiredCheckBranchesVar(raw);
|
|
30574
|
+
} else if (key === "notifyOnDeploy") {
|
|
30575
|
+
patch[key] = parseNotifyOnDeployVar(raw);
|
|
30576
|
+
} else if (key === "vercelVersionEnv") {
|
|
30577
|
+
patch[key] = parseVercelVersionEnvVar(raw);
|
|
30348
30578
|
} else if (key === "ciExemptReason") {
|
|
30349
30579
|
patch[key] = parseCiExemptReasonVar(raw);
|
|
30350
30580
|
} else if (key === "gate") {
|
|
@@ -30389,6 +30619,8 @@ var DEPLOY_SHELL_SAFE_RE = /^[A-Za-z0-9./:_?&=%-]*$/;
|
|
|
30389
30619
|
var DEPLOY_ROLE_ARN_RE = /^arn:aws:iam::\d{12}:role\/[A-Za-z0-9+=,.@_/-]{1,128}$/;
|
|
30390
30620
|
var DEPLOY_BUCKET_RE = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
|
|
30391
30621
|
var DEPLOY_DISTRIBUTION_ID_RE = /^[A-Z0-9]{5,32}$/;
|
|
30622
|
+
var COMPOSE_PROFILE_RE = /^[a-z0-9][a-z0-9_-]{0,62}$/;
|
|
30623
|
+
var MAX_COMPOSE_PROFILES = 8;
|
|
30392
30624
|
function buildSetDeployPatch(_slug, input) {
|
|
30393
30625
|
const clean4 = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
|
|
30394
30626
|
const stage = clean4(input.stage);
|
|
@@ -30432,6 +30664,23 @@ function buildSetDeployPatch(_slug, input) {
|
|
|
30432
30664
|
if (input.noEnvFile !== void 0 && typeof input.noEnvFile !== "boolean") {
|
|
30433
30665
|
throw new Error("org project set-deploy: --no-env-file must be true or false");
|
|
30434
30666
|
}
|
|
30667
|
+
if (input.clearComposeProfiles && (input.composeProfiles?.length ?? 0) > 0) {
|
|
30668
|
+
throw new Error("org project set-deploy: --clear-compose-profiles cannot be combined with --compose-profiles (they contradict)");
|
|
30669
|
+
}
|
|
30670
|
+
let composeProfiles;
|
|
30671
|
+
if (input.clearComposeProfiles) {
|
|
30672
|
+
composeProfiles = [];
|
|
30673
|
+
} else if (input.composeProfiles !== void 0) {
|
|
30674
|
+
const names = input.composeProfiles.map((p) => typeof p === "string" ? p.trim() : "");
|
|
30675
|
+
if (names.length > MAX_COMPOSE_PROFILES) throw new Error(`org project set-deploy: --compose-profiles carries at most ${MAX_COMPOSE_PROFILES} profiles`);
|
|
30676
|
+
const seen = /* @__PURE__ */ new Set();
|
|
30677
|
+
for (const name of names) {
|
|
30678
|
+
if (!COMPOSE_PROFILE_RE.test(name)) throw new Error(`org project set-deploy: --compose-profiles must be comma-separated profile names ([a-z0-9_-], up to 63 chars), got ${JSON.stringify(name)}`);
|
|
30679
|
+
if (seen.has(name)) throw new Error(`org project set-deploy: --compose-profiles carries a duplicate profile ${JSON.stringify(name)}`);
|
|
30680
|
+
seen.add(name);
|
|
30681
|
+
}
|
|
30682
|
+
composeProfiles = [...seen];
|
|
30683
|
+
}
|
|
30435
30684
|
return {
|
|
30436
30685
|
stage,
|
|
30437
30686
|
...substrate !== void 0 ? { substrate } : {},
|
|
@@ -30448,7 +30697,10 @@ function buildSetDeployPatch(_slug, input) {
|
|
|
30448
30697
|
// #2986: an explicit `[]` is what the route reads as "clear them"; omitting the key is what it reads as
|
|
30449
30698
|
// "keep them". Those are different instructions and they now have different flags.
|
|
30450
30699
|
...input.clearAliases ? { aliases: [] } : uniqueAliases.length ? { aliases: uniqueAliases } : {},
|
|
30451
|
-
...input.noEnvFile !== void 0 ? { noEnvFile: input.noEnvFile } : {}
|
|
30700
|
+
...input.noEnvFile !== void 0 ? { noEnvFile: input.noEnvFile } : {},
|
|
30701
|
+
// #7353: an explicit `[]` is what the route reads as "clear them"; omitting the key is what it reads as
|
|
30702
|
+
// "keep them" — same patch contract as aliases (#2986/#2984).
|
|
30703
|
+
...composeProfiles !== void 0 ? { composeProfiles } : {}
|
|
30452
30704
|
};
|
|
30453
30705
|
}
|
|
30454
30706
|
var DEPLOY_STAGE_BRANCH = { dev: "development", rc: "rc", main: "main" };
|
|
@@ -34422,6 +34674,22 @@ async function suiteRcand(deps, opts) {
|
|
|
34422
34674
|
Object.assign(member, { carried: true }, await carriedPins(deps, member, modules.find((m) => m.repo === member.repo).bundles));
|
|
34423
34675
|
writeSuiteCut(deps.recordPath, record);
|
|
34424
34676
|
}
|
|
34677
|
+
const refusals = [];
|
|
34678
|
+
for (const member of record.members) {
|
|
34679
|
+
if (member.carried || member.published || member.id === "katip") continue;
|
|
34680
|
+
try {
|
|
34681
|
+
await deps.preflight(member.repo);
|
|
34682
|
+
} catch (e) {
|
|
34683
|
+
refusals.push(`${member.repo}: ${e.message}`);
|
|
34684
|
+
}
|
|
34685
|
+
}
|
|
34686
|
+
if (refusals.length) {
|
|
34687
|
+
const untouched = record.members.every((m) => !m.tag) && !record.pinPr;
|
|
34688
|
+
if (untouched) (0, import_node_fs29.rmSync)(deps.recordPath, { force: true });
|
|
34689
|
+
throw new Error(`module rc serving is not ready; ${untouched ? "no suite cut started" : "no new tag was cut"}:
|
|
34690
|
+
${refusals.join("\n")}
|
|
34691
|
+
Each module must ship a live release that bind-mounts <deployPath>/data read-only and serves <data>/rc/module.js at /rc/module.js (docs/Guides/suite-trains.md).`);
|
|
34692
|
+
}
|
|
34425
34693
|
for (const member of record.members) {
|
|
34426
34694
|
if ((await deps.head(member.repo, "development"))?.toLowerCase() !== member.development.toLowerCase())
|
|
34427
34695
|
throw new Error(`${member.repo}: development moved after the cut snapshot; refuse to add untested code`);
|
|
@@ -34631,7 +34899,7 @@ function announceSummaryFidelity(lines2, rangeLog) {
|
|
|
34631
34899
|
if (shipped.size === 0) return null;
|
|
34632
34900
|
const invented = [...new Set(lines2.flatMap((l) => l.match(/#\d+/g) ?? []))].filter((ref) => !shipped.has(ref));
|
|
34633
34901
|
if (invented.length === 0) return null;
|
|
34634
|
-
return `--announce-summary-file cites ${invented.join(", ")}, which ship in no commit of this release range \u2014 a summary line must describe work that actually shipped. Rebuild the summary from \`git log origin/main..HEAD\` (read commit BODIES too: a bundled PR closes issues its subject never names), then rerun the same release command \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
34902
|
+
return `--announce-summary-file cites ${invented.join(", ")}, which ship in no commit of this release range \u2014 a summary line must describe work that actually shipped. Rebuild the summary from \`git log origin/main..HEAD\` (read commit BODIES too: a bundled PR closes issues its subject never names), then rerun the same release command \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
34635
34903
|
}
|
|
34636
34904
|
async function resolveAnnounceTarget(deps, repo) {
|
|
34637
34905
|
if (repo.toLowerCase() === ANNOUNCE_REPO.toLowerCase()) return { kind: "alerts" };
|
|
@@ -34709,6 +34977,12 @@ function normalizeHotfixVersion(input) {
|
|
|
34709
34977
|
return { tag: `v${m[1]}`, version: m[1] };
|
|
34710
34978
|
}
|
|
34711
34979
|
var deriveHotfixVersion = resolveHotfixVersion;
|
|
34980
|
+
function bumpHotfixPatchVersion(version) {
|
|
34981
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
|
|
34982
|
+
if (!m) throw new Error(`cannot bump non-X.Y.Z hotfix version "${version}"`);
|
|
34983
|
+
const next = `${m[1]}.${m[2]}.${Number(m[3]) + 1}`;
|
|
34984
|
+
return { tag: `v${next}`, version: next };
|
|
34985
|
+
}
|
|
34712
34986
|
function hotfixBranch(tag) {
|
|
34713
34987
|
return `hotfix/${tag}`;
|
|
34714
34988
|
}
|
|
@@ -34762,7 +35036,7 @@ function formatHotfixBatchPreflightRefusal(failures, fromSpecs) {
|
|
|
34762
35036
|
const batchFrom = fromSpecs.join(",");
|
|
34763
35037
|
return `hotfix start batch preflight refused: ${failures.length} pick(s) would hard-stop on origin/main:
|
|
34764
35038
|
${lines2.join("\n")}
|
|
34765
|
-
Land ONE development-based cite-only synthesis PR for the whole batch (#4643 supersedes the old cut-from-origin/main wording, which produced main-lineage PRs into development \u2014 the shape portFoldToDevelopment forbids, #4365/#4371): cut the branch FROM origin/development, regenerate the listed conflict content so it cherry-picks cleanly onto current origin/main, and land that development-parented PR with the cite-only marker (the proven #5096 flow), then rerun \`mmi-cli devops hotfix start --from ${batchFrom}\` with the port merge SHA(s) \u2014 never hand-resolve onto main; never a main-parented branch into development; never N serial ports (#3167) \u2014 see docs/Guides/train-troubleshooting.md#hotfix-pick-refused`;
|
|
35039
|
+
Land ONE development-based cite-only synthesis PR for the whole batch (#4643 supersedes the old cut-from-origin/main wording, which produced main-lineage PRs into development \u2014 the shape portFoldToDevelopment forbids, #4365/#4371): cut the branch FROM origin/development, regenerate the listed conflict content so it cherry-picks cleanly onto current origin/main, and land that development-parented PR with the cite-only marker (the proven #5096 flow), then rerun \`mmi-cli devops hotfix start --from ${batchFrom}\` with the port merge SHA(s) \u2014 never hand-resolve onto main; never a main-parented branch into development; never N serial ports (#3167) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-pick-refused`;
|
|
34766
35040
|
}
|
|
34767
35041
|
var CHERRY_PICKED_FROM = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
34768
35042
|
async function findHotfixPickGaps(deps, sources, tolerated) {
|
|
@@ -34791,32 +35065,44 @@ ${lines2.join("\n")}
|
|
|
34791
35065
|
A hotfix branch is cut from origin/main and carries ONLY the fixes --from names, in order (#4411) \u2014 every other merged development fix stays unshipped while the channel reports healthy (#6994). Add each omitted fix to the pick list and rerun \`mmi-cli devops hotfix start --from ${[...fromSpecs, ...gaps.map((g) => g.sha.slice(0, 7))].join(",")}\`, or, when a listed fix is provably not needed on main (a development-only change, already superseded), rerun with \`--ack ${gaps.map((g) => g.sha.slice(0, 7)).join(",")}\` naming exactly the sha(s) a human excluded \u2014 the ack is recorded in the PR body, never silent.`;
|
|
34792
35066
|
}
|
|
34793
35067
|
async function preflightHotfixBatchPicks(deps, sources, pickTolerated) {
|
|
34794
|
-
if (sources.length === 0) return [];
|
|
34795
|
-
const
|
|
34796
|
-
const
|
|
35068
|
+
if (sources.length === 0) return { failures: [], skipped: [] };
|
|
35069
|
+
const { mkdtemp: mkdtemp2, rm: rm2 } = await import("node:fs/promises");
|
|
35070
|
+
const { tmpdir: tmpdir10 } = await import("node:os");
|
|
35071
|
+
const { join: join44 } = await import("node:path");
|
|
35072
|
+
const dir = await mkdtemp2(join44(tmpdir10(), "mmi-hotfix-preflight-"));
|
|
35073
|
+
const wt = { run: (file, args) => deps.run(file, file === "git" ? ["-C", dir, ...args] : args) };
|
|
34797
35074
|
const failures = [];
|
|
35075
|
+
const skipped = [];
|
|
34798
35076
|
try {
|
|
34799
|
-
await deps.run("git", ["
|
|
34800
|
-
|
|
34801
|
-
|
|
34802
|
-
await
|
|
34803
|
-
|
|
34804
|
-
|
|
34805
|
-
|
|
34806
|
-
|
|
34807
|
-
|
|
34808
|
-
|
|
34809
|
-
|
|
34810
|
-
|
|
35077
|
+
await deps.run("git", ["worktree", "add", "--detach", dir, "origin/main"]);
|
|
35078
|
+
try {
|
|
35079
|
+
for (const source of sources) {
|
|
35080
|
+
const before = await wt.run("git", ["status", "--porcelain"]);
|
|
35081
|
+
try {
|
|
35082
|
+
await cherryPickWithToleratedPaths(wt, source.sha, pickTolerated);
|
|
35083
|
+
} catch (e) {
|
|
35084
|
+
const message2 = e.message ?? String(e);
|
|
35085
|
+
const after = await wt.run("git", ["status", "--porcelain"]).catch(() => null);
|
|
35086
|
+
if (after !== null && after === before && /empty|nothing to commit/i.test(message2)) {
|
|
35087
|
+
await wt.run("git", ["cherry-pick", "--quit"]).catch(() => void 0);
|
|
35088
|
+
skipped.push({ sha: source.sha, label: source.label });
|
|
35089
|
+
continue;
|
|
35090
|
+
}
|
|
35091
|
+
const blocking = parseCherryPickBlockingPaths(message2);
|
|
35092
|
+
failures.push({
|
|
35093
|
+
label: source.label,
|
|
35094
|
+
sha: source.sha,
|
|
35095
|
+
blocking: blocking.length > 0 ? blocking : [message2.split("\n")[0] ?? message2]
|
|
35096
|
+
});
|
|
35097
|
+
}
|
|
34811
35098
|
}
|
|
35099
|
+
} finally {
|
|
35100
|
+
await deps.run("git", ["worktree", "remove", "--force", dir]).catch(() => void 0);
|
|
34812
35101
|
}
|
|
34813
35102
|
} finally {
|
|
34814
|
-
|
|
34815
|
-
await deps.run("git", ["checkout", startBranch]).catch(() => void 0);
|
|
34816
|
-
}
|
|
34817
|
-
await deps.run("git", ["branch", "-D", preflightBranch]).catch(() => void 0);
|
|
35103
|
+
await rm2(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
34818
35104
|
}
|
|
34819
|
-
return failures;
|
|
35105
|
+
return { failures, skipped };
|
|
34820
35106
|
}
|
|
34821
35107
|
async function restoreAfterFailedPort(deps, opts) {
|
|
34822
35108
|
const { startBranch, branch, created } = opts;
|
|
@@ -34939,10 +35225,20 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
34939
35225
|
throw new Error(`working tree must be clean before hotfix start${describeBlockingPaths(status)}`);
|
|
34940
35226
|
}
|
|
34941
35227
|
await deps.run("git", ["fetch", "origin", "--tags"]);
|
|
34942
|
-
const { deployModel } = await hotfixPreflight(deps, ctx, "start");
|
|
34943
|
-
const
|
|
35228
|
+
const { deployModel, meta } = await hotfixPreflight(deps, ctx, "start");
|
|
35229
|
+
const bumpNotes = [];
|
|
35230
|
+
let resolved = await resolveHotfixVersion(deps);
|
|
35231
|
+
for (; ; ) {
|
|
35232
|
+
const priorPr = await findHotfixPr(deps, ctx, resolved.tag);
|
|
35233
|
+
if ((priorPr?.state ?? "").toUpperCase() !== "MERGED") break;
|
|
35234
|
+
const priorReleased = await hotfixReleaseExists(deps, ctx, resolved.tag);
|
|
35235
|
+
const next = bumpHotfixPatchVersion(resolved.version);
|
|
35236
|
+
bumpNotes.push(`hotfix ${resolved.tag} is already merged on origin/main (PR #${priorPr?.number}, ${priorReleased ? "released" : "unreleased"}) \u2014 origin/main already carries its fold, so this cycle mints ${next.tag} instead of reusing an already-bumped version (#7338)`);
|
|
35237
|
+
resolved = next;
|
|
35238
|
+
}
|
|
35239
|
+
const { tag, version } = resolved;
|
|
34944
35240
|
const branch = hotfixBranch(tag);
|
|
34945
|
-
const notes = [];
|
|
35241
|
+
const notes = [...bumpNotes];
|
|
34946
35242
|
const existingPr = await findHotfixPr(deps, ctx, tag);
|
|
34947
35243
|
const existingState = (existingPr?.state ?? "").toUpperCase();
|
|
34948
35244
|
const releaseExists = existingState === "MERGED" ? await hotfixReleaseExists(deps, ctx, tag) : false;
|
|
@@ -34974,8 +35270,8 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
34974
35270
|
if (specs.length === 0) throw new Error("hotfix start: --from named no PR or SHA");
|
|
34975
35271
|
const sources = [];
|
|
34976
35272
|
for (const spec of specs) {
|
|
34977
|
-
const
|
|
34978
|
-
if (!sources.some((s) => s.sha ===
|
|
35273
|
+
const resolved2 = await resolveHotfixSource(deps, ctx, spec);
|
|
35274
|
+
if (!sources.some((s) => s.sha === resolved2.sha)) sources.push(resolved2);
|
|
34979
35275
|
}
|
|
34980
35276
|
const label = sources.map((s) => s.label).join(", ");
|
|
34981
35277
|
const { foldPaths, pickTolerated } = await resolveHotfixPickTolerated(deps, deployModel);
|
|
@@ -34991,21 +35287,33 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
34991
35287
|
if (pickGaps.length > 0) {
|
|
34992
35288
|
notes.push(`pick-completeness ack: excluded ${pickGaps.map((g) => `${g.sha.slice(0, 7)} ${g.subject}`).join("; ")} by --ack (#6994)`);
|
|
34993
35289
|
}
|
|
35290
|
+
let activeSources = sources;
|
|
35291
|
+
let activeLabel = label;
|
|
34994
35292
|
const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
34995
35293
|
if (remoteBranch) {
|
|
34996
35294
|
await deps.run("git", ["checkout", branch]);
|
|
34997
35295
|
await deps.run("git", ["pull", "--ff-only", "origin", branch]);
|
|
34998
35296
|
notes.push(`branch ${branch} already on origin \u2014 reused (cherry-pick/bump assumed present; PR step resumes)`);
|
|
34999
35297
|
} else {
|
|
35000
|
-
const
|
|
35001
|
-
if (
|
|
35002
|
-
throw new Error(formatHotfixBatchPreflightRefusal(
|
|
35298
|
+
const preflight2 = await preflightHotfixBatchPicks(deps, sources, pickTolerated);
|
|
35299
|
+
if (preflight2.failures.length > 0) {
|
|
35300
|
+
throw new Error(formatHotfixBatchPreflightRefusal(preflight2.failures, specs));
|
|
35301
|
+
}
|
|
35302
|
+
if (preflight2.skipped.length > 0) {
|
|
35303
|
+
const skippedShas = new Set(preflight2.skipped.map((s) => s.sha));
|
|
35304
|
+
const remaining = sources.filter((s) => !skippedShas.has(s.sha));
|
|
35305
|
+
notes.push(`auto-skipped ${preflight2.skipped.length} pick(s) already present on origin/main (empty cherry-pick \u2014 content already shipped there, #7338): ${preflight2.skipped.map((s) => s.label).join(", ")}`);
|
|
35306
|
+
if (remaining.length === 0) {
|
|
35307
|
+
throw new Error(`hotfix start: every named pick is already present on origin/main (${preflight2.skipped.map((s) => s.label).join(", ")}) \u2014 nothing to carry, so no ${branch} branch was cut`);
|
|
35308
|
+
}
|
|
35309
|
+
activeSources = remaining;
|
|
35310
|
+
activeLabel = remaining.map((s) => s.label).join(", ");
|
|
35003
35311
|
}
|
|
35004
35312
|
const startBranch = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
35005
35313
|
const preexistingLocal = clean3(await deps.run("git", ["branch", "--list", branch]));
|
|
35006
35314
|
await deps.run("git", ["checkout", "-B", branch, "origin/main"]);
|
|
35007
35315
|
try {
|
|
35008
|
-
for (const source of
|
|
35316
|
+
for (const source of activeSources) {
|
|
35009
35317
|
const autoResolved = await cherryPickWithToleratedPaths(deps, source.sha, pickTolerated);
|
|
35010
35318
|
if (autoResolved.length > 0) {
|
|
35011
35319
|
notes.push(`auto-resolved regenerable cherry-pick conflict(s) for ${source.label}: ${autoResolved.join(", ")} (regenerated in bump step)`);
|
|
@@ -35014,12 +35322,12 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
35014
35322
|
} catch (e) {
|
|
35015
35323
|
const recovery = await restoreAfterFailedPort(deps, { startBranch, branch, created: !preexistingLocal });
|
|
35016
35324
|
throw new Error(
|
|
35017
|
-
`cherry-pick of ${
|
|
35325
|
+
`cherry-pick of ${activeLabel} onto ${branch} conflicted \u2014 aborted; nothing was pushed. Land the conflict resolution on development through a normal PR, then rerun \`mmi-cli devops hotfix start --from <that PR's merge sha>\` \u2014 never hand-resolve the port onto main. ${recovery} (${e.message ?? e})`
|
|
35018
35326
|
);
|
|
35019
35327
|
}
|
|
35020
|
-
notes.push(`cherry-picked ${
|
|
35328
|
+
notes.push(`cherry-picked ${activeLabel} onto ${branch} (from origin/main, -x trailer recorded)`);
|
|
35329
|
+
const durableSource = clean3(await deps.run("git", ["merge-base", "HEAD", "origin/main"]));
|
|
35021
35330
|
if (deployModel === "hub-serverless") {
|
|
35022
|
-
const durableSource = clean3(await deps.run("git", ["merge-base", "HEAD", "origin/main"]));
|
|
35023
35331
|
await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version, "--source-commit", durableSource]);
|
|
35024
35332
|
const changedFiles = (await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
35025
35333
|
await deps.run("git", ["add", "-f", "--", ...changedFiles]);
|
|
@@ -35031,13 +35339,16 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
35031
35339
|
notes.push("distribution prepare produced no changes \u2014 nothing extra committed");
|
|
35032
35340
|
}
|
|
35033
35341
|
} else if (foldPaths.length > 0) {
|
|
35034
|
-
const foldNote = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
|
|
35342
|
+
const foldNote = await withFoldSourceCommit(durableSource, () => foldReleaseVersion(deps, deployModel, tag, foldPaths));
|
|
35035
35343
|
notes.push(foldNote);
|
|
35036
35344
|
} else {
|
|
35037
35345
|
notes.push(`version fold skipped (deployModel=${deployModel} carries no version manifest \u2014 the tag is the version)`);
|
|
35038
35346
|
}
|
|
35347
|
+
await assertHotfixFoldBomStamp(deps, tag);
|
|
35039
35348
|
await deps.run("git", ["push", "-u", "origin", branch]);
|
|
35040
35349
|
}
|
|
35350
|
+
const versionEnv = await setVercelVersionEnv(vercelVersionEnvDeps(deps), meta, "main", tag);
|
|
35351
|
+
if (versionEnv.note) notes.push(versionEnv.note);
|
|
35041
35352
|
const bumpNote = deployModel === "hub-serverless" ? " with the locked distribution bump" : foldPaths.length > 0 ? " with the package version bump" : "";
|
|
35042
35353
|
const prUrl = clean3(await deps.run("gh", [
|
|
35043
35354
|
"pr",
|
|
@@ -35053,16 +35364,16 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
35053
35364
|
// Every picked sha goes in the marker (#4411) — `hotfix release` reads it when --carries is omitted,
|
|
35054
35365
|
// so a multi-fix cycle proves ALL of its targets present before tagging, not just the first.
|
|
35055
35366
|
"--body",
|
|
35056
|
-
`Hotfix ${tag}: cherry-pick of ${
|
|
35367
|
+
`Hotfix ${tag}: cherry-pick of ${activeLabel} onto origin/main${bumpNote}.
|
|
35057
35368
|
|
|
35058
|
-
<!-- mmi-hotfix-carries: ${
|
|
35369
|
+
<!-- mmi-hotfix-carries: ${activeSources.map((s) => s.sha).join(",")} -->${pickGaps.length > 0 ? `
|
|
35059
35370
|
|
|
35060
35371
|
<!-- mmi-hotfix-acked-excluded: ${pickGaps.map((g) => g.sha).join(",")} (#6994 pick-completeness) -->` : ""}
|
|
35061
35372
|
|
|
35062
35373
|
Merge this PR (human-initiated), then run \`mmi-cli devops hotfix release ${tag}\`.`
|
|
35063
35374
|
]));
|
|
35064
35375
|
notes.push(`opened hotfix PR ${prUrl} \u2014 merge it (human-initiated), then: mmi-cli devops hotfix release ${tag}`);
|
|
35065
|
-
return { ...ctx, command: "hotfix-start", tag, version, branch, source:
|
|
35376
|
+
return { ...ctx, command: "hotfix-start", tag, version, branch, source: activeLabel, prUrl, reused: Boolean(remoteBranch), notes };
|
|
35066
35377
|
}
|
|
35067
35378
|
function hotfixWorkflowRunFromDispatch(workflow, dispatch) {
|
|
35068
35379
|
return {
|
|
@@ -35099,6 +35410,13 @@ async function watchReleaseRun(deps, ctx, workflow, sha) {
|
|
|
35099
35410
|
const run = rows.find((r) => r.headSha === sha && typeof r.databaseId === "number");
|
|
35100
35411
|
if (!run) continue;
|
|
35101
35412
|
if (run.status === "completed") return { workflow, runId: run.databaseId, url: run.url, conclusion: run.conclusion ?? "unknown" };
|
|
35413
|
+
try {
|
|
35414
|
+
const viewed = JSON.parse(await deps.run("gh", ["run", "view", String(run.databaseId), "--repo", ctx.repo, "--json", "status,conclusion"]));
|
|
35415
|
+
if (viewed.status === "completed") {
|
|
35416
|
+
return { workflow, runId: run.databaseId, url: run.url, conclusion: typeof viewed.conclusion === "string" ? viewed.conclusion : "unknown" };
|
|
35417
|
+
}
|
|
35418
|
+
} catch {
|
|
35419
|
+
}
|
|
35102
35420
|
try {
|
|
35103
35421
|
await deps.run("gh", ghRunWatchArgs(run.databaseId, ctx.repo));
|
|
35104
35422
|
return { workflow, runId: run.databaseId, url: run.url, conclusion: "success" };
|
|
@@ -35126,9 +35444,9 @@ async function portFoldToDevelopment(deps, ctx, deployModel, tag) {
|
|
|
35126
35444
|
const branch = devFoldBranch(tag);
|
|
35127
35445
|
const existing = await findFoldPr(deps, ctx, tag);
|
|
35128
35446
|
if (existing?.state === "OPEN") {
|
|
35129
|
-
return (await enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `development fold PR #${existing.number} for ${tag} is OPEN \u2014 reused${existing.url ? ` (${existing.url})` : ""} \u2014 see docs/Guides/train-troubleshooting.md#hotfix-fold-port`)).note;
|
|
35447
|
+
return (await enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `development fold PR #${existing.number} for ${tag} is OPEN \u2014 reused${existing.url ? ` (${existing.url})` : ""} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-fold-port`)).note;
|
|
35130
35448
|
}
|
|
35131
|
-
if (existing) return `development fold PR #${existing.number} for ${tag} is ${existing.state} \u2014 reused${existing.url ? ` (${existing.url})` : ""} \u2014 see docs/Guides/train-troubleshooting.md#hotfix-fold-port`;
|
|
35449
|
+
if (existing) return `development fold PR #${existing.number} for ${tag} is ${existing.state} \u2014 reused${existing.url ? ` (${existing.url})` : ""} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-fold-port`;
|
|
35132
35450
|
const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
35133
35451
|
await reapFoldScratchBranches(deps);
|
|
35134
35452
|
const scratch = devFoldScratchBranch(tag, deps);
|
|
@@ -35156,7 +35474,7 @@ async function portFoldToDevelopment(deps, ctx, deployModel, tag) {
|
|
|
35156
35474
|
|
|
35157
35475
|
Generated by \`mmi-cli devops hotfix release\` \u2014 no main-parented commit is merged in, so the squash parents stay clean (#4365/#4371). Auto-merge is enqueued through the same path as the release alignment PR (#6068); it lands once the checks it triggers pass.`
|
|
35158
35476
|
]));
|
|
35159
|
-
return (await enqueueAlignmentAutoMerge(deps, ctx, parsePrNumber(prUrl), prUrl || void 0, `opened development fold PR ${prUrl} (${foldNote}) \u2014 see docs/Guides/train-troubleshooting.md#hotfix-fold-port`)).note;
|
|
35477
|
+
return (await enqueueAlignmentAutoMerge(deps, ctx, parsePrNumber(prUrl), prUrl || void 0, `opened development fold PR ${prUrl} (${foldNote}) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-fold-port`)).note;
|
|
35160
35478
|
} finally {
|
|
35161
35479
|
if (previousRef && previousRef !== "HEAD") await deps.run("git", ["checkout", previousRef]).catch(() => void 0);
|
|
35162
35480
|
await deps.run("git", ["branch", "-D", scratch]).catch(() => void 0);
|
|
@@ -35344,6 +35662,41 @@ ${recovery.note}; the full required-check wall did not revalidate`);
|
|
|
35344
35662
|
}, `the immutable ${anchors.tag} pushed at ${mergedSha.slice(0, 12)}`);
|
|
35345
35663
|
return checks;
|
|
35346
35664
|
}
|
|
35665
|
+
var FOLD_SOURCE_COMMIT_ENV = "MMI_FOLD_SOURCE_COMMIT";
|
|
35666
|
+
async function withFoldSourceCommit(durableSource, fn) {
|
|
35667
|
+
const prev = process.env[FOLD_SOURCE_COMMIT_ENV];
|
|
35668
|
+
process.env[FOLD_SOURCE_COMMIT_ENV] = durableSource;
|
|
35669
|
+
try {
|
|
35670
|
+
return await fn();
|
|
35671
|
+
} finally {
|
|
35672
|
+
if (prev === void 0) delete process.env[FOLD_SOURCE_COMMIT_ENV];
|
|
35673
|
+
else process.env[FOLD_SOURCE_COMMIT_ENV] = prev;
|
|
35674
|
+
}
|
|
35675
|
+
}
|
|
35676
|
+
async function assertHotfixFoldBomStamp(deps, tag) {
|
|
35677
|
+
let bomText;
|
|
35678
|
+
try {
|
|
35679
|
+
bomText = await deps.run("git", ["show", "HEAD:distribution-bom.json"]);
|
|
35680
|
+
} catch {
|
|
35681
|
+
return;
|
|
35682
|
+
}
|
|
35683
|
+
let stamp;
|
|
35684
|
+
try {
|
|
35685
|
+
stamp = JSON.parse(bomText).sourceCommit;
|
|
35686
|
+
} catch (e) {
|
|
35687
|
+
throw new Error(`hotfix ${tag}: distribution-bom.json at the fold commit is unparseable (${e.message}) \u2014 fix the BOM before the hotfix PR is opened, then rerun hotfix start`);
|
|
35688
|
+
}
|
|
35689
|
+
if (typeof stamp !== "string" || !/^[0-9a-f]{40}$/.test(stamp)) {
|
|
35690
|
+
throw new Error(`hotfix ${tag}: distribution-bom.json at the fold commit carries no 40-hex sourceCommit \u2014 the updater would refuse this release; fix the BOM before the hotfix PR is opened, then rerun hotfix start`);
|
|
35691
|
+
}
|
|
35692
|
+
try {
|
|
35693
|
+
await deps.run("git", ["merge-base", "--is-ancestor", stamp, "origin/main"]);
|
|
35694
|
+
} catch {
|
|
35695
|
+
throw new Error(
|
|
35696
|
+
`hotfix ${tag}: distribution-bom.json at the fold commit stamps sourceCommit ${stamp.slice(0, 12)}, which is NOT on origin/main \u2014 the squash merge of the hotfix PR would orphan it and hotfix release would refuse (hotfix-bom-lineage). Make the consumer stamp ${FOLD_SOURCE_COMMIT_ENV} when set (the train exports the durable merge-base during the fold, Jerv-Hub#1084), land that on development, and rerun hotfix start \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-bom-lineage`
|
|
35697
|
+
);
|
|
35698
|
+
}
|
|
35699
|
+
}
|
|
35347
35700
|
async function assertConsumerBomLineage(deps, mergedSha, tag) {
|
|
35348
35701
|
let bomText;
|
|
35349
35702
|
try {
|
|
@@ -35364,7 +35717,7 @@ async function assertConsumerBomLineage(deps, mergedSha, tag) {
|
|
|
35364
35717
|
await deps.run("git", ["merge-base", "--is-ancestor", stamp, mergedSha]);
|
|
35365
35718
|
} catch {
|
|
35366
35719
|
throw new Error(
|
|
35367
|
-
`hotfix ${tag}: distribution-bom.json at ${mergedSha.slice(0, 12)} stamps sourceCommit ${stamp.slice(0, 12)}, which the merged main commit does not contain \u2014 every updater would reject ${tag} (gateCandidate ancestry). The fold was stamped on the hotfix branch and squash-merged away: make the consumer stamp a durable merge-base (Jerv-Hub#1084), land that on development, and port it with the fix \u2014 see docs/Guides/train-troubleshooting.md#hotfix-bom-lineage`
|
|
35720
|
+
`hotfix ${tag}: distribution-bom.json at ${mergedSha.slice(0, 12)} stamps sourceCommit ${stamp.slice(0, 12)}, which the merged main commit does not contain \u2014 every updater would reject ${tag} (gateCandidate ancestry). The fold was stamped on the hotfix branch and squash-merged away: make the consumer stamp a durable merge-base (Jerv-Hub#1084), land that on development, and port it with the fix \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-bom-lineage`
|
|
35368
35721
|
);
|
|
35369
35722
|
}
|
|
35370
35723
|
}
|
|
@@ -35410,16 +35763,16 @@ async function runHotfixReleaseLocked(deps, versionInput, options, doctor) {
|
|
|
35410
35763
|
const releaseExists = await hotfixReleaseExists(deps, ctx, tag);
|
|
35411
35764
|
if (!releaseExists && isHubControlRepo(ctx.repo)) {
|
|
35412
35765
|
if (!options.announceSummaryFile) {
|
|
35413
|
-
throw new Error("a new MMI-Hub hotfix Release requires --announce-summary-file <path> with 3-6 short LLM-curated lines; create the summary file, then rerun the same hotfix release command \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing");
|
|
35766
|
+
throw new Error("a new MMI-Hub hotfix Release requires --announce-summary-file <path> with 3-6 short LLM-curated lines; create the summary file, then rerun the same hotfix release command \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing");
|
|
35414
35767
|
}
|
|
35415
35768
|
let lines2;
|
|
35416
35769
|
try {
|
|
35417
35770
|
lines2 = summaryFileLines(await (deps.readFile ?? ((p) => (0, import_promises4.readFile)(p, "utf8")))(options.announceSummaryFile));
|
|
35418
35771
|
} catch (e) {
|
|
35419
|
-
throw new Error(`could not read --announce-summary-file ${options.announceSummaryFile}: ${e.message} \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`);
|
|
35772
|
+
throw new Error(`could not read --announce-summary-file ${options.announceSummaryFile}: ${e.message} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`);
|
|
35420
35773
|
}
|
|
35421
35774
|
if (lines2.length < 3 || lines2.length > 6) {
|
|
35422
|
-
throw new Error(`--announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${lines2.length}) \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`);
|
|
35775
|
+
throw new Error(`--announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${lines2.length}) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`);
|
|
35423
35776
|
}
|
|
35424
35777
|
const jerv = hubSummaryJervRefusal(lines2);
|
|
35425
35778
|
if (jerv) throw new Error(jerv);
|
|
@@ -35566,7 +35919,7 @@ ${decision.note}`);
|
|
|
35566
35919
|
foldNote = await portFoldToDevelopment(deps, ctx, deployModel, tag);
|
|
35567
35920
|
} catch (e) {
|
|
35568
35921
|
foldStatus = "failure";
|
|
35569
|
-
foldNote = `development fold port FAILED: ${e.message ?? e} \u2014 the release stands; port it by hand on a fresh branch: git checkout -B ${devFoldBranch(tag)}-manual origin/development && ${foldPortManualCommand(deployModel, version)}, then push it as the canonical head (git push origin ${devFoldBranch(tag)}-manual:refs/heads/${devFoldBranch(tag)}) and open a development-base PR \u2014 see docs/Guides/train-troubleshooting.md#hotfix-fold-port`;
|
|
35922
|
+
foldNote = `development fold port FAILED: ${e.message ?? e} \u2014 the release stands; port it by hand on a fresh branch: git checkout -B ${devFoldBranch(tag)}-manual origin/development && ${foldPortManualCommand(deployModel, version)}, then push it as the canonical head (git push origin ${devFoldBranch(tag)}-manual:refs/heads/${devFoldBranch(tag)}) and open a development-base PR \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-fold-port`;
|
|
35570
35923
|
}
|
|
35571
35924
|
await recordHotfixPhase(ledger, anchors, "alignment", hotfixFoldAlignmentEntry(foldStatus, foldNote, mergedSha), "the tagged, released, deployed hotfix");
|
|
35572
35925
|
const ledgerReport = releaseLedgerReport(await ledger.load(anchors).catch(() => void 0), ledger.path);
|
|
@@ -35615,7 +35968,7 @@ async function runHotfixRetryDeployLocked(deps, versionInput) {
|
|
|
35615
35968
|
foldNote2 = await portFoldToDevelopment(deps, ctx, deployModel, prior.tag);
|
|
35616
35969
|
} catch (e) {
|
|
35617
35970
|
foldStatus2 = "failure";
|
|
35618
|
-
foldNote2 = `development fold port FAILED: ${e.message ?? e} \u2014 the release stands; port it by hand on a fresh branch: git checkout -B ${devFoldBranch(prior.tag)}-manual origin/development && ${foldPortManualCommand(deployModel, prior.tag.replace(/^v/, ""))}, then push it as the canonical head (git push origin ${devFoldBranch(prior.tag)}-manual:refs/heads/${devFoldBranch(prior.tag)}) and open a development-base PR \u2014 see docs/Guides/train-troubleshooting.md#hotfix-fold-port`;
|
|
35971
|
+
foldNote2 = `development fold port FAILED: ${e.message ?? e} \u2014 the release stands; port it by hand on a fresh branch: git checkout -B ${devFoldBranch(prior.tag)}-manual origin/development && ${foldPortManualCommand(deployModel, prior.tag.replace(/^v/, ""))}, then push it as the canonical head (git push origin ${devFoldBranch(prior.tag)}-manual:refs/heads/${devFoldBranch(prior.tag)}) and open a development-base PR \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-fold-port`;
|
|
35619
35972
|
}
|
|
35620
35973
|
return hotfixFoldAlignmentEntry(foldStatus2, foldNote2, prior.tagSha);
|
|
35621
35974
|
}
|
|
@@ -36135,17 +36488,17 @@ function prHeadBehindBase(input) {
|
|
|
36135
36488
|
return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
|
|
36136
36489
|
}
|
|
36137
36490
|
function prHeadUpdateRemedy(head, base) {
|
|
36138
|
-
return `git fetch origin ${base} && git merge origin/${base} (on ${head}), resolve the conflicts, push, then rerun \u2014 docs/Guides/train-troubleshooting.md#head-behind-base`;
|
|
36491
|
+
return `git fetch origin ${base} && git merge origin/${base} (on ${head}), resolve the conflicts, push, then rerun \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#head-behind-base`;
|
|
36139
36492
|
}
|
|
36140
36493
|
function prLandedBranchFinality(prNumber, base) {
|
|
36141
|
-
return `PR #${prNumber} is merged, and that is final \u2014 stopping a local waiter or monitor does not unmerge it. The head branch is now landed; further commits pushed to it reach nothing. If more work is needed, run \`git fetch origin ${base} && git switch -c <new-branch> origin/${base}\`, then cherry-pick anything already committed \u2014 docs/Guides/train-troubleshooting.md#merged-pr-head-reuse`;
|
|
36494
|
+
return `PR #${prNumber} is merged, and that is final \u2014 stopping a local waiter or monitor does not unmerge it. The head branch is now landed; further commits pushed to it reach nothing. If more work is needed, run \`git fetch origin ${base} && git switch -c <new-branch> origin/${base}\`, then cherry-pick anything already committed \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#merged-pr-head-reuse`;
|
|
36142
36495
|
}
|
|
36143
36496
|
var PROTECTED_PR_HEAD_BRANCHES = ["main", "master", "development", "rc"];
|
|
36144
36497
|
function isProtectedPrHead(head) {
|
|
36145
36498
|
return PROTECTED_PR_HEAD_BRANCHES.includes(head);
|
|
36146
36499
|
}
|
|
36147
36500
|
function protectedPrHeadRemedy(head, base) {
|
|
36148
|
-
return `PR head \`${head}\` is a train branch \u2014 updating it from \`${base}\` would merge \`${base}\` into \`${head}\`, promoting every unreleased commit. Never do that and never hand-merge on \`${head}\`. This is the pre-#6288 alignment shape: close this PR and let the train rebuild the alignment on a \`${base}\`-based branch that merges \`${head}\` into it (\`mmi-cli devops release --resume\`), then land THAT with a true merge \u2014 docs/Guides/train-troubleshooting.md#head-behind-base`;
|
|
36501
|
+
return `PR head \`${head}\` is a train branch \u2014 updating it from \`${base}\` would merge \`${base}\` into \`${head}\`, promoting every unreleased commit. Never do that and never hand-merge on \`${head}\`. This is the pre-#6288 alignment shape: close this PR and let the train rebuild the alignment on a \`${base}\`-based branch that merges \`${head}\` into it (\`mmi-cli devops release --resume\`), then land THAT with a true merge \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#head-behind-base`;
|
|
36149
36502
|
}
|
|
36150
36503
|
var PR_HEAD_UPDATE_API_READ_RETRIES = 5;
|
|
36151
36504
|
var PR_HEAD_UPDATE_API_READ_DELAY_MS = 2e3;
|
|
@@ -37247,12 +37600,12 @@ async function resolveBoardAdvanceForPr(prNumber, repoOption) {
|
|
|
37247
37600
|
}
|
|
37248
37601
|
function evaluatePrMergeHousekeeping(report, context, force = false) {
|
|
37249
37602
|
const scratch = report.scratch;
|
|
37250
|
-
const
|
|
37603
|
+
const failed2 = scratch?.status === "failed";
|
|
37251
37604
|
const prunable = scratch?.safeAuto ?? 0;
|
|
37252
37605
|
const unprunable = scratch?.skipped ?? 0;
|
|
37253
|
-
if (!(
|
|
37606
|
+
if (!(failed2 || prunable > 0 || unprunable > 0)) return { blocked: false };
|
|
37254
37607
|
if (force) return { blocked: false };
|
|
37255
|
-
const detail =
|
|
37608
|
+
const detail = failed2 ? `scratch housekeeping failed${scratch?.error ? `: ${scratch.error}` : ""}` : `${prunable} prunable + ${unprunable} un-prunable scratch item(s) remain after cleanup`;
|
|
37256
37609
|
return {
|
|
37257
37610
|
blocked: true,
|
|
37258
37611
|
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.)`
|
|
@@ -37635,7 +37988,7 @@ function readRepoVersion() {
|
|
|
37635
37988
|
}
|
|
37636
37989
|
function registerTrainCommands(program3, trainDeps) {
|
|
37637
37990
|
const train = program3.command("train").description("release-train observability \u2014 track position, version lag, and the shared preflight-and-heal doctor (#2688, #6067)");
|
|
37638
|
-
train.command("doctor").description("one shared preflight-and-heal verdict for the release, rcand and hotfix lanes; read-only unless --heal (#6067)").addOption(new Option("--lane <lane>", "train lane to check (defaults from the release track: direct \u2192 release)").choices([...TRAIN_LANES])).option("--dev", "judge the `release --dev` lane (development -> main, skipping rc): the lane starts from development and is read on development's workflows (#6318)").option("--heal", "repair safe local reconstructible state (scratch gitignore, churn, stray local tags, ff-only branch lag, finished scratch branches, stale alignment ledger leg); it never clears a ledger-pending, ledger-failed or branch-mismatch blocker (#6404, #6401, #6399) \u2014 those need `mmi-cli devops release --resume` (or, with fresh approval, `--abort --apply`) and `git checkout <start branch>`, so exit 1 after --heal is the gate working, not a failed heal").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").option("--suite-solo", "judge an owner-approved solo release of one suite module, as `release --suite-solo --owner-go` does (requires --owner-go; never on rcand, #7327)").option("--owner-go", "explicit owner approval for the suite-solo lane (requires --suite-solo)").option("--json", "machine-readable output").option("--out <path>", "write the verdict to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802/#6563); pair with --json for a machine-readable file (e.g. `--json --out .jerv/tmp/train-doctor.json`); the file is written even when the verdict is NOT READY").addHelpText("after", "\nExit codes:\n 0 ready \u2014 no blocker and origin verified\n 1 a blocker remains, or origin could not be verified (never green on an unread origin)\n\nEvery finding carries code, severity (blocker|warning|healed|info), source (local|origin), remedy and a\
|
|
37991
|
+
train.command("doctor").description("one shared preflight-and-heal verdict for the release, rcand and hotfix lanes; read-only unless --heal (#6067)").addOption(new Option("--lane <lane>", "train lane to check (defaults from the release track: direct \u2192 release)").choices([...TRAIN_LANES])).option("--dev", "judge the `release --dev` lane (development -> main, skipping rc): the lane starts from development and is read on development's workflows (#6318)").option("--heal", "repair safe local reconstructible state (scratch gitignore, churn, stray local tags, ff-only branch lag, finished scratch branches, stale alignment ledger leg); it never clears a ledger-pending, ledger-failed or branch-mismatch blocker (#6404, #6401, #6399) \u2014 those need `mmi-cli devops release --resume` (or, with fresh approval, `--abort --apply`) and `git checkout <start branch>`, so exit 1 after --heal is the gate working, not a failed heal").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").option("--suite-solo", "judge an owner-approved solo release of one suite module, as `release --suite-solo --owner-go` does (requires --owner-go; never on rcand, #7327)").option("--owner-go", "explicit owner approval for the suite-solo lane (requires --suite-solo)").option("--json", "machine-readable output").option("--out <path>", "write the verdict to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802/#6563); pair with --json for a machine-readable file (e.g. `--json --out .jerv/tmp/train-doctor.json`); the file is written even when the verdict is NOT READY").addHelpText("after", "\nExit codes:\n 0 ready \u2014 no blocker and origin verified\n 1 a blocker remains, or origin could not be verified (never green on an unread origin)\n\nEvery finding carries code, severity (blocker|warning|healed|info), source (local|origin), remedy and a\nhttps://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#<code> anchor. `release --apply` and `rcand --apply` run this at step 0 with --heal.\n").action(async (o) => {
|
|
37639
37992
|
try {
|
|
37640
37993
|
if (o.suiteSolo !== o.ownerGo || o.lane === "rcand" && o.suiteSolo) {
|
|
37641
37994
|
return failGraceful("train doctor: a suite-solo verdict needs BOTH --suite-solo --owner-go, and never on the rcand lane");
|
|
@@ -37815,7 +38168,7 @@ async function openSuiteCutPinPr(deps, input) {
|
|
|
37815
38168
|
}
|
|
37816
38169
|
async function openSuitePinPr(deps, input) {
|
|
37817
38170
|
const { repo, tag, stage } = input;
|
|
37818
|
-
const remedy = `open/merge the ${SUITE_REPO} development pin PR for ${repo} ${tag}, then rerun the same train command; do not recut the tag \u2014 see docs/Guides/train-troubleshooting.md#suite-pin-pr`;
|
|
38171
|
+
const remedy = `open/merge the ${SUITE_REPO} development pin PR for ${repo} ${tag}, then rerun the same train command; do not recut the tag \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#suite-pin-pr`;
|
|
37819
38172
|
let scratch = "";
|
|
37820
38173
|
try {
|
|
37821
38174
|
const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=development`]));
|
|
@@ -39874,7 +40227,7 @@ function withRateLimitRetry(client, seams = {}) {
|
|
|
39874
40227
|
};
|
|
39875
40228
|
}
|
|
39876
40229
|
async function prCreateMergedHeadWarning(input, deps = {}) {
|
|
39877
|
-
const guide = "docs/Guides/train-troubleshooting.md#merged-pr-head-reuse";
|
|
40230
|
+
const guide = "https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#merged-pr-head-reuse";
|
|
39878
40231
|
if (!input.head) {
|
|
39879
40232
|
return `pr create: WARNING \u2014 could not check whether the current head branch already landed (the current branch could not be resolved); continuing with PR creation \u2014 ${guide}`;
|
|
39880
40233
|
}
|
|
@@ -39899,6 +40252,11 @@ async function prCreateMergedHeadWarning(input, deps = {}) {
|
|
|
39899
40252
|
return `pr create: WARNING \u2014 could not check whether head branch '${input.head}' already landed (${detail}); continuing with PR creation \u2014 ${guide}`;
|
|
39900
40253
|
}
|
|
39901
40254
|
}
|
|
40255
|
+
function isIssueMissingError(e) {
|
|
40256
|
+
if (typeof e.status === "number") return e.status === 404;
|
|
40257
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
40258
|
+
return /HTTP 404|Not Found|\(404\)/i.test(msg);
|
|
40259
|
+
}
|
|
39902
40260
|
async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
39903
40261
|
const issues = [...new Set(findClosingMentions(body).map((mention) => mention.issue))];
|
|
39904
40262
|
if (!issues.length) return void 0;
|
|
@@ -39909,7 +40267,14 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
|
39909
40267
|
const requester = `requesting lane: ${actor.surface}/${actor.session}@${actor.host} (session id from ${source})`;
|
|
39910
40268
|
const checkContest = deps.checkContest ?? checkLaneContest;
|
|
39911
40269
|
for (const number of issues) {
|
|
39912
|
-
|
|
40270
|
+
let state;
|
|
40271
|
+
try {
|
|
40272
|
+
state = (await client.rest("GET", `repos/${repo}/issues/${number}`))?.state;
|
|
40273
|
+
} catch (e) {
|
|
40274
|
+
if (isIssueMissingError(e)) {
|
|
40275
|
+
return `pr create: REFUSED \u2014 ${repo}#${number} does not exist \u2014 the PR body closes an issue that was never filed; file the issue first, or drop the closing line before creating this PR. There is no lane to claim, so claiming cannot create it; ${requester}`;
|
|
40276
|
+
}
|
|
40277
|
+
}
|
|
39913
40278
|
if (state?.toLowerCase() === "closed") continue;
|
|
39914
40279
|
const contest = await checkContest(client, { repository: repo, number }, actor);
|
|
39915
40280
|
if (!contest.contested) continue;
|
|
@@ -43402,10 +43767,10 @@ async function announceSummaryRefusal(path2, isHub = false) {
|
|
|
43402
43767
|
try {
|
|
43403
43768
|
lines2 = summaryFileLines(await (0, import_promises8.readFile)(path2, "utf8"));
|
|
43404
43769
|
} catch (e) {
|
|
43405
|
-
return `release: could not read --announce-summary-file ${path2}: ${e.message} \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
43770
|
+
return `release: could not read --announce-summary-file ${path2}: ${e.message} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
43406
43771
|
}
|
|
43407
43772
|
if (lines2.length < 3 || lines2.length > 6) {
|
|
43408
|
-
return `release: --announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${lines2.length}) \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
43773
|
+
return `release: --announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${lines2.length}) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
43409
43774
|
}
|
|
43410
43775
|
if (isHub) {
|
|
43411
43776
|
const jerv = hubSummaryJervRefusal(lines2);
|
|
@@ -43824,7 +44189,7 @@ function registerTrainCommandsAndInventory(program3) {
|
|
|
43824
44189
|
function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
43825
44190
|
async function pinSoloMember(repo, tag) {
|
|
43826
44191
|
const read = await fetchProjectBySlugChecked(slugOf(repo), registryClientDeps(await loadConfig()));
|
|
43827
|
-
if (!read.ok || !read.project) throw new Error(`suite pin PR FAILED: registry META unreadable for ${repo} (${read.ok ? "missing" : read.error}); rerun the same train command after restoring registry access \u2014 see docs/Guides/train-troubleshooting.md#suite-pin-pr`);
|
|
44192
|
+
if (!read.ok || !read.project) throw new Error(`suite pin PR FAILED: registry META unreadable for ${repo} (${read.ok ? "missing" : read.error}); rerun the same train command after restoring registry access \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#suite-pin-pr`);
|
|
43828
44193
|
if (!suiteMemberRefusal(repo, read.project, "release")) return void 0;
|
|
43829
44194
|
const root = (await execFileP("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
43830
44195
|
return openSuitePinPr({ root, run: async (file, args, cwd) => (await execFileP(file, args, { cwd, timeout: GIT_TIMEOUT_MS })).stdout }, { repo, tag, stage: "main" });
|
|
@@ -44069,7 +44434,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44069
44434
|
if (announcesToChannel) {
|
|
44070
44435
|
if (!o.announceSummaryFile) {
|
|
44071
44436
|
return fail(
|
|
44072
|
-
isHub ? "release: a new MMI-Hub release requires --announce-summary-file <path> with 3-6 short LLM-curated lines; create the summary file, then rerun the same release command \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing" : `release: ${slug} announces to its project release channel, so a new release requires --announce-summary-file <path> with 3-6 short plain-language lines in the project's releaseLanguage (${releaseLanguage2}); create the summary file, then rerun the same release command \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`
|
|
44437
|
+
isHub ? "release: a new MMI-Hub release requires --announce-summary-file <path> with 3-6 short LLM-curated lines; create the summary file, then rerun the same release command \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing" : `release: ${slug} announces to its project release channel, so a new release requires --announce-summary-file <path> with 3-6 short plain-language lines in the project's releaseLanguage (${releaseLanguage2}); create the summary file, then rerun the same release command \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`
|
|
44073
44438
|
);
|
|
44074
44439
|
}
|
|
44075
44440
|
const refusal = await announceSummaryRefusal(o.announceSummaryFile, isHub);
|
|
@@ -44252,7 +44617,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44252
44617
|
if (o.suiteSolo !== o.ownerGo) return fail("hotfix start: solo suite hotfix requires BOTH --suite-solo --owner-go");
|
|
44253
44618
|
return runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from, suiteSolo: o.suiteSolo, ack: o.ack ? o.ack.split(",").map((s) => s.trim()).filter(Boolean) : void 0 }), o, renderHotfixStart);
|
|
44254
44619
|
});
|
|
44255
|
-
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line release Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068), optional for a product repo announcing to its project release channel (#6517)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--retry-deploy", "re-run only the failed deploy and/or fold-port alignment legs of a published hotfix of this same tag (#6985)").option("--suite-solo", "release one suite module alone (requires --owner-go)").option("--owner-go", "explicit owner approval for a solo backend hotfix").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) => {
|
|
44620
|
+
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line release Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068), optional for a product repo announcing to its project release channel (#6517)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--ack <shas>", "accepted for flag symmetry with hotfix start; hotfix release proves every carry by ancestry, so it is ignored here, never an error (#7338)").option("--retry-deploy", "re-run only the failed deploy and/or fold-port alignment legs of a published hotfix of this same tag (#6985)").option("--suite-solo", "release one suite module alone (requires --owner-go)").option("--owner-go", "explicit owner approval for a solo backend hotfix").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) => {
|
|
44256
44621
|
if (o.suiteSolo !== o.ownerGo) return fail("hotfix release: solo suite release requires BOTH --suite-solo --owner-go");
|
|
44257
44622
|
if (o.retryDeploy) {
|
|
44258
44623
|
if (o.announceSummaryFile || o.carries) {
|
|
@@ -44261,7 +44626,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44261
44626
|
return runHotfixSub("retry-deploy", () => runHotfixRetryDeploy(trainApplyDeps(), version), o, renderHotfixRetryDeploy);
|
|
44262
44627
|
}
|
|
44263
44628
|
return runHotfixSub("release", async () => {
|
|
44264
|
-
const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [], suiteSolo: o.suiteSolo });
|
|
44629
|
+
const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [], suiteSolo: o.suiteSolo, ...o.ack ? { ack: o.ack.split(",").map((s) => s.trim()).filter(Boolean) } : {} });
|
|
44265
44630
|
const suitePin = o.suiteSolo ? await pinSoloMember(result.repo, result.tag) : void 0;
|
|
44266
44631
|
const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
44267
44632
|
return { ...result, projectInfoSync, ...o.suiteSolo ? { suiteSolo: true, suitePin } : {} };
|
|
@@ -44297,6 +44662,10 @@ async function exec(file, args, cwd, timeout = GH_TIMEOUT) {
|
|
|
44297
44662
|
async function gh(args) {
|
|
44298
44663
|
return exec("gh", args);
|
|
44299
44664
|
}
|
|
44665
|
+
function rcRunCauseFromLog(log) {
|
|
44666
|
+
const causes = [...new Set([...log.matchAll(/##\[error\](?!Process completed with exit code)([^\r\n]+)/g)].map((m) => m[1].trim()).filter(Boolean))];
|
|
44667
|
+
return causes.length ? `: ${causes.join("; ")}` : "";
|
|
44668
|
+
}
|
|
44300
44669
|
function suiteTrainIo(root, summaryFile) {
|
|
44301
44670
|
const recordPath = repoRuntimeStatePath(root, "suite", "cut.json");
|
|
44302
44671
|
const base = (0, import_node_path42.join)(repoRuntimeStatePath(root, "suite"), "checkouts");
|
|
@@ -44410,23 +44779,41 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
44410
44779
|
if (result.tag !== expected || result.deployStatus !== "success") throw new Error(`${repo}: release is not green at expected ${expected} (${result.tag}/${result.deployStatus})`);
|
|
44411
44780
|
}
|
|
44412
44781
|
});
|
|
44413
|
-
const
|
|
44414
|
-
|
|
44415
|
-
const rows = async () => JSON.parse(await gh(["run", "list", "--repo", "mutmutco/MMI-Hub", "--workflow", "module-rc-publish.yml", "--event", "workflow_dispatch", "--limit", "100", "--json", "databaseId,displayTitle,conclusion,status,createdAt"]));
|
|
44416
|
-
const existing = (await rows()).filter((r) => r.displayTitle === title).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
|
|
44417
|
-
if (existing[0]?.conclusion === "success") return;
|
|
44418
|
-
if (existing[0]?.conclusion === "failure") throw new Error(`${repo}: prior module-rc-publish ${tag} failed; inspect the run and retry it explicitly before continuing`);
|
|
44782
|
+
const rcRuns = async () => JSON.parse(await gh(["run", "list", "--repo", "mutmutco/MMI-Hub", "--workflow", "module-rc-publish.yml", "--event", "workflow_dispatch", "--limit", "100", "--json", "databaseId,displayTitle,conclusion,status,createdAt"]));
|
|
44783
|
+
const watchRcRun = async (title, fields, reuse) => {
|
|
44419
44784
|
const since = Date.now() - 1e4;
|
|
44420
|
-
if (!
|
|
44421
|
-
let candidate =
|
|
44785
|
+
if (!reuse) await gh(["workflow", "run", "module-rc-publish.yml", "--repo", "mutmutco/MMI-Hub", ...fields.flatMap((f) => ["--field", f])]);
|
|
44786
|
+
let candidate = reuse;
|
|
44422
44787
|
for (let i = 0; !candidate && i < 25; i++) {
|
|
44423
44788
|
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
44424
|
-
candidate = (await
|
|
44789
|
+
candidate = (await rcRuns()).find((r) => r.displayTitle === title && Date.parse(r.createdAt) >= since);
|
|
44790
|
+
}
|
|
44791
|
+
if (!candidate) throw new Error(`workflow dispatch outcome ambiguous for "${title}"; inspect Actions before retrying`);
|
|
44792
|
+
try {
|
|
44793
|
+
await exec("gh", ["run", "watch", String(candidate.databaseId), "--repo", "mutmutco/MMI-Hub", "--exit-status"], void 0, WATCH_TIMEOUT);
|
|
44794
|
+
} catch {
|
|
44425
44795
|
}
|
|
44426
|
-
if (!candidate) throw new Error(`${repo}: workflow dispatch outcome ambiguous for ${tag}; inspect Actions before retrying`);
|
|
44427
|
-
await exec("gh", ["run", "watch", String(candidate.databaseId), "--repo", "mutmutco/MMI-Hub", "--exit-status"], void 0, WATCH_TIMEOUT);
|
|
44428
44796
|
const verdict = JSON.parse(await gh(["run", "view", String(candidate.databaseId), "--repo", "mutmutco/MMI-Hub", "--json", "conclusion"]));
|
|
44429
|
-
|
|
44797
|
+
return { id: candidate.databaseId, conclusion: verdict.conclusion };
|
|
44798
|
+
};
|
|
44799
|
+
const publish = async (repo, tag) => {
|
|
44800
|
+
const title = `module-rc-publish ${repo} @ ${tag}`;
|
|
44801
|
+
const existing = (await rcRuns()).filter((r) => r.displayTitle === title).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
|
|
44802
|
+
if (existing[0]?.conclusion === "success") return;
|
|
44803
|
+
if (existing[0]?.conclusion === "failure") throw new Error(`${repo}: prior module-rc-publish ${tag} failed; inspect the run and retry it explicitly before continuing`);
|
|
44804
|
+
const run = await watchRcRun(title, [`repo=${repo}`, `tag=${tag}`], existing[0]);
|
|
44805
|
+
if (run.conclusion !== "success") throw new Error(`${repo}: module rc publish run ${run.id} not green${await rcRunCause(run.id)}`);
|
|
44806
|
+
};
|
|
44807
|
+
const preflight2 = async (repo) => {
|
|
44808
|
+
const run = await watchRcRun(`module-rc-preflight ${repo}`, [`repo=${repo}`, "preflight=true"], void 0);
|
|
44809
|
+
if (run.conclusion !== "success") throw new Error(`rc serving preflight run ${run.id} failed${await rcRunCause(run.id)}`);
|
|
44810
|
+
};
|
|
44811
|
+
const rcRunCause = async (id) => {
|
|
44812
|
+
try {
|
|
44813
|
+
return rcRunCauseFromLog(await exec("gh", ["run", "view", String(id), "--repo", "mutmutco/MMI-Hub", "--log-failed"]));
|
|
44814
|
+
} catch {
|
|
44815
|
+
return "";
|
|
44816
|
+
}
|
|
44430
44817
|
};
|
|
44431
44818
|
const registry2 = async () => {
|
|
44432
44819
|
const file = await defaultGitHubClient().rest("GET", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=development`);
|
|
@@ -44460,6 +44847,7 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
44460
44847
|
},
|
|
44461
44848
|
rcand: runRcand,
|
|
44462
44849
|
publish,
|
|
44850
|
+
preflight: preflight2,
|
|
44463
44851
|
pin: (input) => openSuiteCutPinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
|
|
44464
44852
|
verifyLeadCut: async (sha) => {
|
|
44465
44853
|
const current = await gh(["api", `repos/${SUITE_REPO}/git/ref/heads/development`, "--jq", ".object.sha"]);
|
|
@@ -44962,15 +45350,15 @@ function renderReport(checks, opts) {
|
|
|
44962
45350
|
}
|
|
44963
45351
|
function renderTally(checks) {
|
|
44964
45352
|
const total = checks.length;
|
|
44965
|
-
const
|
|
45353
|
+
const failed2 = checks.filter((c) => !c.ok).length;
|
|
44966
45354
|
const unverified = checks.filter((c) => c.ok && c.verified === false).length;
|
|
44967
45355
|
const warned = checks.filter((c) => c.ok && c.verified !== false && c.warn).length;
|
|
44968
|
-
const healthy = total -
|
|
44969
|
-
if (
|
|
45356
|
+
const healthy = total - failed2 - unverified - warned;
|
|
45357
|
+
if (failed2 === 0 && unverified === 0 && warned === 0) return `\u2713 all ${total} checks healthy`;
|
|
44970
45358
|
const parts = [`\u2713 ${healthy} verified healthy`];
|
|
44971
45359
|
if (warned > 0) parts.push(`\u26A0 ${warned} warn`);
|
|
44972
45360
|
if (unverified > 0) parts.push(`? ${unverified} unverified`);
|
|
44973
|
-
if (
|
|
45361
|
+
if (failed2 > 0) parts.push(`\u2717 ${failed2} failed`);
|
|
44974
45362
|
return `${parts.join(" \xB7 ")} \u2014 ${total} checks`;
|
|
44975
45363
|
}
|
|
44976
45364
|
function doctorReportExitCode(checks) {
|
|
@@ -50522,7 +50910,7 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
|
|
|
50522
50910
|
console.log(JSON.stringify(report, null, 2));
|
|
50523
50911
|
if (!report.rcand.canApply) process.exitCode = 1;
|
|
50524
50912
|
});
|
|
50525
|
-
project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY row \u2014 project-admin may set only --no-env-file on their own existing dev/rc row; master may seed/change all coords and main; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").makeOptionMandatory().choices(["dev", "rc", "main"])).option("--ssh-host <host>", "the box address the deploy ssh-es into; omit to keep the stored value (required only for a NEW hetzner-ssh row \u2014 `mmi-cli devops runtime box list` finds it)").option("--ssh-user <user>", "ssh user; omit to leave the row unchanged (default root on a new row)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535); omit to leave the row unchanged").addOption(new Option("--substrate <substrate>", "hetzner-ssh | s3-cloudfront; omit to leave the row unchanged").choices([...DEPLOY_SUBSTRATES])).option("--deploy-role-arn <arn>", "s3-cloudfront: the scoped OIDC deployer role tenant-deploy.yml assumes for this stage").option("--app-bucket <bucket>", "s3-cloudfront: the S3 bucket the built dist/ is synced into").option("--distribution-id <id>", "s3-cloudfront: the CloudFront distribution invalidated after the sync").option("--distribution-domain <domain>", "s3-cloudfront: the host the post-deploy security-header proof is measured against").option("--deploy-path <path>", "on-box per-stage release root; omit to leave the row unchanged (default /opt/mmi/<slug>/<stage> on a new row)").option("--service <name>", "systemd/compose service name; omit to leave the row unchanged (default the slug on a new row)").option("--domain <domain>", "canonical serving host; omit to leave the row unchanged").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable); omit to leave the stored aliases unchanged").option("--clear-aliases", "remove EVERY serving alias from the row (#2986) \u2014 omitting --alias preserves them, so clearing needs saying out loud").option("--no-env-file <bool>", "set the DEPLOY# fileless flag (true|false) \u2014 own-repo project-admin dev/rc; master main; true = env passthrough with no .env symlink").option("--force", "explicit recovery override: skip fileless-compose verification only after independent proof").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
50913
|
+
project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY row \u2014 project-admin may set only --no-env-file/--compose-profiles on their own existing dev/rc row; master may seed/change all coords and main; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").makeOptionMandatory().choices(["dev", "rc", "main"])).option("--ssh-host <host>", "the box address the deploy ssh-es into; omit to keep the stored value (required only for a NEW hetzner-ssh row \u2014 `mmi-cli devops runtime box list` finds it)").option("--ssh-user <user>", "ssh user; omit to leave the row unchanged (default root on a new row)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535); omit to leave the row unchanged").addOption(new Option("--substrate <substrate>", "hetzner-ssh | s3-cloudfront; omit to leave the row unchanged").choices([...DEPLOY_SUBSTRATES])).option("--deploy-role-arn <arn>", "s3-cloudfront: the scoped OIDC deployer role tenant-deploy.yml assumes for this stage").option("--app-bucket <bucket>", "s3-cloudfront: the S3 bucket the built dist/ is synced into").option("--distribution-id <id>", "s3-cloudfront: the CloudFront distribution invalidated after the sync").option("--distribution-domain <domain>", "s3-cloudfront: the host the post-deploy security-header proof is measured against").option("--deploy-path <path>", "on-box per-stage release root; omit to leave the row unchanged (default /opt/mmi/<slug>/<stage> on a new row)").option("--service <name>", "systemd/compose service name; omit to leave the row unchanged (default the slug on a new row)").option("--domain <domain>", "canonical serving host; omit to leave the row unchanged").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable); omit to leave the stored aliases unchanged").option("--clear-aliases", "remove EVERY serving alias from the row (#2986) \u2014 omitting --alias preserves them, so clearing needs saying out loud").option("--no-env-file <bool>", "set the DEPLOY# fileless flag (true|false) \u2014 own-repo project-admin dev/rc; master main; true = env passthrough with no .env symlink").option("--compose-profiles <profiles>", "comma-separated compose profiles to enable for this stage (e.g. huddles,slack-bridge); omit to leave unchanged").option("--clear-compose-profiles", "remove EVERY compose profile from the row \u2014 omitting --compose-profiles preserves them, so clearing needs saying out loud").option("--force", "explicit recovery override: skip fileless-compose verification only after independent proof").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
50526
50914
|
const cfg = await loadConfig();
|
|
50527
50915
|
let target;
|
|
50528
50916
|
try {
|
|
@@ -50536,6 +50924,10 @@ project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY ro
|
|
|
50536
50924
|
if (v !== "true" && v !== "false") return fail("org project set-deploy: --no-env-file must be true or false");
|
|
50537
50925
|
noEnvFile = v === "true";
|
|
50538
50926
|
}
|
|
50927
|
+
let composeProfiles;
|
|
50928
|
+
if (typeof o.composeProfiles === "string") {
|
|
50929
|
+
composeProfiles = o.composeProfiles.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
50930
|
+
}
|
|
50539
50931
|
if (noEnvFile !== void 0) {
|
|
50540
50932
|
const branch = DEPLOY_STAGE_BRANCH[o.stage.trim().toLowerCase()];
|
|
50541
50933
|
if (branch) {
|
|
@@ -50574,7 +50966,9 @@ ${filelessTransitionGuide(target, o.stage)}`
|
|
|
50574
50966
|
distributionDomain: o.distributionDomain,
|
|
50575
50967
|
aliases: o.alias,
|
|
50576
50968
|
clearAliases: o.clearAliases,
|
|
50577
|
-
noEnvFile
|
|
50969
|
+
noEnvFile,
|
|
50970
|
+
composeProfiles,
|
|
50971
|
+
clearComposeProfiles: o.clearComposeProfiles
|
|
50578
50972
|
});
|
|
50579
50973
|
if (target.includes("/")) body.repo = target;
|
|
50580
50974
|
} catch (e) {
|