@mutmutco/cli 4.4.1 → 4.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +258 -54
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -15870,10 +15870,10 @@ var rollout_plan_default = {
|
|
|
15870
15870
|
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)."
|
|
15871
15871
|
},
|
|
15872
15872
|
baseline: {
|
|
15873
|
-
version: "4.4.
|
|
15874
|
-
tag: "v4.4.
|
|
15875
|
-
commit: "
|
|
15876
|
-
npm: "@mutmutco/cli@4.4.
|
|
15873
|
+
version: "4.4.2",
|
|
15874
|
+
tag: "v4.4.2",
|
|
15875
|
+
commit: "678e071c6859",
|
|
15876
|
+
npm: "@mutmutco/cli@4.4.2"
|
|
15877
15877
|
},
|
|
15878
15878
|
exitCriterion: "fleet-n-of-n",
|
|
15879
15879
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15890,14 +15890,14 @@ var rollout_plan_default = {
|
|
|
15890
15890
|
repo: "mutmutco/mmi-hub",
|
|
15891
15891
|
role: "canary",
|
|
15892
15892
|
schedule: "train",
|
|
15893
|
-
v3Target: "v4.4.
|
|
15893
|
+
v3Target: "v4.4.2"
|
|
15894
15894
|
}
|
|
15895
15895
|
],
|
|
15896
15896
|
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.",
|
|
15897
15897
|
rollback: {
|
|
15898
15898
|
independent: true,
|
|
15899
|
-
mechanism: "npm dist-tag latest -> 4.4.
|
|
15900
|
-
v3Target: "v4.4.
|
|
15899
|
+
mechanism: "npm dist-tag latest -> 4.4.2 and redeploy the Hub Lambda from tag v4.4.2 (678e071c6859); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15900
|
+
v3Target: "v4.4.2 (@mutmutco/cli@4.4.2, tag commit 678e071c6859 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15901
15901
|
}
|
|
15902
15902
|
},
|
|
15903
15903
|
{
|
|
@@ -21631,6 +21631,48 @@ function supersedeablePublishLegShape(rec) {
|
|
|
21631
21631
|
function deferredGatewayDeployLegShape(rec) {
|
|
21632
21632
|
return rec.state === "pending" && rec.runId == null && rec.workflow === "jerv-gateway";
|
|
21633
21633
|
}
|
|
21634
|
+
async function proveOriginPromotionOrArchived(deps, prior, path2, label, ref) {
|
|
21635
|
+
let liveSha = "";
|
|
21636
|
+
try {
|
|
21637
|
+
liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
|
|
21638
|
+
} catch (e) {
|
|
21639
|
+
throw new Error(`${label}: could not prove ${prior.tag} live on origin (${e instanceof Error ? e.message : String(e)}) \u2014 refusing to close the leg on an unproven state; retry when origin answers (${ref})`);
|
|
21640
|
+
}
|
|
21641
|
+
if (liveSha.toLowerCase() === prior.tagSha.toLowerCase()) return "live";
|
|
21642
|
+
if (liveSha) {
|
|
21643
|
+
throw new Error(
|
|
21644
|
+
`${label}: origin reports ${prior.tag} at ${liveSha.slice(0, 12)} but the ledger anchors ${prior.tagSha.slice(0, 12)} \u2014 a divergent release is NOT supersedeable; nothing was written (${ref})`
|
|
21645
|
+
);
|
|
21646
|
+
}
|
|
21647
|
+
let repository;
|
|
21648
|
+
try {
|
|
21649
|
+
repository = JSON.parse(await deps.run("gh", ["repo", "view", prior.repo, "--json", "nameWithOwner"]));
|
|
21650
|
+
} catch (e) {
|
|
21651
|
+
throw new Error(`${label}: could not prove repository access for ${prior.repo} (${e instanceof Error ? e.message.split("\n")[0] : String(e)}) \u2014 a Release 404 is untrusted without that proof; nothing was written (${ref})`);
|
|
21652
|
+
}
|
|
21653
|
+
if (repository.nameWithOwner !== prior.repo) {
|
|
21654
|
+
throw new Error(`${label}: repository access proof named ${String(repository.nameWithOwner || "(missing)")}, not ${prior.repo} \u2014 nothing was written (${ref})`);
|
|
21655
|
+
}
|
|
21656
|
+
let releaseAbsent = false;
|
|
21657
|
+
try {
|
|
21658
|
+
await deps.run("gh", ["release", "view", prior.tag, "--repo", prior.repo, "--json", "tagName"]);
|
|
21659
|
+
} catch (e) {
|
|
21660
|
+
const detail = `${e instanceof Error ? e.message : String(e)} ${String(e.stderr ?? "")}`;
|
|
21661
|
+
releaseAbsent = /release not found|HTTP 404|\(404\)/i.test(detail);
|
|
21662
|
+
if (!releaseAbsent) {
|
|
21663
|
+
throw new Error(`${label}: could not prove the GitHub Release for ${prior.tag} absent (${detail.split("\n")[0]}) \u2014 refusing to close on an unproven state; nothing was written (${ref})`);
|
|
21664
|
+
}
|
|
21665
|
+
}
|
|
21666
|
+
if (!releaseAbsent) {
|
|
21667
|
+
throw new Error(`${label}: origin no longer has ${prior.tag} but its GitHub Release still exists \u2014 resolve the mixed external state before closing the ledger; nothing was written (${ref})`);
|
|
21668
|
+
}
|
|
21669
|
+
const archivePath = (0, import_node_path21.join)((0, import_node_path21.dirname)(path2), `phases.archive-${prior.tag}.json`);
|
|
21670
|
+
if ((0, import_node_fs21.existsSync)(archivePath)) {
|
|
21671
|
+
throw new Error(`${label}: archive already exists at ${archivePath} \u2014 preserving both ledger records; resolve the collision by hand; nothing was written (${ref})`);
|
|
21672
|
+
}
|
|
21673
|
+
(0, import_node_fs21.renameSync)(path2, archivePath);
|
|
21674
|
+
return "deleted";
|
|
21675
|
+
}
|
|
21634
21676
|
async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
21635
21677
|
const path2 = releaseLedgerPath(cwd);
|
|
21636
21678
|
return withFileLock(`${path2}.lock`, { label: "release ledger" }, async () => {
|
|
@@ -21664,45 +21706,7 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
21664
21706
|
`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)`
|
|
21665
21707
|
);
|
|
21666
21708
|
}
|
|
21667
|
-
|
|
21668
|
-
try {
|
|
21669
|
-
liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
|
|
21670
|
-
} catch (e) {
|
|
21671
|
-
throw new Error(`release --supersede-publish: could not prove ${prior.tag} live on origin (${e instanceof Error ? e.message : String(e)}) \u2014 refusing to close the leg on an unproven state; retry when origin answers (#6638)`);
|
|
21672
|
-
}
|
|
21673
|
-
if (liveSha.toLowerCase() !== prior.tagSha.toLowerCase()) {
|
|
21674
|
-
if (liveSha) {
|
|
21675
|
-
throw new Error(
|
|
21676
|
-
`release --supersede-publish: origin reports ${prior.tag} at ${liveSha.slice(0, 12)} but the ledger anchors ${prior.tagSha.slice(0, 12)} \u2014 a divergent release is NOT supersedeable; nothing was written (#6638)`
|
|
21677
|
-
);
|
|
21678
|
-
}
|
|
21679
|
-
let repository;
|
|
21680
|
-
try {
|
|
21681
|
-
repository = JSON.parse(await deps.run("gh", ["repo", "view", prior.repo, "--json", "nameWithOwner"]));
|
|
21682
|
-
} catch (e) {
|
|
21683
|
-
throw new Error(`release --supersede-publish: could not prove repository access for ${prior.repo} (${e instanceof Error ? e.message.split("\n")[0] : String(e)}) \u2014 a Release 404 is untrusted without that proof; nothing was written (#6638)`);
|
|
21684
|
-
}
|
|
21685
|
-
if (repository.nameWithOwner !== prior.repo) {
|
|
21686
|
-
throw new Error(`release --supersede-publish: repository access proof named ${String(repository.nameWithOwner || "(missing)")}, not ${prior.repo} \u2014 nothing was written (#6638)`);
|
|
21687
|
-
}
|
|
21688
|
-
let releaseAbsent = false;
|
|
21689
|
-
try {
|
|
21690
|
-
await deps.run("gh", ["release", "view", prior.tag, "--repo", prior.repo, "--json", "tagName"]);
|
|
21691
|
-
} catch (e) {
|
|
21692
|
-
const detail = `${e instanceof Error ? e.message : String(e)} ${String(e.stderr ?? "")}`;
|
|
21693
|
-
releaseAbsent = /release not found|HTTP 404|\(404\)/i.test(detail);
|
|
21694
|
-
if (!releaseAbsent) {
|
|
21695
|
-
throw new Error(`release --supersede-publish: could not prove the GitHub Release for ${prior.tag} absent (${detail.split("\n")[0]}) \u2014 refusing to close on an unproven state; nothing was written (#6638)`);
|
|
21696
|
-
}
|
|
21697
|
-
}
|
|
21698
|
-
if (!releaseAbsent) {
|
|
21699
|
-
throw new Error(`release --supersede-publish: origin no longer has ${prior.tag} but its GitHub Release still exists \u2014 resolve the mixed external state before closing the ledger; nothing was written (#6638)`);
|
|
21700
|
-
}
|
|
21701
|
-
const archivePath = (0, import_node_path21.join)((0, import_node_path21.dirname)(path2), `phases.archive-${prior.tag}.json`);
|
|
21702
|
-
if ((0, import_node_fs21.existsSync)(archivePath)) {
|
|
21703
|
-
throw new Error(`release --supersede-publish: archive already exists at ${archivePath} \u2014 preserving both ledger records; resolve the collision by hand; nothing was written (#6638)`);
|
|
21704
|
-
}
|
|
21705
|
-
(0, import_node_fs21.renameSync)(path2, archivePath);
|
|
21709
|
+
if (await proveOriginPromotionOrArchived(deps, prior, path2, "release --supersede-publish", "#6638") === "deleted") {
|
|
21706
21710
|
return {
|
|
21707
21711
|
command: "release-supersede-publish",
|
|
21708
21712
|
repo: prior.repo,
|
|
@@ -21749,6 +21753,115 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
21749
21753
|
};
|
|
21750
21754
|
});
|
|
21751
21755
|
}
|
|
21756
|
+
async function supersedeContentCausedDeployAndPublishLegs(deps, cwd) {
|
|
21757
|
+
const label = "release --supersede-deploy --supersede-publish";
|
|
21758
|
+
const ref = "#6752";
|
|
21759
|
+
const path2 = releaseLedgerPath(cwd);
|
|
21760
|
+
return withFileLock(`${path2}.lock`, { label: "release ledger" }, async () => {
|
|
21761
|
+
let prior;
|
|
21762
|
+
try {
|
|
21763
|
+
prior = parseReleaseLedger((0, import_node_fs21.readFileSync)(path2, "utf8"));
|
|
21764
|
+
} catch (e) {
|
|
21765
|
+
throw new ReleaseLedgerError(
|
|
21766
|
+
"unreadable",
|
|
21767
|
+
`${label}: the active release ledger at ${path2} could not be read (${e instanceof Error ? e.message : String(e)}) \u2014 resolve it by hand before any train; nothing was written (${ref})`
|
|
21768
|
+
);
|
|
21769
|
+
}
|
|
21770
|
+
const deploy = prior.phases.deploy;
|
|
21771
|
+
const publish = prior.phases.publish;
|
|
21772
|
+
const unresolvedLeg = (rec) => rec.superseded !== true && (rec.state === "pending" || rec.state === "failed");
|
|
21773
|
+
const deployUnresolved = unresolvedLeg(deploy);
|
|
21774
|
+
const publishUnresolved = unresolvedLeg(publish);
|
|
21775
|
+
if (!deployUnresolved && !publishUnresolved) {
|
|
21776
|
+
throw new Error(
|
|
21777
|
+
deploy.superseded === true && publish.superseded === true ? `${label}: the ${prior.tag} deploy and publish legs are already superseded \u2014 nothing to do; the next train runs normally` : `${label}: the ${prior.tag} deploy and publish legs are both already resolved (deploy=${deploy.state}, publish=${publish.state}) \u2014 nothing to close; nothing was written (${ref})`
|
|
21778
|
+
);
|
|
21779
|
+
}
|
|
21780
|
+
if (!deployUnresolved) {
|
|
21781
|
+
throw new Error(`${label}: only the ${prior.tag} publish leg is unresolved (deploy=${deploy.state}) \u2014 use --supersede-publish for a single content-caused leg; nothing was written (${ref})`);
|
|
21782
|
+
}
|
|
21783
|
+
if (!publishUnresolved) {
|
|
21784
|
+
throw new Error(`${label}: only the ${prior.tag} deploy leg is unresolved (publish=${publish.state}) \u2014 use --supersede-deploy for a single content-caused leg; nothing was written (${ref})`);
|
|
21785
|
+
}
|
|
21786
|
+
if (!supersedeablePublishLegShape(publish)) {
|
|
21787
|
+
throw new Error(
|
|
21788
|
+
`${label}: the ${prior.tag} publish leg is ${publish.state}` + (publish.runId != null ? ` with run ${publish.runId} \u2014 a run-backed leg is resolved by watching/reverifying its run, never superseded` : "") + `; the combined closure closes only a FAILED publish leg, or a PENDING one no run correlates (the uncorrelatable historical shape); nothing was written (${ref})`
|
|
21789
|
+
);
|
|
21790
|
+
}
|
|
21791
|
+
const deferredGateway = isJervHubRepo(prior.repo) && deferredGatewayDeployLegShape(deploy);
|
|
21792
|
+
if (!(deploy.state === "failed" && deploy.runId != null || deferredGateway)) {
|
|
21793
|
+
throw new Error(
|
|
21794
|
+
`${label}: the ${prior.tag} deploy leg is ${deploy.state}` + (deploy.runId != null ? ` with run ${deploy.runId} \u2014 a run-backed leg is resolved by watching/reverifying its run, never superseded` : "") + `; the combined closure closes only a content-caused FAILED deploy leg with its run id, or the deferred Jerv-Hub operator-host Gateway leg (#6742); nothing was written (${ref})`
|
|
21795
|
+
);
|
|
21796
|
+
}
|
|
21797
|
+
const phases = { ...prior.phases };
|
|
21798
|
+
if (phases.alignment.state === "pending") {
|
|
21799
|
+
const closed = await alignmentLegClosedLive(deps, prior);
|
|
21800
|
+
if (closed) phases.alignment = closed;
|
|
21801
|
+
}
|
|
21802
|
+
const unresolved = ["promotion", "ordinaryCi", "githubRelease", "alignment"].filter((phase) => phases[phase].state === "pending" || phases[phase].state === "failed");
|
|
21803
|
+
if (unresolved.length > 0) {
|
|
21804
|
+
throw new Error(
|
|
21805
|
+
`${label}: ${prior.tag} has unresolved legs besides deploy and publish (${unresolved.map((p) => `${p}=${phases[p].state}`).join(", ")}) \u2014 the combined closure closes only the two content-caused legs of an otherwise-complete release; resolve those first, nothing was written (${ref})`
|
|
21806
|
+
);
|
|
21807
|
+
}
|
|
21808
|
+
if (await proveOriginPromotionOrArchived(deps, prior, path2, label, ref) === "deleted") {
|
|
21809
|
+
return {
|
|
21810
|
+
command: "release-supersede-deploy-and-publish",
|
|
21811
|
+
repo: prior.repo,
|
|
21812
|
+
tag: prior.tag,
|
|
21813
|
+
tagSha: prior.tagSha,
|
|
21814
|
+
note: `closed the deleted ${prior.tag} deploy and publish failures by archiving its ledger after origin and GitHub both proved absent; start a fresh release train to carry the cause fix`
|
|
21815
|
+
};
|
|
21816
|
+
}
|
|
21817
|
+
let deployEntry;
|
|
21818
|
+
let gatewayDeploy;
|
|
21819
|
+
if (deferredGateway) {
|
|
21820
|
+
const dispatch = await appendJervGatewayReleaseDeploy(
|
|
21821
|
+
deps,
|
|
21822
|
+
prior.repo,
|
|
21823
|
+
prior.tag,
|
|
21824
|
+
prior.tagSha,
|
|
21825
|
+
{ note: `Jerv Gateway operator-host deploy leg for ${prior.tag} run during the deploy-and-publish supersession`, deployStatus: "success" }
|
|
21826
|
+
);
|
|
21827
|
+
deployEntry = {
|
|
21828
|
+
...phaseEntry(deployPhaseInput("registry-publish", dispatch, { releaseSha: prior.tagSha })),
|
|
21829
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21830
|
+
};
|
|
21831
|
+
if (dispatch.deployStatus !== "success") {
|
|
21832
|
+
throw new Error(
|
|
21833
|
+
`${label}: the deferred ${prior.tag} Jerv Gateway operator-host deploy leg did not verify (${dispatch.note}) \u2014 run \`scripts/jerv-gateway-release-deploy.sh --tag ${prior.tag}\` to a healthy receipt naming the released tag and commit, then retry; nothing was written (#6742)`
|
|
21834
|
+
);
|
|
21835
|
+
}
|
|
21836
|
+
gatewayDeploy = { workflow: "jerv-gateway", state: "complete", note: dispatch.note };
|
|
21837
|
+
} else {
|
|
21838
|
+
deployEntry = {
|
|
21839
|
+
state: "skipped",
|
|
21840
|
+
sha: prior.tagSha,
|
|
21841
|
+
superseded: true,
|
|
21842
|
+
note: `superseded: content-caused deploy failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, ${ref})`,
|
|
21843
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21844
|
+
};
|
|
21845
|
+
}
|
|
21846
|
+
phases.deploy = deployEntry;
|
|
21847
|
+
phases.publish = {
|
|
21848
|
+
state: "skipped",
|
|
21849
|
+
sha: prior.tagSha,
|
|
21850
|
+
superseded: true,
|
|
21851
|
+
note: `superseded: content-caused publish failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, ${ref})`,
|
|
21852
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21853
|
+
};
|
|
21854
|
+
atomicWriteJson(path2, { ...prior, phases, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
21855
|
+
return {
|
|
21856
|
+
command: "release-supersede-deploy-and-publish",
|
|
21857
|
+
repo: prior.repo,
|
|
21858
|
+
tag: prior.tag,
|
|
21859
|
+
tagSha: prior.tagSha,
|
|
21860
|
+
note: `closed the ${prior.tag} deploy and publish legs 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`,
|
|
21861
|
+
...gatewayDeploy ? { gatewayDeploy } : {}
|
|
21862
|
+
};
|
|
21863
|
+
});
|
|
21864
|
+
}
|
|
21752
21865
|
async function reverifyPriorRunLegs(deps, path2, targetTag) {
|
|
21753
21866
|
let prior;
|
|
21754
21867
|
try {
|
|
@@ -23835,6 +23948,17 @@ async function runReleaseSupersedePublish(deps, options = {}) {
|
|
|
23835
23948
|
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
23836
23949
|
}
|
|
23837
23950
|
}
|
|
23951
|
+
async function runReleaseSupersedeDeployAndPublish(deps, options = {}) {
|
|
23952
|
+
if (!options.approved) {
|
|
23953
|
+
throw new Error("release --supersede-deploy --supersede-publish requires --apply after explicit master approval; nothing was written");
|
|
23954
|
+
}
|
|
23955
|
+
try {
|
|
23956
|
+
return await supersedeContentCausedDeployAndPublishLegs(deps, process.cwd());
|
|
23957
|
+
} catch (e) {
|
|
23958
|
+
const message2 = e instanceof Error ? e.message : String(e);
|
|
23959
|
+
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
23960
|
+
}
|
|
23961
|
+
}
|
|
23838
23962
|
async function runReleasePublishRetry(deps, runId, options = {}) {
|
|
23839
23963
|
if (!options.approved) {
|
|
23840
23964
|
throw new Error("release --retry-publish requires --apply after explicit approval; nothing was written");
|
|
@@ -29699,6 +29823,7 @@ var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
|
29699
29823
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
29700
29824
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
29701
29825
|
var SECRET_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
29826
|
+
var SECRET_CATALOG_MAP_KEY_RE = /^[A-Za-z][A-Za-z0-9_]*(?:@(?:dev|rc|main))?$/;
|
|
29702
29827
|
var BUILD_SECRET_REF_RE = /^(?:[A-Z_][A-Z0-9_]*=)?(?:[A-Z_][A-Z0-9_]*|(?:_org\/[a-z0-9][a-z0-9-]*):[A-Z_][A-Z0-9_]*|@github-packages-token)$/;
|
|
29703
29828
|
function previewRegistryMetaMerge(existing, patch) {
|
|
29704
29829
|
const out = { ...existing ?? {} };
|
|
@@ -30140,7 +30265,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
30140
30265
|
requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
|
|
30141
30266
|
requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
|
|
30142
30267
|
tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required"}}; use {artifact} in argv when required',
|
|
30143
|
-
secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 merged per entry; clear with --unset secrets; prefer --secrets-file",
|
|
30268
|
+
secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 merged per entry; drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets; prefer --secrets-file",
|
|
30144
30269
|
edgeDomains: "JSON {dev,rc,main} domain map",
|
|
30145
30270
|
statusOptions: "JSON name\u2192id map",
|
|
30146
30271
|
priorityOptions: "JSON {Urgent,High,Medium,Low}\u2192id map",
|
|
@@ -30167,6 +30292,40 @@ function duplicateVarKeyAcrossFlags(vars, sets) {
|
|
|
30167
30292
|
}
|
|
30168
30293
|
return null;
|
|
30169
30294
|
}
|
|
30295
|
+
function secretRemovalRefusal(existing, patch) {
|
|
30296
|
+
const remove2 = Array.isArray(patch.secretsRemove) ? patch.secretsRemove : [];
|
|
30297
|
+
if (remove2.length === 0) return null;
|
|
30298
|
+
const declared = new Set(Object.keys(existing?.secrets ?? {}));
|
|
30299
|
+
const runtime = patch.requiredRuntimeSecrets ?? existing?.requiredRuntimeSecrets ?? {};
|
|
30300
|
+
const build = patch.requiredBuildSecrets ?? existing?.requiredBuildSecrets ?? [];
|
|
30301
|
+
for (const mapKey of remove2) {
|
|
30302
|
+
if (!declared.has(mapKey)) {
|
|
30303
|
+
return `org project set: --unset-secret ${mapKey} is not declared in ${existing?.slug ?? "this project"}'s secrets catalog \u2014 nothing to remove`;
|
|
30304
|
+
}
|
|
30305
|
+
const at = mapKey.indexOf("@");
|
|
30306
|
+
const canonical = at === -1 ? mapKey : mapKey.slice(0, at);
|
|
30307
|
+
for (const [stage, names] of Object.entries(runtime)) {
|
|
30308
|
+
if (Array.isArray(names) && names.includes(canonical)) {
|
|
30309
|
+
return `org project set: ${canonical} is still required by stage ${stage} \u2014 drop it there first (--var requiredRuntimeSecrets=...)`;
|
|
30310
|
+
}
|
|
30311
|
+
}
|
|
30312
|
+
for (const raw of build) {
|
|
30313
|
+
if (typeof raw !== "string") continue;
|
|
30314
|
+
const eq = raw.indexOf("=");
|
|
30315
|
+
const ref = eq === -1 ? raw : raw.slice(eq + 1);
|
|
30316
|
+
if (ref === canonical) {
|
|
30317
|
+
return `org project set: ${canonical} is still required by the build secret contract \u2014 drop it there first (--var requiredBuildSecrets=...)`;
|
|
30318
|
+
}
|
|
30319
|
+
}
|
|
30320
|
+
}
|
|
30321
|
+
return null;
|
|
30322
|
+
}
|
|
30323
|
+
function clearSecretsConfirmationError(unsets, existing, confirmed) {
|
|
30324
|
+
if (!unsets.includes("secrets")) return null;
|
|
30325
|
+
const count = Object.keys(existing?.secrets ?? {}).length;
|
|
30326
|
+
if (confirmed) return null;
|
|
30327
|
+
return `org project set: --unset secrets clears the WHOLE catalog for ${existing?.slug ?? "this project"} (${count} declared entr${count === 1 ? "y" : "ies"} would be removed). To drop ONE entry use --unset-secret <KEY>; re-run with --confirm-clear-secrets to wipe all ${count}.`;
|
|
30328
|
+
}
|
|
30170
30329
|
function buildProjectSetPatch(input) {
|
|
30171
30330
|
const patch = {};
|
|
30172
30331
|
if (input.class) {
|
|
@@ -30266,12 +30425,26 @@ function buildProjectSetPatch(input) {
|
|
|
30266
30425
|
}
|
|
30267
30426
|
patch[key] = null;
|
|
30268
30427
|
}
|
|
30428
|
+
if (input.unsetSecrets?.length) {
|
|
30429
|
+
if (patch.secrets === null) {
|
|
30430
|
+
throw new Error("org project set: --unset secrets clears the WHOLE catalog and cannot be combined with --unset-secret \u2014 pass one or the other");
|
|
30431
|
+
}
|
|
30432
|
+
const keys = [];
|
|
30433
|
+
for (const raw of input.unsetSecrets) {
|
|
30434
|
+
const key = raw.trim();
|
|
30435
|
+
if (!SECRET_CATALOG_MAP_KEY_RE.test(key)) {
|
|
30436
|
+
throw new Error(`org project set: --unset-secret must be a catalog KEY (UPPER_SNAKE, optionally KEY@dev|rc|main) \u2014 got ${JSON.stringify(raw)}`);
|
|
30437
|
+
}
|
|
30438
|
+
if (!keys.includes(key)) keys.push(key);
|
|
30439
|
+
}
|
|
30440
|
+
patch.secretsRemove = keys;
|
|
30441
|
+
}
|
|
30269
30442
|
if (input.clearWebProfile) {
|
|
30270
30443
|
patch.oauth = null;
|
|
30271
30444
|
patch.edgeDomains = null;
|
|
30272
30445
|
}
|
|
30273
30446
|
if (Object.keys(patch).length === 0) {
|
|
30274
|
-
throw new Error("org project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, and/or --clear-web-profile");
|
|
30447
|
+
throw new Error("org project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, --unset-secret KEY, and/or --clear-web-profile");
|
|
30275
30448
|
}
|
|
30276
30449
|
return patch;
|
|
30277
30450
|
}
|
|
@@ -44641,6 +44814,10 @@ function renderReleaseSupersedePublish(r) {
|
|
|
44641
44814
|
const base = `mmi-cli devops release --supersede-publish --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
|
|
44642
44815
|
return r.gatewayDeploy ? `${base}; the deferred Jerv Gateway operator-host deploy leg was run and verified during the supersession (jerv-gateway=complete): ${r.gatewayDeploy.note}` : base;
|
|
44643
44816
|
}
|
|
44817
|
+
function renderReleaseSupersedeDeployAndPublish(r) {
|
|
44818
|
+
const base = `mmi-cli devops release --supersede-deploy --supersede-publish --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
|
|
44819
|
+
return r.gatewayDeploy ? `${base}; the deferred Jerv Gateway operator-host deploy leg was run and verified during the closure (jerv-gateway=complete): ${r.gatewayDeploy.note}` : base;
|
|
44820
|
+
}
|
|
44644
44821
|
function renderReleasePublishRetry(r) {
|
|
44645
44822
|
return `mmi-cli devops release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}; ${r.runUrl}`;
|
|
44646
44823
|
}
|
|
@@ -44870,8 +45047,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44870
45047
|
{ flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
|
|
44871
45048
|
{ flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" },
|
|
44872
45049
|
{ flags: "--abort", description: "with --apply, delete only a proven unpublished failed release candidate on any registered deploy model and restore local main for a same-version recut (#3944/#5650/#5657)" },
|
|
44873
|
-
{ flags: "--supersede-deploy", description: "with --apply, master-approved closure of a content-caused failed deploy leg on a PUBLISHED release whose immutable tag can never deploy green \u2014 the next patch train then carries the fix (#6553)" },
|
|
44874
|
-
{ flags: "--supersede-publish", description: "with --apply, master-approved closure of a content-caused failed publish leg on a fully PROMOTED release whose immutable tag can never publish green \u2014 a deferred Jerv-Hub operator-host Gateway deploy leg is run and verified as part of the closure \u2014 the next patch train then carries the fix (#6638/#6742)" },
|
|
45050
|
+
{ flags: "--supersede-deploy", description: "with --apply, master-approved closure of a content-caused failed deploy leg on a PUBLISHED release whose immutable tag can never deploy green \u2014 the next patch train then carries the fix (#6553); combined with --supersede-publish it closes BOTH content-caused legs in one approved closure (#6752)" },
|
|
45051
|
+
{ flags: "--supersede-publish", description: "with --apply, master-approved closure of a content-caused failed publish leg on a fully PROMOTED release whose immutable tag can never publish green \u2014 a deferred Jerv-Hub operator-host Gateway deploy leg is run and verified as part of the closure \u2014 the next patch train then carries the fix (#6638/#6742); combined with --supersede-deploy it closes BOTH content-caused legs in one approved closure (#6752)" },
|
|
44875
45052
|
{ flags: "--retry-publish <run-id>", description: "with --apply, retry one proven failed or cancelled Hub publish release run (#3949/#5797)" }
|
|
44876
45053
|
];
|
|
44877
45054
|
for (const f of RELEASE_ONLY_FLAGS) {
|
|
@@ -44911,6 +45088,25 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44911
45088
|
if (o.out && !o.apply && !o.resume) {
|
|
44912
45089
|
return fail(`${commandName}: --out writes a terminal result receipt (--apply, --resume, --abort, --retry-publish) \u2014 the dry-run plan keeps its stdout/JSON output. Drop --out or rerun with --apply`);
|
|
44913
45090
|
}
|
|
45091
|
+
if (o.supersedeDeploy && o.supersedePublish) {
|
|
45092
|
+
if (o.resume) return fail("release: --supersede-deploy --supersede-publish and --resume are mutually exclusive \u2014 the closure settles the ledger so a NEW train can run, resume re-derives the very legs it closes");
|
|
45093
|
+
if (o.abort) return fail("release: --supersede-deploy --supersede-publish and --abort are mutually exclusive \u2014 the closure is for a PUBLISHED release, abort only ever deletes a proven unpublished candidate");
|
|
45094
|
+
if (o.retryPublish) return fail("release: --supersede-deploy --supersede-publish cannot be combined with --retry-publish \u2014 a content-caused failure on the immutable tag cannot be retried green");
|
|
45095
|
+
if (!o.apply) return fail("release: --supersede-deploy --supersede-publish requires --apply after explicit master approval; nothing was written");
|
|
45096
|
+
if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
|
|
45097
|
+
return fail("release: --supersede-deploy --supersede-publish accepts only --apply, --repo, --json and --out; it closes ledger legs and dispatches nothing");
|
|
45098
|
+
}
|
|
45099
|
+
if (o.repo) {
|
|
45100
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), "mmi-cli devops release --supersede-deploy --supersede-publish --apply");
|
|
45101
|
+
if (!guard.ok) return fail(`release: ${guard.message}`);
|
|
45102
|
+
}
|
|
45103
|
+
try {
|
|
45104
|
+
const result = await runReleaseSupersedeDeployAndPublish(trainApplyDeps(), { approved: true });
|
|
45105
|
+
return emitTrainResult("release --supersede-deploy --supersede-publish", o.json ? JSON.stringify(result, null, 2) : renderReleaseSupersedeDeployAndPublish(result), o.out);
|
|
45106
|
+
} catch (e) {
|
|
45107
|
+
return failGraceful(`release --supersede-deploy --supersede-publish: ${e.message}`);
|
|
45108
|
+
}
|
|
45109
|
+
}
|
|
44914
45110
|
if (o.retryPublish) {
|
|
44915
45111
|
if (o.resume || o.abort) return fail("release: --retry-publish cannot be combined with --resume or --abort");
|
|
44916
45112
|
if (!o.apply) return fail("release: --retry-publish requires --apply after explicit approval; nothing was written");
|
|
@@ -44952,9 +45148,6 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44952
45148
|
return failGraceful(`release --abort: ${e.message}`);
|
|
44953
45149
|
}
|
|
44954
45150
|
}
|
|
44955
|
-
if (o.supersedeDeploy && o.supersedePublish) {
|
|
44956
|
-
return fail("release: --supersede-deploy and --supersede-publish are mutually exclusive \u2014 close the one leg that failed content-caused; the other leg has its own resolution paths");
|
|
44957
|
-
}
|
|
44958
45151
|
if (o.supersedeDeploy) {
|
|
44959
45152
|
if (o.resume) return fail("release: --supersede-deploy and --resume are mutually exclusive \u2014 the closure settles the ledger so a NEW train can run, resume re-derives the very leg it closes");
|
|
44960
45153
|
if (o.abort) return fail("release: --supersede-deploy and --abort are mutually exclusive \u2014 the closure is for a PUBLISHED release, abort only ever deletes a proven unpublished candidate");
|
|
@@ -46052,7 +46245,7 @@ projectDeploy.command("doctor").description("read-only estate scan for duplicate
|
|
|
46052
46245
|
if (report.error && report.status !== 403) return failGraceful(`org project deploy doctor: ${report.error}`);
|
|
46053
46246
|
if (!report.ok && !report.error) process.exitCode = 1;
|
|
46054
46247
|
});
|
|
46055
|
-
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear
|
|
46248
|
+
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|?], "provider":"<optional>"}').option("--unset-secret <KEY...>", "remove exactly these #2244 catalog entries (repeatable; KEY or KEY@dev|rc|main). Refuses while a stage still requires the key \u2014 drop it from requiredRuntimeSecrets/requiredBuildSecrets first").option("--confirm-clear-secrets", "required to let --unset secrets wipe the WHOLE catalog (the refusal names how many entries that removes)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets (the WHOLE catalog \u2014 needs --confirm-clear-secrets), edgeDomains, requiredGcpApis, publishRequired, releaseChannel, releaseLanguage").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
46056
46249
|
const cfg = await loadConfig();
|
|
46057
46250
|
let target;
|
|
46058
46251
|
try {
|
|
@@ -46082,15 +46275,26 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
46082
46275
|
releaseTrack: o.releaseTrack,
|
|
46083
46276
|
vars,
|
|
46084
46277
|
unsets: o.unset ?? [],
|
|
46278
|
+
unsetSecrets: o.unsetSecret ?? [],
|
|
46085
46279
|
clearWebProfile: Boolean(o.clearWebProfile)
|
|
46086
46280
|
});
|
|
46087
46281
|
} catch (e) {
|
|
46088
46282
|
return fail(e.message.replace(/^org project set: /, "org project set: "));
|
|
46089
46283
|
}
|
|
46090
46284
|
const existing = await fetchProjectBySlug(slug, registryClientDeps(cfg));
|
|
46285
|
+
const clearRefusal = clearSecretsConfirmationError(o.unset ?? [], existing, Boolean(o.confirmClearSecrets));
|
|
46286
|
+
if (clearRefusal) return fail(clearRefusal);
|
|
46287
|
+
const removalRefusal = secretRemovalRefusal(existing, patch);
|
|
46288
|
+
if (removalRefusal) return fail(removalRefusal);
|
|
46091
46289
|
const boardError = boardLinkWriteError(patch, existing);
|
|
46092
46290
|
if (boardError) return fail(`org project set: ${boardError}`);
|
|
46093
46291
|
const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
|
|
46292
|
+
const removedKeys = Array.isArray(patch.secretsRemove) ? patch.secretsRemove : [];
|
|
46293
|
+
if (removedKeys.length && res.ok) {
|
|
46294
|
+
const body = res.body;
|
|
46295
|
+
const remaining = Object.keys(body?.project?.secrets ?? {}).sort();
|
|
46296
|
+
printLine(`org project set: removed ${removedKeys.join(", ")} \u2014 catalog now declares ${remaining.length}: ${remaining.join(", ") || "(none)"}`);
|
|
46297
|
+
}
|
|
46094
46298
|
return reportWrite("org project set", res);
|
|
46095
46299
|
});
|
|
46096
46300
|
project.command("retire [owner/repo]").description("retire an orphaned registry slug (master-only): soft-delete the PROJECT# META row + drop the repo's board items; secrets/vault left alone. DRY-RUN by default \u2014 pass --apply to actually delete; defaults to the current repo").option("--apply", "actually delete (default is a dry-run preview of what would be removed)").option("--skip-board", "retire the registry META only \u2014 leave board items in place").option("--json", "machine-readable {slug, removedMeta, removedBoardItem} output").action(async (repoOrSlug, o) => {
|
package/package.json
CHANGED