@mutmutco/cli 4.5.35 → 4.5.36
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 +282 -101
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -7295,11 +7295,11 @@ function decorateAwsModel(entries, registry2, verdicts) {
|
|
|
7295
7295
|
});
|
|
7296
7296
|
}
|
|
7297
7297
|
var DECLARED_ENTRIES = [
|
|
7298
|
-
{ name: "mmi-backup (mm-central, mm-work, mmi-oguz
|
|
7298
|
+
{ 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
7299
|
// #7148: a backup nobody restored is a hope. Monthly, restore the latest artifact of every
|
|
7300
7300
|
// archived stage (including a stage whose containers are down) into scratch Postgres and compare
|
|
7301
7301
|
// 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
|
|
7302
|
+
{ 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
7303
|
// #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
7304
|
// setup-node before it can prune, so at 100% it dies at checkout and never prunes — the cleanup is
|
|
7305
7305
|
// unavailable exactly when it is needed. This one is root cron on the box: no checkout, no node, and
|
|
@@ -15715,9 +15715,9 @@ function trainFollowUpExitCode(status) {
|
|
|
15715
15715
|
}
|
|
15716
15716
|
function trainFollowUpNote(status) {
|
|
15717
15717
|
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`;
|
|
15718
|
+
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
15719
|
}
|
|
15720
|
-
var TRAIN_DEPLOY_FAILED_NOTE = "promotion stands; retry the deploy, do not re-tag \u2014 see docs/Guides/train-troubleshooting.md#deploy-failed";
|
|
15720
|
+
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
15721
|
function missingRunUrlNote(runId) {
|
|
15722
15722
|
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
15723
|
}
|
|
@@ -16840,10 +16840,10 @@ var rollout_plan_default = {
|
|
|
16840
16840
|
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
16841
|
},
|
|
16842
16842
|
baseline: {
|
|
16843
|
-
version: "4.5.
|
|
16844
|
-
tag: "v4.5.
|
|
16845
|
-
commit: "
|
|
16846
|
-
npm: "@mutmutco/cli@4.5.
|
|
16843
|
+
version: "4.5.36",
|
|
16844
|
+
tag: "v4.5.36",
|
|
16845
|
+
commit: "dfc9e43e72eb",
|
|
16846
|
+
npm: "@mutmutco/cli@4.5.36"
|
|
16847
16847
|
},
|
|
16848
16848
|
exitCriterion: "fleet-n-of-n",
|
|
16849
16849
|
hubOnlyShortcut: "forbidden",
|
|
@@ -16860,14 +16860,14 @@ var rollout_plan_default = {
|
|
|
16860
16860
|
repo: "mutmutco/mmi-hub",
|
|
16861
16861
|
role: "canary",
|
|
16862
16862
|
schedule: "train",
|
|
16863
|
-
v3Target: "v4.5.
|
|
16863
|
+
v3Target: "v4.5.36"
|
|
16864
16864
|
}
|
|
16865
16865
|
],
|
|
16866
16866
|
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
16867
|
rollback: {
|
|
16868
16868
|
independent: true,
|
|
16869
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
16870
|
-
v3Target: "v4.5.
|
|
16869
|
+
mechanism: "npm dist-tag latest -> 4.5.36 and redeploy the Hub Lambda from tag v4.5.36 (dfc9e43e72eb); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
16870
|
+
v3Target: "v4.5.36 (@mutmutco/cli@4.5.36, tag commit dfc9e43e72eb \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
16871
16871
|
}
|
|
16872
16872
|
},
|
|
16873
16873
|
{
|
|
@@ -19826,7 +19826,7 @@ function installerDeclarationError(value, division) {
|
|
|
19826
19826
|
|
|
19827
19827
|
// src/train-suite.ts
|
|
19828
19828
|
var SUITE_GUARD_CODE = "suite-member-solo";
|
|
19829
|
-
var SUITE_GUARD_ANCHOR = `docs/Guides/train-troubleshooting.md#${SUITE_GUARD_CODE}`;
|
|
19829
|
+
var SUITE_GUARD_ANCHOR = `https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#${SUITE_GUARD_CODE}`;
|
|
19830
19830
|
function suiteLeadRepo(repo, suite) {
|
|
19831
19831
|
const owner = repo.split("/")[0];
|
|
19832
19832
|
return `${owner}/${suite === "mm-work" ? "MM-Work" : suite}`;
|
|
@@ -20521,7 +20521,7 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
|
|
|
20521
20521
|
...dispatch,
|
|
20522
20522
|
// #6095: the helper's receipt, health or rollback failed after a proven publish — the release resumes, never re-tags.
|
|
20523
20523
|
// #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`,
|
|
20524
|
+
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
20525
|
workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "jerv-gateway", runUrlNote: JERV_GATEWAY_RUN_URL_NOTE, conclusion: "failure" }],
|
|
20526
20526
|
deployStatus: "failure"
|
|
20527
20527
|
};
|
|
@@ -20742,7 +20742,7 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
|
|
|
20742
20742
|
const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
20743
20743
|
let note = `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage})`;
|
|
20744
20744
|
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`;
|
|
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 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#deploy-exit-78-fileless`;
|
|
20746
20746
|
}
|
|
20747
20747
|
return { note, runId, runUrl, deployStatus };
|
|
20748
20748
|
}
|
|
@@ -21057,7 +21057,7 @@ function isCanaryAccessBlockText(text) {
|
|
|
21057
21057
|
}
|
|
21058
21058
|
function actionsCanaryUndispatchedRefusal(detail) {
|
|
21059
21059
|
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`
|
|
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 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#actions-job-start-canary`
|
|
21061
21061
|
);
|
|
21062
21062
|
}
|
|
21063
21063
|
function actionsBillingRefusal(detail) {
|
|
@@ -21221,11 +21221,11 @@ async function assertActionsJobsCanStart(deps, targetRepo2) {
|
|
|
21221
21221
|
return;
|
|
21222
21222
|
}
|
|
21223
21223
|
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`);
|
|
21224
|
+
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
21225
|
}
|
|
21226
21226
|
}
|
|
21227
21227
|
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`
|
|
21228
|
+
`${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
21229
|
);
|
|
21230
21230
|
}
|
|
21231
21231
|
|
|
@@ -23641,7 +23641,7 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
23641
23641
|
}
|
|
23642
23642
|
let gatewayDeploy;
|
|
23643
23643
|
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");
|
|
23644
|
+
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
23645
|
if (unresolved.length > 0) {
|
|
23646
23646
|
throw new Error(
|
|
23647
23647
|
`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 +23689,7 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
23689
23689
|
repo: prior.repo,
|
|
23690
23690
|
tag: prior.tag,
|
|
23691
23691
|
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`,
|
|
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${phases.alignment.state === "pending" ? "; alignment still pending (fold-port PR open \u2014 closes live at the next pre-flight, #6061)" : ""}`,
|
|
23693
23693
|
...gatewayDeploy ? { gatewayDeploy } : {}
|
|
23694
23694
|
};
|
|
23695
23695
|
});
|
|
@@ -24793,7 +24793,7 @@ async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix
|
|
|
24793
24793
|
const predicted = await predictMergeConflicts(deps, ours, remoteRef);
|
|
24794
24794
|
const predictedBlocking = predicted.filter((f) => !pathIsTolerated(f, tolerated));
|
|
24795
24795
|
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`);
|
|
24796
|
+
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
24797
|
}
|
|
24798
24798
|
return { foldPaths, tolerated, predicted };
|
|
24799
24799
|
}
|
|
@@ -25022,7 +25022,7 @@ async function recoverFailedFold(deps, cause, startBranch, preFoldMainSha, resum
|
|
|
25022
25022
|
const causeMessage = anchor ? `${rawCause} \u2014 see ${anchor}` : rawCause;
|
|
25023
25023
|
const probe = await probeFoldFailureState(deps);
|
|
25024
25024
|
if (probe.mergeInProgress) {
|
|
25025
|
-
const conflictCause = `${rawCause} \u2014 see docs/Guides/train-troubleshooting.md#merge-conflict`;
|
|
25025
|
+
const conflictCause = `${rawCause} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#merge-conflict`;
|
|
25026
25026
|
try {
|
|
25027
25027
|
await deps.run("git", ["merge", "--abort"]);
|
|
25028
25028
|
} catch (e) {
|
|
@@ -26593,7 +26593,7 @@ ${spent.note}`), rcandRecovery(tag2));
|
|
|
26593
26593
|
if (!coverage.ok) {
|
|
26594
26594
|
const list = coverage.uncovered.map((c) => `${c.sha.slice(0, 8)} ${c.subject}`).join("; ");
|
|
26595
26595
|
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`
|
|
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 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-coverage`
|
|
26597
26597
|
// #6095: the guard names its lesson
|
|
26598
26598
|
);
|
|
26599
26599
|
}
|
|
@@ -26673,7 +26673,7 @@ async function resolveTrainBumpIntent(deps, env = process.env, latestRelease) {
|
|
|
26673
26673
|
const explicitIntent = (env.MMI_BUMP_INTENT ?? "").trim();
|
|
26674
26674
|
if (explicitIntent) {
|
|
26675
26675
|
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");
|
|
26676
|
+
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
26677
|
}
|
|
26678
26678
|
return explicitIntent;
|
|
26679
26679
|
}
|
|
@@ -26718,12 +26718,12 @@ async function resolveHotfixVersion(deps, env = process.env) {
|
|
|
26718
26718
|
}
|
|
26719
26719
|
const nonPatch = declared.find((d) => d.intent !== "patch");
|
|
26720
26720
|
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`);
|
|
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 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#bump-intent-invalid`);
|
|
26722
26722
|
}
|
|
26723
26723
|
const explicit = explicitVersion.replace(/^v/, "");
|
|
26724
26724
|
const nextPatch = `${latest.major}.${latest.minor}.${latest.patch + 1}`;
|
|
26725
26725
|
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`);
|
|
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 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#bump-intent-invalid`);
|
|
26727
26727
|
}
|
|
26728
26728
|
const version = explicit || nextPatch;
|
|
26729
26729
|
return { tag: `v${version}`, version, baseTag };
|
|
@@ -29694,7 +29694,7 @@ init_hub_auth();
|
|
|
29694
29694
|
init_github_client();
|
|
29695
29695
|
|
|
29696
29696
|
// 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"];
|
|
29697
|
+
var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy"];
|
|
29698
29698
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
29699
29699
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
29700
29700
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -30033,6 +30033,21 @@ function parseRequiredCheckBranchesVar(raw) {
|
|
|
30033
30033
|
}
|
|
30034
30034
|
return REQUIRED_CHECK_BRANCHES.filter((branch) => parsed.includes(branch));
|
|
30035
30035
|
}
|
|
30036
|
+
function parseNotifyOnDeployVar(raw) {
|
|
30037
|
+
let parsed;
|
|
30038
|
+
try {
|
|
30039
|
+
parsed = JSON.parse(raw);
|
|
30040
|
+
} catch {
|
|
30041
|
+
throw new Error('org project set: notifyOnDeploy must be a JSON array, e.g. ["dev"]');
|
|
30042
|
+
}
|
|
30043
|
+
if (!Array.isArray(parsed) || parsed.some((s) => s !== "dev" && s !== "rc" && s !== "main")) {
|
|
30044
|
+
throw new Error('org project set: notifyOnDeploy must be a JSON array of stages: dev, rc, main (e.g. ["dev"])');
|
|
30045
|
+
}
|
|
30046
|
+
if (new Set(parsed).size !== parsed.length) {
|
|
30047
|
+
throw new Error("org project set: notifyOnDeploy must not contain duplicate stages");
|
|
30048
|
+
}
|
|
30049
|
+
return parsed;
|
|
30050
|
+
}
|
|
30036
30051
|
function parseCiExemptReasonVar(raw) {
|
|
30037
30052
|
const trimmed = raw.trim();
|
|
30038
30053
|
if (!trimmed) throw new Error("org project set: ciExemptReason must be a non-empty reason (or use --unset ciExemptReason to clear it)");
|
|
@@ -30173,7 +30188,8 @@ var SETTABLE_VAR_KEYS = [
|
|
|
30173
30188
|
"gate",
|
|
30174
30189
|
"secrets",
|
|
30175
30190
|
"seedCanary",
|
|
30176
|
-
"installer"
|
|
30191
|
+
"installer",
|
|
30192
|
+
"notifyOnDeploy"
|
|
30177
30193
|
];
|
|
30178
30194
|
var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
30179
30195
|
var SETTABLE_VAR_HINTS = {
|
|
@@ -30202,6 +30218,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
30202
30218
|
ci: "none \u2014 declare intentional no-ci",
|
|
30203
30219
|
requiredChecks: 'JSON array, e.g. ["gate"] or [] for no-ci',
|
|
30204
30220
|
requiredCheckBranches: "non-empty JSON branch-name array; exact product-ruleset scope (#5210)",
|
|
30221
|
+
notifyOnDeploy: 'JSON stage array, e.g. ["dev"] \u2014 successful tenant-deploy sends <stage>-deployed to the project repo (#7358)',
|
|
30205
30222
|
ciExemptReason: "non-empty string \u2014 declares this deployable repo has no CI surface (#4265)",
|
|
30206
30223
|
gate: "JSON {runtime,cmd,affectedCmd,workdir,cacheDepPath,pyVersion}"
|
|
30207
30224
|
};
|
|
@@ -30345,6 +30362,8 @@ function buildProjectSetPatch(input) {
|
|
|
30345
30362
|
patch[key] = parseRequiredChecksVar(raw);
|
|
30346
30363
|
} else if (key === "requiredCheckBranches") {
|
|
30347
30364
|
patch[key] = parseRequiredCheckBranchesVar(raw);
|
|
30365
|
+
} else if (key === "notifyOnDeploy") {
|
|
30366
|
+
patch[key] = parseNotifyOnDeployVar(raw);
|
|
30348
30367
|
} else if (key === "ciExemptReason") {
|
|
30349
30368
|
patch[key] = parseCiExemptReasonVar(raw);
|
|
30350
30369
|
} else if (key === "gate") {
|
|
@@ -30389,6 +30408,8 @@ var DEPLOY_SHELL_SAFE_RE = /^[A-Za-z0-9./:_?&=%-]*$/;
|
|
|
30389
30408
|
var DEPLOY_ROLE_ARN_RE = /^arn:aws:iam::\d{12}:role\/[A-Za-z0-9+=,.@_/-]{1,128}$/;
|
|
30390
30409
|
var DEPLOY_BUCKET_RE = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
|
|
30391
30410
|
var DEPLOY_DISTRIBUTION_ID_RE = /^[A-Z0-9]{5,32}$/;
|
|
30411
|
+
var COMPOSE_PROFILE_RE = /^[a-z0-9][a-z0-9_-]{0,62}$/;
|
|
30412
|
+
var MAX_COMPOSE_PROFILES = 8;
|
|
30392
30413
|
function buildSetDeployPatch(_slug, input) {
|
|
30393
30414
|
const clean4 = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
|
|
30394
30415
|
const stage = clean4(input.stage);
|
|
@@ -30432,6 +30453,23 @@ function buildSetDeployPatch(_slug, input) {
|
|
|
30432
30453
|
if (input.noEnvFile !== void 0 && typeof input.noEnvFile !== "boolean") {
|
|
30433
30454
|
throw new Error("org project set-deploy: --no-env-file must be true or false");
|
|
30434
30455
|
}
|
|
30456
|
+
if (input.clearComposeProfiles && (input.composeProfiles?.length ?? 0) > 0) {
|
|
30457
|
+
throw new Error("org project set-deploy: --clear-compose-profiles cannot be combined with --compose-profiles (they contradict)");
|
|
30458
|
+
}
|
|
30459
|
+
let composeProfiles;
|
|
30460
|
+
if (input.clearComposeProfiles) {
|
|
30461
|
+
composeProfiles = [];
|
|
30462
|
+
} else if (input.composeProfiles !== void 0) {
|
|
30463
|
+
const names = input.composeProfiles.map((p) => typeof p === "string" ? p.trim() : "");
|
|
30464
|
+
if (names.length > MAX_COMPOSE_PROFILES) throw new Error(`org project set-deploy: --compose-profiles carries at most ${MAX_COMPOSE_PROFILES} profiles`);
|
|
30465
|
+
const seen = /* @__PURE__ */ new Set();
|
|
30466
|
+
for (const name of names) {
|
|
30467
|
+
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)}`);
|
|
30468
|
+
if (seen.has(name)) throw new Error(`org project set-deploy: --compose-profiles carries a duplicate profile ${JSON.stringify(name)}`);
|
|
30469
|
+
seen.add(name);
|
|
30470
|
+
}
|
|
30471
|
+
composeProfiles = [...seen];
|
|
30472
|
+
}
|
|
30435
30473
|
return {
|
|
30436
30474
|
stage,
|
|
30437
30475
|
...substrate !== void 0 ? { substrate } : {},
|
|
@@ -30448,7 +30486,10 @@ function buildSetDeployPatch(_slug, input) {
|
|
|
30448
30486
|
// #2986: an explicit `[]` is what the route reads as "clear them"; omitting the key is what it reads as
|
|
30449
30487
|
// "keep them". Those are different instructions and they now have different flags.
|
|
30450
30488
|
...input.clearAliases ? { aliases: [] } : uniqueAliases.length ? { aliases: uniqueAliases } : {},
|
|
30451
|
-
...input.noEnvFile !== void 0 ? { noEnvFile: input.noEnvFile } : {}
|
|
30489
|
+
...input.noEnvFile !== void 0 ? { noEnvFile: input.noEnvFile } : {},
|
|
30490
|
+
// #7353: an explicit `[]` is what the route reads as "clear them"; omitting the key is what it reads as
|
|
30491
|
+
// "keep them" — same patch contract as aliases (#2986/#2984).
|
|
30492
|
+
...composeProfiles !== void 0 ? { composeProfiles } : {}
|
|
30452
30493
|
};
|
|
30453
30494
|
}
|
|
30454
30495
|
var DEPLOY_STAGE_BRANCH = { dev: "development", rc: "rc", main: "main" };
|
|
@@ -34422,6 +34463,22 @@ async function suiteRcand(deps, opts) {
|
|
|
34422
34463
|
Object.assign(member, { carried: true }, await carriedPins(deps, member, modules.find((m) => m.repo === member.repo).bundles));
|
|
34423
34464
|
writeSuiteCut(deps.recordPath, record);
|
|
34424
34465
|
}
|
|
34466
|
+
const refusals = [];
|
|
34467
|
+
for (const member of record.members) {
|
|
34468
|
+
if (member.carried || member.published || member.id === "katip") continue;
|
|
34469
|
+
try {
|
|
34470
|
+
await deps.preflight(member.repo);
|
|
34471
|
+
} catch (e) {
|
|
34472
|
+
refusals.push(`${member.repo}: ${e.message}`);
|
|
34473
|
+
}
|
|
34474
|
+
}
|
|
34475
|
+
if (refusals.length) {
|
|
34476
|
+
const untouched = record.members.every((m) => !m.tag) && !record.pinPr;
|
|
34477
|
+
if (untouched) (0, import_node_fs29.rmSync)(deps.recordPath, { force: true });
|
|
34478
|
+
throw new Error(`module rc serving is not ready; ${untouched ? "no suite cut started" : "no new tag was cut"}:
|
|
34479
|
+
${refusals.join("\n")}
|
|
34480
|
+
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).`);
|
|
34481
|
+
}
|
|
34425
34482
|
for (const member of record.members) {
|
|
34426
34483
|
if ((await deps.head(member.repo, "development"))?.toLowerCase() !== member.development.toLowerCase())
|
|
34427
34484
|
throw new Error(`${member.repo}: development moved after the cut snapshot; refuse to add untested code`);
|
|
@@ -34631,7 +34688,7 @@ function announceSummaryFidelity(lines2, rangeLog) {
|
|
|
34631
34688
|
if (shipped.size === 0) return null;
|
|
34632
34689
|
const invented = [...new Set(lines2.flatMap((l) => l.match(/#\d+/g) ?? []))].filter((ref) => !shipped.has(ref));
|
|
34633
34690
|
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`;
|
|
34691
|
+
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
34692
|
}
|
|
34636
34693
|
async function resolveAnnounceTarget(deps, repo) {
|
|
34637
34694
|
if (repo.toLowerCase() === ANNOUNCE_REPO.toLowerCase()) return { kind: "alerts" };
|
|
@@ -34709,6 +34766,12 @@ function normalizeHotfixVersion(input) {
|
|
|
34709
34766
|
return { tag: `v${m[1]}`, version: m[1] };
|
|
34710
34767
|
}
|
|
34711
34768
|
var deriveHotfixVersion = resolveHotfixVersion;
|
|
34769
|
+
function bumpHotfixPatchVersion(version) {
|
|
34770
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
|
|
34771
|
+
if (!m) throw new Error(`cannot bump non-X.Y.Z hotfix version "${version}"`);
|
|
34772
|
+
const next = `${m[1]}.${m[2]}.${Number(m[3]) + 1}`;
|
|
34773
|
+
return { tag: `v${next}`, version: next };
|
|
34774
|
+
}
|
|
34712
34775
|
function hotfixBranch(tag) {
|
|
34713
34776
|
return `hotfix/${tag}`;
|
|
34714
34777
|
}
|
|
@@ -34762,7 +34825,7 @@ function formatHotfixBatchPreflightRefusal(failures, fromSpecs) {
|
|
|
34762
34825
|
const batchFrom = fromSpecs.join(",");
|
|
34763
34826
|
return `hotfix start batch preflight refused: ${failures.length} pick(s) would hard-stop on origin/main:
|
|
34764
34827
|
${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`;
|
|
34828
|
+
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
34829
|
}
|
|
34767
34830
|
var CHERRY_PICKED_FROM = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
34768
34831
|
async function findHotfixPickGaps(deps, sources, tolerated) {
|
|
@@ -34791,32 +34854,44 @@ ${lines2.join("\n")}
|
|
|
34791
34854
|
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
34855
|
}
|
|
34793
34856
|
async function preflightHotfixBatchPicks(deps, sources, pickTolerated) {
|
|
34794
|
-
if (sources.length === 0) return [];
|
|
34795
|
-
const
|
|
34796
|
-
const
|
|
34857
|
+
if (sources.length === 0) return { failures: [], skipped: [] };
|
|
34858
|
+
const { mkdtemp: mkdtemp2, rm: rm2 } = await import("node:fs/promises");
|
|
34859
|
+
const { tmpdir: tmpdir10 } = await import("node:os");
|
|
34860
|
+
const { join: join44 } = await import("node:path");
|
|
34861
|
+
const dir = await mkdtemp2(join44(tmpdir10(), "mmi-hotfix-preflight-"));
|
|
34862
|
+
const wt = { run: (file, args) => deps.run(file, file === "git" ? ["-C", dir, ...args] : args) };
|
|
34797
34863
|
const failures = [];
|
|
34864
|
+
const skipped = [];
|
|
34798
34865
|
try {
|
|
34799
|
-
await deps.run("git", ["
|
|
34800
|
-
|
|
34801
|
-
|
|
34802
|
-
await
|
|
34803
|
-
|
|
34804
|
-
|
|
34805
|
-
|
|
34806
|
-
|
|
34807
|
-
|
|
34808
|
-
|
|
34809
|
-
|
|
34810
|
-
|
|
34866
|
+
await deps.run("git", ["worktree", "add", "--detach", dir, "origin/main"]);
|
|
34867
|
+
try {
|
|
34868
|
+
for (const source of sources) {
|
|
34869
|
+
const before = await wt.run("git", ["status", "--porcelain"]);
|
|
34870
|
+
try {
|
|
34871
|
+
await cherryPickWithToleratedPaths(wt, source.sha, pickTolerated);
|
|
34872
|
+
} catch (e) {
|
|
34873
|
+
const message2 = e.message ?? String(e);
|
|
34874
|
+
const after = await wt.run("git", ["status", "--porcelain"]).catch(() => null);
|
|
34875
|
+
if (after !== null && after === before && /empty|nothing to commit/i.test(message2)) {
|
|
34876
|
+
await wt.run("git", ["cherry-pick", "--quit"]).catch(() => void 0);
|
|
34877
|
+
skipped.push({ sha: source.sha, label: source.label });
|
|
34878
|
+
continue;
|
|
34879
|
+
}
|
|
34880
|
+
const blocking = parseCherryPickBlockingPaths(message2);
|
|
34881
|
+
failures.push({
|
|
34882
|
+
label: source.label,
|
|
34883
|
+
sha: source.sha,
|
|
34884
|
+
blocking: blocking.length > 0 ? blocking : [message2.split("\n")[0] ?? message2]
|
|
34885
|
+
});
|
|
34886
|
+
}
|
|
34811
34887
|
}
|
|
34888
|
+
} finally {
|
|
34889
|
+
await deps.run("git", ["worktree", "remove", "--force", dir]).catch(() => void 0);
|
|
34812
34890
|
}
|
|
34813
34891
|
} finally {
|
|
34814
|
-
|
|
34815
|
-
await deps.run("git", ["checkout", startBranch]).catch(() => void 0);
|
|
34816
|
-
}
|
|
34817
|
-
await deps.run("git", ["branch", "-D", preflightBranch]).catch(() => void 0);
|
|
34892
|
+
await rm2(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
34818
34893
|
}
|
|
34819
|
-
return failures;
|
|
34894
|
+
return { failures, skipped };
|
|
34820
34895
|
}
|
|
34821
34896
|
async function restoreAfterFailedPort(deps, opts) {
|
|
34822
34897
|
const { startBranch, branch, created } = opts;
|
|
@@ -34940,9 +35015,19 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
34940
35015
|
}
|
|
34941
35016
|
await deps.run("git", ["fetch", "origin", "--tags"]);
|
|
34942
35017
|
const { deployModel } = await hotfixPreflight(deps, ctx, "start");
|
|
34943
|
-
const
|
|
35018
|
+
const bumpNotes = [];
|
|
35019
|
+
let resolved = await resolveHotfixVersion(deps);
|
|
35020
|
+
for (; ; ) {
|
|
35021
|
+
const priorPr = await findHotfixPr(deps, ctx, resolved.tag);
|
|
35022
|
+
if ((priorPr?.state ?? "").toUpperCase() !== "MERGED") break;
|
|
35023
|
+
const priorReleased = await hotfixReleaseExists(deps, ctx, resolved.tag);
|
|
35024
|
+
const next = bumpHotfixPatchVersion(resolved.version);
|
|
35025
|
+
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)`);
|
|
35026
|
+
resolved = next;
|
|
35027
|
+
}
|
|
35028
|
+
const { tag, version } = resolved;
|
|
34944
35029
|
const branch = hotfixBranch(tag);
|
|
34945
|
-
const notes = [];
|
|
35030
|
+
const notes = [...bumpNotes];
|
|
34946
35031
|
const existingPr = await findHotfixPr(deps, ctx, tag);
|
|
34947
35032
|
const existingState = (existingPr?.state ?? "").toUpperCase();
|
|
34948
35033
|
const releaseExists = existingState === "MERGED" ? await hotfixReleaseExists(deps, ctx, tag) : false;
|
|
@@ -34974,8 +35059,8 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
34974
35059
|
if (specs.length === 0) throw new Error("hotfix start: --from named no PR or SHA");
|
|
34975
35060
|
const sources = [];
|
|
34976
35061
|
for (const spec of specs) {
|
|
34977
|
-
const
|
|
34978
|
-
if (!sources.some((s) => s.sha ===
|
|
35062
|
+
const resolved2 = await resolveHotfixSource(deps, ctx, spec);
|
|
35063
|
+
if (!sources.some((s) => s.sha === resolved2.sha)) sources.push(resolved2);
|
|
34979
35064
|
}
|
|
34980
35065
|
const label = sources.map((s) => s.label).join(", ");
|
|
34981
35066
|
const { foldPaths, pickTolerated } = await resolveHotfixPickTolerated(deps, deployModel);
|
|
@@ -34991,21 +35076,33 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
34991
35076
|
if (pickGaps.length > 0) {
|
|
34992
35077
|
notes.push(`pick-completeness ack: excluded ${pickGaps.map((g) => `${g.sha.slice(0, 7)} ${g.subject}`).join("; ")} by --ack (#6994)`);
|
|
34993
35078
|
}
|
|
35079
|
+
let activeSources = sources;
|
|
35080
|
+
let activeLabel = label;
|
|
34994
35081
|
const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
34995
35082
|
if (remoteBranch) {
|
|
34996
35083
|
await deps.run("git", ["checkout", branch]);
|
|
34997
35084
|
await deps.run("git", ["pull", "--ff-only", "origin", branch]);
|
|
34998
35085
|
notes.push(`branch ${branch} already on origin \u2014 reused (cherry-pick/bump assumed present; PR step resumes)`);
|
|
34999
35086
|
} else {
|
|
35000
|
-
const
|
|
35001
|
-
if (
|
|
35002
|
-
throw new Error(formatHotfixBatchPreflightRefusal(
|
|
35087
|
+
const preflight2 = await preflightHotfixBatchPicks(deps, sources, pickTolerated);
|
|
35088
|
+
if (preflight2.failures.length > 0) {
|
|
35089
|
+
throw new Error(formatHotfixBatchPreflightRefusal(preflight2.failures, specs));
|
|
35090
|
+
}
|
|
35091
|
+
if (preflight2.skipped.length > 0) {
|
|
35092
|
+
const skippedShas = new Set(preflight2.skipped.map((s) => s.sha));
|
|
35093
|
+
const remaining = sources.filter((s) => !skippedShas.has(s.sha));
|
|
35094
|
+
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(", ")}`);
|
|
35095
|
+
if (remaining.length === 0) {
|
|
35096
|
+
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`);
|
|
35097
|
+
}
|
|
35098
|
+
activeSources = remaining;
|
|
35099
|
+
activeLabel = remaining.map((s) => s.label).join(", ");
|
|
35003
35100
|
}
|
|
35004
35101
|
const startBranch = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
35005
35102
|
const preexistingLocal = clean3(await deps.run("git", ["branch", "--list", branch]));
|
|
35006
35103
|
await deps.run("git", ["checkout", "-B", branch, "origin/main"]);
|
|
35007
35104
|
try {
|
|
35008
|
-
for (const source of
|
|
35105
|
+
for (const source of activeSources) {
|
|
35009
35106
|
const autoResolved = await cherryPickWithToleratedPaths(deps, source.sha, pickTolerated);
|
|
35010
35107
|
if (autoResolved.length > 0) {
|
|
35011
35108
|
notes.push(`auto-resolved regenerable cherry-pick conflict(s) for ${source.label}: ${autoResolved.join(", ")} (regenerated in bump step)`);
|
|
@@ -35014,12 +35111,12 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
35014
35111
|
} catch (e) {
|
|
35015
35112
|
const recovery = await restoreAfterFailedPort(deps, { startBranch, branch, created: !preexistingLocal });
|
|
35016
35113
|
throw new Error(
|
|
35017
|
-
`cherry-pick of ${
|
|
35114
|
+
`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
35115
|
);
|
|
35019
35116
|
}
|
|
35020
|
-
notes.push(`cherry-picked ${
|
|
35117
|
+
notes.push(`cherry-picked ${activeLabel} onto ${branch} (from origin/main, -x trailer recorded)`);
|
|
35118
|
+
const durableSource = clean3(await deps.run("git", ["merge-base", "HEAD", "origin/main"]));
|
|
35021
35119
|
if (deployModel === "hub-serverless") {
|
|
35022
|
-
const durableSource = clean3(await deps.run("git", ["merge-base", "HEAD", "origin/main"]));
|
|
35023
35120
|
await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version, "--source-commit", durableSource]);
|
|
35024
35121
|
const changedFiles = (await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
35025
35122
|
await deps.run("git", ["add", "-f", "--", ...changedFiles]);
|
|
@@ -35031,11 +35128,12 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
35031
35128
|
notes.push("distribution prepare produced no changes \u2014 nothing extra committed");
|
|
35032
35129
|
}
|
|
35033
35130
|
} else if (foldPaths.length > 0) {
|
|
35034
|
-
const foldNote = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
|
|
35131
|
+
const foldNote = await withFoldSourceCommit(durableSource, () => foldReleaseVersion(deps, deployModel, tag, foldPaths));
|
|
35035
35132
|
notes.push(foldNote);
|
|
35036
35133
|
} else {
|
|
35037
35134
|
notes.push(`version fold skipped (deployModel=${deployModel} carries no version manifest \u2014 the tag is the version)`);
|
|
35038
35135
|
}
|
|
35136
|
+
await assertHotfixFoldBomStamp(deps, tag);
|
|
35039
35137
|
await deps.run("git", ["push", "-u", "origin", branch]);
|
|
35040
35138
|
}
|
|
35041
35139
|
const bumpNote = deployModel === "hub-serverless" ? " with the locked distribution bump" : foldPaths.length > 0 ? " with the package version bump" : "";
|
|
@@ -35053,16 +35151,16 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
35053
35151
|
// Every picked sha goes in the marker (#4411) — `hotfix release` reads it when --carries is omitted,
|
|
35054
35152
|
// so a multi-fix cycle proves ALL of its targets present before tagging, not just the first.
|
|
35055
35153
|
"--body",
|
|
35056
|
-
`Hotfix ${tag}: cherry-pick of ${
|
|
35154
|
+
`Hotfix ${tag}: cherry-pick of ${activeLabel} onto origin/main${bumpNote}.
|
|
35057
35155
|
|
|
35058
|
-
<!-- mmi-hotfix-carries: ${
|
|
35156
|
+
<!-- mmi-hotfix-carries: ${activeSources.map((s) => s.sha).join(",")} -->${pickGaps.length > 0 ? `
|
|
35059
35157
|
|
|
35060
35158
|
<!-- mmi-hotfix-acked-excluded: ${pickGaps.map((g) => g.sha).join(",")} (#6994 pick-completeness) -->` : ""}
|
|
35061
35159
|
|
|
35062
35160
|
Merge this PR (human-initiated), then run \`mmi-cli devops hotfix release ${tag}\`.`
|
|
35063
35161
|
]));
|
|
35064
35162
|
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:
|
|
35163
|
+
return { ...ctx, command: "hotfix-start", tag, version, branch, source: activeLabel, prUrl, reused: Boolean(remoteBranch), notes };
|
|
35066
35164
|
}
|
|
35067
35165
|
function hotfixWorkflowRunFromDispatch(workflow, dispatch) {
|
|
35068
35166
|
return {
|
|
@@ -35099,6 +35197,13 @@ async function watchReleaseRun(deps, ctx, workflow, sha) {
|
|
|
35099
35197
|
const run = rows.find((r) => r.headSha === sha && typeof r.databaseId === "number");
|
|
35100
35198
|
if (!run) continue;
|
|
35101
35199
|
if (run.status === "completed") return { workflow, runId: run.databaseId, url: run.url, conclusion: run.conclusion ?? "unknown" };
|
|
35200
|
+
try {
|
|
35201
|
+
const viewed = JSON.parse(await deps.run("gh", ["run", "view", String(run.databaseId), "--repo", ctx.repo, "--json", "status,conclusion"]));
|
|
35202
|
+
if (viewed.status === "completed") {
|
|
35203
|
+
return { workflow, runId: run.databaseId, url: run.url, conclusion: typeof viewed.conclusion === "string" ? viewed.conclusion : "unknown" };
|
|
35204
|
+
}
|
|
35205
|
+
} catch {
|
|
35206
|
+
}
|
|
35102
35207
|
try {
|
|
35103
35208
|
await deps.run("gh", ghRunWatchArgs(run.databaseId, ctx.repo));
|
|
35104
35209
|
return { workflow, runId: run.databaseId, url: run.url, conclusion: "success" };
|
|
@@ -35126,9 +35231,9 @@ async function portFoldToDevelopment(deps, ctx, deployModel, tag) {
|
|
|
35126
35231
|
const branch = devFoldBranch(tag);
|
|
35127
35232
|
const existing = await findFoldPr(deps, ctx, tag);
|
|
35128
35233
|
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;
|
|
35234
|
+
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
35235
|
}
|
|
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`;
|
|
35236
|
+
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
35237
|
const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
35133
35238
|
await reapFoldScratchBranches(deps);
|
|
35134
35239
|
const scratch = devFoldScratchBranch(tag, deps);
|
|
@@ -35156,7 +35261,7 @@ async function portFoldToDevelopment(deps, ctx, deployModel, tag) {
|
|
|
35156
35261
|
|
|
35157
35262
|
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
35263
|
]));
|
|
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;
|
|
35264
|
+
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
35265
|
} finally {
|
|
35161
35266
|
if (previousRef && previousRef !== "HEAD") await deps.run("git", ["checkout", previousRef]).catch(() => void 0);
|
|
35162
35267
|
await deps.run("git", ["branch", "-D", scratch]).catch(() => void 0);
|
|
@@ -35344,6 +35449,41 @@ ${recovery.note}; the full required-check wall did not revalidate`);
|
|
|
35344
35449
|
}, `the immutable ${anchors.tag} pushed at ${mergedSha.slice(0, 12)}`);
|
|
35345
35450
|
return checks;
|
|
35346
35451
|
}
|
|
35452
|
+
var FOLD_SOURCE_COMMIT_ENV = "MMI_FOLD_SOURCE_COMMIT";
|
|
35453
|
+
async function withFoldSourceCommit(durableSource, fn) {
|
|
35454
|
+
const prev = process.env[FOLD_SOURCE_COMMIT_ENV];
|
|
35455
|
+
process.env[FOLD_SOURCE_COMMIT_ENV] = durableSource;
|
|
35456
|
+
try {
|
|
35457
|
+
return await fn();
|
|
35458
|
+
} finally {
|
|
35459
|
+
if (prev === void 0) delete process.env[FOLD_SOURCE_COMMIT_ENV];
|
|
35460
|
+
else process.env[FOLD_SOURCE_COMMIT_ENV] = prev;
|
|
35461
|
+
}
|
|
35462
|
+
}
|
|
35463
|
+
async function assertHotfixFoldBomStamp(deps, tag) {
|
|
35464
|
+
let bomText;
|
|
35465
|
+
try {
|
|
35466
|
+
bomText = await deps.run("git", ["show", "HEAD:distribution-bom.json"]);
|
|
35467
|
+
} catch {
|
|
35468
|
+
return;
|
|
35469
|
+
}
|
|
35470
|
+
let stamp;
|
|
35471
|
+
try {
|
|
35472
|
+
stamp = JSON.parse(bomText).sourceCommit;
|
|
35473
|
+
} catch (e) {
|
|
35474
|
+
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`);
|
|
35475
|
+
}
|
|
35476
|
+
if (typeof stamp !== "string" || !/^[0-9a-f]{40}$/.test(stamp)) {
|
|
35477
|
+
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`);
|
|
35478
|
+
}
|
|
35479
|
+
try {
|
|
35480
|
+
await deps.run("git", ["merge-base", "--is-ancestor", stamp, "origin/main"]);
|
|
35481
|
+
} catch {
|
|
35482
|
+
throw new Error(
|
|
35483
|
+
`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`
|
|
35484
|
+
);
|
|
35485
|
+
}
|
|
35486
|
+
}
|
|
35347
35487
|
async function assertConsumerBomLineage(deps, mergedSha, tag) {
|
|
35348
35488
|
let bomText;
|
|
35349
35489
|
try {
|
|
@@ -35364,7 +35504,7 @@ async function assertConsumerBomLineage(deps, mergedSha, tag) {
|
|
|
35364
35504
|
await deps.run("git", ["merge-base", "--is-ancestor", stamp, mergedSha]);
|
|
35365
35505
|
} catch {
|
|
35366
35506
|
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`
|
|
35507
|
+
`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
35508
|
);
|
|
35369
35509
|
}
|
|
35370
35510
|
}
|
|
@@ -35410,16 +35550,16 @@ async function runHotfixReleaseLocked(deps, versionInput, options, doctor) {
|
|
|
35410
35550
|
const releaseExists = await hotfixReleaseExists(deps, ctx, tag);
|
|
35411
35551
|
if (!releaseExists && isHubControlRepo(ctx.repo)) {
|
|
35412
35552
|
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");
|
|
35553
|
+
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
35554
|
}
|
|
35415
35555
|
let lines2;
|
|
35416
35556
|
try {
|
|
35417
35557
|
lines2 = summaryFileLines(await (deps.readFile ?? ((p) => (0, import_promises4.readFile)(p, "utf8")))(options.announceSummaryFile));
|
|
35418
35558
|
} 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`);
|
|
35559
|
+
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
35560
|
}
|
|
35421
35561
|
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`);
|
|
35562
|
+
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
35563
|
}
|
|
35424
35564
|
const jerv = hubSummaryJervRefusal(lines2);
|
|
35425
35565
|
if (jerv) throw new Error(jerv);
|
|
@@ -35566,7 +35706,7 @@ ${decision.note}`);
|
|
|
35566
35706
|
foldNote = await portFoldToDevelopment(deps, ctx, deployModel, tag);
|
|
35567
35707
|
} catch (e) {
|
|
35568
35708
|
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`;
|
|
35709
|
+
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
35710
|
}
|
|
35571
35711
|
await recordHotfixPhase(ledger, anchors, "alignment", hotfixFoldAlignmentEntry(foldStatus, foldNote, mergedSha), "the tagged, released, deployed hotfix");
|
|
35572
35712
|
const ledgerReport = releaseLedgerReport(await ledger.load(anchors).catch(() => void 0), ledger.path);
|
|
@@ -35615,7 +35755,7 @@ async function runHotfixRetryDeployLocked(deps, versionInput) {
|
|
|
35615
35755
|
foldNote2 = await portFoldToDevelopment(deps, ctx, deployModel, prior.tag);
|
|
35616
35756
|
} catch (e) {
|
|
35617
35757
|
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`;
|
|
35758
|
+
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
35759
|
}
|
|
35620
35760
|
return hotfixFoldAlignmentEntry(foldStatus2, foldNote2, prior.tagSha);
|
|
35621
35761
|
}
|
|
@@ -36135,17 +36275,17 @@ function prHeadBehindBase(input) {
|
|
|
36135
36275
|
return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
|
|
36136
36276
|
}
|
|
36137
36277
|
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`;
|
|
36278
|
+
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
36279
|
}
|
|
36140
36280
|
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`;
|
|
36281
|
+
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
36282
|
}
|
|
36143
36283
|
var PROTECTED_PR_HEAD_BRANCHES = ["main", "master", "development", "rc"];
|
|
36144
36284
|
function isProtectedPrHead(head) {
|
|
36145
36285
|
return PROTECTED_PR_HEAD_BRANCHES.includes(head);
|
|
36146
36286
|
}
|
|
36147
36287
|
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`;
|
|
36288
|
+
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
36289
|
}
|
|
36150
36290
|
var PR_HEAD_UPDATE_API_READ_RETRIES = 5;
|
|
36151
36291
|
var PR_HEAD_UPDATE_API_READ_DELAY_MS = 2e3;
|
|
@@ -37635,7 +37775,7 @@ function readRepoVersion() {
|
|
|
37635
37775
|
}
|
|
37636
37776
|
function registerTrainCommands(program3, trainDeps) {
|
|
37637
37777
|
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\
|
|
37778
|
+
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
37779
|
try {
|
|
37640
37780
|
if (o.suiteSolo !== o.ownerGo || o.lane === "rcand" && o.suiteSolo) {
|
|
37641
37781
|
return failGraceful("train doctor: a suite-solo verdict needs BOTH --suite-solo --owner-go, and never on the rcand lane");
|
|
@@ -37815,7 +37955,7 @@ async function openSuiteCutPinPr(deps, input) {
|
|
|
37815
37955
|
}
|
|
37816
37956
|
async function openSuitePinPr(deps, input) {
|
|
37817
37957
|
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`;
|
|
37958
|
+
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
37959
|
let scratch = "";
|
|
37820
37960
|
try {
|
|
37821
37961
|
const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=development`]));
|
|
@@ -39874,7 +40014,7 @@ function withRateLimitRetry(client, seams = {}) {
|
|
|
39874
40014
|
};
|
|
39875
40015
|
}
|
|
39876
40016
|
async function prCreateMergedHeadWarning(input, deps = {}) {
|
|
39877
|
-
const guide = "docs/Guides/train-troubleshooting.md#merged-pr-head-reuse";
|
|
40017
|
+
const guide = "https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#merged-pr-head-reuse";
|
|
39878
40018
|
if (!input.head) {
|
|
39879
40019
|
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
40020
|
}
|
|
@@ -39899,6 +40039,11 @@ async function prCreateMergedHeadWarning(input, deps = {}) {
|
|
|
39899
40039
|
return `pr create: WARNING \u2014 could not check whether head branch '${input.head}' already landed (${detail}); continuing with PR creation \u2014 ${guide}`;
|
|
39900
40040
|
}
|
|
39901
40041
|
}
|
|
40042
|
+
function isIssueMissingError(e) {
|
|
40043
|
+
if (typeof e.status === "number") return e.status === 404;
|
|
40044
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
40045
|
+
return /HTTP 404|Not Found|\(404\)/i.test(msg);
|
|
40046
|
+
}
|
|
39902
40047
|
async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
39903
40048
|
const issues = [...new Set(findClosingMentions(body).map((mention) => mention.issue))];
|
|
39904
40049
|
if (!issues.length) return void 0;
|
|
@@ -39909,7 +40054,14 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
|
39909
40054
|
const requester = `requesting lane: ${actor.surface}/${actor.session}@${actor.host} (session id from ${source})`;
|
|
39910
40055
|
const checkContest = deps.checkContest ?? checkLaneContest;
|
|
39911
40056
|
for (const number of issues) {
|
|
39912
|
-
|
|
40057
|
+
let state;
|
|
40058
|
+
try {
|
|
40059
|
+
state = (await client.rest("GET", `repos/${repo}/issues/${number}`))?.state;
|
|
40060
|
+
} catch (e) {
|
|
40061
|
+
if (isIssueMissingError(e)) {
|
|
40062
|
+
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}`;
|
|
40063
|
+
}
|
|
40064
|
+
}
|
|
39913
40065
|
if (state?.toLowerCase() === "closed") continue;
|
|
39914
40066
|
const contest = await checkContest(client, { repository: repo, number }, actor);
|
|
39915
40067
|
if (!contest.contested) continue;
|
|
@@ -43402,10 +43554,10 @@ async function announceSummaryRefusal(path2, isHub = false) {
|
|
|
43402
43554
|
try {
|
|
43403
43555
|
lines2 = summaryFileLines(await (0, import_promises8.readFile)(path2, "utf8"));
|
|
43404
43556
|
} catch (e) {
|
|
43405
|
-
return `release: could not read --announce-summary-file ${path2}: ${e.message} \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
43557
|
+
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
43558
|
}
|
|
43407
43559
|
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`;
|
|
43560
|
+
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
43561
|
}
|
|
43410
43562
|
if (isHub) {
|
|
43411
43563
|
const jerv = hubSummaryJervRefusal(lines2);
|
|
@@ -43824,7 +43976,7 @@ function registerTrainCommandsAndInventory(program3) {
|
|
|
43824
43976
|
function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
43825
43977
|
async function pinSoloMember(repo, tag) {
|
|
43826
43978
|
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`);
|
|
43979
|
+
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
43980
|
if (!suiteMemberRefusal(repo, read.project, "release")) return void 0;
|
|
43829
43981
|
const root = (await execFileP("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
43830
43982
|
return openSuitePinPr({ root, run: async (file, args, cwd) => (await execFileP(file, args, { cwd, timeout: GIT_TIMEOUT_MS })).stdout }, { repo, tag, stage: "main" });
|
|
@@ -44069,7 +44221,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44069
44221
|
if (announcesToChannel) {
|
|
44070
44222
|
if (!o.announceSummaryFile) {
|
|
44071
44223
|
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`
|
|
44224
|
+
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
44225
|
);
|
|
44074
44226
|
}
|
|
44075
44227
|
const refusal = await announceSummaryRefusal(o.announceSummaryFile, isHub);
|
|
@@ -44252,7 +44404,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44252
44404
|
if (o.suiteSolo !== o.ownerGo) return fail("hotfix start: solo suite hotfix requires BOTH --suite-solo --owner-go");
|
|
44253
44405
|
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
44406
|
});
|
|
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) => {
|
|
44407
|
+
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
44408
|
if (o.suiteSolo !== o.ownerGo) return fail("hotfix release: solo suite release requires BOTH --suite-solo --owner-go");
|
|
44257
44409
|
if (o.retryDeploy) {
|
|
44258
44410
|
if (o.announceSummaryFile || o.carries) {
|
|
@@ -44261,7 +44413,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44261
44413
|
return runHotfixSub("retry-deploy", () => runHotfixRetryDeploy(trainApplyDeps(), version), o, renderHotfixRetryDeploy);
|
|
44262
44414
|
}
|
|
44263
44415
|
return runHotfixSub("release", async () => {
|
|
44264
|
-
const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [], suiteSolo: o.suiteSolo });
|
|
44416
|
+
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
44417
|
const suitePin = o.suiteSolo ? await pinSoloMember(result.repo, result.tag) : void 0;
|
|
44266
44418
|
const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
44267
44419
|
return { ...result, projectInfoSync, ...o.suiteSolo ? { suiteSolo: true, suitePin } : {} };
|
|
@@ -44297,6 +44449,10 @@ async function exec(file, args, cwd, timeout = GH_TIMEOUT) {
|
|
|
44297
44449
|
async function gh(args) {
|
|
44298
44450
|
return exec("gh", args);
|
|
44299
44451
|
}
|
|
44452
|
+
function rcRunCauseFromLog(log) {
|
|
44453
|
+
const causes = [...new Set([...log.matchAll(/##\[error\](?!Process completed with exit code)([^\r\n]+)/g)].map((m) => m[1].trim()).filter(Boolean))];
|
|
44454
|
+
return causes.length ? `: ${causes.join("; ")}` : "";
|
|
44455
|
+
}
|
|
44300
44456
|
function suiteTrainIo(root, summaryFile) {
|
|
44301
44457
|
const recordPath = repoRuntimeStatePath(root, "suite", "cut.json");
|
|
44302
44458
|
const base = (0, import_node_path42.join)(repoRuntimeStatePath(root, "suite"), "checkouts");
|
|
@@ -44410,23 +44566,41 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
44410
44566
|
if (result.tag !== expected || result.deployStatus !== "success") throw new Error(`${repo}: release is not green at expected ${expected} (${result.tag}/${result.deployStatus})`);
|
|
44411
44567
|
}
|
|
44412
44568
|
});
|
|
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`);
|
|
44569
|
+
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"]));
|
|
44570
|
+
const watchRcRun = async (title, fields, reuse) => {
|
|
44419
44571
|
const since = Date.now() - 1e4;
|
|
44420
|
-
if (!
|
|
44421
|
-
let candidate =
|
|
44572
|
+
if (!reuse) await gh(["workflow", "run", "module-rc-publish.yml", "--repo", "mutmutco/MMI-Hub", ...fields.flatMap((f) => ["--field", f])]);
|
|
44573
|
+
let candidate = reuse;
|
|
44422
44574
|
for (let i = 0; !candidate && i < 25; i++) {
|
|
44423
44575
|
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
44424
|
-
candidate = (await
|
|
44576
|
+
candidate = (await rcRuns()).find((r) => r.displayTitle === title && Date.parse(r.createdAt) >= since);
|
|
44577
|
+
}
|
|
44578
|
+
if (!candidate) throw new Error(`workflow dispatch outcome ambiguous for "${title}"; inspect Actions before retrying`);
|
|
44579
|
+
try {
|
|
44580
|
+
await exec("gh", ["run", "watch", String(candidate.databaseId), "--repo", "mutmutco/MMI-Hub", "--exit-status"], void 0, WATCH_TIMEOUT);
|
|
44581
|
+
} catch {
|
|
44425
44582
|
}
|
|
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
44583
|
const verdict = JSON.parse(await gh(["run", "view", String(candidate.databaseId), "--repo", "mutmutco/MMI-Hub", "--json", "conclusion"]));
|
|
44429
|
-
|
|
44584
|
+
return { id: candidate.databaseId, conclusion: verdict.conclusion };
|
|
44585
|
+
};
|
|
44586
|
+
const publish = async (repo, tag) => {
|
|
44587
|
+
const title = `module-rc-publish ${repo} @ ${tag}`;
|
|
44588
|
+
const existing = (await rcRuns()).filter((r) => r.displayTitle === title).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
|
|
44589
|
+
if (existing[0]?.conclusion === "success") return;
|
|
44590
|
+
if (existing[0]?.conclusion === "failure") throw new Error(`${repo}: prior module-rc-publish ${tag} failed; inspect the run and retry it explicitly before continuing`);
|
|
44591
|
+
const run = await watchRcRun(title, [`repo=${repo}`, `tag=${tag}`], existing[0]);
|
|
44592
|
+
if (run.conclusion !== "success") throw new Error(`${repo}: module rc publish run ${run.id} not green${await rcRunCause(run.id)}`);
|
|
44593
|
+
};
|
|
44594
|
+
const preflight2 = async (repo) => {
|
|
44595
|
+
const run = await watchRcRun(`module-rc-preflight ${repo}`, [`repo=${repo}`, "preflight=true"], void 0);
|
|
44596
|
+
if (run.conclusion !== "success") throw new Error(`rc serving preflight run ${run.id} failed${await rcRunCause(run.id)}`);
|
|
44597
|
+
};
|
|
44598
|
+
const rcRunCause = async (id) => {
|
|
44599
|
+
try {
|
|
44600
|
+
return rcRunCauseFromLog(await exec("gh", ["run", "view", String(id), "--repo", "mutmutco/MMI-Hub", "--log-failed"]));
|
|
44601
|
+
} catch {
|
|
44602
|
+
return "";
|
|
44603
|
+
}
|
|
44430
44604
|
};
|
|
44431
44605
|
const registry2 = async () => {
|
|
44432
44606
|
const file = await defaultGitHubClient().rest("GET", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=development`);
|
|
@@ -44460,6 +44634,7 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
44460
44634
|
},
|
|
44461
44635
|
rcand: runRcand,
|
|
44462
44636
|
publish,
|
|
44637
|
+
preflight: preflight2,
|
|
44463
44638
|
pin: (input) => openSuiteCutPinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
|
|
44464
44639
|
verifyLeadCut: async (sha) => {
|
|
44465
44640
|
const current = await gh(["api", `repos/${SUITE_REPO}/git/ref/heads/development`, "--jq", ".object.sha"]);
|
|
@@ -50522,7 +50697,7 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
|
|
|
50522
50697
|
console.log(JSON.stringify(report, null, 2));
|
|
50523
50698
|
if (!report.rcand.canApply) process.exitCode = 1;
|
|
50524
50699
|
});
|
|
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) => {
|
|
50700
|
+
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
50701
|
const cfg = await loadConfig();
|
|
50527
50702
|
let target;
|
|
50528
50703
|
try {
|
|
@@ -50536,6 +50711,10 @@ project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY ro
|
|
|
50536
50711
|
if (v !== "true" && v !== "false") return fail("org project set-deploy: --no-env-file must be true or false");
|
|
50537
50712
|
noEnvFile = v === "true";
|
|
50538
50713
|
}
|
|
50714
|
+
let composeProfiles;
|
|
50715
|
+
if (typeof o.composeProfiles === "string") {
|
|
50716
|
+
composeProfiles = o.composeProfiles.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
50717
|
+
}
|
|
50539
50718
|
if (noEnvFile !== void 0) {
|
|
50540
50719
|
const branch = DEPLOY_STAGE_BRANCH[o.stage.trim().toLowerCase()];
|
|
50541
50720
|
if (branch) {
|
|
@@ -50574,7 +50753,9 @@ ${filelessTransitionGuide(target, o.stage)}`
|
|
|
50574
50753
|
distributionDomain: o.distributionDomain,
|
|
50575
50754
|
aliases: o.alias,
|
|
50576
50755
|
clearAliases: o.clearAliases,
|
|
50577
|
-
noEnvFile
|
|
50756
|
+
noEnvFile,
|
|
50757
|
+
composeProfiles,
|
|
50758
|
+
clearComposeProfiles: o.clearComposeProfiles
|
|
50578
50759
|
});
|
|
50579
50760
|
if (target.includes("/")) body.repo = target;
|
|
50580
50761
|
} catch (e) {
|
package/package.json
CHANGED