@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.
@@ -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
- return { rules };
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
- for (const result of results) {
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(results, {
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 = results.filter((r) => r.failed).map((r) => ({ ruleName: r.rule.name, messages: r.failMessages || [] }));
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: results.length,
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(results));
1954
+ lines.push(formatGovernanceOutput(filteredResults));
1833
1955
  if (actionOutput.length > 0) {
1834
1956
  lines.push("");
1835
1957
  lines.push(...actionOutput);
1836
1958
  }
1837
- if (results.length > 0) {
1959
+ if (filteredResults.length > 0) {
1838
1960
  const webhookCount = actionOutput.filter((l) => l.includes("Webhook sent")).length;
1839
- const parts = [`${results.length} rule${results.length === 1 ? "" : "s"} triggered`];
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(", ") + ".");