@eventcatalog/cli 0.5.3 → 0.5.5

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
@@ -1251,7 +1251,10 @@ var snapshotList = async (opts) => {
1251
1251
  // src/cli/governance/rules.ts
1252
1252
  var import_node_fs5 = __toESM(require("fs"));
1253
1253
  var import_node_path4 = __toESM(require("path"));
1254
+ var import_node_crypto2 = require("crypto");
1254
1255
  var import_js_yaml = __toESM(require("js-yaml"));
1256
+ var import_semver = require("semver");
1257
+ var import_sdk6 = __toESM(require("@eventcatalog/sdk"));
1255
1258
  var loadGovernanceConfig = (catalogDir) => {
1256
1259
  const yamlPath = import_node_path4.default.join(catalogDir, "governance.yaml");
1257
1260
  const ymlPath = import_node_path4.default.join(catalogDir, "governance.yml");
@@ -1261,7 +1264,15 @@ var loadGovernanceConfig = (catalogDir) => {
1261
1264
  }
1262
1265
  const content = import_node_fs5.default.readFileSync(configPath, "utf-8");
1263
1266
  const parsed = import_js_yaml.default.load(content);
1264
- return { rules: parsed?.rules || [] };
1267
+ const rules = parsed?.rules || [];
1268
+ for (const rule of rules) {
1269
+ for (const action of rule.actions) {
1270
+ if (action.type === "fail" && action.message !== void 0 && typeof action.message !== "string") {
1271
+ throw new Error(`Invalid "message" in fail action for rule "${rule.name}". Must be a string.`);
1272
+ }
1273
+ }
1274
+ }
1275
+ return { rules };
1265
1276
  };
1266
1277
  var TRIGGER_FILTERS = {
1267
1278
  consumer_added: (c2) => c2.direction === "receives" && c2.changeType === "added",
@@ -1309,15 +1320,16 @@ var buildMessageMap = (snapshot2) => {
1309
1320
  for (const msg of snapshot2.resources.messages.queries) map.set(msg.id, msg);
1310
1321
  return map;
1311
1322
  };
1312
- var buildProducerIndex = (snapshot2) => {
1323
+ var buildServiceIndex = (snapshot2, direction) => {
1313
1324
  const index = /* @__PURE__ */ new Map();
1314
1325
  for (const service of snapshot2.resources.services) {
1315
- if (!service.sends) continue;
1316
- for (const s of service.sends) {
1317
- let producers = index.get(s.id);
1318
- if (!producers) {
1319
- producers = [];
1320
- index.set(s.id, producers);
1326
+ const pointers = service[direction];
1327
+ if (!pointers) continue;
1328
+ for (const pointer of pointers) {
1329
+ let entries = index.get(pointer.id);
1330
+ if (!entries) {
1331
+ entries = [];
1332
+ index.set(pointer.id, entries);
1321
1333
  }
1322
1334
  const entry = {
1323
1335
  id: service.id,
@@ -1326,17 +1338,85 @@ var buildProducerIndex = (snapshot2) => {
1326
1338
  if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) {
1327
1339
  entry.owners = service.owners;
1328
1340
  }
1329
- producers.push(entry);
1341
+ entries.push(entry);
1330
1342
  }
1331
1343
  }
1332
1344
  return index;
1333
1345
  };
1346
+ var getMessageTypeKey = (resourceId, type) => `${type}:${resourceId}`;
1347
+ var buildLatestMessageVersionMap = (snapshot2) => {
1348
+ const versions = /* @__PURE__ */ new Map();
1349
+ for (const event of snapshot2.resources.messages.events) {
1350
+ versions.set(getMessageTypeKey(event.id, "event"), event.version);
1351
+ }
1352
+ for (const command of snapshot2.resources.messages.commands) {
1353
+ versions.set(getMessageTypeKey(command.id, "command"), command.version);
1354
+ }
1355
+ for (const query of snapshot2.resources.messages.queries) {
1356
+ versions.set(getMessageTypeKey(query.id, "query"), query.version);
1357
+ }
1358
+ return versions;
1359
+ };
1360
+ var getTargetMessageVersion = (resourceChange) => {
1361
+ if (resourceChange.changeType === "versioned") {
1362
+ return resourceChange.newVersion || resourceChange.version;
1363
+ }
1364
+ return resourceChange.version;
1365
+ };
1366
+ var pointerTargetsChangedVersion = (pointer, resourceChange, latestMessageVersions) => {
1367
+ if (pointer.id !== resourceChange.resourceId) return false;
1368
+ const targetVersion = getTargetMessageVersion(resourceChange);
1369
+ const pointerVersion = pointer.version;
1370
+ if (!pointerVersion || pointerVersion === "latest") {
1371
+ const latestVersion = latestMessageVersions.get(getMessageTypeKey(resourceChange.resourceId, resourceChange.type));
1372
+ if (!latestVersion) return true;
1373
+ return latestVersion === targetVersion;
1374
+ }
1375
+ if ((0, import_semver.validRange)(pointerVersion)) {
1376
+ try {
1377
+ return (0, import_semver.satisfies)(targetVersion, pointerVersion);
1378
+ } catch {
1379
+ return false;
1380
+ }
1381
+ }
1382
+ return pointerVersion === targetVersion;
1383
+ };
1384
+ var getServicesForSchemaChange = (snapshot2, direction, resourceChange, latestMessageVersions) => {
1385
+ const matches = [];
1386
+ for (const service of snapshot2.resources.services) {
1387
+ const pointers = service[direction];
1388
+ if (!pointers) continue;
1389
+ const hasMatch = pointers.some((pointer) => pointerTargetsChangedVersion(pointer, resourceChange, latestMessageVersions));
1390
+ if (!hasMatch) continue;
1391
+ const entry = {
1392
+ id: service.id,
1393
+ version: service.version
1394
+ };
1395
+ if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) {
1396
+ entry.owners = service.owners;
1397
+ }
1398
+ matches.push(entry);
1399
+ }
1400
+ return matches;
1401
+ };
1402
+ var matchesSchemaChangeResource = (schemaChange, resources) => {
1403
+ return resources.some((resource) => {
1404
+ if (resource === "*") return true;
1405
+ if (resource.startsWith("message:")) return schemaChange.resourceChange.resourceId === resource.slice(8);
1406
+ if (resource.startsWith("consumes:"))
1407
+ return schemaChange.consumerServices.some((service) => service.id === resource.slice(9));
1408
+ if (resource.startsWith("produces:"))
1409
+ return schemaChange.producerServices.some((service) => service.id === resource.slice(9));
1410
+ if (resource.startsWith("service:")) return schemaChange.producerServices.some((service) => service.id === resource.slice(8));
1411
+ return false;
1412
+ });
1413
+ };
1334
1414
  var evaluateDeprecationRules = (diff, config, targetSnapshot, targetMessageSets, baseSnapshot) => {
1335
1415
  const deprecationRules = config.rules.filter((rule) => rule.when.includes("message_deprecated"));
1336
1416
  if (deprecationRules.length === 0) return [];
1337
1417
  const targetMessages = buildMessageMap(targetSnapshot);
1338
1418
  const baseMessages = baseSnapshot ? buildMessageMap(baseSnapshot) : void 0;
1339
- const producerIndex = buildProducerIndex(targetSnapshot);
1419
+ const producerIndex = buildServiceIndex(targetSnapshot, "sends");
1340
1420
  const deprecatedResources = diff.resources.filter((rc) => {
1341
1421
  if (!MESSAGE_RESOURCE_TYPES.has(rc.type)) return false;
1342
1422
  if (!rc.changedFields?.includes("deprecated")) return false;
@@ -1363,6 +1443,29 @@ var evaluateDeprecationRules = (diff, config, targetSnapshot, targetMessageSets,
1363
1443
  }
1364
1444
  return results;
1365
1445
  };
1446
+ var evaluateSchemaChangeRules = (diff, config, targetSnapshot) => {
1447
+ const schemaRules = config.rules.filter((rule) => rule.when.includes("schema_changed"));
1448
+ if (schemaRules.length === 0) return [];
1449
+ const schemaChangedResources = diff.resources.filter((rc) => {
1450
+ if (!MESSAGE_RESOURCE_TYPES.has(rc.type)) return false;
1451
+ return rc.changedFields?.includes("schemaHash");
1452
+ });
1453
+ if (schemaChangedResources.length === 0) return [];
1454
+ const latestMessageVersions = buildLatestMessageVersionMap(targetSnapshot);
1455
+ const schemaChanges = schemaChangedResources.map((resourceChange) => ({
1456
+ resourceChange,
1457
+ producerServices: getServicesForSchemaChange(targetSnapshot, "sends", resourceChange, latestMessageVersions),
1458
+ consumerServices: getServicesForSchemaChange(targetSnapshot, "receives", resourceChange, latestMessageVersions)
1459
+ }));
1460
+ const results = [];
1461
+ for (const rule of schemaRules) {
1462
+ const matched = schemaChanges.filter((schemaChange) => matchesSchemaChangeResource(schemaChange, rule.resources));
1463
+ if (matched.length > 0) {
1464
+ results.push({ rule, trigger: "schema_changed", matchedChanges: [], schemaChanges: matched });
1465
+ }
1466
+ }
1467
+ return results;
1468
+ };
1366
1469
  var evaluateGovernanceRules = (diff, config, targetSnapshot, baseSnapshot) => {
1367
1470
  const results = [];
1368
1471
  const targetMessageSets = targetSnapshot ? buildServiceMessageSets(targetSnapshot) : void 0;
@@ -1382,6 +1485,7 @@ var evaluateGovernanceRules = (diff, config, targetSnapshot, baseSnapshot) => {
1382
1485
  }
1383
1486
  if (targetSnapshot && targetMessageSets) {
1384
1487
  results.push(...evaluateDeprecationRules(diff, config, targetSnapshot, targetMessageSets, baseSnapshot));
1488
+ results.push(...evaluateSchemaChangeRules(diff, config, targetSnapshot));
1385
1489
  }
1386
1490
  return results;
1387
1491
  };
@@ -1400,9 +1504,51 @@ var resolveEnvVars = (value) => {
1400
1504
  return envValue;
1401
1505
  });
1402
1506
  };
1507
+ var readSchemaDetails = async (sdk, resourceId, version2, type) => {
1508
+ if (!MESSAGE_RESOURCE_TYPES.has(type)) return {};
1509
+ try {
1510
+ const schema = await sdk.getSchemaForMessage(resourceId, version2);
1511
+ if (!schema) return {};
1512
+ return {
1513
+ content: schema.schema,
1514
+ schemaPath: schema.fileName,
1515
+ schemaHash: (0, import_node_crypto2.createHash)("sha256").update(schema.schema).digest("hex")
1516
+ };
1517
+ } catch {
1518
+ return {};
1519
+ }
1520
+ };
1521
+ var enrichSchemaContent = async (results, baseCatalogDir, targetCatalogDir) => {
1522
+ const baseSDK = (0, import_sdk6.default)(baseCatalogDir);
1523
+ const targetSDK = (0, import_sdk6.default)(targetCatalogDir);
1524
+ const promises = [];
1525
+ for (const result of results) {
1526
+ if (!result.schemaChanges) continue;
1527
+ for (const sc of result.schemaChanges) {
1528
+ const { resourceId, version: version2, type, changeType, previousVersion, newVersion } = sc.resourceChange;
1529
+ const baseVersion = changeType === "versioned" ? previousVersion || version2 : version2;
1530
+ const targetVersion = changeType === "versioned" ? newVersion || version2 : version2;
1531
+ promises.push(
1532
+ (async () => {
1533
+ const [before, after] = await Promise.all([
1534
+ readSchemaDetails(baseSDK, resourceId, baseVersion, type),
1535
+ readSchemaDetails(targetSDK, resourceId, targetVersion, type)
1536
+ ]);
1537
+ sc.before = before.content;
1538
+ sc.after = after.content;
1539
+ sc.beforeSchemaPath = before.schemaPath;
1540
+ sc.afterSchemaPath = after.schemaPath;
1541
+ sc.beforeSchemaHash = before.schemaHash;
1542
+ sc.afterSchemaHash = after.schemaHash;
1543
+ })()
1544
+ );
1545
+ }
1546
+ }
1547
+ await Promise.all(promises);
1548
+ };
1403
1549
 
1404
1550
  // src/cli/governance/actions.ts
1405
- var import_node_crypto2 = require("crypto");
1551
+ var import_node_crypto3 = require("crypto");
1406
1552
  var buildMessageTypeMap = (snapshot2) => {
1407
1553
  const map = /* @__PURE__ */ new Map();
1408
1554
  for (const event of snapshot2.resources.messages.events) {
@@ -1426,7 +1572,7 @@ var buildServiceOwnersMap = (snapshot2) => {
1426
1572
  return map;
1427
1573
  };
1428
1574
  var executeGovernanceActions = async (results, opts = {}) => {
1429
- const { messageTypes, status, serviceOwners } = opts;
1575
+ const { messageTypes, status, serviceOwners, baseRef, targetRef } = opts;
1430
1576
  const webhookCalls = [];
1431
1577
  const now = (/* @__PURE__ */ new Date()).toISOString();
1432
1578
  for (const result of results) {
@@ -1439,6 +1585,46 @@ var executeGovernanceActions = async (results, opts = {}) => {
1439
1585
  headers[key] = resolveEnvVars(value);
1440
1586
  }
1441
1587
  }
1588
+ if (result.schemaChanges && result.schemaChanges.length > 0) {
1589
+ for (const sc of result.schemaChanges) {
1590
+ const messageType = messageTypes?.get(sc.resourceChange.resourceId) || "message";
1591
+ const payload = {
1592
+ specversion: "1.0",
1593
+ type: "eventcatalog.governance.schema_changed",
1594
+ source: "eventcatalog/governance",
1595
+ id: (0, import_node_crypto3.randomUUID)(),
1596
+ time: now,
1597
+ datacontenttype: "application/json",
1598
+ data: {
1599
+ schemaVersion: 1,
1600
+ ...status && { status },
1601
+ summary: `Schema changed for ${messageType} ${sc.resourceChange.resourceId}`,
1602
+ message: {
1603
+ id: sc.resourceChange.resourceId,
1604
+ version: sc.resourceChange.version,
1605
+ type: messageType
1606
+ },
1607
+ schema: {
1608
+ beforeHash: sc.beforeSchemaHash ?? null,
1609
+ afterHash: sc.afterSchemaHash ?? null,
1610
+ beforePath: sc.beforeSchemaPath ?? null,
1611
+ afterPath: sc.afterSchemaPath ?? null
1612
+ },
1613
+ refs: {
1614
+ base: baseRef ?? null,
1615
+ target: targetRef ?? null
1616
+ },
1617
+ consumers: sc.consumerServices,
1618
+ producers: sc.producerServices
1619
+ }
1620
+ };
1621
+ webhookCalls.push({
1622
+ urlTemplate: action.url,
1623
+ request: fetch(url, { method: "POST", headers, body: JSON.stringify(payload) })
1624
+ });
1625
+ }
1626
+ continue;
1627
+ }
1442
1628
  if (result.deprecationChanges && result.deprecationChanges.length > 0) {
1443
1629
  for (const dc of result.deprecationChanges) {
1444
1630
  const messageType = messageTypes?.get(dc.resourceChange.resourceId) || "message";
@@ -1448,7 +1634,7 @@ var executeGovernanceActions = async (results, opts = {}) => {
1448
1634
  specversion: "1.0",
1449
1635
  type: `eventcatalog.governance.message_deprecated`,
1450
1636
  source: "eventcatalog/governance",
1451
- id: (0, import_node_crypto2.randomUUID)(),
1637
+ id: (0, import_node_crypto3.randomUUID)(),
1452
1638
  time: now,
1453
1639
  datacontenttype: "application/json",
1454
1640
  data: {
@@ -1483,7 +1669,7 @@ var executeGovernanceActions = async (results, opts = {}) => {
1483
1669
  specversion: "1.0",
1484
1670
  type: `eventcatalog.governance.${result.trigger}`,
1485
1671
  source: "eventcatalog/governance",
1486
- id: (0, import_node_crypto2.randomUUID)(),
1672
+ id: (0, import_node_crypto3.randomUUID)(),
1487
1673
  time: now,
1488
1674
  datacontenttype: "application/json",
1489
1675
  data: {
@@ -1531,7 +1717,14 @@ var formatGovernanceOutput = (results) => {
1531
1717
  const lines = ["Governance:", ""];
1532
1718
  for (const result of results) {
1533
1719
  lines.push(` Rule "${result.rule.name}" triggered (${result.trigger}):`);
1534
- if (result.deprecationChanges && result.deprecationChanges.length > 0) {
1720
+ if (result.schemaChanges && result.schemaChanges.length > 0) {
1721
+ for (const sc of result.schemaChanges) {
1722
+ const consumers = sc.consumerServices.length > 0 ? sc.consumerServices.map((c2) => c2.id).join(", ") : "no known consumers";
1723
+ lines.push(
1724
+ ` ! Schema changed for ${sc.resourceChange.resourceId} (${sc.resourceChange.type}) \u2014 consumers: ${consumers}`
1725
+ );
1726
+ }
1727
+ } else if (result.deprecationChanges && result.deprecationChanges.length > 0) {
1535
1728
  for (const dc of result.deprecationChanges) {
1536
1729
  const producers = dc.producerServices.length > 0 ? dc.producerServices.map((p) => p.id).join(", ") : "unknown producer";
1537
1730
  lines.push(` ! ${dc.resourceChange.resourceId} (${dc.resourceChange.type}) deprecated by ${producers}`);
@@ -1547,6 +1740,17 @@ var formatGovernanceOutput = (results) => {
1547
1740
  }
1548
1741
  return lines.join("\n");
1549
1742
  };
1743
+ var formatFailureOutput = (failures) => {
1744
+ if (failures.length === 0) return "";
1745
+ const lines = [];
1746
+ for (const f of failures) {
1747
+ lines.push(`FAILED: ${f.ruleName}`);
1748
+ for (const msg of f.messages) {
1749
+ lines.push(` ${msg}`);
1750
+ }
1751
+ }
1752
+ return lines.join("\n");
1753
+ };
1550
1754
 
1551
1755
  // src/cli/governance/check.ts
1552
1756
  var import_node_path5 = __toESM(require("path"));
@@ -1554,7 +1758,7 @@ var import_node_child_process = require("child_process");
1554
1758
  var import_node_fs6 = require("fs");
1555
1759
  var import_node_os = require("os");
1556
1760
  var import_dotenv = __toESM(require("dotenv"));
1557
- var import_sdk6 = __toESM(require("@eventcatalog/sdk"));
1761
+ var import_sdk7 = __toESM(require("@eventcatalog/sdk"));
1558
1762
  var import_license = require("@eventcatalog/license");
1559
1763
  var BRANCH_NAME_RE = /^[a-zA-Z0-9._\-/]+$/;
1560
1764
  var extractBranchToTempDir = (branch, catalogDir, tempDirs) => {
@@ -1588,32 +1792,56 @@ var governanceCheck = async (opts) => {
1588
1792
  const baseTmpDir = extractBranchToTempDir(baseBranch, dir, tempDirs);
1589
1793
  const baseSnapshotDir = trackTempDir("ec-snap-base-");
1590
1794
  const targetSnapshotDir = trackTempDir("ec-snap-target-");
1591
- const baseSDK = (0, import_sdk6.default)(baseTmpDir);
1795
+ const baseSDK = (0, import_sdk7.default)(baseTmpDir);
1592
1796
  const baseResult = await baseSDK.createSnapshot({ label: `base-${baseBranch}`, outputDir: baseSnapshotDir });
1593
1797
  let targetResult;
1798
+ let targetCatalogDir;
1594
1799
  if (opts.target) {
1595
- const targetTmpDir = extractBranchToTempDir(opts.target, dir, tempDirs);
1596
- const targetSDK = (0, import_sdk6.default)(targetTmpDir);
1800
+ targetCatalogDir = extractBranchToTempDir(opts.target, dir, tempDirs);
1801
+ const targetSDK = (0, import_sdk7.default)(targetCatalogDir);
1597
1802
  targetResult = await targetSDK.createSnapshot({ label: `target-${opts.target}`, outputDir: targetSnapshotDir });
1598
1803
  } else {
1599
- const targetSDK = (0, import_sdk6.default)(dir);
1804
+ targetCatalogDir = dir;
1805
+ const targetSDK = (0, import_sdk7.default)(dir);
1600
1806
  targetResult = await targetSDK.createSnapshot({ label: "current", outputDir: targetSnapshotDir });
1601
1807
  }
1602
1808
  const diff = await baseSDK.diffSnapshots(baseResult.filePath, targetResult.filePath);
1603
1809
  const config = loadGovernanceConfig(dir);
1604
1810
  if (config.rules.length === 0) {
1605
- return "No governance.yaml (or governance.yml) found or no rules defined.";
1811
+ return { output: "No governance.yaml (or governance.yml) found or no rules defined.", exitCode: 0, failures: [] };
1606
1812
  }
1607
1813
  const results = evaluateGovernanceRules(diff, config, targetResult.snapshot, baseResult.snapshot);
1814
+ for (const result of results) {
1815
+ const failActions = result.rule.actions.filter((a) => a.type === "fail");
1816
+ if (failActions.length > 0) {
1817
+ result.failed = true;
1818
+ result.failMessages = failActions.map((a) => "message" in a && a.message ? resolveEnvVars(a.message) : void 0).filter((m) => m !== void 0);
1819
+ }
1820
+ }
1821
+ await enrichSchemaContent(results, baseTmpDir, targetCatalogDir);
1608
1822
  const messageTypes = buildMessageTypeMap(targetResult.snapshot);
1609
1823
  const serviceOwners = buildServiceOwnersMap(targetResult.snapshot);
1610
1824
  const actionOutput = await executeGovernanceActions(results, {
1611
1825
  messageTypes,
1612
1826
  status: opts.status,
1613
- serviceOwners
1827
+ serviceOwners,
1828
+ baseRef: baseBranch,
1829
+ targetRef: opts.target || "working-directory"
1614
1830
  });
1831
+ const failures = results.filter((r) => r.failed).map((r) => ({ ruleName: r.rule.name, messages: r.failMessages || [] }));
1615
1832
  if (opts.format === "json") {
1616
- return JSON.stringify({ baseBranch, target: opts.target || "working directory", results, diff: diff.summary }, null, 2);
1833
+ const jsonOutput = {
1834
+ baseBranch,
1835
+ target: opts.target || "working directory",
1836
+ results,
1837
+ summary: {
1838
+ rulesTriggered: results.length,
1839
+ failures: failures.length,
1840
+ passed: failures.length === 0
1841
+ },
1842
+ diff: diff.summary
1843
+ };
1844
+ return { output: JSON.stringify(jsonOutput, null, 2), exitCode: failures.length > 0 ? 1 : 0, failures };
1617
1845
  }
1618
1846
  const targetLabel = opts.target || "working directory";
1619
1847
  const lines = [`Governance check: comparing ${targetLabel} against ${baseBranch}`, ""];
@@ -1629,7 +1857,12 @@ var governanceCheck = async (opts) => {
1629
1857
  lines.push("");
1630
1858
  lines.push(parts.join(", ") + ".");
1631
1859
  }
1632
- return lines.join("\n");
1860
+ const failureOutput = formatFailureOutput(failures);
1861
+ if (failureOutput) {
1862
+ lines.push("");
1863
+ lines.push(failureOutput);
1864
+ }
1865
+ return { output: lines.join("\n"), exitCode: failures.length > 0 ? 1 : 0, failures };
1633
1866
  } finally {
1634
1867
  for (const d of tempDirs) {
1635
1868
  (0, import_node_fs6.rmSync)(d, { recursive: true, force: true });
@@ -1755,7 +1988,8 @@ governance.command("check").description("Compare catalog against a base branch a
1755
1988
  status: opts.status,
1756
1989
  dir
1757
1990
  });
1758
- console.log(result);
1991
+ console.log(result.output);
1992
+ process.exitCode = result.exitCode;
1759
1993
  } catch (error) {
1760
1994
  console.error(error instanceof Error ? error.message : String(error));
1761
1995
  process.exit(1);