UNPKG

auto-publishing-mcp-server

Version:

Enterprise-grade MCP Server for Auto-Publishing with pre-publish validation, multi-cloud deployment, and monitoring

1,023 lines (904 loc) 39.6 kB
import { exec } from 'child_process'; import { promisify } from 'util'; import { promises as fs } from 'fs'; import path from 'path'; const execAsync = promisify(exec); class PrePublishValidator { constructor() { this.validationResults = new Map(); this.validationRules = new Map(); this.autoFixEnabled = true; this.configPath = '/tmp/pre-publish-config.json'; this.reportsPath = '/tmp/validation-reports'; this.loadConfiguration(); this.ensureReportsDirectory(); this.initializeDefaultRules(); } async loadConfiguration() { try { const config = await fs.readFile(this.configPath, 'utf8'); const parsedConfig = JSON.parse(config); this.validationResults = new Map(Object.entries(parsedConfig.validationResults || {})); this.autoFixEnabled = parsedConfig.autoFixEnabled !== false; } catch (error) { console.log('No existing validation configuration found, using defaults'); } } async saveConfiguration() { const config = { validationResults: Object.fromEntries(this.validationResults), autoFixEnabled: this.autoFixEnabled, lastUpdated: new Date().toISOString() }; await fs.writeFile(this.configPath, JSON.stringify(config, null, 2)); } async ensureReportsDirectory() { try { await fs.mkdir(this.reportsPath, { recursive: true }); } catch (error) { console.warn('Failed to create reports directory:', error.message); } } initializeDefaultRules() { // Code quality rules this.validationRules.set('syntax-check', { name: 'Syntax Check', description: 'Validate code syntax', autoFixable: false, critical: true }); this.validationRules.set('lint-check', { name: 'Linting Check', description: 'Check code style and quality', autoFixable: true, critical: false }); this.validationRules.set('test-coverage', { name: 'Test Coverage', description: 'Ensure minimum test coverage', autoFixable: false, critical: true }); // Security rules this.validationRules.set('security-scan', { name: 'Security Scan', description: 'Scan for security vulnerabilities', autoFixable: false, critical: true }); this.validationRules.set('dependency-check', { name: 'Dependency Check', description: 'Check for vulnerable dependencies', autoFixable: true, critical: true }); // Build and deployment rules this.validationRules.set('build-test', { name: 'Build Test', description: 'Verify application builds successfully', autoFixable: false, critical: true }); this.validationRules.set('docker-build', { name: 'Docker Build', description: 'Verify Docker image builds successfully', autoFixable: false, critical: true }); // Configuration rules this.validationRules.set('config-validation', { name: 'Configuration Validation', description: 'Validate configuration files', autoFixable: true, critical: false }); this.validationRules.set('env-check', { name: 'Environment Check', description: 'Validate environment variables', autoFixable: false, critical: true }); // Documentation rules this.validationRules.set('docs-check', { name: 'Documentation Check', description: 'Ensure required documentation exists', autoFixable: true, critical: false }); } async runPrePublishValidation(args) { const { projectPath = '.', validationId = `validation_${Date.now()}`, rules = [], autoFix = this.autoFixEnabled, skipNonCritical = false } = args; console.log(`Starting pre-publish validation: ${validationId}`); const validation = { validationId, projectPath, startTime: new Date().toISOString(), status: 'running', rules: rules.length > 0 ? rules : Array.from(this.validationRules.keys()), results: [], summary: {}, autoFixEnabled: autoFix, fixedIssues: [] }; this.validationResults.set(validationId, validation); try { // Run each validation rule for (const ruleName of validation.rules) { if (!this.validationRules.has(ruleName)) { console.warn(`Unknown validation rule: ${ruleName}`); continue; } const rule = this.validationRules.get(ruleName); if (skipNonCritical && !rule.critical) { console.log(`Skipping non-critical rule: ${ruleName}`); continue; } console.log(`Running validation rule: ${rule.name}`); const ruleResult = await this.runValidationRule(ruleName, projectPath, autoFix); validation.results.push(ruleResult); // If this is a critical rule and it failed, stop validation if (rule.critical && ruleResult.status === 'failed') { console.error(`Critical validation failed: ${rule.name}`); validation.status = 'failed'; break; } } // Generate summary validation.summary = this.generateValidationSummary(validation); if (validation.status !== 'failed') { validation.status = validation.summary.criticalIssues > 0 ? 'failed' : 'passed'; } validation.endTime = new Date().toISOString(); this.validationResults.set(validationId, validation); await this.saveConfiguration(); // Generate report await this.generateValidationReport(validation); return { success: validation.status === 'passed', message: validation.status === 'passed' ? 'All validations passed successfully' : `Validation failed with ${validation.summary.criticalIssues} critical issues`, data: { validationId: validationId, status: validation.status, summary: validation.summary, reportPath: path.join(this.reportsPath, `${validationId}.json`) } }; } catch (error) { validation.status = 'error'; validation.error = error.message; validation.endTime = new Date().toISOString(); this.validationResults.set(validationId, validation); await this.saveConfiguration(); return { success: false, message: `Validation error: ${error.message}`, data: { validationId: validationId } }; } } async runValidationRule(ruleName, projectPath, autoFix) { const rule = this.validationRules.get(ruleName); const result = { rule: ruleName, name: rule.name, status: 'passed', issues: [], fixedIssues: [], duration: 0, output: '' }; const startTime = Date.now(); try { switch (ruleName) { case 'syntax-check': await this.runSyntaxCheck(projectPath, result); break; case 'lint-check': await this.runLintCheck(projectPath, result, autoFix); break; case 'test-coverage': await this.runTestCoverage(projectPath, result); break; case 'security-scan': await this.runSecurityScan(projectPath, result); break; case 'dependency-check': await this.runDependencyCheck(projectPath, result, autoFix); break; case 'build-test': await this.runBuildTest(projectPath, result); break; case 'docker-build': await this.runDockerBuild(projectPath, result); break; case 'config-validation': await this.runConfigValidation(projectPath, result, autoFix); break; case 'env-check': await this.runEnvCheck(projectPath, result); break; case 'docs-check': await this.runDocsCheck(projectPath, result, autoFix); break; default: result.status = 'skipped'; result.output = 'Unknown validation rule'; } } catch (error) { result.status = 'failed'; result.issues.push({ type: 'error', message: error.message, file: '', line: 0 }); } result.duration = Date.now() - startTime; return result; } async runSyntaxCheck(projectPath, result) { try { // Check JavaScript/TypeScript syntax const { stdout: jsFiles } = await execAsync(`find ${projectPath} -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" | head -50`); for (const file of jsFiles.split('\n').filter(f => f.trim())) { try { if (file.endsWith('.ts') || file.endsWith('.tsx')) { await execAsync(`npx tsc --noEmit --skipLibCheck ${file}`, { cwd: projectPath }); } else { await execAsync(`node --check ${file}`, { cwd: projectPath }); } } catch (syntaxError) { result.status = 'failed'; result.issues.push({ type: 'syntax_error', message: syntaxError.stderr || syntaxError.message, file: file, line: this.extractLineNumber(syntaxError.stderr || syntaxError.message) }); } } // Check JSON syntax const { stdout: jsonFiles } = await execAsync(`find ${projectPath} -name "*.json" | head -20`); for (const file of jsonFiles.split('\n').filter(f => f.trim())) { try { const content = await fs.readFile(file, 'utf8'); JSON.parse(content); } catch (jsonError) { result.status = 'failed'; result.issues.push({ type: 'json_syntax_error', message: jsonError.message, file: file, line: 0 }); } } result.output = `Checked syntax for ${jsFiles.split('\n').length + jsonFiles.split('\n').length} files`; } catch (error) { result.output = `Syntax check error: ${error.message}`; } } async runLintCheck(projectPath, result, autoFix) { try { // Try ESLint first try { const lintCommand = autoFix ? 'npx eslint . --fix --format json' : 'npx eslint . --format json'; const { stdout } = await execAsync(lintCommand, { cwd: projectPath }); const lintResults = JSON.parse(stdout); for (const fileResult of lintResults) { for (const message of fileResult.messages) { if (message.severity === 2) { // Error result.status = 'failed'; } result.issues.push({ type: message.severity === 2 ? 'error' : 'warning', message: `${message.ruleId}: ${message.message}`, file: fileResult.filePath, line: message.line }); if (autoFix && message.fix) { result.fixedIssues.push({ file: fileResult.filePath, rule: message.ruleId, fix: 'auto-fixed' }); } } } } catch (eslintError) { // Try with basic JavaScript linting const { stdout } = await execAsync(`find ${projectPath} -name "*.js" | xargs -I {} node -c {}`, { cwd: projectPath }); result.output = 'Basic linting completed (ESLint not available)'; } } catch (error) { result.output = `Lint check error: ${error.message}`; } } async runTestCoverage(projectPath, result) { try { // Try to run tests with coverage let testCommand = ''; // Check for different test frameworks if (await this.fileExists(path.join(projectPath, 'package.json'))) { const packageJson = JSON.parse(await fs.readFile(path.join(projectPath, 'package.json'), 'utf8')); if (packageJson.scripts && packageJson.scripts.test) { testCommand = 'npm test'; } else if (packageJson.devDependencies && packageJson.devDependencies.jest) { testCommand = 'npx jest --coverage'; } else if (packageJson.devDependencies && packageJson.devDependencies.mocha) { testCommand = 'npx nyc mocha'; } } if (testCommand) { const { stdout, stderr } = await execAsync(testCommand, { cwd: projectPath }); // Parse coverage information const coverageMatch = stdout.match(/All files\s+\|\s+(\d+\.?\d*)/); const coverage = coverageMatch ? parseFloat(coverageMatch[1]) : 0; if (coverage < 80) { result.status = 'failed'; result.issues.push({ type: 'low_coverage', message: `Test coverage ${coverage}% is below minimum threshold of 80%`, file: '', line: 0 }); } result.output = `Test coverage: ${coverage}%`; } else { result.status = 'failed'; result.issues.push({ type: 'no_tests', message: 'No test configuration found', file: '', line: 0 }); } } catch (error) { result.status = 'failed'; result.issues.push({ type: 'test_failure', message: error.message, file: '', line: 0 }); } } async runSecurityScan(projectPath, result) { try { // Use npm audit or yarn audit let auditCommand = 'npm audit --json'; try { const { stdout } = await execAsync(auditCommand, { cwd: projectPath }); const auditResult = JSON.parse(stdout); if (auditResult.vulnerabilities) { for (const [pkg, vuln] of Object.entries(auditResult.vulnerabilities)) { if (vuln.severity === 'high' || vuln.severity === 'critical') { result.status = 'failed'; } result.issues.push({ type: 'vulnerability', message: `${pkg}: ${vuln.title} (${vuln.severity})`, file: 'package.json', line: 0, severity: vuln.severity }); } } } catch (auditError) { result.output = 'Security audit not available or failed'; } } catch (error) { result.output = `Security scan error: ${error.message}`; } } async runDependencyCheck(projectPath, result, autoFix) { try { if (await this.fileExists(path.join(projectPath, 'package.json'))) { // Check for outdated packages const { stdout } = await execAsync('npm outdated --json', { cwd: projectPath }); if (stdout.trim()) { const outdated = JSON.parse(stdout); for (const [pkg, info] of Object.entries(outdated)) { const isMajorUpdate = this.isMajorVersionDifference(info.current, info.latest); if (isMajorUpdate) { result.issues.push({ type: 'major_update_available', message: `${pkg}: ${info.current} -> ${info.latest} (major update)`, file: 'package.json', line: 0 }); } else if (autoFix) { // Auto-update minor/patch versions try { await execAsync(`npm update ${pkg}`, { cwd: projectPath }); result.fixedIssues.push({ package: pkg, from: info.current, to: info.latest, type: 'dependency_update' }); } catch (updateError) { result.issues.push({ type: 'update_failed', message: `Failed to update ${pkg}: ${updateError.message}`, file: 'package.json', line: 0 }); } } } } } } catch (error) { result.output = `Dependency check error: ${error.message}`; } } async runBuildTest(projectPath, result) { try { if (await this.fileExists(path.join(projectPath, 'package.json'))) { const packageJson = JSON.parse(await fs.readFile(path.join(projectPath, 'package.json'), 'utf8')); if (packageJson.scripts && packageJson.scripts.build) { const { stdout, stderr } = await execAsync('npm run build', { cwd: projectPath }); result.output = 'Build completed successfully'; } else { result.output = 'No build script found, skipping build test'; } } else { // Try other build systems if (await this.fileExists(path.join(projectPath, 'Makefile'))) { await execAsync('make', { cwd: projectPath }); result.output = 'Make build completed successfully'; } else { result.output = 'No build configuration found'; } } } catch (error) { result.status = 'failed'; result.issues.push({ type: 'build_failure', message: error.message, file: '', line: 0 }); } } async runDockerBuild(projectPath, result) { try { if (await this.fileExists(path.join(projectPath, 'Dockerfile'))) { const imageName = `validation-test-${Date.now()}`; try { await execAsync(`docker build -t ${imageName} .`, { cwd: projectPath }); result.output = 'Docker build completed successfully'; // Clean up test image try { await execAsync(`docker rmi ${imageName}`); } catch (cleanupError) { console.warn('Failed to clean up test image:', cleanupError.message); } } catch (buildError) { result.status = 'failed'; result.issues.push({ type: 'docker_build_failure', message: buildError.message, file: 'Dockerfile', line: 0 }); } } else { result.output = 'No Dockerfile found, skipping Docker build test'; } } catch (error) { result.output = `Docker build test error: ${error.message}`; } } async runConfigValidation(projectPath, result, autoFix) { try { const configFiles = [ 'config.json', 'config.yaml', 'config.yml', '.env.example', 'docker-compose.yml' ]; for (const configFile of configFiles) { const filePath = path.join(projectPath, configFile); if (await this.fileExists(filePath)) { try { const content = await fs.readFile(filePath, 'utf8'); if (configFile.endsWith('.json')) { JSON.parse(content); } else if (configFile.endsWith('.yml') || configFile.endsWith('.yaml')) { // Basic YAML validation (would need yaml parser for full validation) if (content.includes('\t')) { if (autoFix) { const fixed = content.replace(/\t/g, ' '); await fs.writeFile(filePath, fixed); result.fixedIssues.push({ file: configFile, issue: 'tabs_to_spaces', fix: 'Converted tabs to spaces' }); } else { result.issues.push({ type: 'yaml_formatting', message: 'YAML file contains tabs instead of spaces', file: configFile, line: 0 }); } } } } catch (parseError) { result.status = 'failed'; result.issues.push({ type: 'config_parse_error', message: `Invalid ${configFile}: ${parseError.message}`, file: configFile, line: 0 }); } } } } catch (error) { result.output = `Config validation error: ${error.message}`; } } async runEnvCheck(projectPath, result) { try { const envExamplePath = path.join(projectPath, '.env.example'); const envPath = path.join(projectPath, '.env'); if (await this.fileExists(envExamplePath)) { const envExample = await fs.readFile(envExamplePath, 'utf8'); const requiredVars = envExample.match(/^[A-Z_][A-Z0-9_]*=/gm) || []; if (await this.fileExists(envPath)) { const envContent = await fs.readFile(envPath, 'utf8'); for (const varLine of requiredVars) { const varName = varLine.split('=')[0]; if (!envContent.includes(`${varName}=`)) { result.issues.push({ type: 'missing_env_var', message: `Missing environment variable: ${varName}`, file: '.env', line: 0 }); } } } else { result.issues.push({ type: 'missing_env_file', message: '.env file is missing but .env.example exists', file: '.env', line: 0 }); } } result.output = 'Environment variables check completed'; } catch (error) { result.output = `Environment check error: ${error.message}`; } } async runDocsCheck(projectPath, result, autoFix) { try { const requiredDocs = ['README.md', 'CHANGELOG.md', 'LICENSE']; for (const docFile of requiredDocs) { const filePath = path.join(projectPath, docFile); if (!(await this.fileExists(filePath))) { if (autoFix && docFile === 'README.md') { // Auto-generate basic README const packageJsonPath = path.join(projectPath, 'package.json'); let projectName = 'Project'; if (await this.fileExists(packageJsonPath)) { const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); projectName = packageJson.name || 'Project'; } const basicReadme = `# ${projectName} ## Description Add a brief description of your project here. ## Installation \`\`\`bash npm install \`\`\` ## Usage Add usage instructions here. ## Contributing Add contribution guidelines here. ## License Add license information here. `; await fs.writeFile(filePath, basicReadme); result.fixedIssues.push({ file: docFile, issue: 'missing_documentation', fix: 'Generated basic README.md' }); } else { result.issues.push({ type: 'missing_documentation', message: `Missing required documentation: ${docFile}`, file: docFile, line: 0 }); } } } } catch (error) { result.output = `Documentation check error: ${error.message}`; } } generateValidationSummary(validation) { const summary = { totalRules: validation.results.length, passedRules: 0, failedRules: 0, totalIssues: 0, criticalIssues: 0, fixedIssues: 0, duration: 0 }; for (const result of validation.results) { if (result.status === 'passed') { summary.passedRules++; } else if (result.status === 'failed') { summary.failedRules++; } summary.totalIssues += result.issues.length; summary.fixedIssues += result.fixedIssues.length; summary.duration += result.duration; // Count critical issues const rule = this.validationRules.get(result.rule); if (rule && rule.critical && result.status === 'failed') { summary.criticalIssues += result.issues.length; } } return summary; } async generateValidationReport(validation) { const reportFile = path.join(this.reportsPath, `${validation.validationId}.json`); try { await fs.writeFile(reportFile, JSON.stringify(validation, null, 2)); // Generate HTML report const htmlReport = this.generateHTMLValidationReport(validation); const htmlFile = path.join(this.reportsPath, `${validation.validationId}.html`); await fs.writeFile(htmlFile, htmlReport); return { jsonReport: reportFile, htmlReport: htmlFile }; } catch (error) { console.warn('Failed to generate validation report:', error.message); return {}; } } generateHTMLValidationReport(validation) { const { summary } = validation; return ` <!DOCTYPE html> <html> <head> <title>Pre-Publish Validation Report</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .header { background-color: #f4f4f4; padding: 20px; border-radius: 5px; margin-bottom: 20px; } .summary { margin: 20px 0; } .status-passed { color: #4caf50; } .status-failed { color: #f44336; } .status-warning { color: #ff9800; } .rule-result { border: 1px solid #ddd; margin: 10px 0; padding: 15px; border-radius: 5px; } .rule-passed { border-left: 5px solid #4caf50; } .rule-failed { border-left: 5px solid #f44336; } .issue { background-color: #fff3cd; padding: 8px; margin: 5px 0; border-radius: 3px; } .fixed-issue { background-color: #d4edda; padding: 8px; margin: 5px 0; border-radius: 3px; } table { width: 100%; border-collapse: collapse; margin: 20px 0; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } </style> </head> <body> <div class="header"> <h1>Pre-Publish Validation Report</h1> <p><strong>Project:</strong> ${validation.projectPath}</p> <p><strong>Validation ID:</strong> ${validation.validationId}</p> <p><strong>Status:</strong> <span class="status-${validation.status}">${validation.status.toUpperCase()}</span></p> <p><strong>Duration:</strong> ${Math.round(summary.duration / 1000)}s</p> </div> <div class="summary"> <h2>Summary</h2> <table> <tr><th>Metric</th><th>Count</th></tr> <tr><td>Total Rules</td><td>${summary.totalRules}</td></tr> <tr><td>Passed Rules</td><td class="status-passed">${summary.passedRules}</td></tr> <tr><td>Failed Rules</td><td class="status-failed">${summary.failedRules}</td></tr> <tr><td>Total Issues</td><td>${summary.totalIssues}</td></tr> <tr><td>Critical Issues</td><td class="status-failed">${summary.criticalIssues}</td></tr> <tr><td>Fixed Issues</td><td class="status-passed">${summary.fixedIssues}</td></tr> </table> </div> <div class="results"> <h2>Validation Results</h2> ${validation.results.map(result => ` <div class="rule-result rule-${result.status}"> <h3>${result.name} <span class="status-${result.status}">(${result.status})</span></h3> <p><strong>Duration:</strong> ${result.duration}ms</p> <p><strong>Output:</strong> ${result.output}</p> ${result.issues.length > 0 ? ` <h4>Issues Found:</h4> ${result.issues.map(issue => ` <div class="issue"> <strong>${issue.type}:</strong> ${issue.message} ${issue.file ? `<br><em>File: ${issue.file}${issue.line ? `:${issue.line}` : ''}</em>` : ''} </div> `).join('')} ` : ''} ${result.fixedIssues.length > 0 ? ` <h4>Auto-Fixed Issues:</h4> ${result.fixedIssues.map(fix => ` <div class="fixed-issue"> <strong>Fixed:</strong> ${fix.issue || fix.type} in ${fix.file || fix.package} ${fix.fix ? `<br><em>Action: ${fix.fix}</em>` : ''} </div> `).join('')} ` : ''} </div> `).join('')} </div> </body> </html>`; } // Utility methods async fileExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } extractLineNumber(errorMessage) { const lineMatch = errorMessage.match(/:(\d+):/); return lineMatch ? parseInt(lineMatch[1]) : 0; } isMajorVersionDifference(current, latest) { const currentMajor = parseInt(current.split('.')[0]); const latestMajor = parseInt(latest.split('.')[0]); return latestMajor > currentMajor; } async getValidationResults(args) { const { validationId, detailed = false } = args; if (validationId) { if (!this.validationResults.has(validationId)) { return { success: false, message: `Validation ${validationId} not found` }; } const result = this.validationResults.get(validationId); return { success: true, data: detailed ? result : { validationId: result.validationId, status: result.status, summary: result.summary, startTime: result.startTime, endTime: result.endTime } }; } const allResults = Array.from(this.validationResults.values()).map(result => ({ validationId: result.validationId, projectPath: result.projectPath, status: result.status, summary: result.summary, startTime: result.startTime, endTime: result.endTime })); return { success: true, data: { validations: allResults, totalCount: allResults.length } }; } async configureValidationRules(args) { const { rules = {}, autoFixEnabled } = args; // Update existing rules or add new ones for (const [ruleName, ruleConfig] of Object.entries(rules)) { this.validationRules.set(ruleName, { name: ruleConfig.name || ruleName, description: ruleConfig.description || '', autoFixable: ruleConfig.autoFixable || false, critical: ruleConfig.critical || false, ...ruleConfig }); } if (autoFixEnabled !== undefined) { this.autoFixEnabled = autoFixEnabled; } await this.saveConfiguration(); return { success: true, message: 'Validation rules configured successfully', data: { totalRules: this.validationRules.size, autoFixEnabled: this.autoFixEnabled } }; } getToolDefinitions() { return [ { name: 'validation/run-pre-publish', description: 'Run comprehensive pre-publish validation with auto-fix capabilities', inputSchema: { type: 'object', properties: { projectPath: { type: 'string', description: 'Path to project directory', default: '.' }, validationId: { type: 'string', description: 'Custom validation ID (auto-generated if not provided)' }, rules: { type: 'array', items: { type: 'string' }, description: 'Specific rules to run (runs all if not specified)', default: [] }, autoFix: { type: 'boolean', description: 'Enable automatic fixing of issues', default: true }, skipNonCritical: { type: 'boolean', description: 'Skip non-critical validation rules', default: false } } } }, { name: 'validation/get-results', description: 'Get validation results', inputSchema: { type: 'object', properties: { validationId: { type: 'string', description: 'Specific validation ID to retrieve (returns all if not provided)' }, detailed: { type: 'boolean', description: 'Include detailed results', default: false } } } }, { name: 'validation/configure-rules', description: 'Configure validation rules and settings', inputSchema: { type: 'object', properties: { rules: { type: 'object', description: 'Validation rules configuration', default: {} }, autoFixEnabled: { type: 'boolean', description: 'Enable/disable automatic fixing globally' } } } } ]; } } export default PrePublishValidator;