@eventcatalog/cli 0.5.2 → 0.5.4

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.
@@ -1235,7 +1235,10 @@ var snapshotList = async (opts) => {
1235
1235
  // src/cli/governance/rules.ts
1236
1236
  import fs from "fs";
1237
1237
  import path2 from "path";
1238
+ import { createHash } from "crypto";
1238
1239
  import yaml from "js-yaml";
1240
+ import { satisfies, validRange } from "semver";
1241
+ import createSDK6 from "@eventcatalog/sdk";
1239
1242
  var loadGovernanceConfig = (catalogDir) => {
1240
1243
  const yamlPath = path2.join(catalogDir, "governance.yaml");
1241
1244
  const ymlPath = path2.join(catalogDir, "governance.yml");
@@ -1293,15 +1296,16 @@ var buildMessageMap = (snapshot2) => {
1293
1296
  for (const msg of snapshot2.resources.messages.queries) map.set(msg.id, msg);
1294
1297
  return map;
1295
1298
  };
1296
- var buildProducerIndex = (snapshot2) => {
1299
+ var buildServiceIndex = (snapshot2, direction) => {
1297
1300
  const index = /* @__PURE__ */ new Map();
1298
1301
  for (const service of snapshot2.resources.services) {
1299
- if (!service.sends) continue;
1300
- for (const s of service.sends) {
1301
- let producers = index.get(s.id);
1302
- if (!producers) {
1303
- producers = [];
1304
- index.set(s.id, producers);
1302
+ const pointers = service[direction];
1303
+ if (!pointers) continue;
1304
+ for (const pointer of pointers) {
1305
+ let entries = index.get(pointer.id);
1306
+ if (!entries) {
1307
+ entries = [];
1308
+ index.set(pointer.id, entries);
1305
1309
  }
1306
1310
  const entry = {
1307
1311
  id: service.id,
@@ -1310,17 +1314,85 @@ var buildProducerIndex = (snapshot2) => {
1310
1314
  if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) {
1311
1315
  entry.owners = service.owners;
1312
1316
  }
1313
- producers.push(entry);
1317
+ entries.push(entry);
1314
1318
  }
1315
1319
  }
1316
1320
  return index;
1317
1321
  };
1322
+ var getMessageTypeKey = (resourceId, type) => `${type}:${resourceId}`;
1323
+ var buildLatestMessageVersionMap = (snapshot2) => {
1324
+ const versions = /* @__PURE__ */ new Map();
1325
+ for (const event of snapshot2.resources.messages.events) {
1326
+ versions.set(getMessageTypeKey(event.id, "event"), event.version);
1327
+ }
1328
+ for (const command of snapshot2.resources.messages.commands) {
1329
+ versions.set(getMessageTypeKey(command.id, "command"), command.version);
1330
+ }
1331
+ for (const query of snapshot2.resources.messages.queries) {
1332
+ versions.set(getMessageTypeKey(query.id, "query"), query.version);
1333
+ }
1334
+ return versions;
1335
+ };
1336
+ var getTargetMessageVersion = (resourceChange) => {
1337
+ if (resourceChange.changeType === "versioned") {
1338
+ return resourceChange.newVersion || resourceChange.version;
1339
+ }
1340
+ return resourceChange.version;
1341
+ };
1342
+ var pointerTargetsChangedVersion = (pointer, resourceChange, latestMessageVersions) => {
1343
+ if (pointer.id !== resourceChange.resourceId) return false;
1344
+ const targetVersion = getTargetMessageVersion(resourceChange);
1345
+ const pointerVersion = pointer.version;
1346
+ if (!pointerVersion || pointerVersion === "latest") {
1347
+ const latestVersion = latestMessageVersions.get(getMessageTypeKey(resourceChange.resourceId, resourceChange.type));
1348
+ if (!latestVersion) return true;
1349
+ return latestVersion === targetVersion;
1350
+ }
1351
+ if (validRange(pointerVersion)) {
1352
+ try {
1353
+ return satisfies(targetVersion, pointerVersion);
1354
+ } catch {
1355
+ return false;
1356
+ }
1357
+ }
1358
+ return pointerVersion === targetVersion;
1359
+ };
1360
+ var getServicesForSchemaChange = (snapshot2, direction, resourceChange, latestMessageVersions) => {
1361
+ const matches = [];
1362
+ for (const service of snapshot2.resources.services) {
1363
+ const pointers = service[direction];
1364
+ if (!pointers) continue;
1365
+ const hasMatch = pointers.some((pointer) => pointerTargetsChangedVersion(pointer, resourceChange, latestMessageVersions));
1366
+ if (!hasMatch) continue;
1367
+ const entry = {
1368
+ id: service.id,
1369
+ version: service.version
1370
+ };
1371
+ if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) {
1372
+ entry.owners = service.owners;
1373
+ }
1374
+ matches.push(entry);
1375
+ }
1376
+ return matches;
1377
+ };
1378
+ var matchesSchemaChangeResource = (schemaChange, resources) => {
1379
+ return resources.some((resource) => {
1380
+ if (resource === "*") return true;
1381
+ if (resource.startsWith("message:")) return schemaChange.resourceChange.resourceId === resource.slice(8);
1382
+ if (resource.startsWith("consumes:"))
1383
+ return schemaChange.consumerServices.some((service) => service.id === resource.slice(9));
1384
+ if (resource.startsWith("produces:"))
1385
+ return schemaChange.producerServices.some((service) => service.id === resource.slice(9));
1386
+ if (resource.startsWith("service:")) return schemaChange.producerServices.some((service) => service.id === resource.slice(8));
1387
+ return false;
1388
+ });
1389
+ };
1318
1390
  var evaluateDeprecationRules = (diff, config, targetSnapshot, targetMessageSets, baseSnapshot) => {
1319
1391
  const deprecationRules = config.rules.filter((rule) => rule.when.includes("message_deprecated"));
1320
1392
  if (deprecationRules.length === 0) return [];
1321
1393
  const targetMessages = buildMessageMap(targetSnapshot);
1322
1394
  const baseMessages = baseSnapshot ? buildMessageMap(baseSnapshot) : void 0;
1323
- const producerIndex = buildProducerIndex(targetSnapshot);
1395
+ const producerIndex = buildServiceIndex(targetSnapshot, "sends");
1324
1396
  const deprecatedResources = diff.resources.filter((rc) => {
1325
1397
  if (!MESSAGE_RESOURCE_TYPES.has(rc.type)) return false;
1326
1398
  if (!rc.changedFields?.includes("deprecated")) return false;
@@ -1347,6 +1419,29 @@ var evaluateDeprecationRules = (diff, config, targetSnapshot, targetMessageSets,
1347
1419
  }
1348
1420
  return results;
1349
1421
  };
1422
+ var evaluateSchemaChangeRules = (diff, config, targetSnapshot) => {
1423
+ const schemaRules = config.rules.filter((rule) => rule.when.includes("schema_changed"));
1424
+ if (schemaRules.length === 0) return [];
1425
+ const schemaChangedResources = diff.resources.filter((rc) => {
1426
+ if (!MESSAGE_RESOURCE_TYPES.has(rc.type)) return false;
1427
+ return rc.changedFields?.includes("schemaHash");
1428
+ });
1429
+ if (schemaChangedResources.length === 0) return [];
1430
+ const latestMessageVersions = buildLatestMessageVersionMap(targetSnapshot);
1431
+ const schemaChanges = schemaChangedResources.map((resourceChange) => ({
1432
+ resourceChange,
1433
+ producerServices: getServicesForSchemaChange(targetSnapshot, "sends", resourceChange, latestMessageVersions),
1434
+ consumerServices: getServicesForSchemaChange(targetSnapshot, "receives", resourceChange, latestMessageVersions)
1435
+ }));
1436
+ const results = [];
1437
+ for (const rule of schemaRules) {
1438
+ const matched = schemaChanges.filter((schemaChange) => matchesSchemaChangeResource(schemaChange, rule.resources));
1439
+ if (matched.length > 0) {
1440
+ results.push({ rule, trigger: "schema_changed", matchedChanges: [], schemaChanges: matched });
1441
+ }
1442
+ }
1443
+ return results;
1444
+ };
1350
1445
  var evaluateGovernanceRules = (diff, config, targetSnapshot, baseSnapshot) => {
1351
1446
  const results = [];
1352
1447
  const targetMessageSets = targetSnapshot ? buildServiceMessageSets(targetSnapshot) : void 0;
@@ -1366,6 +1461,7 @@ var evaluateGovernanceRules = (diff, config, targetSnapshot, baseSnapshot) => {
1366
1461
  }
1367
1462
  if (targetSnapshot && targetMessageSets) {
1368
1463
  results.push(...evaluateDeprecationRules(diff, config, targetSnapshot, targetMessageSets, baseSnapshot));
1464
+ results.push(...evaluateSchemaChangeRules(diff, config, targetSnapshot));
1369
1465
  }
1370
1466
  return results;
1371
1467
  };
@@ -1384,6 +1480,48 @@ var resolveEnvVars = (value) => {
1384
1480
  return envValue;
1385
1481
  });
1386
1482
  };
1483
+ var readSchemaDetails = async (sdk, resourceId, version2, type) => {
1484
+ if (!MESSAGE_RESOURCE_TYPES.has(type)) return {};
1485
+ try {
1486
+ const schema = await sdk.getSchemaForMessage(resourceId, version2);
1487
+ if (!schema) return {};
1488
+ return {
1489
+ content: schema.schema,
1490
+ schemaPath: schema.fileName,
1491
+ schemaHash: createHash("sha256").update(schema.schema).digest("hex")
1492
+ };
1493
+ } catch {
1494
+ return {};
1495
+ }
1496
+ };
1497
+ var enrichSchemaContent = async (results, baseCatalogDir, targetCatalogDir) => {
1498
+ const baseSDK = createSDK6(baseCatalogDir);
1499
+ const targetSDK = createSDK6(targetCatalogDir);
1500
+ const promises = [];
1501
+ for (const result of results) {
1502
+ if (!result.schemaChanges) continue;
1503
+ for (const sc of result.schemaChanges) {
1504
+ const { resourceId, version: version2, type, changeType, previousVersion, newVersion } = sc.resourceChange;
1505
+ const baseVersion = changeType === "versioned" ? previousVersion || version2 : version2;
1506
+ const targetVersion = changeType === "versioned" ? newVersion || version2 : version2;
1507
+ promises.push(
1508
+ (async () => {
1509
+ const [before, after] = await Promise.all([
1510
+ readSchemaDetails(baseSDK, resourceId, baseVersion, type),
1511
+ readSchemaDetails(targetSDK, resourceId, targetVersion, type)
1512
+ ]);
1513
+ sc.before = before.content;
1514
+ sc.after = after.content;
1515
+ sc.beforeSchemaPath = before.schemaPath;
1516
+ sc.afterSchemaPath = after.schemaPath;
1517
+ sc.beforeSchemaHash = before.schemaHash;
1518
+ sc.afterSchemaHash = after.schemaHash;
1519
+ })()
1520
+ );
1521
+ }
1522
+ }
1523
+ await Promise.all(promises);
1524
+ };
1387
1525
 
1388
1526
  // src/cli/governance/actions.ts
1389
1527
  import { randomUUID as randomUUID2 } from "crypto";
@@ -1410,7 +1548,7 @@ var buildServiceOwnersMap = (snapshot2) => {
1410
1548
  return map;
1411
1549
  };
1412
1550
  var executeGovernanceActions = async (results, opts = {}) => {
1413
- const { messageTypes, status, serviceOwners } = opts;
1551
+ const { messageTypes, status, serviceOwners, baseRef, targetRef } = opts;
1414
1552
  const webhookCalls = [];
1415
1553
  const now = (/* @__PURE__ */ new Date()).toISOString();
1416
1554
  for (const result of results) {
@@ -1423,6 +1561,46 @@ var executeGovernanceActions = async (results, opts = {}) => {
1423
1561
  headers[key] = resolveEnvVars(value);
1424
1562
  }
1425
1563
  }
1564
+ if (result.schemaChanges && result.schemaChanges.length > 0) {
1565
+ for (const sc of result.schemaChanges) {
1566
+ const messageType = messageTypes?.get(sc.resourceChange.resourceId) || "message";
1567
+ const payload = {
1568
+ specversion: "1.0",
1569
+ type: "eventcatalog.governance.schema_changed",
1570
+ source: "eventcatalog/governance",
1571
+ id: randomUUID2(),
1572
+ time: now,
1573
+ datacontenttype: "application/json",
1574
+ data: {
1575
+ schemaVersion: 1,
1576
+ ...status && { status },
1577
+ summary: `Schema changed for ${messageType} ${sc.resourceChange.resourceId}`,
1578
+ message: {
1579
+ id: sc.resourceChange.resourceId,
1580
+ version: sc.resourceChange.version,
1581
+ type: messageType
1582
+ },
1583
+ schema: {
1584
+ beforeHash: sc.beforeSchemaHash ?? null,
1585
+ afterHash: sc.afterSchemaHash ?? null,
1586
+ beforePath: sc.beforeSchemaPath ?? null,
1587
+ afterPath: sc.afterSchemaPath ?? null
1588
+ },
1589
+ refs: {
1590
+ base: baseRef ?? null,
1591
+ target: targetRef ?? null
1592
+ },
1593
+ consumers: sc.consumerServices,
1594
+ producers: sc.producerServices
1595
+ }
1596
+ };
1597
+ webhookCalls.push({
1598
+ urlTemplate: action.url,
1599
+ request: fetch(url, { method: "POST", headers, body: JSON.stringify(payload) })
1600
+ });
1601
+ }
1602
+ continue;
1603
+ }
1426
1604
  if (result.deprecationChanges && result.deprecationChanges.length > 0) {
1427
1605
  for (const dc of result.deprecationChanges) {
1428
1606
  const messageType = messageTypes?.get(dc.resourceChange.resourceId) || "message";
@@ -1515,7 +1693,14 @@ var formatGovernanceOutput = (results) => {
1515
1693
  const lines = ["Governance:", ""];
1516
1694
  for (const result of results) {
1517
1695
  lines.push(` Rule "${result.rule.name}" triggered (${result.trigger}):`);
1518
- if (result.deprecationChanges && result.deprecationChanges.length > 0) {
1696
+ if (result.schemaChanges && result.schemaChanges.length > 0) {
1697
+ for (const sc of result.schemaChanges) {
1698
+ const consumers = sc.consumerServices.length > 0 ? sc.consumerServices.map((c2) => c2.id).join(", ") : "no known consumers";
1699
+ lines.push(
1700
+ ` ! Schema changed for ${sc.resourceChange.resourceId} (${sc.resourceChange.type}) \u2014 consumers: ${consumers}`
1701
+ );
1702
+ }
1703
+ } else if (result.deprecationChanges && result.deprecationChanges.length > 0) {
1519
1704
  for (const dc of result.deprecationChanges) {
1520
1705
  const producers = dc.producerServices.length > 0 ? dc.producerServices.map((p) => p.id).join(", ") : "unknown producer";
1521
1706
  lines.push(` ! ${dc.resourceChange.resourceId} (${dc.resourceChange.type}) deprecated by ${producers}`);
@@ -1538,7 +1723,7 @@ import { execSync } from "child_process";
1538
1723
  import { mkdtempSync, rmSync as rmSync2 } from "fs";
1539
1724
  import { tmpdir } from "os";
1540
1725
  import dotenv from "dotenv";
1541
- import createSDK6 from "@eventcatalog/sdk";
1726
+ import createSDK7 from "@eventcatalog/sdk";
1542
1727
  import { isEventCatalogScaleEnabled } from "@eventcatalog/license";
1543
1728
  var BRANCH_NAME_RE = /^[a-zA-Z0-9._\-/]+$/;
1544
1729
  var extractBranchToTempDir = (branch, catalogDir, tempDirs) => {
@@ -1572,15 +1757,17 @@ var governanceCheck = async (opts) => {
1572
1757
  const baseTmpDir = extractBranchToTempDir(baseBranch, dir, tempDirs);
1573
1758
  const baseSnapshotDir = trackTempDir("ec-snap-base-");
1574
1759
  const targetSnapshotDir = trackTempDir("ec-snap-target-");
1575
- const baseSDK = createSDK6(baseTmpDir);
1760
+ const baseSDK = createSDK7(baseTmpDir);
1576
1761
  const baseResult = await baseSDK.createSnapshot({ label: `base-${baseBranch}`, outputDir: baseSnapshotDir });
1577
1762
  let targetResult;
1763
+ let targetCatalogDir;
1578
1764
  if (opts.target) {
1579
- const targetTmpDir = extractBranchToTempDir(opts.target, dir, tempDirs);
1580
- const targetSDK = createSDK6(targetTmpDir);
1765
+ targetCatalogDir = extractBranchToTempDir(opts.target, dir, tempDirs);
1766
+ const targetSDK = createSDK7(targetCatalogDir);
1581
1767
  targetResult = await targetSDK.createSnapshot({ label: `target-${opts.target}`, outputDir: targetSnapshotDir });
1582
1768
  } else {
1583
- const targetSDK = createSDK6(dir);
1769
+ targetCatalogDir = dir;
1770
+ const targetSDK = createSDK7(dir);
1584
1771
  targetResult = await targetSDK.createSnapshot({ label: "current", outputDir: targetSnapshotDir });
1585
1772
  }
1586
1773
  const diff = await baseSDK.diffSnapshots(baseResult.filePath, targetResult.filePath);
@@ -1589,12 +1776,15 @@ var governanceCheck = async (opts) => {
1589
1776
  return "No governance.yaml (or governance.yml) found or no rules defined.";
1590
1777
  }
1591
1778
  const results = evaluateGovernanceRules(diff, config, targetResult.snapshot, baseResult.snapshot);
1779
+ await enrichSchemaContent(results, baseTmpDir, targetCatalogDir);
1592
1780
  const messageTypes = buildMessageTypeMap(targetResult.snapshot);
1593
1781
  const serviceOwners = buildServiceOwnersMap(targetResult.snapshot);
1594
1782
  const actionOutput = await executeGovernanceActions(results, {
1595
1783
  messageTypes,
1596
1784
  status: opts.status,
1597
- serviceOwners
1785
+ serviceOwners,
1786
+ baseRef: baseBranch,
1787
+ targetRef: opts.target || "working-directory"
1598
1788
  });
1599
1789
  if (opts.format === "json") {
1600
1790
  return JSON.stringify({ baseBranch, target: opts.target || "working directory", results, diff: diff.summary }, null, 2);