Skip to main content
Glama
git-release.ts21.5 kB
/** * Git Release Tool * * Release management tool providing comprehensive Git release operations. * Supports both local Git operations and remote provider operations. * * Operations: create, list, get, update, delete, publish, download */ import { GitCommandExecutor } from '../utils/git-command-executor.js'; import { ParameterValidator, ToolParams } from '../utils/parameter-validator.js'; import { OperationErrorHandler, ToolResult } from '../utils/operation-error-handler.js'; import { ProviderOperationHandler } from '../providers/provider-operation-handler.js'; import { ProviderConfig, ProviderOperation } from '../providers/types.js'; import { configManager } from '../config.js'; export interface GitReleaseParams extends ToolParams { action: 'create' | 'list' | 'get' | 'update' | 'delete' | 'publish' | 'download'; // Release operation parameters tagName?: string; // For create, get, update, delete, publish, download releaseName?: string; // For create, update (defaults to tagName) description?: string; // For create, update body?: string; // For create, update (release notes) // Create parameters commitish?: string; // For create (commit/branch to create release from) draft?: boolean; // For create, update prerelease?: boolean; // For create, update // Asset parameters assets?: string[]; // For create, update (file paths to upload) assetName?: string; // For download (specific asset to download) downloadPath?: string; // For download (where to save) // Options force?: boolean; // For delete, update generateNotes?: boolean; // For create (auto-generate release notes) // Remote operation parameters repo?: string; // For remote operations // List/search parameters limit?: number; // For list (max releases to return) includeDrafts?: boolean; // For list includePrerelease?: boolean; // For list } export class GitReleaseTool { private gitExecutor: GitCommandExecutor; private providerHandler?: ProviderOperationHandler; constructor(providerConfig?: ProviderConfig) { this.gitExecutor = new GitCommandExecutor(); if (providerConfig) { this.providerHandler = new ProviderOperationHandler(providerConfig); } } /** * Execute git-release operation */ async execute(params: GitReleaseParams): Promise<ToolResult> { const startTime = Date.now(); try { // Validate basic parameters const validation = ParameterValidator.validateToolParams('git-release', params); if (!validation.isValid) { return OperationErrorHandler.createToolError( 'VALIDATION_ERROR', `Parameter validation failed: ${validation.errors.join(', ')}`, params.action, { validationErrors: validation.errors }, validation.suggestions ); } // Validate operation-specific parameters const operationValidation = ParameterValidator.validateOperationParams('git-release', params.action, params); if (!operationValidation.isValid) { return OperationErrorHandler.createToolError( 'VALIDATION_ERROR', `Operation validation failed: ${operationValidation.errors.join(', ')}`, params.action, { validationErrors: operationValidation.errors }, operationValidation.suggestions ); } // Route to appropriate handler const isRemoteOperation = this.isRemoteOperation(params.action); if (isRemoteOperation) { return await this.executeRemoteOperation(params, startTime); } else { return await this.executeLocalOperation(params, startTime); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'EXECUTION_ERROR', `Failed to execute ${params.action}: ${errorMessage}`, params.action, { error: errorMessage }, ['Check the error details and try again'] ); } } /** * Execute local Git release operations */ private async executeLocalOperation(params: GitReleaseParams, startTime: number): Promise<ToolResult> { switch (params.action) { case 'create': return await this.handleCreateLocalRelease(params, startTime); case 'list': return await this.handleListLocalReleases(params, startTime); case 'get': return await this.handleGetLocalRelease(params, startTime); default: return OperationErrorHandler.createToolError( 'UNSUPPORTED_LOCAL_OPERATION', `Local operation '${params.action}' is not supported. Use provider for remote release operations.`, params.action, {}, ['Specify a provider (github, gitea, or both) for remote release operations'] ); } } /** * Execute remote provider operations */ private async executeRemoteOperation(params: GitReleaseParams, startTime: number): Promise<ToolResult> { if (!this.providerHandler) { return OperationErrorHandler.createToolError( 'PROVIDER_NOT_CONFIGURED', 'Provider handler is not configured for remote operations', params.action, {}, ['Configure GitHub or Gitea provider to use remote operations'] ); } if (!params.provider) { if (configManager.isUniversalMode()) { params.provider = 'both'; console.error(`[Universal Mode] Auto-applying both providers for ${params.action}`); } else { return OperationErrorHandler.createToolError( 'PROVIDER_REQUIRED', 'Provider parameter is required for remote release operations', params.action, {}, ['Specify provider as: github, gitea, or both'] ); } } const operation: ProviderOperation = { provider: params.provider, operation: this.mapActionToProviderOperation(params.action), parameters: this.extractRemoteParameters(params), requiresAuth: true, isRemoteOperation: true }; try { const result = await this.providerHandler.executeOperation(operation); return { success: result.success, data: result.partialFailure ? result : result.results[0]?.data, error: result.success ? undefined : { code: result.errors[0]?.error?.code || 'REMOTE_OPERATION_ERROR', message: result.errors[0]?.error?.message || 'Remote operation failed', details: result.errors, suggestions: ['Check provider configuration and credentials'] }, metadata: { provider: params.provider, operation: params.action, timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'REMOTE_OPERATION_ERROR', `Remote operation failed: ${errorMessage}`, params.action, { error: errorMessage }, ['Check provider configuration and network connectivity'] ); } } /** * Handle create local release operation (creates tag with release info) */ private async handleCreateLocalRelease(params: GitReleaseParams, startTime: number): Promise<ToolResult> { try { if (!params.tagName) { return OperationErrorHandler.createToolError( 'MISSING_PARAMETER', 'tagName is required for release creation', 'create', {}, ['Provide a tag name for the release'] ); } // Check if tag already exists const existingTags = await this.gitExecutor.listTags(params.projectPath); if (existingTags.success && existingTags.tags?.includes(params.tagName)) { if (!params.force) { return OperationErrorHandler.createToolError( 'TAG_EXISTS', `Tag '${params.tagName}' already exists`, 'create', { tagName: params.tagName }, ['Use force=true to overwrite the existing tag or choose a different name'] ); } } // Prepare release message const releaseName = params.releaseName || params.tagName; const releaseBody = params.body || params.description || ''; const releaseMessage = `${releaseName}\n\n${releaseBody}`.trim(); // Create annotated tag with release information const result = await this.gitExecutor.createTag( params.projectPath, params.tagName, { message: releaseMessage, commit: params.commitish, annotated: true, force: Boolean(params.force) } ); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'create release tag', params.projectPath); } return { success: true, data: { message: `Local release '${releaseName}' created successfully`, tagName: params.tagName, releaseName, description: releaseBody, commit: params.commitish || 'HEAD', draft: Boolean(params.draft), prerelease: Boolean(params.prerelease), output: result.stdout, note: 'This is a local release (tag). Use a provider for remote release management.' }, metadata: { operation: 'create', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'CREATE_RELEASE_ERROR', `Failed to create local release: ${errorMessage}`, 'create', { error: errorMessage, projectPath: params.projectPath } ); } } /** * Handle list local releases operation (lists tags as releases) */ private async handleListLocalReleases(params: GitReleaseParams, startTime: number): Promise<ToolResult> { try { const result = await this.gitExecutor.listTags(params.projectPath, { sort: '-version:refname' // Sort by version descending }); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'list release tags', params.projectPath); } // Get detailed information for each tag (treating as releases) const releases = []; const tags = result.tags || []; const limit = params.limit || 20; for (const tag of tags.slice(0, limit)) { const tagInfo = await this.gitExecutor.getTagInfo(params.projectPath, tag); if (tagInfo.success) { releases.push({ tagName: tag, name: tag, type: tagInfo.type, commit: tagInfo.commit, message: tagInfo.message, author: tagInfo.tagger, date: tagInfo.date, isLocal: true }); } } return { success: true, data: { releases, total: releases.length, totalTags: tags.length, limit, note: 'These are local releases (tags). Use a provider for remote release management.' }, metadata: { operation: 'list', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'LIST_RELEASES_ERROR', `Failed to list local releases: ${errorMessage}`, 'list', { error: errorMessage, projectPath: params.projectPath } ); } } /** * Handle get local release operation (gets tag info as release) */ private async handleGetLocalRelease(params: GitReleaseParams, startTime: number): Promise<ToolResult> { try { if (!params.tagName) { return OperationErrorHandler.createToolError( 'MISSING_PARAMETER', 'tagName is required for get release operation', 'get', {}, ['Provide the tag name of the release to get information about'] ); } // Get tag information const tagInfo = await this.gitExecutor.getTagInfo(params.projectPath, params.tagName); if (!tagInfo.success) { return OperationErrorHandler.handleGitError(tagInfo.stderr, 'get release tag info', params.projectPath); } if (!tagInfo.exists) { return OperationErrorHandler.createToolError( 'RELEASE_NOT_FOUND', `Release tag '${params.tagName}' does not exist`, 'get', { tagName: params.tagName }, ['Check the tag name or list available releases first'] ); } // Get commit information for the tag const commitInfo = await this.gitExecutor.getCommitInfo(params.projectPath, tagInfo.commit || params.tagName); // Parse release name and body from tag message const tagMessage = tagInfo.message || ''; const lines = tagMessage.split('\n'); const releaseName = lines[0] || params.tagName; const releaseBody = lines.slice(2).join('\n').trim(); // Skip empty line after title return { success: true, data: { tagName: params.tagName, name: releaseName, body: releaseBody, type: tagInfo.type, commit: tagInfo.commit, author: tagInfo.tagger, date: tagInfo.date, commitInfo: commitInfo.success ? { hash: commitInfo.hash, author: commitInfo.author, date: commitInfo.date, message: commitInfo.message } : null, isLocal: true, note: 'This is a local release (tag). Use a provider for remote release management.' }, metadata: { operation: 'get', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'GET_RELEASE_ERROR', `Failed to get local release information: ${errorMessage}`, 'get', { error: errorMessage, projectPath: params.projectPath } ); } } /** * Check if operation is a remote operation */ private isRemoteOperation(action: string): boolean { // Most release operations require remote provider APIs const remoteOperations = ['create', 'list', 'get', 'update', 'delete', 'publish', 'download']; return remoteOperations.includes(action); } /** * Extract parameters for remote operations */ private extractRemoteParameters(params: GitReleaseParams): Record<string, any> { const remoteParams: Record<string, any> = { projectPath: params.projectPath }; // Common parameters if (params.repo) remoteParams.repo = params.repo; if (params.tagName) { remoteParams.tagName = params.tagName; remoteParams.tag_name = params.tagName; // GitHub API expects tag_name } // Operation-specific parameters switch (params.action) { case 'create': if (params.releaseName) remoteParams.name = params.releaseName; if (params.description) remoteParams.description = params.description; if (params.body) remoteParams.body = params.body; if (params.commitish) remoteParams.target_commitish = params.commitish; if (params.draft !== undefined) remoteParams.draft = params.draft; if (params.prerelease !== undefined) remoteParams.prerelease = params.prerelease; if (params.generateNotes !== undefined) remoteParams.generate_release_notes = params.generateNotes; if (params.assets) remoteParams.assets = params.assets; break; case 'list': if (params.limit) remoteParams.per_page = params.limit; if (params.includeDrafts !== undefined) remoteParams.include_drafts = params.includeDrafts; if (params.includePrerelease !== undefined) remoteParams.include_prerelease = params.includePrerelease; break; case 'get': // Get operations need tagName (already handled above) break; case 'update': if (params.releaseName) remoteParams.name = params.releaseName; if (params.description) remoteParams.description = params.description; if (params.body) remoteParams.body = params.body; if (params.draft !== undefined) remoteParams.draft = params.draft; if (params.prerelease !== undefined) remoteParams.prerelease = params.prerelease; if (params.assets) remoteParams.assets = params.assets; break; case 'delete': // Delete operations need tagName (already handled above) break; case 'publish': // Publish operations (make draft release public) remoteParams.draft = false; break; case 'download': if (params.assetName) remoteParams.asset_name = params.assetName; if (params.downloadPath) remoteParams.download_path = params.downloadPath; break; } return remoteParams; } /** * Map git-release actions to provider operations */ private mapActionToProviderOperation(action: string): string { const actionMap: Record<string, string> = { 'create': 'release-create', 'list': 'release-list', 'get': 'release-get', 'update': 'release-update', 'delete': 'release-delete', 'publish': 'release-publish', 'download': 'release-download' }; return actionMap[action] || action; } /** * Get tool schema for MCP registration */ static getToolSchema() { return { name: 'git-release', description: 'Git release management tool for release operations. Supports create, list, get, update, delete, publish, and download operations. Local operations work with tags, remote operations require a provider. In universal mode (GIT_MCP_MODE=universal), automatically executes on both GitHub and Gitea providers.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['create', 'list', 'get', 'update', 'delete', 'publish', 'download'], description: 'The release operation to perform' }, projectPath: { type: 'string', description: 'Absolute path to the project directory' }, provider: { type: 'string', enum: ['github', 'gitea', 'both'], description: 'Provider for remote operations (required for update, delete, publish, download)' }, tagName: { type: 'string', description: 'Tag name for the release (required for most operations)' }, releaseName: { type: 'string', description: 'Name of the release (defaults to tagName, for create/update)' }, description: { type: 'string', description: 'Short description of the release (for create/update)' }, body: { type: 'string', description: 'Detailed release notes/body (for create/update)' }, commitish: { type: 'string', description: 'Commit or branch to create release from (default: HEAD, for create)' }, draft: { type: 'boolean', description: 'Create as draft release (for create/update)' }, prerelease: { type: 'boolean', description: 'Mark as pre-release (for create/update)' }, assets: { type: 'array', items: { type: 'string' }, description: 'File paths to upload as release assets (for create/update)' }, assetName: { type: 'string', description: 'Specific asset name to download (for download)' }, downloadPath: { type: 'string', description: 'Path to save downloaded assets (for download)' }, force: { type: 'boolean', description: 'Force operation (for delete, update)' }, generateNotes: { type: 'boolean', description: 'Auto-generate release notes (for create)' }, repo: { type: 'string', description: 'Repository name (for remote operations)' }, limit: { type: 'number', description: 'Maximum number of releases to return (for list)' }, includeDrafts: { type: 'boolean', description: 'Include draft releases in list (for list)' }, includePrerelease: { type: 'boolean', description: 'Include pre-releases in list (for list)' } }, required: ['action', 'projectPath'] } }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Andre-Buzeli/git-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server