@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.
- package/dist/cli/index.js +210 -20
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +207 -17
- package/dist/cli/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/cli/index.mjs
CHANGED
|
@@ -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
|
|
1299
|
+
var buildServiceIndex = (snapshot2, direction) => {
|
|
1297
1300
|
const index = /* @__PURE__ */ new Map();
|
|
1298
1301
|
for (const service of snapshot2.resources.services) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1580
|
-
const targetSDK =
|
|
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
|
-
|
|
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);
|