@mutmutco/cli 4.4.0 → 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 +287 -57
- 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
|
{
|
|
@@ -21628,6 +21628,51 @@ async function supersedeContentCausedDeployLeg(deps, cwd) {
|
|
|
21628
21628
|
function supersedeablePublishLegShape(rec) {
|
|
21629
21629
|
return rec.state === "failed" || rec.state === "pending" && rec.runId == null;
|
|
21630
21630
|
}
|
|
21631
|
+
function deferredGatewayDeployLegShape(rec) {
|
|
21632
|
+
return rec.state === "pending" && rec.runId == null && rec.workflow === "jerv-gateway";
|
|
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
|
+
}
|
|
21631
21676
|
async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
21632
21677
|
const path2 = releaseLedgerPath(cwd);
|
|
21633
21678
|
return withFileLock(`${path2}.lock`, { label: "release ledger" }, async () => {
|
|
@@ -21653,51 +21698,15 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
21653
21698
|
const closed = await alignmentLegClosedLive(deps, prior);
|
|
21654
21699
|
if (closed) phases.alignment = closed;
|
|
21655
21700
|
}
|
|
21656
|
-
|
|
21701
|
+
let gatewayDeploy;
|
|
21702
|
+
const deferredGateway = isJervHubRepo(prior.repo) && deferredGatewayDeployLegShape(phases.deploy);
|
|
21703
|
+
const unresolved = ["promotion", "ordinaryCi", "githubRelease", "deploy", "alignment"].filter((phase) => !(phase === "deploy" && deferredGateway)).filter((phase) => phases[phase].state === "pending" || phases[phase].state === "failed");
|
|
21657
21704
|
if (unresolved.length > 0) {
|
|
21658
21705
|
throw new Error(
|
|
21659
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)`
|
|
21660
21707
|
);
|
|
21661
21708
|
}
|
|
21662
|
-
|
|
21663
|
-
try {
|
|
21664
|
-
liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
|
|
21665
|
-
} catch (e) {
|
|
21666
|
-
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)`);
|
|
21667
|
-
}
|
|
21668
|
-
if (liveSha.toLowerCase() !== prior.tagSha.toLowerCase()) {
|
|
21669
|
-
if (liveSha) {
|
|
21670
|
-
throw new Error(
|
|
21671
|
-
`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)`
|
|
21672
|
-
);
|
|
21673
|
-
}
|
|
21674
|
-
let repository;
|
|
21675
|
-
try {
|
|
21676
|
-
repository = JSON.parse(await deps.run("gh", ["repo", "view", prior.repo, "--json", "nameWithOwner"]));
|
|
21677
|
-
} catch (e) {
|
|
21678
|
-
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)`);
|
|
21679
|
-
}
|
|
21680
|
-
if (repository.nameWithOwner !== prior.repo) {
|
|
21681
|
-
throw new Error(`release --supersede-publish: repository access proof named ${String(repository.nameWithOwner || "(missing)")}, not ${prior.repo} \u2014 nothing was written (#6638)`);
|
|
21682
|
-
}
|
|
21683
|
-
let releaseAbsent = false;
|
|
21684
|
-
try {
|
|
21685
|
-
await deps.run("gh", ["release", "view", prior.tag, "--repo", prior.repo, "--json", "tagName"]);
|
|
21686
|
-
} catch (e) {
|
|
21687
|
-
const detail = `${e instanceof Error ? e.message : String(e)} ${String(e.stderr ?? "")}`;
|
|
21688
|
-
releaseAbsent = /release not found|HTTP 404|\(404\)/i.test(detail);
|
|
21689
|
-
if (!releaseAbsent) {
|
|
21690
|
-
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)`);
|
|
21691
|
-
}
|
|
21692
|
-
}
|
|
21693
|
-
if (!releaseAbsent) {
|
|
21694
|
-
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)`);
|
|
21695
|
-
}
|
|
21696
|
-
const archivePath = (0, import_node_path21.join)((0, import_node_path21.dirname)(path2), `phases.archive-${prior.tag}.json`);
|
|
21697
|
-
if ((0, import_node_fs21.existsSync)(archivePath)) {
|
|
21698
|
-
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)`);
|
|
21699
|
-
}
|
|
21700
|
-
(0, import_node_fs21.renameSync)(path2, archivePath);
|
|
21709
|
+
if (await proveOriginPromotionOrArchived(deps, prior, path2, "release --supersede-publish", "#6638") === "deleted") {
|
|
21701
21710
|
return {
|
|
21702
21711
|
command: "release-supersede-publish",
|
|
21703
21712
|
repo: prior.repo,
|
|
@@ -21706,6 +21715,25 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
21706
21715
|
note: `closed the deleted ${prior.tag} publish failure by archiving its ledger after origin and GitHub both proved absent; start a fresh release train to carry the cause fix`
|
|
21707
21716
|
};
|
|
21708
21717
|
}
|
|
21718
|
+
if (deferredGateway) {
|
|
21719
|
+
const dispatch = await appendJervGatewayReleaseDeploy(
|
|
21720
|
+
deps,
|
|
21721
|
+
prior.repo,
|
|
21722
|
+
prior.tag,
|
|
21723
|
+
prior.tagSha,
|
|
21724
|
+
{ note: `Jerv Gateway operator-host deploy leg for ${prior.tag} run during the publish supersession`, deployStatus: "success" }
|
|
21725
|
+
);
|
|
21726
|
+
phases.deploy = {
|
|
21727
|
+
...phaseEntry(deployPhaseInput("registry-publish", dispatch, { releaseSha: prior.tagSha })),
|
|
21728
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21729
|
+
};
|
|
21730
|
+
if (dispatch.deployStatus !== "success") {
|
|
21731
|
+
throw new Error(
|
|
21732
|
+
`release --supersede-publish: 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)`
|
|
21733
|
+
);
|
|
21734
|
+
}
|
|
21735
|
+
gatewayDeploy = { workflow: "jerv-gateway", state: "complete", note: dispatch.note };
|
|
21736
|
+
}
|
|
21709
21737
|
const note = `superseded: content-caused publish failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, #6638)`;
|
|
21710
21738
|
atomicWriteJson(path2, {
|
|
21711
21739
|
...prior,
|
|
@@ -21720,7 +21748,117 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
|
21720
21748
|
repo: prior.repo,
|
|
21721
21749
|
tag: prior.tag,
|
|
21722
21750
|
tagSha: prior.tagSha,
|
|
21723
|
-
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
|
|
21751
|
+
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`,
|
|
21752
|
+
...gatewayDeploy ? { gatewayDeploy } : {}
|
|
21753
|
+
};
|
|
21754
|
+
});
|
|
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 } : {}
|
|
21724
21862
|
};
|
|
21725
21863
|
});
|
|
21726
21864
|
}
|
|
@@ -23810,6 +23948,17 @@ async function runReleaseSupersedePublish(deps, options = {}) {
|
|
|
23810
23948
|
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
23811
23949
|
}
|
|
23812
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
|
+
}
|
|
23813
23962
|
async function runReleasePublishRetry(deps, runId, options = {}) {
|
|
23814
23963
|
if (!options.approved) {
|
|
23815
23964
|
throw new Error("release --retry-publish requires --apply after explicit approval; nothing was written");
|
|
@@ -29674,6 +29823,7 @@ var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
|
29674
29823
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
29675
29824
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
29676
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))?$/;
|
|
29677
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)$/;
|
|
29678
29828
|
function previewRegistryMetaMerge(existing, patch) {
|
|
29679
29829
|
const out = { ...existing ?? {} };
|
|
@@ -30115,7 +30265,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
30115
30265
|
requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
|
|
30116
30266
|
requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
|
|
30117
30267
|
tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required"}}; use {artifact} in argv when required',
|
|
30118
|
-
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",
|
|
30119
30269
|
edgeDomains: "JSON {dev,rc,main} domain map",
|
|
30120
30270
|
statusOptions: "JSON name\u2192id map",
|
|
30121
30271
|
priorityOptions: "JSON {Urgent,High,Medium,Low}\u2192id map",
|
|
@@ -30142,6 +30292,40 @@ function duplicateVarKeyAcrossFlags(vars, sets) {
|
|
|
30142
30292
|
}
|
|
30143
30293
|
return null;
|
|
30144
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
|
+
}
|
|
30145
30329
|
function buildProjectSetPatch(input) {
|
|
30146
30330
|
const patch = {};
|
|
30147
30331
|
if (input.class) {
|
|
@@ -30241,12 +30425,26 @@ function buildProjectSetPatch(input) {
|
|
|
30241
30425
|
}
|
|
30242
30426
|
patch[key] = null;
|
|
30243
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
|
+
}
|
|
30244
30442
|
if (input.clearWebProfile) {
|
|
30245
30443
|
patch.oauth = null;
|
|
30246
30444
|
patch.edgeDomains = null;
|
|
30247
30445
|
}
|
|
30248
30446
|
if (Object.keys(patch).length === 0) {
|
|
30249
|
-
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");
|
|
30250
30448
|
}
|
|
30251
30449
|
return patch;
|
|
30252
30450
|
}
|
|
@@ -44613,7 +44811,12 @@ function renderReleaseSupersedeDeploy(r) {
|
|
|
44613
44811
|
return `mmi-cli devops release --supersede-deploy --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
|
|
44614
44812
|
}
|
|
44615
44813
|
function renderReleaseSupersedePublish(r) {
|
|
44616
|
-
|
|
44814
|
+
const base = `mmi-cli devops release --supersede-publish --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
|
|
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;
|
|
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;
|
|
44617
44820
|
}
|
|
44618
44821
|
function renderReleasePublishRetry(r) {
|
|
44619
44822
|
return `mmi-cli devops release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}; ${r.runUrl}`;
|
|
@@ -44844,8 +45047,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44844
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)" },
|
|
44845
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)" },
|
|
44846
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)" },
|
|
44847
|
-
{ 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)" },
|
|
44848
|
-
{ 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 the next patch train then carries the fix (#6638)" },
|
|
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)" },
|
|
44849
45052
|
{ flags: "--retry-publish <run-id>", description: "with --apply, retry one proven failed or cancelled Hub publish release run (#3949/#5797)" }
|
|
44850
45053
|
];
|
|
44851
45054
|
for (const f of RELEASE_ONLY_FLAGS) {
|
|
@@ -44885,6 +45088,25 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44885
45088
|
if (o.out && !o.apply && !o.resume) {
|
|
44886
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`);
|
|
44887
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
|
+
}
|
|
44888
45110
|
if (o.retryPublish) {
|
|
44889
45111
|
if (o.resume || o.abort) return fail("release: --retry-publish cannot be combined with --resume or --abort");
|
|
44890
45112
|
if (!o.apply) return fail("release: --retry-publish requires --apply after explicit approval; nothing was written");
|
|
@@ -44926,9 +45148,6 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44926
45148
|
return failGraceful(`release --abort: ${e.message}`);
|
|
44927
45149
|
}
|
|
44928
45150
|
}
|
|
44929
|
-
if (o.supersedeDeploy && o.supersedePublish) {
|
|
44930
|
-
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");
|
|
44931
|
-
}
|
|
44932
45151
|
if (o.supersedeDeploy) {
|
|
44933
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");
|
|
44934
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");
|
|
@@ -46026,7 +46245,7 @@ projectDeploy.command("doctor").description("read-only estate scan for duplicate
|
|
|
46026
46245
|
if (report.error && report.status !== 403) return failGraceful(`org project deploy doctor: ${report.error}`);
|
|
46027
46246
|
if (!report.ok && !report.error) process.exitCode = 1;
|
|
46028
46247
|
});
|
|
46029
|
-
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) => {
|
|
46030
46249
|
const cfg = await loadConfig();
|
|
46031
46250
|
let target;
|
|
46032
46251
|
try {
|
|
@@ -46056,15 +46275,26 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
46056
46275
|
releaseTrack: o.releaseTrack,
|
|
46057
46276
|
vars,
|
|
46058
46277
|
unsets: o.unset ?? [],
|
|
46278
|
+
unsetSecrets: o.unsetSecret ?? [],
|
|
46059
46279
|
clearWebProfile: Boolean(o.clearWebProfile)
|
|
46060
46280
|
});
|
|
46061
46281
|
} catch (e) {
|
|
46062
46282
|
return fail(e.message.replace(/^org project set: /, "org project set: "));
|
|
46063
46283
|
}
|
|
46064
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);
|
|
46065
46289
|
const boardError = boardLinkWriteError(patch, existing);
|
|
46066
46290
|
if (boardError) return fail(`org project set: ${boardError}`);
|
|
46067
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
|
+
}
|
|
46068
46298
|
return reportWrite("org project set", res);
|
|
46069
46299
|
});
|
|
46070
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