Skip to content

Commit

Permalink
feat: enhance release description
Browse files Browse the repository at this point in the history
- enhanced release description generation
- added tests
  • Loading branch information
shenbenson committed Jan 15, 2025
1 parent 951e783 commit 714ed47
Show file tree
Hide file tree
Showing 8 changed files with 824 additions and 60 deletions.
225 changes: 166 additions & 59 deletions scripts/api-diff.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,21 @@ const changes = {
};

// Helper function to track component references
function findComponentRefs(obj, components) {
function findComponentRefs(obj, components, spec = currentSpec) {
if (!obj) return;
if (typeof obj === 'object') {
if (obj['$ref'] && obj['$ref'].startsWith('#/components/')) {
components.add(obj['$ref'].split('/').pop());
const componentName = obj['$ref'].split('/').pop();
components.add(componentName);

// Follow the reference to check nested components
const [_, category, name] = obj['$ref'].split('/');
const referencedComponent = spec.components?.[category]?.[name];
if (referencedComponent) {
findComponentRefs(referencedComponent, components, spec);
}
}
Object.values(obj).forEach(value => findComponentRefs(value, components));
Object.values(obj).forEach(value => findComponentRefs(value, components, spec));
}
}

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

for (const [category, components] of Object.entries(currComps)) {
for (const [name, def] of Object.entries(components)) {
if (!prevComps[category]?.[name] ||
JSON.stringify(prevComps[category][name]) !== JSON.stringify(def)) {
const prevDef = prevComps[category]?.[name];
if (!prevDef || JSON.stringify(prevDef) !== JSON.stringify(def)) {
changes.components.add(name);

// Also check which components reference this changed component
Object.entries(currComps[category] || {}).forEach(([otherName, otherDef]) => {
const refsSet = new Set();
findComponentRefs(otherDef, refsSet);
if (refsSet.has(name)) {
changes.components.add(otherName);
}
});
}
}
}
Expand Down Expand Up @@ -110,33 +127,91 @@ function getChanges(previous, current) {
return changes;
}

// Helper function to check if a schema references a component or its dependencies
function schemaReferencesComponent(schema, componentName, visitedRefs = new Set()) {
if (!schema) return false;

// Prevent infinite recursion
const schemaKey = JSON.stringify(schema);
if (visitedRefs.has(schemaKey)) return false;
visitedRefs.add(schemaKey);

// Direct reference check
if (schema.$ref) {
const refPath = schema.$ref;
if (refPath === `#/components/schemas/${componentName}`) return true;

// Follow the reference to check nested components
const [_, category, name] = refPath.split('/');
const referencedComponent = currentSpec.components?.[category]?.[name];
if (referencedComponent && schemaReferencesComponent(referencedComponent, componentName, visitedRefs)) {
return true;
}
}

// Check combiners (oneOf, anyOf, allOf)
for (const combiner of ['oneOf', 'anyOf', 'allOf']) {
if (schema[combiner] && Array.isArray(schema[combiner])) {
if (schema[combiner].some(s => schemaReferencesComponent(s, componentName, visitedRefs))) {
return true;
}
}
}

// Check properties if it's an object
if (schema.properties) {
if (Object.values(schema.properties).some(prop =>
schemaReferencesComponent(prop, componentName, visitedRefs))) {
return true;
}
}

// Check array items
if (schema.items && schemaReferencesComponent(schema.items, componentName, visitedRefs)) {
return true;
}

return false;
}

// Helper function to detect where a component is used in an endpoint
function findComponentUsage(details, componentName) {
const usage = [];

// Check parameters
if (details.parameters) {
const hasComponent = details.parameters.some(p =>
(p.$ref && p.$ref.includes(componentName)) ||
(p.schema && p.schema.$ref && p.schema.$ref.includes(componentName))
(p.$ref && schemaReferencesComponent({ $ref: p.$ref }, componentName)) ||
(p.schema && schemaReferencesComponent(p.schema, componentName))
);
if (hasComponent) usage.push('parameters');
}

// Check requestBody
if (details.requestBody &&
details.requestBody.content &&
Object.values(details.requestBody.content).some(c =>
c.schema && c.schema.$ref && c.schema.$ref.includes(componentName))) {
usage.push('requestBody');
if (details.requestBody) {
let hasComponent = false;
if (details.requestBody.$ref) {
hasComponent = schemaReferencesComponent({ $ref: details.requestBody.$ref }, componentName);
} else if (details.requestBody.content) {
hasComponent = Object.values(details.requestBody.content).some(c =>
c.schema && schemaReferencesComponent(c.schema, componentName)
);
}
if (hasComponent) usage.push('requestBody');
}

// Check responses
if (details.responses &&
Object.values(details.responses).some(r =>
r.content && Object.values(r.content).some(c =>
c.schema && c.schema.$ref && c.schema.$ref.includes(componentName)))) {
usage.push('responses');
if (details.responses) {
const hasComponent = Object.entries(details.responses).some(([code, r]) => {
if (r.$ref) return schemaReferencesComponent({ $ref: r.$ref }, componentName);
if (r.content) {
return Object.values(r.content).some(c =>
c.schema && schemaReferencesComponent(c.schema, componentName)
);
}
return false;
});
if (hasComponent) usage.push('responses');
}

return usage;
Expand All @@ -160,56 +235,88 @@ function generateReleaseNotes() {
sections.push(section);
}

// Modified endpoints
if (Object.keys(changes.modified).length > 0 || Object.keys(changes.affectedByComponents).length > 0) {
let section = '## Modified\n';
// Helper function to generate route modification details
function generateModifiedRouteDetails(path, changes) {
let details = '';
const methodsToProcess = new Set();

// Combine and sort all modified paths
const allModifiedPaths = new Set([
...Object.keys(changes.modified),
...Object.keys(changes.affectedByComponents)
]);
// Collect all affected methods
if (changes.modified[path]) {
changes.modified[path].forEach(({method}) => methodsToProcess.add(method));
}
if (changes.affectedByComponents[path]) {
changes.affectedByComponents[path].methods.forEach(method => methodsToProcess.add(method));
}

Array.from(allModifiedPaths)
// Process each method
Array.from(methodsToProcess)
.sort()
.forEach(path => {
// Handle both direct modifications and component changes for each path
const methodsToProcess = new Set();
.forEach(method => {
details += `- [${method}] \`${path}\`\n`;

// Collect all affected methods
if (changes.modified[path]) {
changes.modified[path].forEach(({method}) => methodsToProcess.add(method));
// Add direct changes
const directChanges = changes.modified[path]?.find(m => m.method === method);
if (directChanges) {
directChanges.changes.sort().forEach(change => {
details += ` - ${change}\n`;
});
}
if (changes.affectedByComponents[path]) {
changes.affectedByComponents[path].methods.forEach(method => methodsToProcess.add(method));

// Add component changes
if (changes.affectedByComponents[path]?.methods.has(method)) {
const methodDetails = currentSpec.paths[path][method.toLowerCase()];
Array.from(changes.affectedByComponents[path].components)
.sort()
.forEach(component => {
const usageLocations = findComponentUsage(methodDetails, component).sort();
details += ` - \`${component}\` modified in ${usageLocations.join(', ')}\n`;
});
}
});
return details;
}

// Process each method
Array.from(methodsToProcess)
.sort()
.forEach(method => {
section += `- [${method}] \`${path}\`\n`;

// Add direct changes
const directChanges = changes.modified[path]?.find(m => m.method === method);
if (directChanges) {
directChanges.changes.sort().forEach(change => {
section += ` - ${change}\n`;
});
}

// Add component changes
if (changes.affectedByComponents[path]?.methods.has(method)) {
const methodDetails = currentSpec.paths[path][method.toLowerCase()];
Array.from(changes.affectedByComponents[path].components)
.sort()
.forEach(component => {
const usageLocations = findComponentUsage(methodDetails, component).sort();
section += ` - \`${component}\` modified in ${usageLocations.join(', ')}\n`;
});
}
});
// Modified endpoints
if (Object.keys(changes.modified).length > 0 || Object.keys(changes.affectedByComponents).length > 0) {
let section = '## Modified\n';

// First show all directly modified paths
const directlyModifiedPaths = Object.keys(changes.modified).sort();
directlyModifiedPaths.forEach(path => {
section += generateModifiedRouteDetails(path, changes);
});

// Then show component-affected paths (but not ones that were directly modified)
const componentAffectedEntries = Object.entries(changes.affectedByComponents)
.filter(([path]) => !changes.modified[path]) // Only paths not already shown above
.flatMap(([path, details]) =>
Array.from(details.methods).map(method => ({path, method}))
)
.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));

// Show first 5 component-affected method-path combinations
const visibleEntries = componentAffectedEntries.slice(0, 5);
const processedPaths = new Set();

visibleEntries.forEach(({path}) => {
if (!processedPaths.has(path)) {
section += generateModifiedRouteDetails(path, changes);
processedPaths.add(path);
}
});

// Collapse any remaining entries
const remainingEntries = componentAffectedEntries.slice(5);
if (remainingEntries.length > 0) {
section += '\n<details><summary>Show more routes affected by component changes...</summary>\n\n';
const remainingPaths = new Set();
remainingEntries.forEach(({path}) => remainingPaths.add(path));
Array.from(remainingPaths).sort().forEach(path => {
section += generateModifiedRouteDetails(path, changes);
});
section += '</details>\n';
}

sections.push(section);
}

Expand Down
Loading

0 comments on commit 714ed47

Please sign in to comment.