import { NextRequest, NextResponse } from 'next/server' import type { ExtractedError, ErrorCategory, LogExtractionResponse } from '@/types/infrastructure-modules' // Woodpecker API configuration const WOODPECKER_URL = process.env.WOODPECKER_URL || 'http://woodpecker-server:8000' const WOODPECKER_TOKEN = process.env.WOODPECKER_TOKEN || '' // ============================================================================= // Error Pattern Matching // ============================================================================= interface ErrorPattern { pattern: RegExp category: ErrorCategory extractMessage?: (match: RegExpMatchArray, line: string) => string } /** * Patterns fuer verschiedene Fehlertypen in CI/CD Logs */ const ERROR_PATTERNS: ErrorPattern[] = [ // Test Failures { pattern: /^(FAIL|FAILED|ERROR):?\s+(.+)$/i, category: 'test_failure', extractMessage: (match, line) => match[2] || line, }, { pattern: /^---\s+FAIL:\s+(.+)\s+\([\d.]+s\)$/, category: 'test_failure', extractMessage: (match) => `Test failed: ${match[1]}`, }, { pattern: /pytest.*FAILED\s+(.+)$/, category: 'test_failure', extractMessage: (match) => `pytest: ${match[1]}`, }, { pattern: /AssertionError:\s+(.+)$/, category: 'test_failure', extractMessage: (match) => `Assertion failed: ${match[1]}`, }, { pattern: /FAIL\s+[\w\/]+\s+\[build failed\]/, category: 'build_error', }, // Build Errors { pattern: /^(error|Error)\[[\w-]+\]:\s+(.+)$/, category: 'build_error', extractMessage: (match) => match[2], }, { pattern: /cannot find (module|package)\s+["'](.+)["']/i, category: 'build_error', extractMessage: (match) => `Missing ${match[1]}: ${match[2]}`, }, { pattern: /undefined:\s+(.+)$/, category: 'build_error', extractMessage: (match) => `Undefined: ${match[1]}`, }, { pattern: /compilation failed/i, category: 'build_error', }, { pattern: /npm ERR!\s+(.+)$/, category: 'build_error', extractMessage: (match) => `npm error: ${match[1]}`, }, { pattern: /go:\s+(.+):\s+(.+)$/, category: 'build_error', extractMessage: (match) => `Go: ${match[1]}: ${match[2]}`, }, // Security Warnings { pattern: /\[CRITICAL\]\s+(.+)$/i, category: 'security_warning', extractMessage: (match) => `Critical: ${match[1]}`, }, { pattern: /\[HIGH\]\s+(.+)$/i, category: 'security_warning', extractMessage: (match) => `High severity: ${match[1]}`, }, { pattern: /CVE-\d{4}-\d+/, category: 'security_warning', extractMessage: (match, line) => line.trim(), }, { pattern: /vulnerability found/i, category: 'security_warning', }, { pattern: /secret.*detected/i, category: 'security_warning', }, { pattern: /gitleaks.*found/i, category: 'security_warning', }, { pattern: /semgrep.*finding/i, category: 'security_warning', }, // License Violations { pattern: /license.*violation/i, category: 'license_violation', }, { pattern: /incompatible license/i, category: 'license_violation', }, { pattern: /AGPL|GPL-3|SSPL/, category: 'license_violation', extractMessage: (match, line) => `Potentially problematic license found: ${match[0]}`, }, // Dependency Issues { pattern: /dependency.*not found/i, category: 'dependency_issue', }, { pattern: /outdated.*dependency/i, category: 'dependency_issue', }, { pattern: /version conflict/i, category: 'dependency_issue', }, ] /** * Patterns to extract file paths from error lines */ const FILE_PATH_PATTERNS = [ /([\/\w.-]+\.(go|py|ts|tsx|js|jsx|rs)):(\d+)/, /File "([^"]+)", line (\d+)/, /at ([\/\w.-]+):(\d+):\d+/, ] /** * Patterns to extract service names from log lines or paths */ const SERVICE_PATTERNS = [ /service[s]?\/([a-z-]+)/i, /\/([a-z-]+-service)\//i, /^([a-z-]+):\s/, ] // ============================================================================= // Log Parsing Functions // ============================================================================= interface LogLine { pos: number out: string time: number } function extractFilePath(line: string): { path?: string; lineNumber?: number } { for (const pattern of FILE_PATH_PATTERNS) { const match = line.match(pattern) if (match) { return { path: match[1], lineNumber: parseInt(match[2] || match[3], 10) || undefined, } } } return {} } function extractService(line: string, filePath?: string): string | undefined { // First try to extract from file path if (filePath) { for (const pattern of SERVICE_PATTERNS) { const match = filePath.match(pattern) if (match) return match[1] } } // Then try from the line itself for (const pattern of SERVICE_PATTERNS) { const match = line.match(pattern) if (match) return match[1] } return undefined } function parseLogLines(logs: LogLine[], stepName: string): ExtractedError[] { const errors: ExtractedError[] = [] const seenMessages = new Set() for (const logLine of logs) { const line = logLine.out.trim() if (!line) continue for (const errorPattern of ERROR_PATTERNS) { const match = line.match(errorPattern.pattern) if (match) { const message = errorPattern.extractMessage ? errorPattern.extractMessage(match, line) : line // Deduplicate similar errors const messageKey = `${errorPattern.category}:${message.substring(0, 100)}` if (seenMessages.has(messageKey)) continue seenMessages.add(messageKey) const fileInfo = extractFilePath(line) const service = extractService(line, fileInfo.path) errors.push({ step: stepName, line: logLine.pos, message, category: errorPattern.category, file_path: fileInfo.path, service, }) break // Only match first pattern per line } } } return errors } // ============================================================================= // API Handler // ============================================================================= /** * POST /api/infrastructure/logs/extract * * Extrahiert Fehler aus Woodpecker Pipeline Logs. * * Request Body: * - pipeline_number: number (required) * - repo_id?: string (default: '1') * * Response: * - errors: ExtractedError[] * - pipeline_number: number * - extracted_at: string * - lines_parsed: number */ export async function POST(request: NextRequest) { try { const body = await request.json() const { pipeline_number, repo_id = '1' } = body if (!pipeline_number) { return NextResponse.json( { error: 'pipeline_number ist erforderlich' }, { status: 400 } ) } // 1. Fetch pipeline details to get step IDs const pipelineResponse = await fetch( `${WOODPECKER_URL}/api/repos/${repo_id}/pipelines/${pipeline_number}`, { headers: { 'Authorization': `Bearer ${WOODPECKER_TOKEN}`, 'Content-Type': 'application/json', }, cache: 'no-store', } ) if (!pipelineResponse.ok) { return NextResponse.json( { error: `Pipeline ${pipeline_number} nicht gefunden` }, { status: 404 } ) } const pipeline = await pipelineResponse.json() // 2. Extract step IDs from workflows const failedSteps: { id: number; name: string }[] = [] if (pipeline.workflows) { for (const workflow of pipeline.workflows) { if (workflow.children) { for (const child of workflow.children) { if (child.state === 'failure' || child.state === 'error') { failedSteps.push({ id: child.id, name: child.name, }) } } } } } // 3. Fetch logs for each failed step const allErrors: ExtractedError[] = [] let totalLinesParsed = 0 for (const step of failedSteps) { try { const logsResponse = await fetch( `${WOODPECKER_URL}/api/repos/${repo_id}/pipelines/${pipeline_number}/logs/${step.id}`, { headers: { 'Authorization': `Bearer ${WOODPECKER_TOKEN}`, 'Content-Type': 'application/json', }, } ) if (logsResponse.ok) { const logs: LogLine[] = await logsResponse.json() totalLinesParsed += logs.length const stepErrors = parseLogLines(logs, step.name) allErrors.push(...stepErrors) } } catch (logError) { console.error(`Failed to fetch logs for step ${step.name}:`, logError) } } // 4. Sort errors by severity (security > license > build > test > dependency) const categoryPriority: Record = { 'security_warning': 1, 'license_violation': 2, 'build_error': 3, 'test_failure': 4, 'dependency_issue': 5, } allErrors.sort((a, b) => categoryPriority[a.category] - categoryPriority[b.category]) const response: LogExtractionResponse = { errors: allErrors, pipeline_number, extracted_at: new Date().toISOString(), lines_parsed: totalLinesParsed, } return NextResponse.json(response) } catch (error) { console.error('Log extraction error:', error) return NextResponse.json( { error: 'Fehler bei der Log-Extraktion' }, { status: 500 } ) } } /** * GET /api/infrastructure/logs/extract?pipeline_number=123 * * Convenience method - calls POST internally */ export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams const pipeline_number = searchParams.get('pipeline_number') const repo_id = searchParams.get('repo_id') || '1' if (!pipeline_number) { return NextResponse.json( { error: 'pipeline_number Query-Parameter ist erforderlich' }, { status: 400 } ) } // Create a mock request with JSON body const mockRequest = new NextRequest(request.url, { method: 'POST', body: JSON.stringify({ pipeline_number: parseInt(pipeline_number, 10), repo_id }), headers: { 'Content-Type': 'application/json', }, }) return POST(mockRequest) }