verify-deployment.ts•7.42 kB
import { z } from 'zod';
import { promises as fs } from 'fs';
import path from 'path';
import { MCPToolResponse, formatMCPResponse } from '../types/api.js';
const inputSchema = z.object({
repository: z.string(),
url: z.string().optional(),
});
interface DeploymentCheck {
check: string;
status: 'pass' | 'fail' | 'warning';
message: string;
recommendation?: string;
}
export async function verifyDeployment(args: unknown): Promise<{ content: any[] }> {
const startTime = Date.now();
const { repository, url } = inputSchema.parse(args);
try {
const checks: DeploymentCheck[] = [];
// Determine repository path
const repoPath = repository.startsWith('http') ? '.' : repository;
// Check 1: GitHub Actions workflow exists
const workflowPath = path.join(repoPath, '.github', 'workflows');
try {
const workflows = await fs.readdir(workflowPath);
const deployWorkflow = workflows.find(
(f) => f.includes('deploy') || f.includes('pages') || f.includes('docs'),
);
if (deployWorkflow) {
checks.push({
check: 'GitHub Actions Workflow',
status: 'pass',
message: `Found deployment workflow: ${deployWorkflow}`,
});
} else {
checks.push({
check: 'GitHub Actions Workflow',
status: 'fail',
message: 'No deployment workflow found',
recommendation: 'Run deploy_pages tool to create a workflow',
});
}
} catch {
checks.push({
check: 'GitHub Actions Workflow',
status: 'fail',
message: 'No .github/workflows directory found',
recommendation: 'Run deploy_pages tool to set up GitHub Actions',
});
}
// Check 2: Documentation source files exist
const docsPaths = ['docs', 'documentation', 'site', 'content'];
let docsFound = false;
for (const docsPath of docsPaths) {
try {
const fullPath = path.join(repoPath, docsPath);
const stats = await fs.stat(fullPath);
if (stats.isDirectory()) {
const files = await fs.readdir(fullPath);
const mdFiles = files.filter((f) => f.endsWith('.md') || f.endsWith('.mdx'));
if (mdFiles.length > 0) {
docsFound = true;
checks.push({
check: 'Documentation Source Files',
status: 'pass',
message: `Found ${mdFiles.length} documentation files in ${docsPath}/`,
});
break;
}
}
} catch {
// Directory doesn't exist, continue checking
}
}
if (!docsFound) {
checks.push({
check: 'Documentation Source Files',
status: 'warning',
message: 'No documentation files found in standard locations',
recommendation: 'Run setup_structure tool to create documentation structure',
});
}
// Check 3: Configuration files
const configPatterns = [
'docusaurus.config.js',
'mkdocs.yml',
'hugo.toml',
'hugo.yaml',
'_config.yml',
'.eleventy.js',
];
let configFound = false;
for (const config of configPatterns) {
try {
await fs.access(path.join(repoPath, config));
configFound = true;
checks.push({
check: 'SSG Configuration',
status: 'pass',
message: `Found configuration file: ${config}`,
});
break;
} catch {
// File doesn't exist, continue
}
}
if (!configFound) {
checks.push({
check: 'SSG Configuration',
status: 'fail',
message: 'No static site generator configuration found',
recommendation: 'Run generate_config tool to create SSG configuration',
});
}
// Check 4: Build output directory
const buildDirs = ['_site', 'build', 'dist', 'public', 'out'];
let buildFound = false;
for (const buildDir of buildDirs) {
try {
const buildPath = path.join(repoPath, buildDir);
const stats = await fs.stat(buildPath);
if (stats.isDirectory()) {
buildFound = true;
checks.push({
check: 'Build Output',
status: 'pass',
message: `Found build output directory: ${buildDir}/`,
});
break;
}
} catch {
// Directory doesn't exist
}
}
if (!buildFound) {
checks.push({
check: 'Build Output',
status: 'warning',
message: 'No build output directory found',
recommendation: 'Run your SSG build command to generate the site',
});
}
// Check 5: GitHub Pages settings (if URL provided)
if (url) {
checks.push({
check: 'Deployment URL',
status: 'warning',
message: `Expected URL: ${url}`,
recommendation: 'Verify GitHub Pages is enabled in repository settings',
});
}
// Generate summary
const passCount = checks.filter((c) => c.status === 'pass').length;
const failCount = checks.filter((c) => c.status === 'fail').length;
const warningCount = checks.filter((c) => c.status === 'warning').length;
let overallStatus = 'Ready for deployment';
if (failCount > 0) {
overallStatus = 'Configuration required';
} else if (warningCount > 0) {
overallStatus = 'Minor issues detected';
}
const verificationResult = {
repository,
url,
overallStatus,
checks,
summary: {
passed: passCount,
warnings: warningCount,
failed: failCount,
total: checks.length,
},
};
const response: MCPToolResponse<typeof verificationResult> = {
success: true,
data: verificationResult,
metadata: {
toolVersion: '1.0.0',
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString(),
},
recommendations: [
{
type: failCount > 0 ? 'critical' : warningCount > 0 ? 'warning' : 'info',
title: 'Deployment Verification Complete',
description: `${overallStatus}. ${passCount} checks passed, ${warningCount} warnings, ${failCount} failures.`,
},
],
nextSteps: checks
.filter((check) => check.recommendation)
.map((check) => ({
action: check.recommendation!,
toolRequired: check.recommendation!.includes('deploy_pages')
? 'deploy_pages'
: check.recommendation!.includes('setup_structure')
? 'setup_structure'
: check.recommendation!.includes('generate_config')
? 'generate_config'
: 'manual',
description: check.message,
priority: check.status === 'fail' ? 'high' : ('medium' as const),
})),
};
return formatMCPResponse(response);
} catch (error) {
const errorResponse: MCPToolResponse = {
success: false,
error: {
code: 'VERIFICATION_FAILED',
message: `Failed to verify deployment: ${error}`,
resolution: 'Ensure repository path is accessible',
},
metadata: {
toolVersion: '1.0.0',
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString(),
},
};
return formatMCPResponse(errorResponse);
}
}
// Removed unused getStatusEmoji function - status indicators now handled in formatMCPResponse