semver-analyzer
You are semver-analyzer - a specialized skill for analyzing code changes and determining appropriate semantic version bumps, ensuring consistent SDK versioning and clear communication of change impacts to consumers.
Overview
This skill enables AI-powered semantic versioning including:
- Detecting breaking changes automatically
- Suggesting version bumps (major/minor/patch)
- Generating changelog entries from commits
- Validating version consistency across SDKs
- Enforcing conventional commit standards
- Creating release notes automatically
- Tracking version dependencies
Prerequisites
- Git repository with version history
- Conventional commit messages (recommended)
- Package manifest files (package.json, pyproject.toml, etc.)
- semantic-release or similar tooling (optional)
Capabilities
1. Breaking Change Detection
Automatically detect breaking changes in SDK code:
// src/analyzer/breaking-changes.ts
import { parse } from '@typescript-eslint/parser';
import { diff } from 'deep-object-diff';
interface BreakingChange {
type: 'removed' | 'signature-changed' | 'type-changed' | 'behavior-changed';
location: string;
description: string;
severity: 'major' | 'warning';
migration?: string;
}
interface AnalysisResult {
hasBreakingChanges: boolean;
breakingChanges: BreakingChange[];
suggestedBump: 'major' | 'minor' | 'patch';
confidence: number;
}
export async function analyzeChanges(
oldVersion: string,
newVersion: string,
options: AnalyzerOptions
): Promise<AnalysisResult> {
const oldApi = await extractPublicApi(oldVersion);
const newApi = await extractPublicApi(newVersion);
const breakingChanges: BreakingChange[] = [];
// Check for removed exports
for (const [name, oldExport] of Object.entries(oldApi.exports)) {
if (!(name in newApi.exports)) {
breakingChanges.push({
type: 'removed',
location: name,
description: `Export '${name}' was removed`,
severity: 'major',
migration: `Remove usage of '${name}' or use alternative`
});
}
}
// Check for signature changes in functions
for (const [name, newFunc] of Object.entries(newApi.functions)) {
const oldFunc = oldApi.functions[name];
if (!oldFunc) continue;
// Check parameter changes
if (newFunc.requiredParams > oldFunc.requiredParams) {
breakingChanges.push({
type: 'signature-changed',
location: name,
description: `Function '${name}' has new required parameters`,
severity: 'major',
migration: `Update calls to '${name}' to include new required parameters`
});
}
// Check return type changes
if (newFunc.returnType !== oldFunc.returnType) {
if (!isTypeCompatible(oldFunc.returnType, newFunc.returnType)) {
breakingChanges.push({
type: 'type-changed',
location: name,
description: `Return type of '${name}' changed from '${oldFunc.returnType}' to '${newFunc.returnType}'`,
severity: 'major'
});
}
}
}
// Check for type changes in models
for (const [name, newModel] of Object.entries(newApi.models)) {
const oldModel = oldApi.models[name];
if (!oldModel) continue;
// Check for removed fields
for (const field of Object.keys(oldModel.fields)) {
if (!(field in newModel.fields)) {
breakingChanges.push({
type: 'removed',
location: `${name}.${field}`,
description: `Field '${field}' was removed from model '${name}'`,
severity: 'major'
});
}
}
// Check for type changes in fields
for (const [field, newField] of Object.entries(newModel.fields)) {
const oldField = oldModel.fields[field];
if (oldField && oldField.type !== newField.type) {
breakingChanges.push({
type: 'type-changed',
location: `${name}.${field}`,
description: `Field '${name}.${field}' type changed from '${oldField.type}' to '${newField.type}'`,
severity: 'major'
});
}
}
}
const hasBreakingChanges = breakingChanges.length > 0;
return {
hasBreakingChanges,
breakingChanges,
suggestedBump: hasBreakingChanges ? 'major' : await analyzeFeaturesAndFixes(oldVersion, newVersion),
confidence: calculateConfidence(breakingChanges)
};
}
2. Conventional Commit Analysis
Parse and analyze conventional commits:
// src/analyzer/commits.ts
import { execSync } from 'child_process';
interface CommitInfo {
hash: string;
type: string;
scope?: string;
description: string;
body?: string;
breaking: boolean;
footers: Record<string, string>;
}
interface CommitAnalysis {
commits: CommitInfo[];
suggestedBump: 'major' | 'minor' | 'patch';
changelog: ChangelogSection[];
}
const COMMIT_PATTERN = /^(?<type>\w+)(?:\((?<scope>[^)]+)\))?(?<breaking>!)?: (?<description>.+)$/;
export function analyzeCommits(fromRef: string, toRef: string): CommitAnalysis {
const log = execSync(
`git log ${fromRef}..${toRef} --format="%H|||%s|||%b|||%N" --no-merges`,
{ encoding: 'utf8' }
);
const commits: CommitInfo[] = [];
let suggestedBump: 'major' | 'minor' | 'patch' = 'patch';
for (const entry of log.split('\n').filter(Boolean)) {
const [hash, subject, body, notes] = entry.split('|||');
const match = COMMIT_PATTERN.exec(subject);
if (!match?.groups) continue;
const commit: CommitInfo = {
hash,
type: match.groups.type,
scope: match.groups.scope,
description: match.groups.description,
body: body?.trim(),
breaking: match.groups.breaking === '!' || body?.includes('BREAKING CHANGE:'),
footers: parseFooters(body)
};
commits.push(commit);
// Determine version bump
if (commit.breaking) {
suggestedBump = 'major';
} else if (commit.type === 'feat' && suggestedBump !== 'major') {
suggestedBump = 'minor';
}
}
return {
commits,
suggestedBump,
changelog: generateChangelog(commits)
};
}
function parseFooters(body?: string): Record<string, string> {
if (!body) return {};
const footers: Record<string, string> = {};
const lines = body.split('\n');
for (const line of lines) {
const match = /^(?<key>[\w-]+): (?<value>.+)$/.exec(line);
if (match?.groups) {
footers[match.groups.key] = match.groups.value;
}
}
return footers;
}
function generateChangelog(commits: CommitInfo[]): ChangelogSection[] {
const sections: Record<string, CommitInfo[]> = {
'Breaking Changes': [],
'Features': [],
'Bug Fixes': [],
'Performance': [],
'Documentation': [],
'Other': []
};
for (const commit of commits) {
if (commit.breaking) {
sections['Breaking Changes'].push(commit);
}
switch (commit.type) {
case 'feat':
sections['Features'].push(commit);
break;
case 'fix':
sections['Bug Fixes'].push(commit);
break;
case 'perf':
sections['Performance'].push(commit);
break;
case 'docs':
sections['Documentation'].push(commit);
break;
default:
sections['Other'].push(commit);
}
}
return Object.entries(sections)
.filter(([_, commits]) => commits.length > 0)
.map(([title, commits]) => ({
title,
items: commits.map(c => ({
scope: c.scope,
description: c.description,
hash: c.hash.substring(0, 7)
}))
}));
}
3. Semantic Release Configuration
Configure semantic-release for automated versioning:
// release.config.js
module.exports = {
branches: [
'main',
{ name: 'beta', prerelease: true },
{ name: 'alpha', prerelease: true }
],
plugins: [
['@semantic-release/commit-analyzer', {
preset: 'conventionalcommits',
releaseRules: [
{ type: 'feat', release: 'minor' },
{ type: 'fix', re