/**
* 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']
}
};
}
}