Skip to content

Commit f6cec53

Browse files
Merge pull request #27 from BitGo/update-api-diff
feat: enhance release description
2 parents d221014 + 714ed47 commit f6cec53

File tree

8 files changed

+824
-60
lines changed

8 files changed

+824
-60
lines changed

scripts/api-diff.js

Lines changed: 166 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,21 @@ const changes = {
1414
};
1515

1616
// Helper function to track component references
17-
function findComponentRefs(obj, components) {
17+
function findComponentRefs(obj, components, spec = currentSpec) {
1818
if (!obj) return;
1919
if (typeof obj === 'object') {
2020
if (obj['$ref'] && obj['$ref'].startsWith('#/components/')) {
21-
components.add(obj['$ref'].split('/').pop());
21+
const componentName = obj['$ref'].split('/').pop();
22+
components.add(componentName);
23+
24+
// Follow the reference to check nested components
25+
const [_, category, name] = obj['$ref'].split('/');
26+
const referencedComponent = spec.components?.[category]?.[name];
27+
if (referencedComponent) {
28+
findComponentRefs(referencedComponent, components, spec);
29+
}
2230
}
23-
Object.values(obj).forEach(value => findComponentRefs(value, components));
31+
Object.values(obj).forEach(value => findComponentRefs(value, components, spec));
2432
}
2533
}
2634

@@ -31,9 +39,18 @@ function compareComponents() {
3139

3240
for (const [category, components] of Object.entries(currComps)) {
3341
for (const [name, def] of Object.entries(components)) {
34-
if (!prevComps[category]?.[name] ||
35-
JSON.stringify(prevComps[category][name]) !== JSON.stringify(def)) {
42+
const prevDef = prevComps[category]?.[name];
43+
if (!prevDef || JSON.stringify(prevDef) !== JSON.stringify(def)) {
3644
changes.components.add(name);
45+
46+
// Also check which components reference this changed component
47+
Object.entries(currComps[category] || {}).forEach(([otherName, otherDef]) => {
48+
const refsSet = new Set();
49+
findComponentRefs(otherDef, refsSet);
50+
if (refsSet.has(name)) {
51+
changes.components.add(otherName);
52+
}
53+
});
3754
}
3855
}
3956
}
@@ -110,33 +127,91 @@ function getChanges(previous, current) {
110127
return changes;
111128
}
112129

130+
// Helper function to check if a schema references a component or its dependencies
131+
function schemaReferencesComponent(schema, componentName, visitedRefs = new Set()) {
132+
if (!schema) return false;
133+
134+
// Prevent infinite recursion
135+
const schemaKey = JSON.stringify(schema);
136+
if (visitedRefs.has(schemaKey)) return false;
137+
visitedRefs.add(schemaKey);
138+
139+
// Direct reference check
140+
if (schema.$ref) {
141+
const refPath = schema.$ref;
142+
if (refPath === `#/components/schemas/${componentName}`) return true;
143+
144+
// Follow the reference to check nested components
145+
const [_, category, name] = refPath.split('/');
146+
const referencedComponent = currentSpec.components?.[category]?.[name];
147+
if (referencedComponent && schemaReferencesComponent(referencedComponent, componentName, visitedRefs)) {
148+
return true;
149+
}
150+
}
151+
152+
// Check combiners (oneOf, anyOf, allOf)
153+
for (const combiner of ['oneOf', 'anyOf', 'allOf']) {
154+
if (schema[combiner] && Array.isArray(schema[combiner])) {
155+
if (schema[combiner].some(s => schemaReferencesComponent(s, componentName, visitedRefs))) {
156+
return true;
157+
}
158+
}
159+
}
160+
161+
// Check properties if it's an object
162+
if (schema.properties) {
163+
if (Object.values(schema.properties).some(prop =>
164+
schemaReferencesComponent(prop, componentName, visitedRefs))) {
165+
return true;
166+
}
167+
}
168+
169+
// Check array items
170+
if (schema.items && schemaReferencesComponent(schema.items, componentName, visitedRefs)) {
171+
return true;
172+
}
173+
174+
return false;
175+
}
176+
113177
// Helper function to detect where a component is used in an endpoint
114178
function findComponentUsage(details, componentName) {
115179
const usage = [];
116180

117181
// Check parameters
118182
if (details.parameters) {
119183
const hasComponent = details.parameters.some(p =>
120-
(p.$ref && p.$ref.includes(componentName)) ||
121-
(p.schema && p.schema.$ref && p.schema.$ref.includes(componentName))
184+
(p.$ref && schemaReferencesComponent({ $ref: p.$ref }, componentName)) ||
185+
(p.schema && schemaReferencesComponent(p.schema, componentName))
122186
);
123187
if (hasComponent) usage.push('parameters');
124188
}
125189

126190
// Check requestBody
127-
if (details.requestBody &&
128-
details.requestBody.content &&
129-
Object.values(details.requestBody.content).some(c =>
130-
c.schema && c.schema.$ref && c.schema.$ref.includes(componentName))) {
131-
usage.push('requestBody');
191+
if (details.requestBody) {
192+
let hasComponent = false;
193+
if (details.requestBody.$ref) {
194+
hasComponent = schemaReferencesComponent({ $ref: details.requestBody.$ref }, componentName);
195+
} else if (details.requestBody.content) {
196+
hasComponent = Object.values(details.requestBody.content).some(c =>
197+
c.schema && schemaReferencesComponent(c.schema, componentName)
198+
);
199+
}
200+
if (hasComponent) usage.push('requestBody');
132201
}
133202

134203
// Check responses
135-
if (details.responses &&
136-
Object.values(details.responses).some(r =>
137-
r.content && Object.values(r.content).some(c =>
138-
c.schema && c.schema.$ref && c.schema.$ref.includes(componentName)))) {
139-
usage.push('responses');
204+
if (details.responses) {
205+
const hasComponent = Object.entries(details.responses).some(([code, r]) => {
206+
if (r.$ref) return schemaReferencesComponent({ $ref: r.$ref }, componentName);
207+
if (r.content) {
208+
return Object.values(r.content).some(c =>
209+
c.schema && schemaReferencesComponent(c.schema, componentName)
210+
);
211+
}
212+
return false;
213+
});
214+
if (hasComponent) usage.push('responses');
140215
}
141216

142217
return usage;
@@ -160,56 +235,88 @@ function generateReleaseNotes() {
160235
sections.push(section);
161236
}
162237

163-
// Modified endpoints
164-
if (Object.keys(changes.modified).length > 0 || Object.keys(changes.affectedByComponents).length > 0) {
165-
let section = '## Modified\n';
238+
// Helper function to generate route modification details
239+
function generateModifiedRouteDetails(path, changes) {
240+
let details = '';
241+
const methodsToProcess = new Set();
166242

167-
// Combine and sort all modified paths
168-
const allModifiedPaths = new Set([
169-
...Object.keys(changes.modified),
170-
...Object.keys(changes.affectedByComponents)
171-
]);
243+
// Collect all affected methods
244+
if (changes.modified[path]) {
245+
changes.modified[path].forEach(({method}) => methodsToProcess.add(method));
246+
}
247+
if (changes.affectedByComponents[path]) {
248+
changes.affectedByComponents[path].methods.forEach(method => methodsToProcess.add(method));
249+
}
172250

173-
Array.from(allModifiedPaths)
251+
// Process each method
252+
Array.from(methodsToProcess)
174253
.sort()
175-
.forEach(path => {
176-
// Handle both direct modifications and component changes for each path
177-
const methodsToProcess = new Set();
254+
.forEach(method => {
255+
details += `- [${method}] \`${path}\`\n`;
178256

179-
// Collect all affected methods
180-
if (changes.modified[path]) {
181-
changes.modified[path].forEach(({method}) => methodsToProcess.add(method));
257+
// Add direct changes
258+
const directChanges = changes.modified[path]?.find(m => m.method === method);
259+
if (directChanges) {
260+
directChanges.changes.sort().forEach(change => {
261+
details += ` - ${change}\n`;
262+
});
182263
}
183-
if (changes.affectedByComponents[path]) {
184-
changes.affectedByComponents[path].methods.forEach(method => methodsToProcess.add(method));
264+
265+
// Add component changes
266+
if (changes.affectedByComponents[path]?.methods.has(method)) {
267+
const methodDetails = currentSpec.paths[path][method.toLowerCase()];
268+
Array.from(changes.affectedByComponents[path].components)
269+
.sort()
270+
.forEach(component => {
271+
const usageLocations = findComponentUsage(methodDetails, component).sort();
272+
details += ` - \`${component}\` modified in ${usageLocations.join(', ')}\n`;
273+
});
185274
}
275+
});
276+
return details;
277+
}
186278

187-
// Process each method
188-
Array.from(methodsToProcess)
189-
.sort()
190-
.forEach(method => {
191-
section += `- [${method}] \`${path}\`\n`;
192-
193-
// Add direct changes
194-
const directChanges = changes.modified[path]?.find(m => m.method === method);
195-
if (directChanges) {
196-
directChanges.changes.sort().forEach(change => {
197-
section += ` - ${change}\n`;
198-
});
199-
}
200-
201-
// Add component changes
202-
if (changes.affectedByComponents[path]?.methods.has(method)) {
203-
const methodDetails = currentSpec.paths[path][method.toLowerCase()];
204-
Array.from(changes.affectedByComponents[path].components)
205-
.sort()
206-
.forEach(component => {
207-
const usageLocations = findComponentUsage(methodDetails, component).sort();
208-
section += ` - \`${component}\` modified in ${usageLocations.join(', ')}\n`;
209-
});
210-
}
211-
});
279+
// Modified endpoints
280+
if (Object.keys(changes.modified).length > 0 || Object.keys(changes.affectedByComponents).length > 0) {
281+
let section = '## Modified\n';
282+
283+
// First show all directly modified paths
284+
const directlyModifiedPaths = Object.keys(changes.modified).sort();
285+
directlyModifiedPaths.forEach(path => {
286+
section += generateModifiedRouteDetails(path, changes);
287+
});
288+
289+
// Then show component-affected paths (but not ones that were directly modified)
290+
const componentAffectedEntries = Object.entries(changes.affectedByComponents)
291+
.filter(([path]) => !changes.modified[path]) // Only paths not already shown above
292+
.flatMap(([path, details]) =>
293+
Array.from(details.methods).map(method => ({path, method}))
294+
)
295+
.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
296+
297+
// Show first 5 component-affected method-path combinations
298+
const visibleEntries = componentAffectedEntries.slice(0, 5);
299+
const processedPaths = new Set();
300+
301+
visibleEntries.forEach(({path}) => {
302+
if (!processedPaths.has(path)) {
303+
section += generateModifiedRouteDetails(path, changes);
304+
processedPaths.add(path);
305+
}
306+
});
307+
308+
// Collapse any remaining entries
309+
const remainingEntries = componentAffectedEntries.slice(5);
310+
if (remainingEntries.length > 0) {
311+
section += '\n<details><summary>Show more routes affected by component changes...</summary>\n\n';
312+
const remainingPaths = new Set();
313+
remainingEntries.forEach(({path}) => remainingPaths.add(path));
314+
Array.from(remainingPaths).sort().forEach(path => {
315+
section += generateModifiedRouteDetails(path, changes);
212316
});
317+
section += '</details>\n';
318+
}
319+
213320
sections.push(section);
214321
}
215322

0 commit comments

Comments
 (0)