import { NextRequest, NextResponse } from 'next/server' import type { WoodpeckerWebhookPayload, ExtractedError, BacklogSource } from '@/types/infrastructure-modules' // ============================================================================= // Configuration // ============================================================================= // Webhook secret for verification (optional but recommended) const WEBHOOK_SECRET = process.env.WOODPECKER_WEBHOOK_SECRET || '' // Internal API URL for log extraction const LOG_EXTRACT_URL = process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/api/infrastructure/log-extract/extract` : 'http://localhost:3002/api/infrastructure/log-extract/extract' // Test service API URL for backlog insertion const TEST_SERVICE_URL = process.env.TEST_SERVICE_URL || 'http://localhost:8002' // ============================================================================= // Helper Functions // ============================================================================= /** * Verify webhook signature (if secret is configured) */ function verifySignature(request: NextRequest, body: string): boolean { if (!WEBHOOK_SECRET) return true // Skip verification if no secret configured const signature = request.headers.get('X-Woodpecker-Signature') if (!signature) return false // Simple HMAC verification (Woodpecker uses SHA256) const crypto = require('crypto') const expectedSignature = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(body) .digest('hex') return signature === `sha256=${expectedSignature}` } /** * Map error category to backlog priority */ function categoryToPriority(category: string): 'critical' | 'high' | 'medium' | 'low' { switch (category) { case 'security_warning': return 'critical' case 'build_error': return 'high' case 'license_violation': return 'high' case 'test_failure': return 'medium' case 'dependency_issue': return 'low' default: return 'medium' } } /** * Map error category to error_type for backlog */ function categoryToErrorType(category: string): string { switch (category) { case 'security_warning': return 'security' case 'build_error': return 'build' case 'license_violation': return 'license' case 'test_failure': return 'test' case 'dependency_issue': return 'dependency' default: return 'unknown' } } /** * Insert extracted errors into backlog */ async function insertIntoBacklog( errors: ExtractedError[], pipelineNumber: number, source: BacklogSource ): Promise<{ inserted: number; failed: number }> { let inserted = 0 let failed = 0 for (const error of errors) { try { // Create backlog item const backlogItem = { test_name: error.message.substring(0, 200), // Truncate long messages test_file: error.file_path || null, service: error.service || 'unknown', framework: `ci_cd_pipeline_${pipelineNumber}`, error_message: error.message, error_type: categoryToErrorType(error.category), status: 'open', priority: categoryToPriority(error.category), fix_suggestion: error.suggested_fix || null, notes: `Auto-generated from pipeline #${pipelineNumber}, step: ${error.step}, line: ${error.line}`, source, // Custom field to track origin } // Try to insert into test service backlog const response = await fetch(`${TEST_SERVICE_URL}/api/v1/backlog`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(backlogItem), }) if (response.ok) { inserted++ } else { console.warn(`Failed to insert backlog item: ${response.status}`) failed++ } } catch (insertError) { console.error('Backlog insertion error:', insertError) failed++ } } return { inserted, failed } } // ============================================================================= // API Handler // ============================================================================= /** * POST /api/webhooks/woodpecker * * Webhook endpoint fuer Woodpecker CI/CD Events. * * Bei Pipeline-Failure: * 1. Extrahiert Logs mit /api/infrastructure/logs/extract * 2. Parsed Fehler nach Kategorie * 3. Traegt automatisch in Backlog ein * * Request Body (Woodpecker Webhook Format): * - event: 'pipeline_success' | 'pipeline_failure' | 'pipeline_started' * - repo_id: number * - pipeline_number: number * - branch?: string * - commit?: string * - author?: string * - message?: string */ export async function POST(request: NextRequest) { try { const bodyText = await request.text() // Verify webhook signature if (!verifySignature(request, bodyText)) { return NextResponse.json( { error: 'Invalid webhook signature' }, { status: 401 } ) } const payload: WoodpeckerWebhookPayload = JSON.parse(bodyText) // Log all events for debugging console.log(`Woodpecker webhook: ${payload.event} for pipeline #${payload.pipeline_number}`) // Only process pipeline_failure events if (payload.event !== 'pipeline_failure') { return NextResponse.json({ status: 'ignored', message: `Event ${payload.event} wird nicht verarbeitet`, pipeline_number: payload.pipeline_number, }) } // 1. Extract logs from failed pipeline console.log(`Extracting logs for failed pipeline #${payload.pipeline_number}`) const extractResponse = await fetch(LOG_EXTRACT_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ pipeline_number: payload.pipeline_number, repo_id: String(payload.repo_id), }), }) if (!extractResponse.ok) { const errorText = await extractResponse.text() console.error('Log extraction failed:', errorText) return NextResponse.json({ status: 'error', message: 'Log-Extraktion fehlgeschlagen', pipeline_number: payload.pipeline_number, }, { status: 500 }) } const extractionResult = await extractResponse.json() const errors: ExtractedError[] = extractionResult.errors || [] console.log(`Extracted ${errors.length} errors from pipeline #${payload.pipeline_number}`) // 2. Insert errors into backlog if (errors.length > 0) { const backlogResult = await insertIntoBacklog( errors, payload.pipeline_number, 'ci_cd' ) console.log(`Backlog: ${backlogResult.inserted} inserted, ${backlogResult.failed} failed`) return NextResponse.json({ status: 'processed', pipeline_number: payload.pipeline_number, branch: payload.branch, commit: payload.commit, errors_found: errors.length, backlog_inserted: backlogResult.inserted, backlog_failed: backlogResult.failed, categories: { test_failure: errors.filter(e => e.category === 'test_failure').length, build_error: errors.filter(e => e.category === 'build_error').length, security_warning: errors.filter(e => e.category === 'security_warning').length, license_violation: errors.filter(e => e.category === 'license_violation').length, dependency_issue: errors.filter(e => e.category === 'dependency_issue').length, }, }) } return NextResponse.json({ status: 'processed', pipeline_number: payload.pipeline_number, message: 'Keine Fehler extrahiert', errors_found: 0, }) } catch (error) { console.error('Webhook processing error:', error) return NextResponse.json( { error: 'Webhook-Verarbeitung fehlgeschlagen' }, { status: 500 } ) } } /** * GET /api/webhooks/woodpecker * * Health check endpoint */ export async function GET() { return NextResponse.json({ status: 'ready', endpoint: '/api/webhooks/woodpecker', events: ['pipeline_failure'], description: 'Woodpecker CI/CD Webhook Handler', configured: { webhook_secret: WEBHOOK_SECRET ? 'yes' : 'no', log_extract_url: LOG_EXTRACT_URL, test_service_url: TEST_SERVICE_URL, }, }) }