feat: Add integrated release workflow with AI version suggestion

Adds a streamlined release workflow that uses AI (Claude Haiku) to analyze git commits and suggest semantic version bumps (major/minor/patch). Automatically updates package.json, commits, and pushes using a safe git workflow that preserves user's current work.

New features:

- RELEASE_SUGGEST_VERSION IPC for AI-powered version suggestions

- bumpVersion() with safe git workflow (stash/checkout/commit/restore)

- VersionSuggestion type with bumpType and reasoning

- Updated ReleaseProgress to include bumping_version stage

Also replaces VERSION file detection with requirements.txt across the codebase.
This commit is contained in:
AndyMik90
2025-12-18 10:32:55 +01:00
parent 0ef0e1588b
commit 7f3cd5969d
14 changed files with 415 additions and 49 deletions
@@ -39,8 +39,8 @@ function setupTestDirs(): void {
// Create auto-claude source directory that getAutoBuildSourcePath looks for
mkdirSync(AUTO_CLAUDE_SOURCE, { recursive: true });
// Create VERSION file (required by getAutoBuildSourcePath)
writeFileSync(path.join(AUTO_CLAUDE_SOURCE, 'VERSION'), '1.0.0');
// Create requirements.txt file (used as marker by getAutoBuildSourcePath)
writeFileSync(path.join(AUTO_CLAUDE_SOURCE, 'requirements.txt'), '# Mock requirements');
// Create runners subdirectory (where spec_runner.py lives after restructure)
mkdirSync(path.join(AUTO_CLAUDE_SOURCE, 'runners'), { recursive: true });
@@ -65,7 +65,8 @@ export class AgentProcessManager {
];
for (const p of possiblePaths) {
if (existsSync(p) && existsSync(path.join(p, 'VERSION'))) {
// Use requirements.txt as marker - it always exists in auto-claude source
if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) {
return p;
}
}
@@ -174,7 +175,8 @@ export class AgentProcessManager {
...process.env,
...extraEnv,
...profileEnv, // Include active Claude profile config
PYTHONUNBUFFERED: '1' // Ensure real-time output
PYTHONUNBUFFERED: '1', // Ensure real-time output
PYTHONIOENCODING: 'utf-8' // Ensure UTF-8 encoding on Windows
}
});
@@ -150,7 +150,8 @@ export class ChangelogService extends EventEmitter {
];
for (const p of possiblePaths) {
if (existsSync(p) && existsSync(path.join(p, 'VERSION'))) {
// Use requirements.txt as marker - it always exists in auto-claude source
if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) {
return p;
}
}
+4 -2
View File
@@ -45,7 +45,8 @@ export class InsightsConfig {
];
for (const p of possiblePaths) {
if (existsSync(p) && existsSync(path.join(p, 'VERSION'))) {
// Use requirements.txt as marker - it always exists in auto-claude source
if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) {
return p;
}
}
@@ -103,7 +104,8 @@ export class InsightsConfig {
...process.env as Record<string, string>,
...autoBuildEnv,
...profileEnv,
PYTHONUNBUFFERED: '1'
PYTHONUNBUFFERED: '1',
PYTHONIOENCODING: 'utf-8'
};
}
}
@@ -4,9 +4,12 @@
import { ipcMain } from 'electron';
import { execSync } from 'child_process';
import { existsSync, readFileSync } from 'fs';
import path from 'path';
import { IPC_CHANNELS } from '../../../shared/constants';
import type { IPCResult } from '../../../shared/types';
import type { IPCResult, GitCommit, VersionSuggestion } from '../../../shared/types';
import { projectStore } from '../../project-store';
import { changelogService } from '../../changelog-service';
import type { ReleaseOptions } from './types';
/**
@@ -118,9 +121,146 @@ export function registerCreateRelease(): void {
);
}
/**
* Get the latest git tag in the repository
*/
function getLatestTag(projectPath: string): string | null {
try {
const tag = execSync('git describe --tags --abbrev=0 2>/dev/null || echo ""', {
cwd: projectPath,
encoding: 'utf-8'
}).trim();
return tag || null;
} catch {
return null;
}
}
/**
* Get commits since a specific tag (or all commits if no tag)
*/
function getCommitsSinceTag(projectPath: string, tag: string | null): GitCommit[] {
try {
const range = tag ? `${tag}..HEAD` : 'HEAD';
const format = '%H|%s|%an|%ae|%aI';
const output = execSync(`git log ${range} --pretty=format:"${format}"`, {
cwd: projectPath,
encoding: 'utf-8'
}).trim();
if (!output) return [];
return output.split('\n').map(line => {
const [fullHash, subject, authorName, authorEmail, date] = line.split('|');
return {
hash: fullHash.substring(0, 7),
fullHash,
subject,
author: authorName,
authorEmail,
date
};
});
} catch {
return [];
}
}
/**
* Get current version from package.json
*/
function getCurrentVersion(projectPath: string): string {
try {
const pkgPath = path.join(projectPath, 'package.json');
if (!existsSync(pkgPath)) {
return '0.0.0';
}
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
return pkg.version || '0.0.0';
} catch {
return '0.0.0';
}
}
/**
* Suggest version for release using AI analysis of commits
*/
export function registerSuggestVersion(): void {
ipcMain.handle(
IPC_CHANNELS.RELEASE_SUGGEST_VERSION,
async (_, projectId: string): Promise<IPCResult<VersionSuggestion>> => {
const project = projectStore.getProject(projectId);
if (!project) {
return { success: false, error: 'Project not found' };
}
try {
// Get current version from package.json
const currentVersion = getCurrentVersion(project.path);
// Get latest tag
const latestTag = getLatestTag(project.path);
// Get commits since last tag
const commits = getCommitsSinceTag(project.path, latestTag);
if (commits.length === 0) {
// No commits since last release, suggest patch bump
const [major, minor, patch] = currentVersion.split('.').map(Number);
return {
success: true,
data: {
suggestedVersion: `${major}.${minor}.${patch + 1}`,
currentVersion,
bumpType: 'patch',
reason: 'No new commits since last release',
commitCount: 0
}
};
}
// Use AI to analyze commits and suggest version
const suggestion = await changelogService.suggestVersionFromCommits(
project.path,
commits,
currentVersion
);
return {
success: true,
data: {
suggestedVersion: suggestion.version,
currentVersion,
bumpType: suggestion.reason.includes('breaking') ? 'major' :
suggestion.reason.includes('feature') || suggestion.reason.includes('minor') ? 'minor' : 'patch',
reason: suggestion.reason,
commitCount: commits.length
}
};
} catch (error) {
// Fallback to patch bump on error
const currentVersion = getCurrentVersion(project.path);
const [major, minor, patch] = currentVersion.split('.').map(Number);
return {
success: true,
data: {
suggestedVersion: `${major}.${minor}.${patch + 1}`,
currentVersion,
bumpType: 'patch',
reason: 'Fallback suggestion (AI analysis unavailable)',
commitCount: 0
}
};
}
}
);
}
/**
* Register all release-related handlers
*/
export function registerReleaseHandlers(): void {
registerCreateRelease();
registerSuggestVersion();
}
@@ -146,8 +146,9 @@ const detectAutoBuildSourcePath = (): string | null => {
}
for (const p of possiblePaths) {
const versionPath = path.join(p, 'VERSION');
const exists = existsSync(p) && existsSync(versionPath);
// Use requirements.txt as marker - it always exists in auto-claude source
const markerPath = path.join(p, 'requirements.txt');
const exists = existsSync(p) && existsSync(markerPath);
if (debug) {
console.log(`[project-handlers:detectAutoBuildSourcePath] Checking ${p}: ${exists ? '✓ FOUND' : '✗ not found'}`);
@@ -60,8 +60,9 @@ const detectAutoBuildSourcePath = (): string | null => {
}
for (const p of possiblePaths) {
const versionPath = path.join(p, 'VERSION');
const exists = existsSync(p) && existsSync(versionPath);
// Use requirements.txt as marker - it always exists in auto-claude source
const markerPath = path.join(p, 'requirements.txt');
const exists = existsSync(p) && existsSync(markerPath);
if (debug) {
console.log(`[detectAutoBuildSourcePath] Checking ${p}: ${exists ? '✓ FOUND' : '✗ not found'}`);
@@ -101,8 +101,9 @@ export interface InitializationResult {
*/
export function hasLocalSource(projectPath: string): boolean {
const localSourcePath = path.join(projectPath, 'auto-claude');
const versionFile = path.join(localSourcePath, 'VERSION');
return existsSync(localSourcePath) && existsSync(versionFile);
// Use requirements.txt as marker - it always exists in auto-claude source
const markerFile = path.join(localSourcePath, 'requirements.txt');
return existsSync(localSourcePath) && existsSync(markerFile);
}
/**
+211 -28
View File
@@ -1,6 +1,6 @@
import { EventEmitter } from 'events';
import path from 'path';
import { existsSync, readFileSync, readdirSync } from 'fs';
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs';
import { execSync, spawn } from 'child_process';
import type {
ReleaseableVersion,
@@ -17,7 +17,7 @@ import { DEFAULT_CHANGELOG_PATH } from '../shared/constants';
/**
* Service for creating GitHub releases with worktree-aware pre-flight checks.
*
*
* Key feature: Worktree checks are SCOPED to tasks in the release version.
* If a worktree exists for a task NOT in this release, it won't block the release.
*/
@@ -32,7 +32,7 @@ export class ReleaseService extends EventEmitter {
*/
parseChangelogVersions(projectPath: string): ReleaseableVersion[] {
const changelogPath = path.join(projectPath, DEFAULT_CHANGELOG_PATH);
if (!existsSync(changelogPath)) {
return [];
}
@@ -49,7 +49,7 @@ export class ReleaseService extends EventEmitter {
const version = match[1];
const date = match[2] || '';
const startIndex = match.index! + match[0].length;
// Content is until next version header or end of file
const endIndex = i < matches.length - 1 ? matches[i + 1].index! : content.length;
const versionContent = content.slice(startIndex, endIndex).trim();
@@ -98,17 +98,17 @@ export class ReleaseService extends EventEmitter {
tasks: Task[]
): Promise<ReleaseableVersion[]> {
const versions = this.parseChangelogVersions(projectPath);
// Populate task spec IDs for each version
for (const version of versions) {
const { specIds } = this.getTasksForVersion(projectPath, version.version, tasks);
version.taskSpecIds = specIds;
// Check if already released on GitHub
try {
const tagExists = this.checkTagExists(projectPath, version.tagName);
version.isReleased = tagExists;
if (tagExists) {
// Try to get release URL
version.releaseUrl = this.getGitHubReleaseUrl(projectPath, version.tagName);
@@ -129,11 +129,11 @@ export class ReleaseService extends EventEmitter {
try {
// Check local tags
execSync(`git tag -l "${tagName}"`, { cwd: projectPath, encoding: 'utf-8' });
const localTags = execSync(`git tag -l "${tagName}"`, {
cwd: projectPath,
encoding: 'utf-8'
const localTags = execSync(`git tag -l "${tagName}"`, {
cwd: projectPath,
encoding: 'utf-8'
}).trim();
if (localTags) return true;
// Check remote tags
@@ -147,7 +147,7 @@ export class ReleaseService extends EventEmitter {
cwd: projectPath,
encoding: 'utf-8'
}).trim();
return !!remoteTags;
} catch {
return false;
@@ -166,7 +166,7 @@ export class ReleaseService extends EventEmitter {
cwd: projectPath,
encoding: 'utf-8'
}).trim();
return result || undefined;
} catch {
return undefined;
@@ -175,7 +175,7 @@ export class ReleaseService extends EventEmitter {
/**
* Run pre-flight checks for a specific version.
*
*
* IMPORTANT: Worktree checks are scoped to tasks in this version only.
* Worktrees for other tasks (future releases) won't block this release.
*/
@@ -186,7 +186,7 @@ export class ReleaseService extends EventEmitter {
): Promise<ReleasePreflightStatus> {
const tagName = `v${version}`;
const { specIds } = this.getTasksForVersion(projectPath, version, tasks);
const status: ReleasePreflightStatus = {
canRelease: false,
checks: {
@@ -303,7 +303,7 @@ export class ReleaseService extends EventEmitter {
if (unmergedWorktrees.length === 0) {
status.checks.worktreesMerged = {
passed: true,
message: specIds.length > 0
message: specIds.length > 0
? `All ${specIds.length} feature(s) in this release are merged`
: 'No features to check (version may have been manually added)',
unmergedWorktrees: []
@@ -314,7 +314,7 @@ export class ReleaseService extends EventEmitter {
message: `${unmergedWorktrees.length} feature(s) have unmerged worktrees`,
unmergedWorktrees
};
for (const wt of unmergedWorktrees) {
status.blockers.push(
`Feature "${wt.taskTitle}" (${wt.specId}) has unmerged changes in worktree`
@@ -330,7 +330,7 @@ export class ReleaseService extends EventEmitter {
/**
* Check worktrees ONLY for tasks that are part of this release version.
*
*
* This is the key function that scopes worktree checks to the release:
* - If a task is in the release AND has an unmerged worktree → BLOCK
* - If a task is NOT in the release but has a worktree → IGNORE (it's for a future release)
@@ -344,7 +344,7 @@ export class ReleaseService extends EventEmitter {
// Get worktrees directory
const worktreesDir = path.join(projectPath, '.worktrees', 'auto-claude');
if (!existsSync(worktreesDir)) {
// No worktrees exist at all - all clear
return [];
@@ -364,7 +364,7 @@ export class ReleaseService extends EventEmitter {
for (const specId of releaseSpecIds) {
// Find the worktree folder for this spec
// Spec IDs are like "001-feature-name", worktree folders match
const worktreeFolder = worktreeFolders.find(folder =>
const worktreeFolder = worktreeFolders.find(folder =>
folder === specId || folder.startsWith(`${specId}-`)
);
@@ -374,7 +374,7 @@ export class ReleaseService extends EventEmitter {
}
const worktreePath = path.join(worktreesDir, worktreeFolder);
// Get the task info for better error messages
const task = tasks.find(t => t.specId === specId);
const taskTitle = task?.title || specId;
@@ -442,7 +442,7 @@ export class ReleaseService extends EventEmitter {
cwd: worktreePath,
encoding: 'utf-8'
}).trim();
return !hasChanges;
}
@@ -454,7 +454,167 @@ export class ReleaseService extends EventEmitter {
}
/**
* Create a GitHub release.
* Bump version in package.json with safe git workflow.
* Preserves user's current work by stashing, switching to main, then restoring.
*/
async bumpVersion(
projectPath: string,
version: string,
mainBranch: string,
projectId: string
): Promise<{ success: boolean; error?: string }> {
// Save current state
let originalBranch: string;
let hadChanges = false;
let stashCreated = false;
try {
originalBranch = execSync('git rev-parse --abbrev-ref HEAD', {
cwd: projectPath,
encoding: 'utf-8'
}).trim();
} catch {
return { success: false, error: 'Failed to get current git branch' };
}
// Check for uncommitted changes
const gitStatus = execSync('git status --porcelain', {
cwd: projectPath,
encoding: 'utf-8'
}).trim();
hadChanges = !!gitStatus;
try {
// Stash any changes (staged or unstaged)
if (hadChanges) {
this.emitProgress(projectId, {
stage: 'bumping_version',
progress: 5,
message: 'Stashing current changes...'
});
execSync('git stash push -m "auto-claude-release-temp"', {
cwd: projectPath,
encoding: 'utf-8'
});
stashCreated = true;
}
// Checkout main branch
this.emitProgress(projectId, {
stage: 'bumping_version',
progress: 10,
message: `Switching to ${mainBranch}...`
});
if (originalBranch !== mainBranch) {
execSync(`git checkout "${mainBranch}"`, {
cwd: projectPath,
encoding: 'utf-8'
});
}
// Pull latest from origin
this.emitProgress(projectId, {
stage: 'bumping_version',
progress: 15,
message: `Pulling latest from origin/${mainBranch}...`
});
try {
execSync(`git pull origin "${mainBranch}"`, {
cwd: projectPath,
encoding: 'utf-8'
});
} catch {
// Pull might fail if no upstream, continue anyway
}
// Update package.json
this.emitProgress(projectId, {
stage: 'bumping_version',
progress: 20,
message: `Updating package.json to ${version}...`
});
const pkgPath = path.join(projectPath, 'package.json');
if (!existsSync(pkgPath)) {
throw new Error('package.json not found in project root');
}
const pkgContent = readFileSync(pkgPath, 'utf-8');
const pkg = JSON.parse(pkgContent);
pkg.version = version;
// Preserve formatting (detect indent)
const indent = pkgContent.match(/^(\s+)/m)?.[1] || ' ';
writeFileSync(pkgPath, JSON.stringify(pkg, null, indent) + '\n');
// Stage and commit only package.json
this.emitProgress(projectId, {
stage: 'bumping_version',
progress: 25,
message: 'Committing version bump...'
});
execSync('git add package.json', {
cwd: projectPath,
encoding: 'utf-8'
});
execSync(`git commit -m "chore: release v${version}"`, {
cwd: projectPath,
encoding: 'utf-8'
});
// Push to origin
this.emitProgress(projectId, {
stage: 'bumping_version',
progress: 30,
message: `Pushing to origin/${mainBranch}...`
});
execSync(`git push origin "${mainBranch}"`, {
cwd: projectPath,
encoding: 'utf-8'
});
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
} finally {
// Always restore user's original state
try {
if (originalBranch !== mainBranch) {
execSync(`git checkout "${originalBranch}"`, {
cwd: projectPath,
encoding: 'utf-8'
});
}
} catch {
// Log but don't fail - user might need to manually switch back
console.warn('[ReleaseService] Failed to restore original branch');
}
if (stashCreated) {
try {
execSync('git stash pop', {
cwd: projectPath,
encoding: 'utf-8'
});
} catch {
// Stash conflict - warn user
console.warn('[ReleaseService] Failed to pop stash - user may need to run "git stash pop" manually');
}
}
}
}
/**
* Create a GitHub release with optional version bump.
*/
async createRelease(
projectPath: string,
@@ -462,12 +622,36 @@ export class ReleaseService extends EventEmitter {
): Promise<CreateReleaseResult> {
const tagName = `v${request.version}`;
const title = request.title || tagName;
const shouldBumpVersion = request.bumpVersion !== false; // Default to true
try {
// Stage 0: Bump version in package.json (if enabled)
if (shouldBumpVersion && request.mainBranch) {
const bumpResult = await this.bumpVersion(
projectPath,
request.version,
request.mainBranch,
request.projectId
);
if (!bumpResult.success) {
this.emitProgress(request.projectId, {
stage: 'error',
progress: 0,
message: `Version bump failed: ${bumpResult.error}`,
error: bumpResult.error
});
return {
success: false,
error: `Version bump failed: ${bumpResult.error}`
};
}
}
// Stage 1: Create local tag
this.emitProgress(request.projectId, {
stage: 'tagging',
progress: 25,
progress: 40,
message: `Creating tag ${tagName}...`
});
@@ -479,7 +663,7 @@ export class ReleaseService extends EventEmitter {
// Stage 2: Push tag to remote
this.emitProgress(request.projectId, {
stage: 'pushing',
progress: 50,
progress: 60,
message: `Pushing tag ${tagName} to origin...`
});
@@ -491,7 +675,7 @@ export class ReleaseService extends EventEmitter {
// Stage 3: Create GitHub release
this.emitProgress(request.projectId, {
stage: 'creating_release',
progress: 75,
progress: 80,
message: 'Creating GitHub release...'
});
@@ -567,7 +751,7 @@ export class ReleaseService extends EventEmitter {
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Try to clean up the tag if it was created but release failed
try {
execSync(`git tag -d "${tagName}" 2>/dev/null || true`, {
@@ -602,4 +786,3 @@ export class ReleaseService extends EventEmitter {
// Export singleton instance
export const releaseService = new ReleaseService();
@@ -55,7 +55,8 @@ export class TerminalNameGenerator extends EventEmitter {
];
for (const p of possiblePaths) {
if (existsSync(p) && existsSync(path.join(p, 'VERSION'))) {
// Use requirements.txt as marker - it always exists in auto-claude source
if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) {
return p;
}
}
@@ -135,7 +136,8 @@ export class TerminalNameGenerator extends EventEmitter {
...process.env,
...autoBuildEnv,
...profileEnv, // Include active Claude profile config
PYTHONUNBUFFERED: '1'
PYTHONUNBUFFERED: '1',
PYTHONIOENCODING: 'utf-8'
}
});
+4 -2
View File
@@ -55,7 +55,8 @@ export class TitleGenerator extends EventEmitter {
];
for (const p of possiblePaths) {
if (existsSync(p) && existsSync(path.join(p, 'VERSION'))) {
// Use requirements.txt as marker - it always exists in auto-claude source
if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) {
return p;
}
}
@@ -134,7 +135,8 @@ export class TitleGenerator extends EventEmitter {
...process.env,
...autoBuildEnv,
...profileEnv, // Include active Claude profile config
PYTHONUNBUFFERED: '1'
PYTHONUNBUFFERED: '1',
PYTHONIOENCODING: 'utf-8'
}
});
@@ -6,7 +6,8 @@ import type {
GitHubImportResult,
GitHubInvestigationStatus,
GitHubInvestigationResult,
IPCResult
IPCResult,
VersionSuggestion
} from '../../../shared/types';
import { createIpcListener, invokeIpc, sendIpc, IpcListenerCleanup } from './ipc-utils';
@@ -28,6 +29,9 @@ export interface GitHubAPI {
options?: { draft?: boolean; prerelease?: boolean }
) => Promise<IPCResult<{ url: string }>>;
/** AI-powered version suggestion based on commits since last release */
suggestReleaseVersion: (projectId: string) => Promise<IPCResult<VersionSuggestion>>;
// OAuth operations (gh CLI)
checkGitHubCli: () => Promise<IPCResult<{ installed: boolean; version?: string }>>;
checkGitHubAuth: () => Promise<IPCResult<{ authenticated: boolean; username?: string }>>;
@@ -79,6 +83,9 @@ export const createGitHubAPI = (): GitHubAPI => ({
): Promise<IPCResult<{ url: string }>> =>
invokeIpc(IPC_CHANNELS.GITHUB_CREATE_RELEASE, projectId, version, releaseNotes, options),
suggestReleaseVersion: (projectId: string): Promise<IPCResult<VersionSuggestion>> =>
invokeIpc(IPC_CHANNELS.RELEASE_SUGGEST_VERSION, projectId),
// OAuth operations (gh CLI)
checkGitHubCli: (): Promise<IPCResult<{ installed: boolean; version?: string }>> =>
invokeIpc(IPC_CHANNELS.GITHUB_CHECK_CLI),
+10 -1
View File
@@ -269,5 +269,14 @@ export const IPC_CHANNELS = {
APP_UPDATE_AVAILABLE: 'app-update:available',
APP_UPDATE_DOWNLOADED: 'app-update:downloaded',
APP_UPDATE_PROGRESS: 'app-update:progress',
APP_UPDATE_ERROR: 'app-update:error'
APP_UPDATE_ERROR: 'app-update:error',
// Release operations
RELEASE_SUGGEST_VERSION: 'release:suggestVersion',
RELEASE_CREATE: 'release:create',
RELEASE_PREFLIGHT: 'release:preflight',
RELEASE_GET_VERSIONS: 'release:getVersions',
// Release events (main -> renderer)
RELEASE_PROGRESS: 'release:progress'
} as const;
+16 -1
View File
@@ -183,6 +183,10 @@ export interface CreateReleaseRequest {
body: string;
draft?: boolean;
prerelease?: boolean;
/** Main branch to push version bump to (uses project setting if not specified) */
mainBranch?: string;
/** Whether to bump version in package.json before release (default: true) */
bumpVersion?: boolean;
}
export interface CreateReleaseResult {
@@ -193,8 +197,19 @@ export interface CreateReleaseResult {
}
export interface ReleaseProgress {
stage: 'checking' | 'tagging' | 'pushing' | 'creating_release' | 'complete' | 'error';
stage: 'bumping_version' | 'checking' | 'tagging' | 'pushing' | 'creating_release' | 'complete' | 'error';
progress: number;
message: string;
error?: string;
}
/**
* AI-powered version suggestion result
*/
export interface VersionSuggestion {
suggestedVersion: string;
currentVersion: string;
bumpType: 'major' | 'minor' | 'patch';
reason: string;
commitCount: number;
}