@eventcatalog/cli 0.5.11 → 0.6.1
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/cli/index.js +135 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +135 -13
- package/dist/cli/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/cli/index.mjs
CHANGED
|
@@ -1239,6 +1239,7 @@ import { createHash } from "crypto";
|
|
|
1239
1239
|
import yaml from "js-yaml";
|
|
1240
1240
|
import { satisfies, validRange } from "semver";
|
|
1241
1241
|
import createSDK6 from "@eventcatalog/sdk";
|
|
1242
|
+
import { detectBreakingChanges } from "@eventcatalog/breaking-changes";
|
|
1242
1243
|
var loadGovernanceConfig = (catalogDir) => {
|
|
1243
1244
|
const yamlPath = path2.join(catalogDir, "governance.yaml");
|
|
1244
1245
|
const ymlPath = path2.join(catalogDir, "governance.yml");
|
|
@@ -1256,7 +1257,18 @@ var loadGovernanceConfig = (catalogDir) => {
|
|
|
1256
1257
|
}
|
|
1257
1258
|
}
|
|
1258
1259
|
}
|
|
1259
|
-
|
|
1260
|
+
if (parsed?.compatibility?.strategy) {
|
|
1261
|
+
const validStrategies = /* @__PURE__ */ new Set(["BACKWARD", "FORWARD", "FULL", "NONE"]);
|
|
1262
|
+
if (!validStrategies.has(parsed.compatibility.strategy)) {
|
|
1263
|
+
throw new Error(
|
|
1264
|
+
`Invalid compatibility strategy "${parsed.compatibility.strategy}". Must be one of: BACKWARD, FORWARD, FULL, NONE.`
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return {
|
|
1269
|
+
...parsed?.compatibility && { compatibility: parsed.compatibility },
|
|
1270
|
+
rules
|
|
1271
|
+
};
|
|
1260
1272
|
};
|
|
1261
1273
|
var TRIGGER_FILTERS = {
|
|
1262
1274
|
consumer_added: (c2) => c2.direction === "receives" && c2.changeType === "added",
|
|
@@ -1450,6 +1462,32 @@ var evaluateSchemaChangeRules = (diff, config, targetSnapshot) => {
|
|
|
1450
1462
|
}
|
|
1451
1463
|
return results;
|
|
1452
1464
|
};
|
|
1465
|
+
var evaluateBreakingSchemaChangeRules = (diff, config, targetSnapshot) => {
|
|
1466
|
+
const breakingRules = config.rules.filter((rule) => rule.when.includes("schema_breaking_change"));
|
|
1467
|
+
if (breakingRules.length === 0) return [];
|
|
1468
|
+
const strategy = config.compatibility?.strategy;
|
|
1469
|
+
if (!strategy || strategy === "NONE") return [];
|
|
1470
|
+
const schemaChangedResources = diff.resources.filter((rc) => {
|
|
1471
|
+
if (!MESSAGE_RESOURCE_TYPES.has(rc.type)) return false;
|
|
1472
|
+
return rc.changedFields?.includes("schemaHash");
|
|
1473
|
+
});
|
|
1474
|
+
if (schemaChangedResources.length === 0) return [];
|
|
1475
|
+
const latestMessageVersions = buildLatestMessageVersionMap(targetSnapshot);
|
|
1476
|
+
const breakingSchemaChanges = schemaChangedResources.map((resourceChange) => ({
|
|
1477
|
+
resourceChange,
|
|
1478
|
+
producerServices: getServicesForSchemaChange(targetSnapshot, "sends", resourceChange, latestMessageVersions),
|
|
1479
|
+
consumerServices: getServicesForSchemaChange(targetSnapshot, "receives", resourceChange, latestMessageVersions),
|
|
1480
|
+
breakingChanges: []
|
|
1481
|
+
}));
|
|
1482
|
+
const results = [];
|
|
1483
|
+
for (const rule of breakingRules) {
|
|
1484
|
+
const matched = breakingSchemaChanges.filter((sc) => matchesSchemaChangeResource(sc, rule.resources));
|
|
1485
|
+
if (matched.length > 0) {
|
|
1486
|
+
results.push({ rule, trigger: "schema_breaking_change", matchedChanges: [], breakingSchemaChanges: matched });
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return results;
|
|
1490
|
+
};
|
|
1453
1491
|
var evaluateGovernanceRules = (diff, config, targetSnapshot, baseSnapshot) => {
|
|
1454
1492
|
const results = [];
|
|
1455
1493
|
const targetMessageSets = targetSnapshot ? buildServiceMessageSets(targetSnapshot) : void 0;
|
|
@@ -1470,6 +1508,7 @@ var evaluateGovernanceRules = (diff, config, targetSnapshot, baseSnapshot) => {
|
|
|
1470
1508
|
if (targetSnapshot && targetMessageSets) {
|
|
1471
1509
|
results.push(...evaluateDeprecationRules(diff, config, targetSnapshot, targetMessageSets, baseSnapshot));
|
|
1472
1510
|
results.push(...evaluateSchemaChangeRules(diff, config, targetSnapshot));
|
|
1511
|
+
results.push(...evaluateBreakingSchemaChangeRules(diff, config, targetSnapshot));
|
|
1473
1512
|
}
|
|
1474
1513
|
return results;
|
|
1475
1514
|
};
|
|
@@ -1502,7 +1541,7 @@ var readSchemaDetails = async (sdk, resourceId, version2, type) => {
|
|
|
1502
1541
|
return {};
|
|
1503
1542
|
}
|
|
1504
1543
|
};
|
|
1505
|
-
var enrichSchemaContent = async (results, baseCatalogDir, targetCatalogDir) => {
|
|
1544
|
+
var enrichSchemaContent = async (results, baseCatalogDir, targetCatalogDir, compatibilityStrategy) => {
|
|
1506
1545
|
const baseSDK = createSDK6(baseCatalogDir);
|
|
1507
1546
|
const targetSDK = createSDK6(targetCatalogDir);
|
|
1508
1547
|
const promises = [];
|
|
@@ -1528,6 +1567,40 @@ var enrichSchemaContent = async (results, baseCatalogDir, targetCatalogDir) => {
|
|
|
1528
1567
|
);
|
|
1529
1568
|
}
|
|
1530
1569
|
}
|
|
1570
|
+
for (const result of results) {
|
|
1571
|
+
if (!result.breakingSchemaChanges || !compatibilityStrategy) continue;
|
|
1572
|
+
for (const bsc of result.breakingSchemaChanges) {
|
|
1573
|
+
const { resourceId, version: version2, type, changeType, previousVersion, newVersion } = bsc.resourceChange;
|
|
1574
|
+
const baseVersion = changeType === "versioned" ? previousVersion || version2 : version2;
|
|
1575
|
+
const targetVersion = changeType === "versioned" ? newVersion || version2 : version2;
|
|
1576
|
+
promises.push(
|
|
1577
|
+
(async () => {
|
|
1578
|
+
const [before, after] = await Promise.all([
|
|
1579
|
+
readSchemaDetails(baseSDK, resourceId, baseVersion, type),
|
|
1580
|
+
readSchemaDetails(targetSDK, resourceId, targetVersion, type)
|
|
1581
|
+
]);
|
|
1582
|
+
bsc.before = before.content;
|
|
1583
|
+
bsc.after = after.content;
|
|
1584
|
+
bsc.beforeSchemaPath = before.schemaPath;
|
|
1585
|
+
bsc.afterSchemaPath = after.schemaPath;
|
|
1586
|
+
bsc.beforeSchemaHash = before.schemaHash;
|
|
1587
|
+
bsc.afterSchemaHash = after.schemaHash;
|
|
1588
|
+
if (before.content && after.content) {
|
|
1589
|
+
let beforeSchema;
|
|
1590
|
+
let afterSchema;
|
|
1591
|
+
try {
|
|
1592
|
+
beforeSchema = JSON.parse(before.content);
|
|
1593
|
+
afterSchema = JSON.parse(after.content);
|
|
1594
|
+
} catch {
|
|
1595
|
+
}
|
|
1596
|
+
if (beforeSchema && afterSchema) {
|
|
1597
|
+
bsc.breakingChanges = detectBreakingChanges(beforeSchema, afterSchema, compatibilityStrategy);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
})()
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1531
1604
|
await Promise.all(promises);
|
|
1532
1605
|
};
|
|
1533
1606
|
|
|
@@ -1556,7 +1629,7 @@ var buildServiceOwnersMap = (snapshot2) => {
|
|
|
1556
1629
|
return map;
|
|
1557
1630
|
};
|
|
1558
1631
|
var executeGovernanceActions = async (results, opts = {}) => {
|
|
1559
|
-
const { messageTypes, status, serviceOwners, baseRef, targetRef } = opts;
|
|
1632
|
+
const { messageTypes, status, serviceOwners, baseRef, targetRef, compatibilityStrategy } = opts;
|
|
1560
1633
|
const webhookCalls = [];
|
|
1561
1634
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1562
1635
|
for (const result of results) {
|
|
@@ -1609,6 +1682,48 @@ var executeGovernanceActions = async (results, opts = {}) => {
|
|
|
1609
1682
|
}
|
|
1610
1683
|
continue;
|
|
1611
1684
|
}
|
|
1685
|
+
if (result.breakingSchemaChanges && result.breakingSchemaChanges.length > 0) {
|
|
1686
|
+
for (const bsc of result.breakingSchemaChanges) {
|
|
1687
|
+
const messageType = messageTypes?.get(bsc.resourceChange.resourceId) || "message";
|
|
1688
|
+
const payload = {
|
|
1689
|
+
specversion: "1.0",
|
|
1690
|
+
type: "eventcatalog.governance.schema_breaking_change",
|
|
1691
|
+
source: "eventcatalog/governance",
|
|
1692
|
+
id: randomUUID2(),
|
|
1693
|
+
time: now,
|
|
1694
|
+
datacontenttype: "application/json",
|
|
1695
|
+
data: {
|
|
1696
|
+
schemaVersion: 1,
|
|
1697
|
+
...status && { status },
|
|
1698
|
+
...compatibilityStrategy && { compatibilityStrategy },
|
|
1699
|
+
summary: `Breaking schema change detected for ${messageType} ${bsc.resourceChange.resourceId}`,
|
|
1700
|
+
message: {
|
|
1701
|
+
id: bsc.resourceChange.resourceId,
|
|
1702
|
+
version: bsc.resourceChange.version,
|
|
1703
|
+
type: messageType
|
|
1704
|
+
},
|
|
1705
|
+
schema: {
|
|
1706
|
+
beforeHash: bsc.beforeSchemaHash ?? null,
|
|
1707
|
+
afterHash: bsc.afterSchemaHash ?? null,
|
|
1708
|
+
beforePath: bsc.beforeSchemaPath ?? null,
|
|
1709
|
+
afterPath: bsc.afterSchemaPath ?? null
|
|
1710
|
+
},
|
|
1711
|
+
breakingChanges: bsc.breakingChanges,
|
|
1712
|
+
refs: {
|
|
1713
|
+
base: baseRef ?? null,
|
|
1714
|
+
target: targetRef ?? null
|
|
1715
|
+
},
|
|
1716
|
+
consumers: bsc.consumerServices,
|
|
1717
|
+
producers: bsc.producerServices
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
webhookCalls.push({
|
|
1721
|
+
urlTemplate: action.url,
|
|
1722
|
+
request: fetch(url, { method: "POST", headers, body: JSON.stringify(payload) })
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1612
1727
|
if (result.deprecationChanges && result.deprecationChanges.length > 0) {
|
|
1613
1728
|
for (const dc of result.deprecationChanges) {
|
|
1614
1729
|
const messageType = messageTypes?.get(dc.resourceChange.resourceId) || "message";
|
|
@@ -1795,31 +1910,38 @@ var governanceCheck = async (opts) => {
|
|
|
1795
1910
|
return { output: "No governance.yaml (or governance.yml) found or no rules defined.", exitCode: 0, failures: [] };
|
|
1796
1911
|
}
|
|
1797
1912
|
const results = evaluateGovernanceRules(diff, config, targetResult.snapshot, baseResult.snapshot);
|
|
1798
|
-
|
|
1913
|
+
await enrichSchemaContent(results, baseTmpDir, targetCatalogDir, config.compatibility?.strategy);
|
|
1914
|
+
const filteredResults = results.filter((r) => {
|
|
1915
|
+
if (r.trigger !== "schema_breaking_change") return true;
|
|
1916
|
+
if (!r.breakingSchemaChanges) return false;
|
|
1917
|
+
r.breakingSchemaChanges = r.breakingSchemaChanges.filter((bsc) => bsc.breakingChanges.length > 0);
|
|
1918
|
+
return r.breakingSchemaChanges.length > 0;
|
|
1919
|
+
});
|
|
1920
|
+
for (const result of filteredResults) {
|
|
1799
1921
|
const failActions = result.rule.actions.filter((a) => a.type === "fail");
|
|
1800
1922
|
if (failActions.length > 0) {
|
|
1801
1923
|
result.failed = true;
|
|
1802
1924
|
result.failMessages = failActions.map((a) => "message" in a && a.message ? resolveEnvVars(a.message) : void 0).filter((m) => m !== void 0);
|
|
1803
1925
|
}
|
|
1804
1926
|
}
|
|
1805
|
-
await enrichSchemaContent(results, baseTmpDir, targetCatalogDir);
|
|
1806
1927
|
const messageTypes = buildMessageTypeMap(targetResult.snapshot);
|
|
1807
1928
|
const serviceOwners = buildServiceOwnersMap(targetResult.snapshot);
|
|
1808
|
-
const actionOutput = await executeGovernanceActions(
|
|
1929
|
+
const actionOutput = await executeGovernanceActions(filteredResults, {
|
|
1809
1930
|
messageTypes,
|
|
1810
1931
|
status: opts.status,
|
|
1811
1932
|
serviceOwners,
|
|
1812
1933
|
baseRef: baseBranch,
|
|
1813
|
-
targetRef: opts.target || "working-directory"
|
|
1934
|
+
targetRef: opts.target || "working-directory",
|
|
1935
|
+
compatibilityStrategy: config.compatibility?.strategy
|
|
1814
1936
|
});
|
|
1815
|
-
const failures =
|
|
1937
|
+
const failures = filteredResults.filter((r) => r.failed).map((r) => ({ ruleName: r.rule.name, messages: r.failMessages || [] }));
|
|
1816
1938
|
if (opts.format === "json") {
|
|
1817
1939
|
const jsonOutput = {
|
|
1818
1940
|
baseBranch,
|
|
1819
1941
|
target: opts.target || "working directory",
|
|
1820
|
-
results,
|
|
1942
|
+
results: filteredResults,
|
|
1821
1943
|
summary: {
|
|
1822
|
-
rulesTriggered:
|
|
1944
|
+
rulesTriggered: filteredResults.length,
|
|
1823
1945
|
failures: failures.length,
|
|
1824
1946
|
passed: failures.length === 0
|
|
1825
1947
|
},
|
|
@@ -1829,14 +1951,14 @@ var governanceCheck = async (opts) => {
|
|
|
1829
1951
|
}
|
|
1830
1952
|
const targetLabel = opts.target || "working directory";
|
|
1831
1953
|
const lines = [`Governance check: comparing ${targetLabel} against ${baseBranch}`, ""];
|
|
1832
|
-
lines.push(formatGovernanceOutput(
|
|
1954
|
+
lines.push(formatGovernanceOutput(filteredResults));
|
|
1833
1955
|
if (actionOutput.length > 0) {
|
|
1834
1956
|
lines.push("");
|
|
1835
1957
|
lines.push(...actionOutput);
|
|
1836
1958
|
}
|
|
1837
|
-
if (
|
|
1959
|
+
if (filteredResults.length > 0) {
|
|
1838
1960
|
const webhookCount = actionOutput.filter((l) => l.includes("Webhook sent")).length;
|
|
1839
|
-
const parts = [`${
|
|
1961
|
+
const parts = [`${filteredResults.length} rule${filteredResults.length === 1 ? "" : "s"} triggered`];
|
|
1840
1962
|
if (webhookCount > 0) parts.push(`${webhookCount} webhook${webhookCount === 1 ? "" : "s"} sent`);
|
|
1841
1963
|
lines.push("");
|
|
1842
1964
|
lines.push(parts.join(", ") + ".");
|