api-tests-coverage 1.0.22 → 1.0.24
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/README.md +39 -0
- package/config.yaml.example +35 -0
- package/dist/dashboard/dist/assets/_basePickBy-BamgEusj.js +1 -0
- package/dist/dashboard/dist/assets/_basePickBy-D4Hl8chy.js +1 -0
- package/dist/dashboard/dist/assets/_baseUniq-BBhq12Ja.js +1 -0
- package/dist/dashboard/dist/assets/_baseUniq-BSUUnV_V.js +1 -0
- package/dist/dashboard/dist/assets/arc-Dh-qL1ea.js +1 -0
- package/dist/dashboard/dist/assets/arc-DhDluTY5.js +1 -0
- package/dist/dashboard/dist/assets/architectureDiagram-VXUJARFQ-BxQ_anmt.js +36 -0
- package/dist/dashboard/dist/assets/architectureDiagram-VXUJARFQ-DGlUU7dC.js +36 -0
- package/dist/dashboard/dist/assets/blockDiagram-VD42YOAC-CgXi3kEZ.js +122 -0
- package/dist/dashboard/dist/assets/blockDiagram-VD42YOAC-Krm3lc7z.js +122 -0
- package/dist/dashboard/dist/assets/c4Diagram-YG6GDRKO-Cfd4OeWg.js +10 -0
- package/dist/dashboard/dist/assets/c4Diagram-YG6GDRKO-Cr3xB15y.js +10 -0
- package/dist/dashboard/dist/assets/channel-DYAie-7m.js +1 -0
- package/dist/dashboard/dist/assets/channel-a4t2URTe.js +1 -0
- package/dist/dashboard/dist/assets/chunk-4BX2VUAB-BaW3__pI.js +1 -0
- package/dist/dashboard/dist/assets/chunk-4BX2VUAB-ljBQ5lHA.js +1 -0
- package/dist/dashboard/dist/assets/chunk-55IACEB6-Cikrdc3Q.js +1 -0
- package/dist/dashboard/dist/assets/chunk-55IACEB6-DyYevfEQ.js +1 -0
- package/dist/dashboard/dist/assets/chunk-B4BG7PRW-C2bwZFec.js +165 -0
- package/dist/dashboard/dist/assets/chunk-B4BG7PRW-dtHgbkmj.js +165 -0
- package/dist/dashboard/dist/assets/chunk-DI55MBZ5-Cv3hm2Ke.js +220 -0
- package/dist/dashboard/dist/assets/chunk-DI55MBZ5-DO0T2xne.js +220 -0
- package/dist/dashboard/dist/assets/chunk-FMBD7UC4-CCYA4j_f.js +15 -0
- package/dist/dashboard/dist/assets/chunk-FMBD7UC4-Ds1_OqKH.js +15 -0
- package/dist/dashboard/dist/assets/chunk-QN33PNHL-B6zkzIAo.js +1 -0
- package/dist/dashboard/dist/assets/chunk-QN33PNHL-Cdhqs7xo.js +1 -0
- package/dist/dashboard/dist/assets/chunk-QZHKN3VN-BzHw38Ki.js +1 -0
- package/dist/dashboard/dist/assets/chunk-QZHKN3VN-C7xuA6tl.js +1 -0
- package/dist/dashboard/dist/assets/chunk-TZMSLE5B-D_ea_wdP.js +1 -0
- package/dist/dashboard/dist/assets/chunk-TZMSLE5B-dkJ0rsgF.js +1 -0
- package/dist/dashboard/dist/assets/classDiagram-2ON5EDUG-B6SxXE6T.js +1 -0
- package/dist/dashboard/dist/assets/classDiagram-2ON5EDUG-DiIv5Pho.js +1 -0
- package/dist/dashboard/dist/assets/classDiagram-v2-WZHVMYZB-B6SxXE6T.js +1 -0
- package/dist/dashboard/dist/assets/classDiagram-v2-WZHVMYZB-DiIv5Pho.js +1 -0
- package/dist/dashboard/dist/assets/clone-B4LorrSy.js +1 -0
- package/dist/dashboard/dist/assets/clone-kcKg1tUH.js +1 -0
- package/dist/dashboard/dist/assets/cose-bilkent-S5V4N54A-DasyAK5c.js +1 -0
- package/dist/dashboard/dist/assets/cose-bilkent-S5V4N54A-jzGbyPIS.js +1 -0
- package/dist/dashboard/dist/assets/dagre-6UL2VRFP-D7rgvBx1.js +4 -0
- package/dist/dashboard/dist/assets/dagre-6UL2VRFP-m-5bs635.js +4 -0
- package/dist/dashboard/dist/assets/diagram-PSM6KHXK-2rYklqon.js +24 -0
- package/dist/dashboard/dist/assets/diagram-PSM6KHXK-CYFwwEdy.js +24 -0
- package/dist/dashboard/dist/assets/diagram-QEK2KX5R-CGrvALqm.js +43 -0
- package/dist/dashboard/dist/assets/diagram-QEK2KX5R-m4Fda1GA.js +43 -0
- package/dist/dashboard/dist/assets/diagram-S2PKOQOG-BDVk4AKU.js +24 -0
- package/dist/dashboard/dist/assets/diagram-S2PKOQOG-DA3c-QP4.js +24 -0
- package/dist/dashboard/dist/assets/erDiagram-Q2GNP2WA-3-jAbxQ6.js +60 -0
- package/dist/dashboard/dist/assets/erDiagram-Q2GNP2WA-BsYH8cLH.js +60 -0
- package/dist/dashboard/dist/assets/flowDiagram-NV44I4VS-Cfv1hkQB.js +162 -0
- package/dist/dashboard/dist/assets/flowDiagram-NV44I4VS-Da_JhBCy.js +162 -0
- package/dist/dashboard/dist/assets/ganttDiagram-JELNMOA3-B7GZPGck.js +267 -0
- package/dist/dashboard/dist/assets/ganttDiagram-JELNMOA3-D8FTswNn.js +267 -0
- package/dist/dashboard/dist/assets/gitGraphDiagram-V2S2FVAM-BFJR-ITH.js +65 -0
- package/dist/dashboard/dist/assets/gitGraphDiagram-V2S2FVAM-K8X-_4av.js +65 -0
- package/dist/dashboard/dist/assets/graph-CIvnjOQQ.js +1 -0
- package/dist/dashboard/dist/assets/graph-CfuGK9GG.js +1 -0
- package/dist/dashboard/dist/assets/index-BWX0sSZn.css +1 -0
- package/dist/dashboard/dist/assets/index-CbAFWEor.js +777 -0
- package/dist/dashboard/dist/assets/index-DS-KIxwV.js +777 -0
- package/dist/dashboard/dist/assets/infoDiagram-HS3SLOUP-CaIaIUhT.js +2 -0
- package/dist/dashboard/dist/assets/infoDiagram-HS3SLOUP-OcK0Lxgi.js +2 -0
- package/dist/dashboard/dist/assets/journeyDiagram-XKPGCS4Q-D6dwPswq.js +139 -0
- package/dist/dashboard/dist/assets/journeyDiagram-XKPGCS4Q-DTJukVOY.js +139 -0
- package/dist/dashboard/dist/assets/kanban-definition-3W4ZIXB7-CERyhhrH.js +89 -0
- package/dist/dashboard/dist/assets/kanban-definition-3W4ZIXB7-Di65fNuD.js +89 -0
- package/dist/dashboard/dist/assets/layout-DAt24RVX.js +1 -0
- package/dist/dashboard/dist/assets/layout-v7cCi3Fl.js +1 -0
- package/dist/dashboard/dist/assets/mindmap-definition-VGOIOE7T-BvNtTz8N.js +68 -0
- package/dist/dashboard/dist/assets/mindmap-definition-VGOIOE7T-DxI8MXCF.js +68 -0
- package/dist/dashboard/dist/assets/pieDiagram-ADFJNKIX-BafKx3_Y.js +30 -0
- package/dist/dashboard/dist/assets/pieDiagram-ADFJNKIX-Cjg80C_b.js +30 -0
- package/dist/dashboard/dist/assets/quadrantDiagram-AYHSOK5B-BcZsArkk.js +7 -0
- package/dist/dashboard/dist/assets/quadrantDiagram-AYHSOK5B-YtFFUYGD.js +7 -0
- package/dist/dashboard/dist/assets/requirementDiagram-UZGBJVZJ-CqFAO2t6.js +64 -0
- package/dist/dashboard/dist/assets/requirementDiagram-UZGBJVZJ-DLV2LTE5.js +64 -0
- package/dist/dashboard/dist/assets/sankeyDiagram-TZEHDZUN-C6_Urrii.js +10 -0
- package/dist/dashboard/dist/assets/sankeyDiagram-TZEHDZUN-CqSaCg-3.js +10 -0
- package/dist/dashboard/dist/assets/sequenceDiagram-WL72ISMW-6IXD1uqW.js +145 -0
- package/dist/dashboard/dist/assets/sequenceDiagram-WL72ISMW-D33UwAtz.js +145 -0
- package/dist/dashboard/dist/assets/stateDiagram-FKZM4ZOC-DSp83t9D.js +1 -0
- package/dist/dashboard/dist/assets/stateDiagram-FKZM4ZOC-DvSVQAfp.js +1 -0
- package/dist/dashboard/dist/assets/stateDiagram-v2-4FDKWEC3-BMFdt0QQ.js +1 -0
- package/dist/dashboard/dist/assets/stateDiagram-v2-4FDKWEC3-OTWrEpQO.js +1 -0
- package/dist/dashboard/dist/assets/timeline-definition-IT6M3QCI-Cll7Nvth.js +61 -0
- package/dist/dashboard/dist/assets/timeline-definition-IT6M3QCI-D5Bb3Jj7.js +61 -0
- package/dist/dashboard/dist/assets/treemap-GDKQZRPO-CKbkkwye.js +162 -0
- package/dist/dashboard/dist/assets/treemap-GDKQZRPO-DtqX8zNC.js +162 -0
- package/dist/dashboard/dist/assets/xychartDiagram-PRI3JC2R-C_Tlzchx.js +7 -0
- package/dist/dashboard/dist/assets/xychartDiagram-PRI3JC2R-zxwS9i0A.js +7 -0
- package/dist/dashboard/dist/index.html +2 -2
- package/dist/dashboard/dist/reports/coverage-summary.json +75 -1
- package/dist/dashboard/dist/reports/security-full.json +157 -0
- package/dist/src/compatibilityCoverage.d.ts +34 -15
- package/dist/src/compatibilityCoverage.d.ts.map +1 -1
- package/dist/src/compatibilityCoverage.js +387 -85
- package/dist/src/config/defaultConfig.d.ts.map +1 -1
- package/dist/src/config/defaultConfig.js +62 -0
- package/dist/src/config/schema.d.ts.map +1 -1
- package/dist/src/config/schema.js +1 -1
- package/dist/src/config/types.d.ts +81 -1
- package/dist/src/config/types.d.ts.map +1 -1
- package/dist/src/config/validateConfig.d.ts.map +1 -1
- package/dist/src/config/validateConfig.js +126 -0
- package/dist/src/contracts/compatibilityMatrix.d.ts +20 -0
- package/dist/src/contracts/compatibilityMatrix.d.ts.map +1 -0
- package/dist/src/contracts/compatibilityMatrix.js +198 -0
- package/dist/src/contracts/pactBrokerClient.d.ts +10 -0
- package/dist/src/contracts/pactBrokerClient.d.ts.map +1 -0
- package/dist/src/contracts/pactBrokerClient.js +117 -0
- package/dist/src/contracts/schemaEvolutionChecker.d.ts +17 -0
- package/dist/src/contracts/schemaEvolutionChecker.d.ts.map +1 -0
- package/dist/src/contracts/schemaEvolutionChecker.js +95 -0
- package/dist/src/contracts/springCloudContractParser.d.ts +10 -0
- package/dist/src/contracts/springCloudContractParser.d.ts.map +1 -0
- package/dist/src/contracts/springCloudContractParser.js +144 -0
- package/dist/src/discovery/fileClassifier.d.ts.map +1 -1
- package/dist/src/discovery/fileClassifier.js +25 -0
- package/dist/src/discovery/projectDiscovery.d.ts +2 -0
- package/dist/src/discovery/projectDiscovery.d.ts.map +1 -1
- package/dist/src/discovery/projectDiscovery.js +25 -25
- package/dist/src/index.js +233 -16
- package/dist/src/inference/routeInference.d.ts +10 -2
- package/dist/src/inference/routeInference.d.ts.map +1 -1
- package/dist/src/inference/routeInference.js +363 -62
- package/dist/src/languageDetection.d.ts.map +1 -1
- package/dist/src/languageDetection.js +21 -4
- package/dist/src/lib/index.d.ts +3 -0
- package/dist/src/lib/index.d.ts.map +1 -1
- package/dist/src/lib/index.js +3 -1
- package/dist/src/pipeline/stages/tia/parameterizedTestExpander.js +152 -79
- package/dist/src/pipeline/stages/tia/testEndpointMapper.d.ts +5 -1
- package/dist/src/pipeline/stages/tia/testEndpointMapper.d.ts.map +1 -1
- package/dist/src/pipeline/stages/tia/testEndpointMapper.js +356 -42
- package/dist/src/pipeline/stages/tia/testLayerClassifier.d.ts.map +1 -1
- package/dist/src/pipeline/stages/tia/testLayerClassifier.js +20 -5
- package/dist/src/pipeline/stages/tia/tiaStage.d.ts.map +1 -1
- package/dist/src/pipeline/stages/tia/tiaStage.js +3 -1
- package/dist/src/pipeline/stages/tia/types.d.ts +11 -2
- package/dist/src/pipeline/stages/tia/types.d.ts.map +1 -1
- package/dist/src/projectDefaults.d.ts +6 -0
- package/dist/src/projectDefaults.d.ts.map +1 -0
- package/dist/src/projectDefaults.js +43 -0
- package/dist/src/security/hub.d.ts +81 -0
- package/dist/src/security/hub.d.ts.map +1 -0
- package/dist/src/security/hub.js +420 -0
- package/dist/src/security/index.d.ts +1 -0
- package/dist/src/security/index.d.ts.map +1 -1
- package/dist/src/security/index.js +8 -2
- package/dist/src/security/normalizers/gitleaks.d.ts +7 -0
- package/dist/src/security/normalizers/gitleaks.d.ts.map +1 -0
- package/dist/src/security/normalizers/gitleaks.js +32 -0
- package/dist/src/security/scanners/gitleaks.d.ts +3 -0
- package/dist/src/security/scanners/gitleaks.d.ts.map +1 -0
- package/dist/src/security/scanners/gitleaks.js +105 -0
- package/dist/src/security/scanners/semgrep.d.ts.map +1 -1
- package/dist/src/security/scanners/semgrep.js +24 -2
- package/dist/src/security/scanners/trivy.d.ts.map +1 -1
- package/dist/src/security/scanners/trivy.js +24 -2
- package/dist/src/security/scanners/zap.d.ts.map +1 -1
- package/dist/src/security/scanners/zap.js +27 -2
- package/dist/src/security/types.d.ts +15 -1
- package/dist/src/security/types.d.ts.map +1 -1
- package/dist/src/streaming/schema/index.d.ts +23 -0
- package/dist/src/streaming/schema/index.d.ts.map +1 -0
- package/dist/src/streaming/schema/index.js +196 -0
- package/dist/src/summary/markdownRenderer.d.ts.map +1 -1
- package/dist/src/summary/markdownRenderer.js +15 -1
- package/dist/src/summary/summaryTypes.d.ts.map +1 -1
- package/dist/src/summary/summaryTypes.js +1 -0
- package/dist/src/unitAnalysis.d.ts +145 -0
- package/dist/src/unitAnalysis.d.ts.map +1 -0
- package/dist/src/unitAnalysis.js +1392 -0
- package/package.json +1 -1
|
@@ -42,6 +42,7 @@ exports.parseContractFiles = parseContractFiles;
|
|
|
42
42
|
exports.verifyContracts = verifyContracts;
|
|
43
43
|
exports.computeCompatibilityPercent = computeCompatibilityPercent;
|
|
44
44
|
exports.computeContractCoveragePercent = computeContractCoveragePercent;
|
|
45
|
+
exports.computeCanDeploy = computeCanDeploy;
|
|
45
46
|
exports.buildCompatibilityReport = buildCompatibilityReport;
|
|
46
47
|
exports.generateCompatibilityReports = generateCompatibilityReports;
|
|
47
48
|
const swagger_parser_1 = __importDefault(require("@apidevtools/swagger-parser"));
|
|
@@ -77,6 +78,183 @@ function getParameters(op) {
|
|
|
77
78
|
function paramKey(p) {
|
|
78
79
|
return `${p.in}:${p.name}`;
|
|
79
80
|
}
|
|
81
|
+
function createEndpointChange(change) {
|
|
82
|
+
var _a;
|
|
83
|
+
const breaking = (_a = change.breaking) !== null && _a !== void 0 ? _a : change.severity === 'BREAKING';
|
|
84
|
+
return { ...change, breaking };
|
|
85
|
+
}
|
|
86
|
+
function schemaObject(schema) {
|
|
87
|
+
if (!schema || typeof schema !== 'object')
|
|
88
|
+
return undefined;
|
|
89
|
+
return schema;
|
|
90
|
+
}
|
|
91
|
+
function getSchemaType(schema) {
|
|
92
|
+
if (!schema)
|
|
93
|
+
return undefined;
|
|
94
|
+
if (Array.isArray(schema.type))
|
|
95
|
+
return schema.type.join('|');
|
|
96
|
+
return typeof schema.type === 'string' ? schema.type : undefined;
|
|
97
|
+
}
|
|
98
|
+
function getRequiredFields(schema) {
|
|
99
|
+
return new Set(Array.isArray(schema === null || schema === void 0 ? void 0 : schema.required) ? schema.required : []);
|
|
100
|
+
}
|
|
101
|
+
function describeFieldRemoval(context, fieldPath) {
|
|
102
|
+
if (context.kind === 'response') {
|
|
103
|
+
return `Response field removed from ${context.method.toUpperCase()} ${context.path}: ${fieldPath}`;
|
|
104
|
+
}
|
|
105
|
+
return `Schema field removed from ${context.method.toUpperCase()} ${context.path}: ${fieldPath}`;
|
|
106
|
+
}
|
|
107
|
+
function getOperationSecurity(api, operation) {
|
|
108
|
+
var _a;
|
|
109
|
+
if (operation.security)
|
|
110
|
+
return operation.security;
|
|
111
|
+
return ((_a = api.security) !== null && _a !== void 0 ? _a : []);
|
|
112
|
+
}
|
|
113
|
+
function isPublicEndpoint(security) {
|
|
114
|
+
return security.length === 0;
|
|
115
|
+
}
|
|
116
|
+
function compareEnumValues(oldSchema, newSchema, context, fieldPath) {
|
|
117
|
+
const oldEnum = Array.isArray(oldSchema.enum) ? oldSchema.enum.map(String) : [];
|
|
118
|
+
const newEnum = Array.isArray(newSchema.enum) ? newSchema.enum.map(String) : [];
|
|
119
|
+
if (oldEnum.length === 0 && newEnum.length === 0)
|
|
120
|
+
return [];
|
|
121
|
+
const changes = [];
|
|
122
|
+
const removedValues = oldEnum.filter((value) => !newEnum.includes(value));
|
|
123
|
+
for (const value of removedValues) {
|
|
124
|
+
changes.push(createEndpointChange({
|
|
125
|
+
method: context.method,
|
|
126
|
+
path: context.path,
|
|
127
|
+
changeType: 'removed-enum-value',
|
|
128
|
+
severity: 'BREAKING',
|
|
129
|
+
description: `Enum value removed from ${context.method.toUpperCase()} ${context.path}: ${fieldPath} no longer allows '${value}'`,
|
|
130
|
+
affectedField: fieldPath,
|
|
131
|
+
oldValue: value,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
const addedValues = newEnum.filter((value) => !oldEnum.includes(value));
|
|
135
|
+
for (const value of addedValues) {
|
|
136
|
+
changes.push(createEndpointChange({
|
|
137
|
+
method: context.method,
|
|
138
|
+
path: context.path,
|
|
139
|
+
changeType: 'added-enum-value',
|
|
140
|
+
severity: 'SAFE',
|
|
141
|
+
description: `Enum value added for ${context.method.toUpperCase()} ${context.path}: ${fieldPath} now allows '${value}'`,
|
|
142
|
+
affectedField: fieldPath,
|
|
143
|
+
newValue: value,
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
return changes;
|
|
147
|
+
}
|
|
148
|
+
function compareSchemasRecursive(oldSchema, newSchema, context, fieldPath) {
|
|
149
|
+
var _a, _b, _c;
|
|
150
|
+
if (fieldPath === void 0) { fieldPath = (_a = context.location) !== null && _a !== void 0 ? _a : 'body'; }
|
|
151
|
+
if (!oldSchema || !newSchema)
|
|
152
|
+
return [];
|
|
153
|
+
const changes = [];
|
|
154
|
+
const oldType = getSchemaType(oldSchema);
|
|
155
|
+
const newType = getSchemaType(newSchema);
|
|
156
|
+
if (oldType && newType && oldType !== newType) {
|
|
157
|
+
changes.push(createEndpointChange({
|
|
158
|
+
method: context.method,
|
|
159
|
+
path: context.path,
|
|
160
|
+
changeType: context.kind === 'response' ? 'changed-response-schema' : 'changed-schema',
|
|
161
|
+
severity: 'BREAKING',
|
|
162
|
+
description: `Schema type changed for ${context.method.toUpperCase()} ${context.path}: ${fieldPath} (${oldType} → ${newType})`,
|
|
163
|
+
affectedField: fieldPath,
|
|
164
|
+
oldValue: oldType,
|
|
165
|
+
newValue: newType,
|
|
166
|
+
}));
|
|
167
|
+
return changes;
|
|
168
|
+
}
|
|
169
|
+
changes.push(...compareEnumValues(oldSchema, newSchema, context, fieldPath));
|
|
170
|
+
if (oldSchema.nullable !== true && newSchema.nullable === true) {
|
|
171
|
+
changes.push(createEndpointChange({
|
|
172
|
+
method: context.method,
|
|
173
|
+
path: context.path,
|
|
174
|
+
changeType: context.kind === 'response' ? 'changed-response-schema' : 'changed-schema',
|
|
175
|
+
severity: 'DANGEROUS',
|
|
176
|
+
description: `Field became nullable in ${context.method.toUpperCase()} ${context.path}: ${fieldPath}`,
|
|
177
|
+
affectedField: fieldPath,
|
|
178
|
+
oldValue: 'non-nullable',
|
|
179
|
+
newValue: 'nullable',
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
if (oldSchema.default !== undefined && newSchema.default !== undefined && oldSchema.default !== newSchema.default) {
|
|
183
|
+
changes.push(createEndpointChange({
|
|
184
|
+
method: context.method,
|
|
185
|
+
path: context.path,
|
|
186
|
+
changeType: context.kind === 'response' ? 'changed-response-schema' : 'changed-schema',
|
|
187
|
+
severity: 'DANGEROUS',
|
|
188
|
+
description: `Default value changed in ${context.method.toUpperCase()} ${context.path}: ${fieldPath}`,
|
|
189
|
+
affectedField: fieldPath,
|
|
190
|
+
oldValue: JSON.stringify(oldSchema.default),
|
|
191
|
+
newValue: JSON.stringify(newSchema.default),
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
const oldProperties = ((_b = oldSchema.properties) !== null && _b !== void 0 ? _b : {});
|
|
195
|
+
const newProperties = ((_c = newSchema.properties) !== null && _c !== void 0 ? _c : {});
|
|
196
|
+
const oldRequired = getRequiredFields(oldSchema);
|
|
197
|
+
const newRequired = getRequiredFields(newSchema);
|
|
198
|
+
for (const [propName, propSchema] of Object.entries(oldProperties)) {
|
|
199
|
+
const nestedPath = fieldPath === 'body' ? propName : `${fieldPath}.${propName}`;
|
|
200
|
+
const newPropSchema = newProperties[propName];
|
|
201
|
+
if (!newPropSchema) {
|
|
202
|
+
changes.push(createEndpointChange({
|
|
203
|
+
method: context.method,
|
|
204
|
+
path: context.path,
|
|
205
|
+
changeType: context.kind === 'response' ? 'changed-response-schema' : 'changed-schema',
|
|
206
|
+
severity: 'BREAKING',
|
|
207
|
+
description: describeFieldRemoval(context, nestedPath),
|
|
208
|
+
affectedField: nestedPath,
|
|
209
|
+
}));
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
changes.push(...compareSchemasRecursive(propSchema, newPropSchema, context, nestedPath));
|
|
213
|
+
}
|
|
214
|
+
for (const [propName] of Object.entries(newProperties)) {
|
|
215
|
+
if (oldProperties[propName])
|
|
216
|
+
continue;
|
|
217
|
+
const nestedPath = fieldPath === 'body' ? propName : `${fieldPath}.${propName}`;
|
|
218
|
+
const requiredAdded = newRequired.has(propName) && !oldRequired.has(propName);
|
|
219
|
+
const severity = context.kind === 'response' ? 'SAFE' : requiredAdded ? 'BREAKING' : 'SAFE';
|
|
220
|
+
changes.push(createEndpointChange({
|
|
221
|
+
method: context.method,
|
|
222
|
+
path: context.path,
|
|
223
|
+
changeType: context.kind === 'response' ? 'changed-response-schema' : 'changed-schema',
|
|
224
|
+
severity,
|
|
225
|
+
description: `${requiredAdded ? 'Required' : 'Optional'} field added to ${context.kind} schema for ${context.method.toUpperCase()} ${context.path}: ${nestedPath}`,
|
|
226
|
+
affectedField: nestedPath,
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
const oldItems = oldSchema.type === 'array' ? schemaObject(oldSchema.items) : undefined;
|
|
230
|
+
const newItems = newSchema.type === 'array' ? schemaObject(newSchema.items) : undefined;
|
|
231
|
+
if (oldItems && newItems) {
|
|
232
|
+
changes.push(...compareSchemasRecursive(oldItems, newItems, context, `${fieldPath}[]`));
|
|
233
|
+
}
|
|
234
|
+
return changes;
|
|
235
|
+
}
|
|
236
|
+
function getRequestBodySchema(op) {
|
|
237
|
+
var _a;
|
|
238
|
+
const requestBody = op.requestBody;
|
|
239
|
+
const content = (_a = requestBody === null || requestBody === void 0 ? void 0 : requestBody.content) !== null && _a !== void 0 ? _a : {};
|
|
240
|
+
for (const media of Object.values(content)) {
|
|
241
|
+
const schema = schemaObject(media.schema);
|
|
242
|
+
if (schema)
|
|
243
|
+
return schema;
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
function getResponseSchema(op, code) {
|
|
248
|
+
var _a, _b;
|
|
249
|
+
const response = (_a = op.responses) === null || _a === void 0 ? void 0 : _a[code];
|
|
250
|
+
const content = (_b = response === null || response === void 0 ? void 0 : response.content) !== null && _b !== void 0 ? _b : {};
|
|
251
|
+
for (const media of Object.values(content)) {
|
|
252
|
+
const schema = schemaObject(media.schema);
|
|
253
|
+
if (schema)
|
|
254
|
+
return schema;
|
|
255
|
+
}
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
80
258
|
// ─── Spec comparison ──────────────────────────────────────────────────────────
|
|
81
259
|
/**
|
|
82
260
|
* Load and dereference an OpenAPI/Swagger spec from a file path.
|
|
@@ -89,7 +267,7 @@ async function loadSpec(specPath) {
|
|
|
89
267
|
* Compare two API specs and return a list of detected changes.
|
|
90
268
|
*/
|
|
91
269
|
function compareSpecs(oldApi, newApi) {
|
|
92
|
-
var _a, _b;
|
|
270
|
+
var _a, _b, _c, _d;
|
|
93
271
|
const changes = [];
|
|
94
272
|
const oldEndpoints = extractEndpoints(oldApi);
|
|
95
273
|
const newEndpoints = extractEndpoints(newApi);
|
|
@@ -101,99 +279,134 @@ function compareSpecs(oldApi, newApi) {
|
|
|
101
279
|
for (const ep of newEndpoints) {
|
|
102
280
|
newMap.set(`${ep.method}:${ep.path}`, ep);
|
|
103
281
|
}
|
|
104
|
-
// Removed endpoints (breaking)
|
|
105
282
|
for (const [key, oldEp] of oldMap) {
|
|
106
283
|
if (!newMap.has(key)) {
|
|
107
|
-
|
|
284
|
+
const removedDeprecated = oldEp.operation.deprecated === true;
|
|
285
|
+
changes.push(createEndpointChange({
|
|
108
286
|
method: oldEp.method,
|
|
109
287
|
path: oldEp.path,
|
|
110
|
-
changeType: 'removed',
|
|
111
|
-
|
|
288
|
+
changeType: removedDeprecated ? 'removed-deprecated' : 'removed',
|
|
289
|
+
severity: 'BREAKING',
|
|
112
290
|
description: `Endpoint removed: ${oldEp.method.toUpperCase()} ${oldEp.path}`,
|
|
113
|
-
});
|
|
291
|
+
}));
|
|
114
292
|
}
|
|
115
293
|
}
|
|
116
|
-
// Added endpoints (non-breaking)
|
|
117
294
|
for (const [key, newEp] of newMap) {
|
|
118
295
|
if (!oldMap.has(key)) {
|
|
119
|
-
changes.push({
|
|
296
|
+
changes.push(createEndpointChange({
|
|
120
297
|
method: newEp.method,
|
|
121
298
|
path: newEp.path,
|
|
122
299
|
changeType: 'added',
|
|
123
|
-
|
|
300
|
+
severity: 'SAFE',
|
|
124
301
|
description: `Endpoint added: ${newEp.method.toUpperCase()} ${newEp.path}`,
|
|
125
|
-
});
|
|
302
|
+
}));
|
|
126
303
|
}
|
|
127
304
|
}
|
|
128
|
-
// Changed endpoints
|
|
129
305
|
for (const [key, oldEp] of oldMap) {
|
|
130
306
|
const newEp = newMap.get(key);
|
|
131
307
|
if (!newEp)
|
|
132
308
|
continue;
|
|
133
|
-
|
|
309
|
+
if (!oldEp.operation.deprecated && newEp.operation.deprecated) {
|
|
310
|
+
changes.push(createEndpointChange({
|
|
311
|
+
method: oldEp.method,
|
|
312
|
+
path: oldEp.path,
|
|
313
|
+
changeType: 'deprecated',
|
|
314
|
+
severity: 'SAFE',
|
|
315
|
+
description: `Endpoint deprecated: ${oldEp.method.toUpperCase()} ${oldEp.path}`,
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
const oldSecurity = getOperationSecurity(oldApi, oldEp.operation);
|
|
319
|
+
const newSecurity = getOperationSecurity(newApi, newEp.operation);
|
|
320
|
+
if (JSON.stringify(oldSecurity) !== JSON.stringify(newSecurity)) {
|
|
321
|
+
const oldPublic = isPublicEndpoint(oldSecurity);
|
|
322
|
+
const newPublic = isPublicEndpoint(newSecurity);
|
|
323
|
+
changes.push(createEndpointChange({
|
|
324
|
+
method: oldEp.method,
|
|
325
|
+
path: oldEp.path,
|
|
326
|
+
changeType: 'changed-auth-requirement',
|
|
327
|
+
severity: oldPublic && !newPublic ? 'BREAKING' : 'SAFE',
|
|
328
|
+
description: `Authentication requirement changed for ${oldEp.method.toUpperCase()} ${oldEp.path}`,
|
|
329
|
+
oldValue: oldPublic ? 'public' : 'authenticated',
|
|
330
|
+
newValue: newPublic ? 'public' : 'authenticated',
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
134
333
|
const oldCodes = new Set(getResponseCodes(oldEp.operation));
|
|
135
334
|
const newCodes = new Set(getResponseCodes(newEp.operation));
|
|
136
335
|
const removedCodes = [...oldCodes].filter((c) => !newCodes.has(c));
|
|
137
336
|
if (removedCodes.length > 0) {
|
|
138
|
-
changes.push({
|
|
337
|
+
changes.push(createEndpointChange({
|
|
139
338
|
method: oldEp.method,
|
|
140
339
|
path: oldEp.path,
|
|
141
340
|
changeType: 'changed-response-codes',
|
|
142
|
-
|
|
341
|
+
severity: 'BREAKING',
|
|
143
342
|
description: `Response codes removed from ${oldEp.method.toUpperCase()} ${oldEp.path}: ${removedCodes.join(', ')}`,
|
|
144
|
-
|
|
343
|
+
oldValue: removedCodes.join(', '),
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
for (const responseCode of [...oldCodes].filter((code) => newCodes.has(code))) {
|
|
347
|
+
changes.push(...compareSchemasRecursive(getResponseSchema(oldEp.operation, responseCode), getResponseSchema(newEp.operation, responseCode), { kind: 'response', method: oldEp.method, path: oldEp.path, location: `responses.${responseCode}` }));
|
|
145
348
|
}
|
|
146
|
-
|
|
349
|
+
changes.push(...compareSchemasRecursive(getRequestBodySchema(oldEp.operation), getRequestBodySchema(newEp.operation), { kind: 'request', method: oldEp.method, path: oldEp.path }));
|
|
147
350
|
const oldParams = getParameters(oldEp.operation);
|
|
148
351
|
const newParams = getParameters(newEp.operation);
|
|
149
352
|
const newParamMap = new Map(newParams.map((p) => [paramKey(p), p]));
|
|
150
353
|
for (const oldParam of oldParams) {
|
|
151
354
|
const newParam = newParamMap.get(paramKey(oldParam));
|
|
152
355
|
if (!newParam) {
|
|
153
|
-
|
|
154
|
-
changes.push({
|
|
356
|
+
changes.push(createEndpointChange({
|
|
155
357
|
method: oldEp.method,
|
|
156
358
|
path: oldEp.path,
|
|
157
359
|
changeType: 'changed-parameter',
|
|
158
|
-
|
|
360
|
+
severity: oldParam.required === true ? 'BREAKING' : 'DANGEROUS',
|
|
159
361
|
description: `Parameter removed from ${oldEp.method.toUpperCase()} ${oldEp.path}: ${oldParam.in} '${oldParam.name}'${oldParam.required ? ' (required)' : ''}`,
|
|
160
|
-
|
|
362
|
+
affectedField: `${oldParam.in}.${oldParam.name}`,
|
|
363
|
+
}));
|
|
161
364
|
continue;
|
|
162
365
|
}
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
changes.push({
|
|
366
|
+
const oldSchema = schemaObject(oldParam.schema);
|
|
367
|
+
const newSchema = schemaObject(newParam.schema);
|
|
368
|
+
if (getSchemaType(oldSchema) !== getSchemaType(newSchema) || (oldSchema === null || oldSchema === void 0 ? void 0 : oldSchema.format) !== (newSchema === null || newSchema === void 0 ? void 0 : newSchema.format)) {
|
|
369
|
+
changes.push(createEndpointChange({
|
|
168
370
|
method: oldEp.method,
|
|
169
371
|
path: oldEp.path,
|
|
170
|
-
changeType: 'changed-parameter',
|
|
171
|
-
|
|
172
|
-
description: `Parameter type/format changed for ${oldEp.method.toUpperCase()} ${oldEp.path}: '${oldParam.name}' (${oldSchema
|
|
173
|
-
|
|
372
|
+
changeType: 'changed-parameter-type',
|
|
373
|
+
severity: 'BREAKING',
|
|
374
|
+
description: `Parameter type/format changed for ${oldEp.method.toUpperCase()} ${oldEp.path}: '${oldParam.name}' (${getSchemaType(oldSchema)}/${oldSchema === null || oldSchema === void 0 ? void 0 : oldSchema.format} → ${getSchemaType(newSchema)}/${newSchema === null || newSchema === void 0 ? void 0 : newSchema.format})`,
|
|
375
|
+
affectedField: `${oldParam.in}.${oldParam.name}`,
|
|
376
|
+
oldValue: `${(_a = getSchemaType(oldSchema)) !== null && _a !== void 0 ? _a : 'unknown'}/${(_b = oldSchema === null || oldSchema === void 0 ? void 0 : oldSchema.format) !== null && _b !== void 0 ? _b : 'none'}`,
|
|
377
|
+
newValue: `${(_c = getSchemaType(newSchema)) !== null && _c !== void 0 ? _c : 'unknown'}/${(_d = newSchema === null || newSchema === void 0 ? void 0 : newSchema.format) !== null && _d !== void 0 ? _d : 'none'}`,
|
|
378
|
+
}));
|
|
174
379
|
}
|
|
175
|
-
// Required flag changed from optional to required — breaking
|
|
176
380
|
if (!oldParam.required && newParam.required) {
|
|
177
|
-
changes.push({
|
|
381
|
+
changes.push(createEndpointChange({
|
|
178
382
|
method: oldEp.method,
|
|
179
383
|
path: oldEp.path,
|
|
180
|
-
changeType: 'changed-parameter',
|
|
181
|
-
|
|
384
|
+
changeType: 'changed-parameter-required',
|
|
385
|
+
severity: 'BREAKING',
|
|
182
386
|
description: `Parameter '${oldParam.name}' became required in ${oldEp.method.toUpperCase()} ${oldEp.path}`,
|
|
183
|
-
|
|
387
|
+
affectedField: `${oldParam.in}.${oldParam.name}`,
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
if (oldSchema && newSchema) {
|
|
391
|
+
changes.push(...compareSchemasRecursive(oldSchema, newSchema, {
|
|
392
|
+
kind: 'parameter',
|
|
393
|
+
method: oldEp.method,
|
|
394
|
+
path: oldEp.path,
|
|
395
|
+
location: `${oldParam.in}.${oldParam.name}`,
|
|
396
|
+
}));
|
|
184
397
|
}
|
|
185
398
|
}
|
|
186
|
-
// New required parameters added — breaking
|
|
187
399
|
const oldParamMap = new Map(oldParams.map((p) => [paramKey(p), p]));
|
|
188
400
|
for (const newParam of newParams) {
|
|
189
|
-
if (!oldParamMap.has(paramKey(newParam))
|
|
190
|
-
changes.push({
|
|
401
|
+
if (!oldParamMap.has(paramKey(newParam))) {
|
|
402
|
+
changes.push(createEndpointChange({
|
|
191
403
|
method: newEp.method,
|
|
192
404
|
path: newEp.path,
|
|
193
|
-
changeType: 'changed-parameter',
|
|
194
|
-
|
|
195
|
-
description:
|
|
196
|
-
|
|
405
|
+
changeType: newParam.required ? 'changed-parameter-required' : 'changed-parameter',
|
|
406
|
+
severity: newParam.required ? 'BREAKING' : 'SAFE',
|
|
407
|
+
description: `${newParam.required ? 'Required' : 'Optional'} parameter added to ${newEp.method.toUpperCase()} ${newEp.path}: ${newParam.in} '${newParam.name}'`,
|
|
408
|
+
affectedField: `${newParam.in}.${newParam.name}`,
|
|
409
|
+
}));
|
|
197
410
|
}
|
|
198
411
|
}
|
|
199
412
|
}
|
|
@@ -204,14 +417,16 @@ function compareSpecs(oldApi, newApi) {
|
|
|
204
417
|
* Parse Pact-format JSON contract files matching the given glob pattern or directory.
|
|
205
418
|
*/
|
|
206
419
|
async function parseContractFiles(contractsGlob) {
|
|
207
|
-
let
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
420
|
+
let patterns = contractsGlob
|
|
421
|
+
.split(',')
|
|
422
|
+
.map((entry) => entry.trim())
|
|
423
|
+
.filter(Boolean);
|
|
424
|
+
if (patterns.length === 1 && fs.existsSync(patterns[0]) && fs.statSync(patterns[0]).isDirectory()) {
|
|
425
|
+
patterns = [path.join(patterns[0], '**/*.json').replace(/\\/g, '/')];
|
|
211
426
|
}
|
|
212
|
-
const files = await (0, fast_glob_1.default)(
|
|
427
|
+
const files = await (0, fast_glob_1.default)(patterns, { absolute: true, onlyFiles: true, unique: true });
|
|
213
428
|
const contracts = [];
|
|
214
|
-
for (const filePath of files) {
|
|
429
|
+
for (const filePath of files.filter((filePath) => filePath.endsWith('.json'))) {
|
|
215
430
|
try {
|
|
216
431
|
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
217
432
|
const contract = parsePactContract(raw, filePath);
|
|
@@ -263,33 +478,20 @@ function parsePactContract(raw, filePath) {
|
|
|
263
478
|
return { consumer, provider, interactions, filePath };
|
|
264
479
|
}
|
|
265
480
|
// ─── Contract verification ────────────────────────────────────────────────────
|
|
266
|
-
/**
|
|
267
|
-
* Normalise a path template by replacing Pact-style `:param` segments with
|
|
268
|
-
* OpenAPI-style `{param}` segments so the two formats can be compared.
|
|
269
|
-
*/
|
|
270
481
|
function normalisePath(p) {
|
|
271
482
|
return p.replace(/:([^/]+)/g, '{$1}');
|
|
272
483
|
}
|
|
273
|
-
/**
|
|
274
|
-
* Check whether a path from a contract matches an OpenAPI spec path,
|
|
275
|
-
* accounting for path parameter placeholders in different styles.
|
|
276
|
-
*/
|
|
277
484
|
function pathMatchesSpec(contractPath, specPath) {
|
|
278
485
|
const normalised = normalisePath(contractPath);
|
|
279
486
|
if (normalised === specPath)
|
|
280
487
|
return true;
|
|
281
|
-
// Build a regex from the spec path template and test the contract path
|
|
282
488
|
const regexStr = '^' + specPath.replace(/\{[^}]+\}/g, '[^/]+') + '$';
|
|
283
489
|
return new RegExp(regexStr).test(contractPath);
|
|
284
490
|
}
|
|
285
|
-
/**
|
|
286
|
-
* Verify consumer contracts against a new API spec.
|
|
287
|
-
*/
|
|
288
491
|
function verifyContracts(contracts, newApi) {
|
|
289
492
|
const endpoints = extractEndpoints(newApi);
|
|
290
493
|
return contracts.map((contract) => {
|
|
291
494
|
const interactionResults = contract.interactions.map((interaction) => {
|
|
292
|
-
// Find matching endpoint in spec
|
|
293
495
|
const match = endpoints.find((ep) => ep.method === interaction.method && pathMatchesSpec(interaction.path, ep.path));
|
|
294
496
|
if (!match) {
|
|
295
497
|
return {
|
|
@@ -298,11 +500,9 @@ function verifyContracts(contracts, newApi) {
|
|
|
298
500
|
reason: `Endpoint not found in new spec: ${interaction.method.toUpperCase()} ${interaction.path}`,
|
|
299
501
|
};
|
|
300
502
|
}
|
|
301
|
-
// Check expected response status code is present in spec
|
|
302
503
|
const responseCodes = getResponseCodes(match.operation);
|
|
303
504
|
const statusStr = String(interaction.expectedStatus);
|
|
304
|
-
if (!responseCodes.includes(statusStr) &&
|
|
305
|
-
!responseCodes.includes('default')) {
|
|
505
|
+
if (!responseCodes.includes(statusStr) && !responseCodes.includes('default')) {
|
|
306
506
|
return {
|
|
307
507
|
interaction,
|
|
308
508
|
passed: false,
|
|
@@ -316,23 +516,18 @@ function verifyContracts(contracts, newApi) {
|
|
|
316
516
|
});
|
|
317
517
|
}
|
|
318
518
|
// ─── Metrics ──────────────────────────────────────────────────────────────────
|
|
319
|
-
/**
|
|
320
|
-
* Compute compatibility percentage based on detected breaking changes.
|
|
321
|
-
*/
|
|
322
519
|
function computeCompatibilityPercent(totalOldEndpoints, breakingChanges) {
|
|
323
520
|
if (totalOldEndpoints === 0)
|
|
324
521
|
return 100;
|
|
325
|
-
|
|
522
|
+
const unaffected = countCompatibleEndpoints(totalOldEndpoints, breakingChanges);
|
|
523
|
+
return parseFloat(((unaffected / totalOldEndpoints) * 100).toFixed(2));
|
|
524
|
+
}
|
|
525
|
+
function countCompatibleEndpoints(totalOldEndpoints, breakingChanges) {
|
|
326
526
|
const affectedKeys = new Set(breakingChanges
|
|
327
527
|
.filter((c) => c.changeType !== 'added')
|
|
328
528
|
.map((c) => `${c.method}:${c.path}`));
|
|
329
|
-
|
|
330
|
-
return parseFloat(((unaffected / totalOldEndpoints) * 100).toFixed(2));
|
|
529
|
+
return Math.max(0, totalOldEndpoints - affectedKeys.size);
|
|
331
530
|
}
|
|
332
|
-
/**
|
|
333
|
-
* Compute what fraction of new spec endpoints are covered by at least one
|
|
334
|
-
* contract interaction.
|
|
335
|
-
*/
|
|
336
531
|
function computeContractCoveragePercent(contracts, newApi) {
|
|
337
532
|
const endpoints = extractEndpoints(newApi);
|
|
338
533
|
const coveredKeys = new Set();
|
|
@@ -348,14 +543,69 @@ function computeContractCoveragePercent(contracts, newApi) {
|
|
|
348
543
|
const coveragePercent = total === 0 ? 100 : parseFloat(((covered / total) * 100).toFixed(2));
|
|
349
544
|
return { coveredEndpoints: covered, totalEndpoints: total, coveragePercent };
|
|
350
545
|
}
|
|
546
|
+
function buildSeveritySummary(changes) {
|
|
547
|
+
return changes.reduce((summary, change) => {
|
|
548
|
+
switch (change.severity) {
|
|
549
|
+
case 'BREAKING':
|
|
550
|
+
summary.breaking += 1;
|
|
551
|
+
break;
|
|
552
|
+
case 'DANGEROUS':
|
|
553
|
+
summary.dangerous += 1;
|
|
554
|
+
break;
|
|
555
|
+
case 'SAFE':
|
|
556
|
+
summary.safe += 1;
|
|
557
|
+
break;
|
|
558
|
+
case 'INFO':
|
|
559
|
+
summary.info += 1;
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
return summary;
|
|
563
|
+
}, { breaking: 0, dangerous: 0, safe: 0, info: 0 });
|
|
564
|
+
}
|
|
565
|
+
function buildPerConsumerCoverage(verificationResults) {
|
|
566
|
+
return verificationResults.map((result) => {
|
|
567
|
+
const interactionCount = result.interactionResults.length;
|
|
568
|
+
const passingInteractions = result.interactionResults.filter((entry) => entry.passed).length;
|
|
569
|
+
const coveragePercent = interactionCount === 0 ? 100 : parseFloat(((passingInteractions / interactionCount) * 100).toFixed(2));
|
|
570
|
+
return {
|
|
571
|
+
consumerName: result.contract.consumer,
|
|
572
|
+
interactionCount,
|
|
573
|
+
passingInteractions,
|
|
574
|
+
coveragePercent,
|
|
575
|
+
};
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
function buildDeployBlockers(report) {
|
|
579
|
+
const blockers = [];
|
|
580
|
+
if (report.breakingChanges.some((c) => c.severity === 'BREAKING')) {
|
|
581
|
+
blockers.push('BREAKING API changes detected');
|
|
582
|
+
}
|
|
583
|
+
if (report.contractVerificationResults.some((r) => !r.passed)) {
|
|
584
|
+
blockers.push('One or more consumer contract verifications failed');
|
|
585
|
+
}
|
|
586
|
+
if (report.schemaEvolutionResults.some((r) => r.overallRisk === 'BREAKING')) {
|
|
587
|
+
blockers.push('Breaking schema evolution detected');
|
|
588
|
+
}
|
|
589
|
+
return blockers;
|
|
590
|
+
}
|
|
591
|
+
function computeCanDeploy(report) {
|
|
592
|
+
if (report.breakingChanges.some((c) => c.severity === 'BREAKING'))
|
|
593
|
+
return false;
|
|
594
|
+
if (report.contractVerificationResults.some((r) => !r.passed))
|
|
595
|
+
return false;
|
|
596
|
+
if (report.schemaEvolutionResults.some((r) => r.overallRisk === 'BREAKING'))
|
|
597
|
+
return false;
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
351
600
|
// ─── Report builder ───────────────────────────────────────────────────────────
|
|
352
|
-
function buildCompatibilityReport(oldApi, newApi, changes, verificationResults, oldSpecPath, newSpecPath) {
|
|
601
|
+
function buildCompatibilityReport(oldApi, newApi, changes, verificationResults, oldSpecPath, newSpecPath, options = {}) {
|
|
602
|
+
var _a, _b, _c;
|
|
353
603
|
const oldEndpoints = extractEndpoints(oldApi);
|
|
354
604
|
const breakingChanges = changes.filter((c) => c.breaking);
|
|
355
605
|
const nonBreakingChanges = changes.filter((c) => !c.breaking);
|
|
356
606
|
const compatibilityPercent = computeCompatibilityPercent(oldEndpoints.length, breakingChanges);
|
|
357
607
|
const coverageMetrics = computeContractCoveragePercent(verificationResults.map((r) => r.contract), newApi);
|
|
358
|
-
|
|
608
|
+
const report = {
|
|
359
609
|
generatedAt: new Date().toISOString(),
|
|
360
610
|
oldSpecPath,
|
|
361
611
|
newSpecPath,
|
|
@@ -367,12 +617,35 @@ function buildCompatibilityReport(oldApi, newApi, changes, verificationResults,
|
|
|
367
617
|
totalNewEndpoints: coverageMetrics.totalEndpoints,
|
|
368
618
|
contractCoveredEndpoints: coverageMetrics.coveredEndpoints,
|
|
369
619
|
contractCoveragePercent: coverageMetrics.coveragePercent,
|
|
620
|
+
severitySummary: buildSeveritySummary(changes),
|
|
621
|
+
springCloudContracts: (_a = options.springCloudContracts) !== null && _a !== void 0 ? _a : [],
|
|
622
|
+
schemaEvolutionResults: (_b = options.schemaEvolutionResults) !== null && _b !== void 0 ? _b : [],
|
|
623
|
+
matrix: (_c = options.matrix) !== null && _c !== void 0 ? _c : null,
|
|
624
|
+
canDeploy: true,
|
|
625
|
+
deployBlockers: [],
|
|
626
|
+
perConsumerCoverage: buildPerConsumerCoverage(verificationResults),
|
|
370
627
|
};
|
|
628
|
+
report.canDeploy = computeCanDeploy(report);
|
|
629
|
+
report.deployBlockers = buildDeployBlockers(report);
|
|
630
|
+
return report;
|
|
371
631
|
}
|
|
372
632
|
// ─── Report writers ───────────────────────────────────────────────────────────
|
|
373
633
|
function writeCompatibilityJson(report, reportsDir) {
|
|
374
|
-
const
|
|
375
|
-
|
|
634
|
+
const payload = {
|
|
635
|
+
...report,
|
|
636
|
+
items: [...report.breakingChanges, ...report.nonBreakingChanges].map((change) => ({
|
|
637
|
+
id: `${change.method.toUpperCase()} ${change.path} (${change.changeType})`,
|
|
638
|
+
covered: !change.breaking,
|
|
639
|
+
description: change.description,
|
|
640
|
+
tests: change.affectedConsumers,
|
|
641
|
+
})),
|
|
642
|
+
coveragePercent: report.compatibilityPercent,
|
|
643
|
+
coveredItems: countCompatibleEndpoints(report.totalOldEndpoints, report.breakingChanges),
|
|
644
|
+
totalItems: report.totalOldEndpoints,
|
|
645
|
+
};
|
|
646
|
+
for (const fileName of ['compatibility-contracts.json', 'compatibility-coverage.json']) {
|
|
647
|
+
fs.writeFileSync(path.join(reportsDir, fileName), JSON.stringify(payload, null, 2), 'utf-8');
|
|
648
|
+
}
|
|
376
649
|
}
|
|
377
650
|
function writeCompatibilityHtml(report, reportsDir) {
|
|
378
651
|
const breakingRows = report.breakingChanges
|
|
@@ -380,6 +653,7 @@ function writeCompatibilityHtml(report, reportsDir) {
|
|
|
380
653
|
<td>${c.method.toUpperCase()}</td>
|
|
381
654
|
<td>${c.path}</td>
|
|
382
655
|
<td>${c.changeType}</td>
|
|
656
|
+
<td>${c.severity}</td>
|
|
383
657
|
<td>${c.description}</td>
|
|
384
658
|
</tr>`)
|
|
385
659
|
.join('\n');
|
|
@@ -388,6 +662,7 @@ function writeCompatibilityHtml(report, reportsDir) {
|
|
|
388
662
|
<td>${c.method.toUpperCase()}</td>
|
|
389
663
|
<td>${c.path}</td>
|
|
390
664
|
<td>${c.changeType}</td>
|
|
665
|
+
<td>${c.severity}</td>
|
|
391
666
|
<td>${c.description}</td>
|
|
392
667
|
</tr>`)
|
|
393
668
|
.join('\n');
|
|
@@ -404,6 +679,16 @@ function writeCompatibilityHtml(report, reportsDir) {
|
|
|
404
679
|
</tr>`;
|
|
405
680
|
}))
|
|
406
681
|
.join('\n');
|
|
682
|
+
const schemaRows = report.schemaEvolutionResults
|
|
683
|
+
.flatMap((result) => result.changes.map((change) => ` <tr class="${change.breaking ? 'fail' : 'pass'}">
|
|
684
|
+
<td>${result.topicOrQueue}</td>
|
|
685
|
+
<td>${result.format}</td>
|
|
686
|
+
<td>${change.field}</td>
|
|
687
|
+
<td>${change.changeType}</td>
|
|
688
|
+
<td>${change.breaking ? 'BREAKING' : 'SAFE'}</td>
|
|
689
|
+
<td>${change.fixHint}</td>
|
|
690
|
+
</tr>`))
|
|
691
|
+
.join('\n');
|
|
407
692
|
const compatColor = report.compatibilityPercent >= 80
|
|
408
693
|
? '#e6ffe6'
|
|
409
694
|
: report.compatibilityPercent >= 50
|
|
@@ -424,7 +709,7 @@ function writeCompatibilityHtml(report, reportsDir) {
|
|
|
424
709
|
h1 { margin-bottom: 0.25rem; }
|
|
425
710
|
h2 { margin-top: 2rem; }
|
|
426
711
|
.meta { color: #666; font-size: 0.9rem; margin-bottom: 1.5rem; }
|
|
427
|
-
table { border-collapse: collapse; width: 100%; max-width:
|
|
712
|
+
table { border-collapse: collapse; width: 100%; max-width: 1100px; margin-bottom: 1.5rem; }
|
|
428
713
|
th, td { border: 1px solid #ccc; padding: 0.5rem 1rem; text-align: left; }
|
|
429
714
|
th { background: #f0f0f0; }
|
|
430
715
|
tr.breaking { background: #ffe6e6; }
|
|
@@ -447,6 +732,9 @@ function writeCompatibilityHtml(report, reportsDir) {
|
|
|
447
732
|
Contract Coverage: ${report.contractCoveragePercent}%
|
|
448
733
|
(${report.contractCoveredEndpoints}/${report.totalNewEndpoints} endpoints)
|
|
449
734
|
</div>
|
|
735
|
+
<div class="summary-box" style="background:${report.canDeploy ? '#e6ffe6' : '#ffe6e6'}">
|
|
736
|
+
Can Deploy: ${report.canDeploy ? 'YES' : 'NO'}
|
|
737
|
+
</div>
|
|
450
738
|
</div>
|
|
451
739
|
|
|
452
740
|
<h2>Breaking Changes</h2>
|
|
@@ -454,7 +742,7 @@ function writeCompatibilityHtml(report, reportsDir) {
|
|
|
454
742
|
? '<p>No breaking changes detected. ✅</p>'
|
|
455
743
|
: `<table>
|
|
456
744
|
<thead>
|
|
457
|
-
<tr><th>Method</th><th>Path</th><th>Change Type</th><th>Description</th></tr>
|
|
745
|
+
<tr><th>Method</th><th>Path</th><th>Change Type</th><th>Severity</th><th>Description</th></tr>
|
|
458
746
|
</thead>
|
|
459
747
|
<tbody>
|
|
460
748
|
${breakingRows}
|
|
@@ -466,7 +754,7 @@ ${breakingRows}
|
|
|
466
754
|
? '<p>No non-breaking changes detected.</p>'
|
|
467
755
|
: `<table>
|
|
468
756
|
<thead>
|
|
469
|
-
<tr><th>Method</th><th>Path</th><th>Change Type</th><th>Description</th></tr>
|
|
757
|
+
<tr><th>Method</th><th>Path</th><th>Change Type</th><th>Severity</th><th>Description</th></tr>
|
|
470
758
|
</thead>
|
|
471
759
|
<tbody>
|
|
472
760
|
${nonBreakingRows}
|
|
@@ -484,14 +772,28 @@ ${nonBreakingRows}
|
|
|
484
772
|
${contractRows}
|
|
485
773
|
</tbody>
|
|
486
774
|
</table>`}
|
|
775
|
+
|
|
776
|
+
<h2>Schema Evolution</h2>
|
|
777
|
+
${schemaRows.length === 0
|
|
778
|
+
? '<p>No schema evolution changes detected.</p>'
|
|
779
|
+
: `<table>
|
|
780
|
+
<thead>
|
|
781
|
+
<tr><th>Topic / Queue</th><th>Format</th><th>Field</th><th>Change Type</th><th>Risk</th><th>Fix Hint</th></tr>
|
|
782
|
+
</thead>
|
|
783
|
+
<tbody>
|
|
784
|
+
${schemaRows}
|
|
785
|
+
</tbody>
|
|
786
|
+
</table>`}
|
|
787
|
+
|
|
788
|
+
${report.deployBlockers.length === 0
|
|
789
|
+
? '<p>No deployment blockers.</p>'
|
|
790
|
+
: `<h2>Deploy Blockers</h2><ul>${report.deployBlockers.map((blocker) => `<li>${blocker}</li>`).join('')}</ul>`}
|
|
487
791
|
</body>
|
|
488
792
|
</html>`;
|
|
489
|
-
const
|
|
490
|
-
|
|
793
|
+
for (const fileName of ['compatibility-contracts.html', 'compatibility-coverage.html']) {
|
|
794
|
+
fs.writeFileSync(path.join(reportsDir, fileName), html, 'utf-8');
|
|
795
|
+
}
|
|
491
796
|
}
|
|
492
|
-
/**
|
|
493
|
-
* Generate JSON and HTML compatibility reports.
|
|
494
|
-
*/
|
|
495
797
|
function generateCompatibilityReports(report, reportsDir) {
|
|
496
798
|
if (!fs.existsSync(reportsDir)) {
|
|
497
799
|
fs.mkdirSync(reportsDir, { recursive: true });
|