@eventcatalog/cli 0.5.10 → 0.6.0

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 CHANGED
@@ -1255,6 +1255,7 @@ var import_node_crypto2 = require("crypto");
1255
1255
  var import_js_yaml = __toESM(require("js-yaml"));
1256
1256
  var import_semver = require("semver");
1257
1257
  var import_sdk6 = __toESM(require("@eventcatalog/sdk"));
1258
+ var import_breaking_changes = require("@eventcatalog/breaking-changes");
1258
1259
  var loadGovernanceConfig = (catalogDir) => {
1259
1260
  const yamlPath = import_node_path4.default.join(catalogDir, "governance.yaml");
1260
1261
  const ymlPath = import_node_path4.default.join(catalogDir, "governance.yml");
@@ -1272,7 +1273,18 @@ var loadGovernanceConfig = (catalogDir) => {
1272
1273
  }
1273
1274
  }
1274
1275
  }
1275
- return { rules };
1276
+ if (parsed?.compatibility?.strategy) {
1277
+ const validStrategies = /* @__PURE__ */ new Set(["BACKWARD", "FORWARD", "FULL", "NONE"]);
1278
+ if (!validStrategies.has(parsed.compatibility.strategy)) {
1279
+ throw new Error(
1280
+ `Invalid compatibility strategy "${parsed.compatibility.strategy}". Must be one of: BACKWARD, FORWARD, FULL, NONE.`
1281
+ );
1282
+ }
1283
+ }
1284
+ return {
1285
+ ...parsed?.compatibility && { compatibility: parsed.compatibility },
1286
+ rules
1287
+ };
1276
1288
  };
1277
1289
  var TRIGGER_FILTERS = {
1278
1290
  consumer_added: (c2) => c2.direction === "receives" && c2.changeType === "added",
@@ -1466,6 +1478,32 @@ var evaluateSchemaChangeRules = (diff, config, targetSnapshot) => {
1466
1478
  }
1467
1479
  return results;
1468
1480
  };
1481
+ var evaluateBreakingSchemaChangeRules = (diff, config, targetSnapshot) => {
1482
+ const breakingRules = config.rules.filter((rule) => rule.when.includes("schema_breaking_change"));
1483
+ if (breakingRules.length === 0) return [];
1484
+ const strategy = config.compatibility?.strategy;
1485
+ if (!strategy || strategy === "NONE") return [];
1486
+ const schemaChangedResources = diff.resources.filter((rc) => {
1487
+ if (!MESSAGE_RESOURCE_TYPES.has(rc.type)) return false;
1488
+ return rc.changedFields?.includes("schemaHash");
1489
+ });
1490
+ if (schemaChangedResources.length === 0) return [];
1491
+ const latestMessageVersions = buildLatestMessageVersionMap(targetSnapshot);
1492
+ const breakingSchemaChanges = schemaChangedResources.map((resourceChange) => ({
1493
+ resourceChange,
1494
+ producerServices: getServicesForSchemaChange(targetSnapshot, "sends", resourceChange, latestMessageVersions),
1495
+ consumerServices: getServicesForSchemaChange(targetSnapshot, "receives", resourceChange, latestMessageVersions),
1496
+ breakingChanges: []
1497
+ }));
1498
+ const results = [];
1499
+ for (const rule of breakingRules) {
1500
+ const matched = breakingSchemaChanges.filter((sc) => matchesSchemaChangeResource(sc, rule.resources));
1501
+ if (matched.length > 0) {
1502
+ results.push({ rule, trigger: "schema_breaking_change", matchedChanges: [], breakingSchemaChanges: matched });
1503
+ }
1504
+ }
1505
+ return results;
1506
+ };
1469
1507
  var evaluateGovernanceRules = (diff, config, targetSnapshot, baseSnapshot) => {
1470
1508
  const results = [];
1471
1509
  const targetMessageSets = targetSnapshot ? buildServiceMessageSets(targetSnapshot) : void 0;
@@ -1486,6 +1524,7 @@ var evaluateGovernanceRules = (diff, config, targetSnapshot, baseSnapshot) => {
1486
1524
  if (targetSnapshot && targetMessageSets) {
1487
1525
  results.push(...evaluateDeprecationRules(diff, config, targetSnapshot, targetMessageSets, baseSnapshot));
1488
1526
  results.push(...evaluateSchemaChangeRules(diff, config, targetSnapshot));
1527
+ results.push(...evaluateBreakingSchemaChangeRules(diff, config, targetSnapshot));
1489
1528
  }
1490
1529
  return results;
1491
1530
  };
@@ -1518,7 +1557,7 @@ var readSchemaDetails = async (sdk, resourceId, version2, type) => {
1518
1557
  return {};
1519
1558
  }
1520
1559
  };
1521
- var enrichSchemaContent = async (results, baseCatalogDir, targetCatalogDir) => {
1560
+ var enrichSchemaContent = async (results, baseCatalogDir, targetCatalogDir, compatibilityStrategy) => {
1522
1561
  const baseSDK = (0, import_sdk6.default)(baseCatalogDir);
1523
1562
  const targetSDK = (0, import_sdk6.default)(targetCatalogDir);
1524
1563
  const promises = [];
@@ -1544,6 +1583,40 @@ var enrichSchemaContent = async (results, baseCatalogDir, targetCatalogDir) => {
1544
1583
  );
1545
1584
  }
1546
1585
  }
1586
+ for (const result of results) {
1587
+ if (!result.breakingSchemaChanges || !compatibilityStrategy) continue;
1588
+ for (const bsc of result.breakingSchemaChanges) {
1589
+ const { resourceId, version: version2, type, changeType, previousVersion, newVersion } = bsc.resourceChange;
1590
+ const baseVersion = changeType === "versioned" ? previousVersion || version2 : version2;
1591
+ const targetVersion = changeType === "versioned" ? newVersion || version2 : version2;
1592
+ promises.push(
1593
+ (async () => {
1594
+ const [before, after] = await Promise.all([
1595
+ readSchemaDetails(baseSDK, resourceId, baseVersion, type),
1596
+ readSchemaDetails(targetSDK, resourceId, targetVersion, type)
1597
+ ]);
1598
+ bsc.before = before.content;
1599
+ bsc.after = after.content;
1600
+ bsc.beforeSchemaPath = before.schemaPath;
1601
+ bsc.afterSchemaPath = after.schemaPath;
1602
+ bsc.beforeSchemaHash = before.schemaHash;
1603
+ bsc.afterSchemaHash = after.schemaHash;
1604
+ if (before.content && after.content) {
1605
+ let beforeSchema;
1606
+ let afterSchema;
1607
+ try {
1608
+ beforeSchema = JSON.parse(before.content);
1609
+ afterSchema = JSON.parse(after.content);
1610
+ } catch {
1611
+ }
1612
+ if (beforeSchema && afterSchema) {
1613
+ bsc.breakingChanges = (0, import_breaking_changes.detectBreakingChanges)(beforeSchema, afterSchema, compatibilityStrategy);
1614
+ }
1615
+ }
1616
+ })()
1617
+ );
1618
+ }
1619
+ }
1547
1620
  await Promise.all(promises);
1548
1621
  };
1549
1622
 
@@ -1572,7 +1645,7 @@ var buildServiceOwnersMap = (snapshot2) => {
1572
1645
  return map;
1573
1646
  };
1574
1647
  var executeGovernanceActions = async (results, opts = {}) => {
1575
- const { messageTypes, status, serviceOwners, baseRef, targetRef } = opts;
1648
+ const { messageTypes, status, serviceOwners, baseRef, targetRef, compatibilityStrategy } = opts;
1576
1649
  const webhookCalls = [];
1577
1650
  const now = (/* @__PURE__ */ new Date()).toISOString();
1578
1651
  for (const result of results) {
@@ -1625,6 +1698,48 @@ var executeGovernanceActions = async (results, opts = {}) => {
1625
1698
  }
1626
1699
  continue;
1627
1700
  }
1701
+ if (result.breakingSchemaChanges && result.breakingSchemaChanges.length > 0) {
1702
+ for (const bsc of result.breakingSchemaChanges) {
1703
+ const messageType = messageTypes?.get(bsc.resourceChange.resourceId) || "message";
1704
+ const payload = {
1705
+ specversion: "1.0",
1706
+ type: "eventcatalog.governance.schema_breaking_change",
1707
+ source: "eventcatalog/governance",
1708
+ id: (0, import_node_crypto3.randomUUID)(),
1709
+ time: now,
1710
+ datacontenttype: "application/json",
1711
+ data: {
1712
+ schemaVersion: 1,
1713
+ ...status && { status },
1714
+ ...compatibilityStrategy && { compatibilityStrategy },
1715
+ summary: `Breaking schema change detected for ${messageType} ${bsc.resourceChange.resourceId}`,
1716
+ message: {
1717
+ id: bsc.resourceChange.resourceId,
1718
+ version: bsc.resourceChange.version,
1719
+ type: messageType
1720
+ },
1721
+ schema: {
1722
+ beforeHash: bsc.beforeSchemaHash ?? null,
1723
+ afterHash: bsc.afterSchemaHash ?? null,
1724
+ beforePath: bsc.beforeSchemaPath ?? null,
1725
+ afterPath: bsc.afterSchemaPath ?? null
1726
+ },
1727
+ breakingChanges: bsc.breakingChanges,
1728
+ refs: {
1729
+ base: baseRef ?? null,
1730
+ target: targetRef ?? null
1731
+ },
1732
+ consumers: bsc.consumerServices,
1733
+ producers: bsc.producerServices
1734
+ }
1735
+ };
1736
+ webhookCalls.push({
1737
+ urlTemplate: action.url,
1738
+ request: fetch(url, { method: "POST", headers, body: JSON.stringify(payload) })
1739
+ });
1740
+ }
1741
+ continue;
1742
+ }
1628
1743
  if (result.deprecationChanges && result.deprecationChanges.length > 0) {
1629
1744
  for (const dc of result.deprecationChanges) {
1630
1745
  const messageType = messageTypes?.get(dc.resourceChange.resourceId) || "message";
@@ -1811,31 +1926,38 @@ var governanceCheck = async (opts) => {
1811
1926
  return { output: "No governance.yaml (or governance.yml) found or no rules defined.", exitCode: 0, failures: [] };
1812
1927
  }
1813
1928
  const results = evaluateGovernanceRules(diff, config, targetResult.snapshot, baseResult.snapshot);
1814
- for (const result of results) {
1929
+ await enrichSchemaContent(results, baseTmpDir, targetCatalogDir, config.compatibility?.strategy);
1930
+ const filteredResults = results.filter((r) => {
1931
+ if (r.trigger !== "schema_breaking_change") return true;
1932
+ if (!r.breakingSchemaChanges) return false;
1933
+ r.breakingSchemaChanges = r.breakingSchemaChanges.filter((bsc) => bsc.breakingChanges.length > 0);
1934
+ return r.breakingSchemaChanges.length > 0;
1935
+ });
1936
+ for (const result of filteredResults) {
1815
1937
  const failActions = result.rule.actions.filter((a) => a.type === "fail");
1816
1938
  if (failActions.length > 0) {
1817
1939
  result.failed = true;
1818
1940
  result.failMessages = failActions.map((a) => "message" in a && a.message ? resolveEnvVars(a.message) : void 0).filter((m) => m !== void 0);
1819
1941
  }
1820
1942
  }
1821
- await enrichSchemaContent(results, baseTmpDir, targetCatalogDir);
1822
1943
  const messageTypes = buildMessageTypeMap(targetResult.snapshot);
1823
1944
  const serviceOwners = buildServiceOwnersMap(targetResult.snapshot);
1824
- const actionOutput = await executeGovernanceActions(results, {
1945
+ const actionOutput = await executeGovernanceActions(filteredResults, {
1825
1946
  messageTypes,
1826
1947
  status: opts.status,
1827
1948
  serviceOwners,
1828
1949
  baseRef: baseBranch,
1829
- targetRef: opts.target || "working-directory"
1950
+ targetRef: opts.target || "working-directory",
1951
+ compatibilityStrategy: config.compatibility?.strategy
1830
1952
  });
1831
- const failures = results.filter((r) => r.failed).map((r) => ({ ruleName: r.rule.name, messages: r.failMessages || [] }));
1953
+ const failures = filteredResults.filter((r) => r.failed).map((r) => ({ ruleName: r.rule.name, messages: r.failMessages || [] }));
1832
1954
  if (opts.format === "json") {
1833
1955
  const jsonOutput = {
1834
1956
  baseBranch,
1835
1957
  target: opts.target || "working directory",
1836
- results,
1958
+ results: filteredResults,
1837
1959
  summary: {
1838
- rulesTriggered: results.length,
1960
+ rulesTriggered: filteredResults.length,
1839
1961
  failures: failures.length,
1840
1962
  passed: failures.length === 0
1841
1963
  },
@@ -1845,14 +1967,14 @@ var governanceCheck = async (opts) => {
1845
1967
  }
1846
1968
  const targetLabel = opts.target || "working directory";
1847
1969
  const lines = [`Governance check: comparing ${targetLabel} against ${baseBranch}`, ""];
1848
- lines.push(formatGovernanceOutput(results));
1970
+ lines.push(formatGovernanceOutput(filteredResults));
1849
1971
  if (actionOutput.length > 0) {
1850
1972
  lines.push("");
1851
1973
  lines.push(...actionOutput);
1852
1974
  }
1853
- if (results.length > 0) {
1975
+ if (filteredResults.length > 0) {
1854
1976
  const webhookCount = actionOutput.filter((l) => l.includes("Webhook sent")).length;
1855
- const parts = [`${results.length} rule${results.length === 1 ? "" : "s"} triggered`];
1977
+ const parts = [`${filteredResults.length} rule${filteredResults.length === 1 ? "" : "s"} triggered`];
1856
1978
  if (webhookCount > 0) parts.push(`${webhookCount} webhook${webhookCount === 1 ? "" : "s"} sent`);
1857
1979
  lines.push("");
1858
1980
  lines.push(parts.join(", ") + ".");