From 77a945bd1ed50635d42baad22889e782c0532680 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 22 Dec 2025 14:50:03 +0000 Subject: [PATCH] feat: smart error extraction for failed workflow logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For failed jobs, instead of just showing the tail (which is cleanup), now scans for error patterns (❌, Error:, FAILED, TypeScript errors, npm/pnpm errors, exit codes, etc.) and shows context around them. Falls back to tail for successful jobs or if no errors detected. --- server.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/server.py b/server.py index 115a175..1ed5d9e 100644 --- a/server.py +++ b/server.py @@ -400,15 +400,71 @@ async def get_workflow_run_logs( elif isinstance(logs_result, str): log_content = logs_result - # Apply tail if requested - if tail_lines > 0 and log_content: - lines = log_content.splitlines() - total_lines = len(lines) - if total_lines > tail_lines: + lines = log_content.splitlines() + total_lines = len(lines) + + # For failed jobs, try to find error context instead of just tail + if run_status == "failure" and tail_lines > 0 and total_lines > tail_lines: + # Error indicators to search for + error_patterns = [ + "❌", "Error:", "ERROR:", "error:", "FAILED", "Failed", + "fatal:", "FATAL:", "Exception:", "exception:", + "Cannot find", "not found", "No such file", + "Permission denied", "command not found", + "npm ERR!", "pnpm ERR!", "yarn error", + "TypeScript error", "TS2", "TS1", # TypeScript errors + "SyntaxError", "ReferenceError", "TypeError", + "Build failed", "build failed", "Compilation failed", + "exit code 1", "exit code 2", "exited with", + ] + + # Find lines containing errors + error_line_indices = [] + for i, line in enumerate(lines): + for pattern in error_patterns: + if pattern in line: + error_line_indices.append(i) + break + + if error_line_indices: + # Get unique error regions with context (5 lines before, 3 after) + context_before = 5 + context_after = 3 + selected_lines = set() + + for idx in error_line_indices: + start = max(0, idx - context_before) + end = min(total_lines, idx + context_after + 1) + for i in range(start, end): + selected_lines.add(i) + + # Cap at tail_lines to avoid overwhelming output + sorted_indices = sorted(selected_lines) + if len(sorted_indices) > tail_lines: + # Take the last error regions + sorted_indices = sorted_indices[-tail_lines:] + + # Build output with line breaks between non-contiguous sections + output_parts = [] + prev_idx = -2 + for idx in sorted_indices: + if idx > prev_idx + 1: + if output_parts: + output_parts.append("...") + output_parts.append(lines[idx]) + prev_idx = idx + + log_content = f"... (showing {len(sorted_indices)} error-relevant lines of {total_lines} total) ...\n" + "\n".join(output_parts) + else: + # No errors found, fall back to tail lines = lines[-tail_lines:] log_content = f"... (showing last {tail_lines} of {total_lines} lines) ...\n" + "\n".join(lines) - else: - log_content = "\n".join(lines) + elif tail_lines > 0 and total_lines > tail_lines: + # Not a failure or no smart extraction, just tail + lines = lines[-tail_lines:] + log_content = f"... (showing last {tail_lines} of {total_lines} lines) ...\n" + "\n".join(lines) + else: + log_content = "\n".join(lines) return json.dumps({ "run_number": run_number,