@eventcatalog/cli 0.4.11 → 0.5.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.
- package/dist/cli/index.js +543 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +541 -12
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli-docs.js +109 -0
- package/dist/cli-docs.js.map +1 -1
- package/dist/cli-docs.mjs +109 -0
- package/dist/cli-docs.mjs.map +1 -1
- package/package.json +7 -3
package/dist/cli/index.mjs
CHANGED
|
@@ -10,7 +10,7 @@ var __dirname = /* @__PURE__ */ getDirname();
|
|
|
10
10
|
// src/cli/index.ts
|
|
11
11
|
import { program } from "commander";
|
|
12
12
|
import { readFileSync as readFileSync2 } from "fs";
|
|
13
|
-
import { resolve as
|
|
13
|
+
import { resolve as resolve4 } from "path";
|
|
14
14
|
|
|
15
15
|
// src/cli/executor.ts
|
|
16
16
|
import { existsSync } from "fs";
|
|
@@ -481,8 +481,8 @@ ${messages.join("\n")}`);
|
|
|
481
481
|
}
|
|
482
482
|
return { outputs, program: program2 };
|
|
483
483
|
}
|
|
484
|
-
function extractResourceTypeFolder(
|
|
485
|
-
const segments =
|
|
484
|
+
function extractResourceTypeFolder(path4) {
|
|
485
|
+
const segments = path4.split("/");
|
|
486
486
|
let lastTypeFolder = segments[0];
|
|
487
487
|
for (const seg of segments) {
|
|
488
488
|
if (RESOURCE_TYPE_FROM_FOLDER[seg]) {
|
|
@@ -674,8 +674,8 @@ function extractServiceContainerRefs(program2, nested = false) {
|
|
|
674
674
|
...stmt.ref.version ? { version: stmt.ref.version } : {}
|
|
675
675
|
}));
|
|
676
676
|
if (writesTo.length === 0 && readsFrom.length === 0) continue;
|
|
677
|
-
const
|
|
678
|
-
refsByPath.set(
|
|
677
|
+
const path4 = buildServiceOutputPath(def.name, body, nested, parentPath);
|
|
678
|
+
refsByPath.set(path4, {
|
|
679
679
|
...writesTo.length > 0 ? { writesTo } : {},
|
|
680
680
|
...readsFrom.length > 0 ? { readsFrom } : {}
|
|
681
681
|
});
|
|
@@ -766,21 +766,21 @@ function getReader(sdk, type) {
|
|
|
766
766
|
}
|
|
767
767
|
}
|
|
768
768
|
function promptConfirm(message) {
|
|
769
|
-
return new Promise((
|
|
769
|
+
return new Promise((resolve5) => {
|
|
770
770
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
771
771
|
rl.question(`${message} `, (answer) => {
|
|
772
772
|
rl.close();
|
|
773
773
|
const normalized = answer.trim().toLowerCase();
|
|
774
|
-
|
|
774
|
+
resolve5(normalized === "" || normalized === "y" || normalized === "yes");
|
|
775
775
|
});
|
|
776
776
|
});
|
|
777
777
|
}
|
|
778
778
|
function promptInput(message, defaultValue) {
|
|
779
|
-
return new Promise((
|
|
779
|
+
return new Promise((resolve5) => {
|
|
780
780
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
781
781
|
rl.question(`${message} `, (answer) => {
|
|
782
782
|
rl.close();
|
|
783
|
-
|
|
783
|
+
resolve5(answer.trim() || defaultValue);
|
|
784
784
|
});
|
|
785
785
|
});
|
|
786
786
|
}
|
|
@@ -1136,18 +1136,495 @@ function formatResult(result, dryRun) {
|
|
|
1136
1136
|
return lines.join("\n");
|
|
1137
1137
|
}
|
|
1138
1138
|
function readStdin() {
|
|
1139
|
-
return new Promise((
|
|
1139
|
+
return new Promise((resolve5, reject) => {
|
|
1140
1140
|
const chunks = [];
|
|
1141
1141
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
1142
|
-
process.stdin.on("end", () =>
|
|
1142
|
+
process.stdin.on("end", () => resolve5(Buffer.concat(chunks).toString("utf-8")));
|
|
1143
1143
|
process.stdin.on("error", reject);
|
|
1144
1144
|
});
|
|
1145
1145
|
}
|
|
1146
1146
|
|
|
1147
|
+
// src/cli/snapshot.ts
|
|
1148
|
+
import { resolve as resolve3 } from "path";
|
|
1149
|
+
import { rmSync } from "fs";
|
|
1150
|
+
import createSDK5 from "@eventcatalog/sdk";
|
|
1151
|
+
var snapshotCreate = async (opts) => {
|
|
1152
|
+
const dir = resolve3(opts.dir);
|
|
1153
|
+
const sdk = createSDK5(dir);
|
|
1154
|
+
const result = await sdk.createSnapshot({
|
|
1155
|
+
label: opts.label,
|
|
1156
|
+
outputDir: opts.output ? resolve3(opts.output) : void 0
|
|
1157
|
+
});
|
|
1158
|
+
if (opts.stdout) {
|
|
1159
|
+
rmSync(result.filePath, { force: true });
|
|
1160
|
+
return JSON.stringify(result.snapshot, null, 2);
|
|
1161
|
+
}
|
|
1162
|
+
const resources = result.snapshot.resources;
|
|
1163
|
+
const counts = [
|
|
1164
|
+
resources.services.length && `${resources.services.length} services`,
|
|
1165
|
+
resources.messages.events.length && `${resources.messages.events.length} events`,
|
|
1166
|
+
resources.messages.commands.length && `${resources.messages.commands.length} commands`,
|
|
1167
|
+
resources.messages.queries.length && `${resources.messages.queries.length} queries`,
|
|
1168
|
+
resources.domains.length && `${resources.domains.length} domains`,
|
|
1169
|
+
resources.channels.length && `${resources.channels.length} channels`
|
|
1170
|
+
].filter(Boolean).join(", ");
|
|
1171
|
+
return `Snapshot created: ${result.filePath}
|
|
1172
|
+
Resources: ${counts}`;
|
|
1173
|
+
};
|
|
1174
|
+
var formatDiffText = (diff) => {
|
|
1175
|
+
const lines = [];
|
|
1176
|
+
const labelA = diff.snapshotA.label;
|
|
1177
|
+
const labelB = diff.snapshotB.label;
|
|
1178
|
+
lines.push(`Comparing: ${labelA} vs ${labelB}`);
|
|
1179
|
+
lines.push("");
|
|
1180
|
+
if (diff.resources.length > 0) {
|
|
1181
|
+
lines.push(`Resources (${diff.resources.length} changes):`);
|
|
1182
|
+
for (const r of diff.resources) {
|
|
1183
|
+
const prefix = r.changeType === "added" ? "+" : r.changeType === "removed" ? "-" : r.changeType === "versioned" ? "^" : "~";
|
|
1184
|
+
const version2 = r.changeType === "versioned" ? `${r.previousVersion} -> ${r.newVersion}` : r.version;
|
|
1185
|
+
const fields = r.changedFields ? ` (${r.changedFields.join(", ")})` : "";
|
|
1186
|
+
lines.push(` ${prefix} ${r.resourceId}@${version2} [${r.type}] ${r.changeType}${fields}`);
|
|
1187
|
+
}
|
|
1188
|
+
lines.push("");
|
|
1189
|
+
}
|
|
1190
|
+
if (diff.relationships.length > 0) {
|
|
1191
|
+
lines.push(`Relationships (${diff.relationships.length} changes):`);
|
|
1192
|
+
for (const r of diff.relationships) {
|
|
1193
|
+
const prefix = r.changeType === "added" ? "+" : "-";
|
|
1194
|
+
lines.push(
|
|
1195
|
+
` ${prefix} ${r.serviceId} --${r.direction}--> ${r.resourceId}${r.resourceVersion ? `@${r.resourceVersion}` : ""}`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
lines.push("");
|
|
1199
|
+
}
|
|
1200
|
+
if (diff.summary.totalChanges === 0) {
|
|
1201
|
+
lines.push("No changes detected.");
|
|
1202
|
+
} else {
|
|
1203
|
+
lines.push(`Summary: ${diff.resources.length} resource changes, ${diff.relationships.length} relationship changes`);
|
|
1204
|
+
}
|
|
1205
|
+
return lines.join("\n");
|
|
1206
|
+
};
|
|
1207
|
+
var snapshotDiff = async (opts) => {
|
|
1208
|
+
const dir = resolve3(opts.dir);
|
|
1209
|
+
const sdk = createSDK5(dir);
|
|
1210
|
+
const diff = await sdk.diffSnapshots(resolve3(opts.fileA), resolve3(opts.fileB));
|
|
1211
|
+
if (opts.format === "json") {
|
|
1212
|
+
return JSON.stringify(diff, null, 2);
|
|
1213
|
+
}
|
|
1214
|
+
return formatDiffText(diff);
|
|
1215
|
+
};
|
|
1216
|
+
var snapshotList = async (opts) => {
|
|
1217
|
+
const dir = resolve3(opts.dir);
|
|
1218
|
+
const sdk = createSDK5(dir);
|
|
1219
|
+
const snapshots = await sdk.listSnapshots();
|
|
1220
|
+
if (opts.format === "json") {
|
|
1221
|
+
return JSON.stringify(snapshots, null, 2);
|
|
1222
|
+
}
|
|
1223
|
+
if (snapshots.length === 0) {
|
|
1224
|
+
return "No snapshots found.";
|
|
1225
|
+
}
|
|
1226
|
+
const lines = ["Snapshots:", ""];
|
|
1227
|
+
for (const s of snapshots) {
|
|
1228
|
+
const git = s.git ? ` (${s.git.branch} ${s.git.commit})` : "";
|
|
1229
|
+
lines.push(` ${s.label} ${s.createdAt}${git}`);
|
|
1230
|
+
lines.push(` ${s.filePath}`);
|
|
1231
|
+
}
|
|
1232
|
+
return lines.join("\n");
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
// src/cli/governance/rules.ts
|
|
1236
|
+
import fs from "fs";
|
|
1237
|
+
import path2 from "path";
|
|
1238
|
+
import yaml from "js-yaml";
|
|
1239
|
+
var loadGovernanceConfig = (catalogDir) => {
|
|
1240
|
+
const yamlPath = path2.join(catalogDir, "governance.yaml");
|
|
1241
|
+
const ymlPath = path2.join(catalogDir, "governance.yml");
|
|
1242
|
+
const configPath = fs.existsSync(yamlPath) ? yamlPath : fs.existsSync(ymlPath) ? ymlPath : null;
|
|
1243
|
+
if (!configPath) {
|
|
1244
|
+
return { rules: [] };
|
|
1245
|
+
}
|
|
1246
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
1247
|
+
const parsed = yaml.load(content);
|
|
1248
|
+
return { rules: parsed?.rules || [] };
|
|
1249
|
+
};
|
|
1250
|
+
var TRIGGER_FILTERS = {
|
|
1251
|
+
consumer_added: (c2) => c2.direction === "receives" && c2.changeType === "added",
|
|
1252
|
+
consumer_removed: (c2) => c2.direction === "receives" && c2.changeType === "removed",
|
|
1253
|
+
producer_added: (c2) => c2.direction === "sends" && c2.changeType === "added",
|
|
1254
|
+
producer_removed: (c2) => c2.direction === "sends" && c2.changeType === "removed"
|
|
1255
|
+
};
|
|
1256
|
+
var buildServiceMessageSets = (snapshot2) => {
|
|
1257
|
+
const produces = /* @__PURE__ */ new Map();
|
|
1258
|
+
const consumes = /* @__PURE__ */ new Map();
|
|
1259
|
+
for (const service of snapshot2.resources.services) {
|
|
1260
|
+
const serviceId = service.id;
|
|
1261
|
+
if (service.sends) {
|
|
1262
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1263
|
+
for (const s of service.sends) ids.add(s.id);
|
|
1264
|
+
produces.set(serviceId, ids);
|
|
1265
|
+
}
|
|
1266
|
+
if (service.receives) {
|
|
1267
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1268
|
+
for (const r of service.receives) ids.add(r.id);
|
|
1269
|
+
consumes.set(serviceId, ids);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return { produces, consumes };
|
|
1273
|
+
};
|
|
1274
|
+
var matchesResourceId = (resourceId, serviceId, resources, messageSets) => {
|
|
1275
|
+
return resources.some((r) => {
|
|
1276
|
+
if (r === "*") return true;
|
|
1277
|
+
if (r.startsWith("service:")) {
|
|
1278
|
+
if (serviceId) return serviceId === r.slice(8);
|
|
1279
|
+
return messageSets?.produces.get(r.slice(8))?.has(resourceId) ?? false;
|
|
1280
|
+
}
|
|
1281
|
+
if (r.startsWith("message:")) return resourceId === r.slice(8);
|
|
1282
|
+
if (r.startsWith("produces:")) return messageSets?.produces.get(r.slice(9))?.has(resourceId) ?? false;
|
|
1283
|
+
if (r.startsWith("consumes:")) return messageSets?.consumes.get(r.slice(9))?.has(resourceId) ?? false;
|
|
1284
|
+
return false;
|
|
1285
|
+
});
|
|
1286
|
+
};
|
|
1287
|
+
var REMOVED_TRIGGERS = /* @__PURE__ */ new Set(["consumer_removed", "producer_removed"]);
|
|
1288
|
+
var MESSAGE_RESOURCE_TYPES = /* @__PURE__ */ new Set(["event", "command", "query"]);
|
|
1289
|
+
var buildMessageMap = (snapshot2) => {
|
|
1290
|
+
const map = /* @__PURE__ */ new Map();
|
|
1291
|
+
for (const msg of snapshot2.resources.messages.events) map.set(msg.id, msg);
|
|
1292
|
+
for (const msg of snapshot2.resources.messages.commands) map.set(msg.id, msg);
|
|
1293
|
+
for (const msg of snapshot2.resources.messages.queries) map.set(msg.id, msg);
|
|
1294
|
+
return map;
|
|
1295
|
+
};
|
|
1296
|
+
var buildProducerIndex = (snapshot2) => {
|
|
1297
|
+
const index = /* @__PURE__ */ new Map();
|
|
1298
|
+
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);
|
|
1305
|
+
}
|
|
1306
|
+
const entry = {
|
|
1307
|
+
id: service.id,
|
|
1308
|
+
version: service.version
|
|
1309
|
+
};
|
|
1310
|
+
if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) {
|
|
1311
|
+
entry.owners = service.owners;
|
|
1312
|
+
}
|
|
1313
|
+
producers.push(entry);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return index;
|
|
1317
|
+
};
|
|
1318
|
+
var evaluateDeprecationRules = (diff, config, targetSnapshot, targetMessageSets, baseSnapshot) => {
|
|
1319
|
+
const deprecationRules = config.rules.filter((rule) => rule.when.includes("message_deprecated"));
|
|
1320
|
+
if (deprecationRules.length === 0) return [];
|
|
1321
|
+
const targetMessages = buildMessageMap(targetSnapshot);
|
|
1322
|
+
const baseMessages = baseSnapshot ? buildMessageMap(baseSnapshot) : void 0;
|
|
1323
|
+
const producerIndex = buildProducerIndex(targetSnapshot);
|
|
1324
|
+
const deprecatedResources = diff.resources.filter((rc) => {
|
|
1325
|
+
if (!MESSAGE_RESOURCE_TYPES.has(rc.type)) return false;
|
|
1326
|
+
if (!rc.changedFields?.includes("deprecated")) return false;
|
|
1327
|
+
const targetMessage = targetMessages.get(rc.resourceId);
|
|
1328
|
+
if (!targetMessage || !targetMessage.deprecated) return false;
|
|
1329
|
+
if (baseMessages) {
|
|
1330
|
+
const baseMessage = baseMessages.get(rc.resourceId);
|
|
1331
|
+
if (baseMessage && baseMessage.deprecated) return false;
|
|
1332
|
+
}
|
|
1333
|
+
return true;
|
|
1334
|
+
});
|
|
1335
|
+
if (deprecatedResources.length === 0) return [];
|
|
1336
|
+
const results = [];
|
|
1337
|
+
for (const rule of deprecationRules) {
|
|
1338
|
+
const matched = [];
|
|
1339
|
+
for (const rc of deprecatedResources) {
|
|
1340
|
+
if (!matchesResourceId(rc.resourceId, void 0, rule.resources, targetMessageSets)) continue;
|
|
1341
|
+
const producers = producerIndex.get(rc.resourceId) || [];
|
|
1342
|
+
matched.push({ resourceChange: rc, producerServices: producers });
|
|
1343
|
+
}
|
|
1344
|
+
if (matched.length > 0) {
|
|
1345
|
+
results.push({ rule, trigger: "message_deprecated", matchedChanges: [], deprecationChanges: matched });
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return results;
|
|
1349
|
+
};
|
|
1350
|
+
var evaluateGovernanceRules = (diff, config, targetSnapshot, baseSnapshot) => {
|
|
1351
|
+
const results = [];
|
|
1352
|
+
const targetMessageSets = targetSnapshot ? buildServiceMessageSets(targetSnapshot) : void 0;
|
|
1353
|
+
const baseMessageSets = baseSnapshot ? buildServiceMessageSets(baseSnapshot) : void 0;
|
|
1354
|
+
for (const rule of config.rules) {
|
|
1355
|
+
for (const trigger of rule.when) {
|
|
1356
|
+
const filter = TRIGGER_FILTERS[trigger];
|
|
1357
|
+
if (!filter) continue;
|
|
1358
|
+
const messageSets = REMOVED_TRIGGERS.has(trigger) && baseMessageSets ? baseMessageSets : targetMessageSets;
|
|
1359
|
+
const matchedChanges = diff.relationships.filter(
|
|
1360
|
+
(c2) => filter(c2) && matchesResourceId(c2.resourceId, c2.serviceId, rule.resources, messageSets)
|
|
1361
|
+
);
|
|
1362
|
+
if (matchedChanges.length > 0) {
|
|
1363
|
+
results.push({ rule, trigger, matchedChanges });
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (targetSnapshot && targetMessageSets) {
|
|
1368
|
+
results.push(...evaluateDeprecationRules(diff, config, targetSnapshot, targetMessageSets, baseSnapshot));
|
|
1369
|
+
}
|
|
1370
|
+
return results;
|
|
1371
|
+
};
|
|
1372
|
+
var PRODUCER_TRIGGERS = /* @__PURE__ */ new Set(["producer_added", "producer_removed"]);
|
|
1373
|
+
var isProducerTrigger = (trigger) => PRODUCER_TRIGGERS.has(trigger);
|
|
1374
|
+
var getChangeVerb = (trigger, changeType) => {
|
|
1375
|
+
const producer = isProducerTrigger(trigger);
|
|
1376
|
+
return changeType === "added" ? producer ? "now producing" : "now consuming" : producer ? "no longer producing" : "no longer consuming";
|
|
1377
|
+
};
|
|
1378
|
+
var resolveEnvVars = (value) => {
|
|
1379
|
+
return value.replace(/\$([A-Z_][A-Z0-9_]*)/g, (match, varName) => {
|
|
1380
|
+
const envValue = process.env[varName];
|
|
1381
|
+
if (envValue === void 0) {
|
|
1382
|
+
throw new Error(`Environment variable ${varName} is not set`);
|
|
1383
|
+
}
|
|
1384
|
+
return envValue;
|
|
1385
|
+
});
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
// src/cli/governance/actions.ts
|
|
1389
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1390
|
+
var buildMessageTypeMap = (snapshot2) => {
|
|
1391
|
+
const map = /* @__PURE__ */ new Map();
|
|
1392
|
+
for (const event of snapshot2.resources.messages.events) {
|
|
1393
|
+
map.set(event.id, "event");
|
|
1394
|
+
}
|
|
1395
|
+
for (const command of snapshot2.resources.messages.commands) {
|
|
1396
|
+
map.set(command.id, "command");
|
|
1397
|
+
}
|
|
1398
|
+
for (const query of snapshot2.resources.messages.queries) {
|
|
1399
|
+
map.set(query.id, "query");
|
|
1400
|
+
}
|
|
1401
|
+
return map;
|
|
1402
|
+
};
|
|
1403
|
+
var buildServiceOwnersMap = (snapshot2) => {
|
|
1404
|
+
const map = /* @__PURE__ */ new Map();
|
|
1405
|
+
for (const service of snapshot2.resources.services) {
|
|
1406
|
+
if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) {
|
|
1407
|
+
map.set(service.id, service.owners);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return map;
|
|
1411
|
+
};
|
|
1412
|
+
var executeGovernanceActions = async (results, opts = {}) => {
|
|
1413
|
+
const { messageTypes, status, serviceOwners } = opts;
|
|
1414
|
+
const webhookCalls = [];
|
|
1415
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1416
|
+
for (const result of results) {
|
|
1417
|
+
for (const action of result.rule.actions) {
|
|
1418
|
+
if (action.type !== "webhook") continue;
|
|
1419
|
+
const url = resolveEnvVars(action.url);
|
|
1420
|
+
const headers = { "Content-Type": "application/json" };
|
|
1421
|
+
if (action.headers) {
|
|
1422
|
+
for (const [key, value] of Object.entries(action.headers)) {
|
|
1423
|
+
headers[key] = resolveEnvVars(value);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (result.deprecationChanges && result.deprecationChanges.length > 0) {
|
|
1427
|
+
for (const dc of result.deprecationChanges) {
|
|
1428
|
+
const messageType = messageTypes?.get(dc.resourceChange.resourceId) || "message";
|
|
1429
|
+
const producers = dc.producerServices.length > 0 ? dc.producerServices : [{ id: "unknown", version: "unknown" }];
|
|
1430
|
+
for (const producer of producers) {
|
|
1431
|
+
const payload = {
|
|
1432
|
+
specversion: "1.0",
|
|
1433
|
+
type: `eventcatalog.governance.message_deprecated`,
|
|
1434
|
+
source: "eventcatalog/governance",
|
|
1435
|
+
id: randomUUID2(),
|
|
1436
|
+
time: now,
|
|
1437
|
+
datacontenttype: "application/json",
|
|
1438
|
+
data: {
|
|
1439
|
+
schemaVersion: 1,
|
|
1440
|
+
...status && { status },
|
|
1441
|
+
summary: `${dc.resourceChange.resourceId} (${messageType}) has been deprecated by ${producer.id}`,
|
|
1442
|
+
producer: {
|
|
1443
|
+
id: producer.id,
|
|
1444
|
+
version: producer.version,
|
|
1445
|
+
...producer.owners && { owners: producer.owners }
|
|
1446
|
+
},
|
|
1447
|
+
message: {
|
|
1448
|
+
id: dc.resourceChange.resourceId,
|
|
1449
|
+
version: dc.resourceChange.version,
|
|
1450
|
+
type: messageType
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
webhookCalls.push({
|
|
1455
|
+
urlTemplate: action.url,
|
|
1456
|
+
request: fetch(url, { method: "POST", headers, body: JSON.stringify(payload) })
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
for (const change of result.matchedChanges) {
|
|
1463
|
+
const verb = getChangeVerb(result.trigger, change.changeType);
|
|
1464
|
+
const messageType = messageTypes?.get(change.resourceId) || "message";
|
|
1465
|
+
const serviceRole = isProducerTrigger(result.trigger) ? "producer" : "consumer";
|
|
1466
|
+
const payload = {
|
|
1467
|
+
specversion: "1.0",
|
|
1468
|
+
type: `eventcatalog.governance.${result.trigger}`,
|
|
1469
|
+
source: "eventcatalog/governance",
|
|
1470
|
+
id: randomUUID2(),
|
|
1471
|
+
time: now,
|
|
1472
|
+
datacontenttype: "application/json",
|
|
1473
|
+
data: {
|
|
1474
|
+
schemaVersion: 1,
|
|
1475
|
+
...status && { status },
|
|
1476
|
+
summary: `${change.serviceId} is ${verb} the ${messageType} ${change.resourceId}`,
|
|
1477
|
+
[serviceRole]: {
|
|
1478
|
+
id: change.serviceId,
|
|
1479
|
+
version: change.serviceVersion,
|
|
1480
|
+
...serviceOwners?.get(change.serviceId) && { owners: serviceOwners.get(change.serviceId) }
|
|
1481
|
+
},
|
|
1482
|
+
message: {
|
|
1483
|
+
id: change.resourceId,
|
|
1484
|
+
version: change.resourceVersion,
|
|
1485
|
+
type: messageType
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
webhookCalls.push({
|
|
1490
|
+
urlTemplate: action.url,
|
|
1491
|
+
request: fetch(url, { method: "POST", headers, body: JSON.stringify(payload) })
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
const settled = await Promise.allSettled(webhookCalls.map((c2) => c2.request));
|
|
1497
|
+
return settled.map((result, i) => {
|
|
1498
|
+
const url = webhookCalls[i].urlTemplate;
|
|
1499
|
+
if (result.status === "fulfilled") {
|
|
1500
|
+
const res = result.value;
|
|
1501
|
+
if (!res.ok) {
|
|
1502
|
+
return ` Webhook failed: ${url} \u2717 (HTTP ${res.status})`;
|
|
1503
|
+
}
|
|
1504
|
+
return ` Webhook sent: ${url} \u2713`;
|
|
1505
|
+
}
|
|
1506
|
+
return ` Webhook failed: ${url} \u2717 (${result.reason instanceof Error ? result.reason.message : String(result.reason)})`;
|
|
1507
|
+
});
|
|
1508
|
+
};
|
|
1509
|
+
|
|
1510
|
+
// src/cli/governance/format.ts
|
|
1511
|
+
var formatGovernanceOutput = (results) => {
|
|
1512
|
+
if (results.length === 0) {
|
|
1513
|
+
return "No governance rules triggered. Catalog is compliant.";
|
|
1514
|
+
}
|
|
1515
|
+
const lines = ["Governance:", ""];
|
|
1516
|
+
for (const result of results) {
|
|
1517
|
+
lines.push(` Rule "${result.rule.name}" triggered (${result.trigger}):`);
|
|
1518
|
+
if (result.deprecationChanges && result.deprecationChanges.length > 0) {
|
|
1519
|
+
for (const dc of result.deprecationChanges) {
|
|
1520
|
+
const producers = dc.producerServices.length > 0 ? dc.producerServices.map((p) => p.id).join(", ") : "unknown producer";
|
|
1521
|
+
lines.push(` ! ${dc.resourceChange.resourceId} (${dc.resourceChange.type}) deprecated by ${producers}`);
|
|
1522
|
+
}
|
|
1523
|
+
} else {
|
|
1524
|
+
for (const change of result.matchedChanges) {
|
|
1525
|
+
const prefix = change.changeType === "added" ? "+" : "-";
|
|
1526
|
+
const verb = getChangeVerb(result.trigger, change.changeType);
|
|
1527
|
+
lines.push(` ${prefix} ${change.serviceId} is ${verb} ${change.resourceId}`);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
lines.push("");
|
|
1531
|
+
}
|
|
1532
|
+
return lines.join("\n");
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
// src/cli/governance/check.ts
|
|
1536
|
+
import path3 from "path";
|
|
1537
|
+
import { execSync } from "child_process";
|
|
1538
|
+
import { mkdtempSync, rmSync as rmSync2 } from "fs";
|
|
1539
|
+
import { tmpdir } from "os";
|
|
1540
|
+
import dotenv from "dotenv";
|
|
1541
|
+
import createSDK6 from "@eventcatalog/sdk";
|
|
1542
|
+
import { isEventCatalogScaleEnabled } from "@eventcatalog/license";
|
|
1543
|
+
var BRANCH_NAME_RE = /^[a-zA-Z0-9._\-/]+$/;
|
|
1544
|
+
var extractBranchToTempDir = (branch, catalogDir, tempDirs) => {
|
|
1545
|
+
if (!BRANCH_NAME_RE.test(branch)) {
|
|
1546
|
+
throw new Error(`Invalid branch name: "${branch}"`);
|
|
1547
|
+
}
|
|
1548
|
+
const tmpDir = mkdtempSync(path3.join(tmpdir(), "ec-governance-"));
|
|
1549
|
+
tempDirs.push(tmpDir);
|
|
1550
|
+
try {
|
|
1551
|
+
execSync(`git archive ${branch} | tar -x -C ${tmpDir}`, { cwd: catalogDir, stdio: "pipe" });
|
|
1552
|
+
} catch {
|
|
1553
|
+
throw new Error(`Failed to extract branch "${branch}". Is it a valid git branch?`);
|
|
1554
|
+
}
|
|
1555
|
+
return tmpDir;
|
|
1556
|
+
};
|
|
1557
|
+
var governanceCheck = async (opts) => {
|
|
1558
|
+
const dir = path3.resolve(opts.dir);
|
|
1559
|
+
dotenv.config({ path: path3.join(dir, ".env") });
|
|
1560
|
+
const isScale = await isEventCatalogScaleEnabled();
|
|
1561
|
+
if (!isScale) {
|
|
1562
|
+
throw new Error("Governance requires an EventCatalog Scale plan. Learn more at https://eventcatalog.dev/pricing");
|
|
1563
|
+
}
|
|
1564
|
+
const baseBranch = opts.base || "main";
|
|
1565
|
+
const tempDirs = [];
|
|
1566
|
+
const trackTempDir = (prefix) => {
|
|
1567
|
+
const d = mkdtempSync(path3.join(tmpdir(), prefix));
|
|
1568
|
+
tempDirs.push(d);
|
|
1569
|
+
return d;
|
|
1570
|
+
};
|
|
1571
|
+
try {
|
|
1572
|
+
const baseTmpDir = extractBranchToTempDir(baseBranch, dir, tempDirs);
|
|
1573
|
+
const baseSnapshotDir = trackTempDir("ec-snap-base-");
|
|
1574
|
+
const targetSnapshotDir = trackTempDir("ec-snap-target-");
|
|
1575
|
+
const baseSDK = createSDK6(baseTmpDir);
|
|
1576
|
+
const baseResult = await baseSDK.createSnapshot({ label: `base-${baseBranch}`, outputDir: baseSnapshotDir });
|
|
1577
|
+
let targetResult;
|
|
1578
|
+
if (opts.target) {
|
|
1579
|
+
const targetTmpDir = extractBranchToTempDir(opts.target, dir, tempDirs);
|
|
1580
|
+
const targetSDK = createSDK6(targetTmpDir);
|
|
1581
|
+
targetResult = await targetSDK.createSnapshot({ label: `target-${opts.target}`, outputDir: targetSnapshotDir });
|
|
1582
|
+
} else {
|
|
1583
|
+
const targetSDK = createSDK6(dir);
|
|
1584
|
+
targetResult = await targetSDK.createSnapshot({ label: "current", outputDir: targetSnapshotDir });
|
|
1585
|
+
}
|
|
1586
|
+
const diff = await baseSDK.diffSnapshots(baseResult.filePath, targetResult.filePath);
|
|
1587
|
+
const config = loadGovernanceConfig(dir);
|
|
1588
|
+
if (config.rules.length === 0) {
|
|
1589
|
+
return "No governance.yaml (or governance.yml) found or no rules defined.";
|
|
1590
|
+
}
|
|
1591
|
+
const results = evaluateGovernanceRules(diff, config, targetResult.snapshot, baseResult.snapshot);
|
|
1592
|
+
const messageTypes = buildMessageTypeMap(targetResult.snapshot);
|
|
1593
|
+
const serviceOwners = buildServiceOwnersMap(targetResult.snapshot);
|
|
1594
|
+
const actionOutput = await executeGovernanceActions(results, {
|
|
1595
|
+
messageTypes,
|
|
1596
|
+
status: opts.status,
|
|
1597
|
+
serviceOwners
|
|
1598
|
+
});
|
|
1599
|
+
if (opts.format === "json") {
|
|
1600
|
+
return JSON.stringify({ baseBranch, target: opts.target || "working directory", results, diff: diff.summary }, null, 2);
|
|
1601
|
+
}
|
|
1602
|
+
const targetLabel = opts.target || "working directory";
|
|
1603
|
+
const lines = [`Governance check: comparing ${targetLabel} against ${baseBranch}`, ""];
|
|
1604
|
+
lines.push(formatGovernanceOutput(results));
|
|
1605
|
+
if (actionOutput.length > 0) {
|
|
1606
|
+
lines.push("");
|
|
1607
|
+
lines.push(...actionOutput);
|
|
1608
|
+
}
|
|
1609
|
+
if (results.length > 0) {
|
|
1610
|
+
const webhookCount = actionOutput.filter((l) => l.includes("Webhook sent")).length;
|
|
1611
|
+
const parts = [`${results.length} rule${results.length === 1 ? "" : "s"} triggered`];
|
|
1612
|
+
if (webhookCount > 0) parts.push(`${webhookCount} webhook${webhookCount === 1 ? "" : "s"} sent`);
|
|
1613
|
+
lines.push("");
|
|
1614
|
+
lines.push(parts.join(", ") + ".");
|
|
1615
|
+
}
|
|
1616
|
+
return lines.join("\n");
|
|
1617
|
+
} finally {
|
|
1618
|
+
for (const d of tempDirs) {
|
|
1619
|
+
rmSync2(d, { recursive: true, force: true });
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1147
1624
|
// src/cli/index.ts
|
|
1148
1625
|
var version = "1.0.0";
|
|
1149
1626
|
try {
|
|
1150
|
-
const packageJsonPath =
|
|
1627
|
+
const packageJsonPath = resolve4(__dirname, "../../package.json");
|
|
1151
1628
|
const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
|
|
1152
1629
|
version = packageJson.version;
|
|
1153
1630
|
} catch {
|
|
@@ -1216,6 +1693,58 @@ program.command("import [files...]").description("Import EventCatalog DSL (.ec)
|
|
|
1216
1693
|
process.exit(1);
|
|
1217
1694
|
}
|
|
1218
1695
|
});
|
|
1696
|
+
var snapshot = program.command("snapshot").description("Create, diff, and list catalog snapshots");
|
|
1697
|
+
snapshot.command("create").description("Take a point-in-time snapshot of the catalog").option("--label <label>", "Human-readable label (default: ISO timestamp)").option("-o, --output <path>", "Output directory for the snapshot file").option("--stdout", "Print JSON to stdout instead of writing a file", false).action(async (opts) => {
|
|
1698
|
+
try {
|
|
1699
|
+
const globalOpts = program.opts();
|
|
1700
|
+
const dir = globalOpts.dir || ".";
|
|
1701
|
+
const result = await snapshotCreate({ label: opts.label, output: opts.output, stdout: opts.stdout, dir });
|
|
1702
|
+
console.log(result);
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1705
|
+
process.exit(1);
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1708
|
+
snapshot.command("diff <fileA> <fileB>").description("Compare two snapshot files and output a structured diff").option("--format <format>", "Output format: text or json", "text").action(async (fileA, fileB, opts) => {
|
|
1709
|
+
try {
|
|
1710
|
+
const globalOpts = program.opts();
|
|
1711
|
+
const dir = globalOpts.dir || ".";
|
|
1712
|
+
const result = await snapshotDiff({ fileA, fileB, format: opts.format, dir });
|
|
1713
|
+
console.log(result);
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1716
|
+
process.exit(1);
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
snapshot.command("list").description("List all snapshots in the catalog").option("--format <format>", "Output format: text or json", "text").action(async (opts) => {
|
|
1720
|
+
try {
|
|
1721
|
+
const globalOpts = program.opts();
|
|
1722
|
+
const dir = globalOpts.dir || ".";
|
|
1723
|
+
const result = await snapshotList({ format: opts.format, dir });
|
|
1724
|
+
console.log(result);
|
|
1725
|
+
} catch (error) {
|
|
1726
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1727
|
+
process.exit(1);
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1730
|
+
var governance = program.command("governance").description("Run governance rules against catalog changes");
|
|
1731
|
+
governance.command("check").description("Compare catalog against a base branch and evaluate governance rules").option("--base <branch>", "Base branch to compare against (default: main)").option("--target <branch>", "Target branch to compare (default: current working directory)").option("--format <format>", "Output format: text or json", "text").option("--status <status>", "Status label to include in webhook payloads (e.g. proposed, approved)").action(async (opts) => {
|
|
1732
|
+
try {
|
|
1733
|
+
const globalOpts = program.opts();
|
|
1734
|
+
const dir = globalOpts.dir || ".";
|
|
1735
|
+
const result = await governanceCheck({
|
|
1736
|
+
base: opts.base,
|
|
1737
|
+
target: opts.target,
|
|
1738
|
+
format: opts.format,
|
|
1739
|
+
status: opts.status,
|
|
1740
|
+
dir
|
|
1741
|
+
});
|
|
1742
|
+
console.log(result);
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1745
|
+
process.exit(1);
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1219
1748
|
program.arguments("<function> [args...]").action(async (functionName, args) => {
|
|
1220
1749
|
try {
|
|
1221
1750
|
const options = program.opts();
|