<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Haadiyah Zafar</title>
    <description>The latest articles on DEV Community by Haadiyah Zafar (@haadiyahzafar).</description>
    <link>https://dev.to/haadiyahzafar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4070104%2F17d6c79f-f632-4b68-98ad-6667743ad283.png</url>
      <title>DEV Community: Haadiyah Zafar</title>
      <link>https://dev.to/haadiyahzafar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/haadiyahzafar"/>
    <language>en</language>
    <item>
      <title>From Logs to Issues: An Agentic Pipeline for Flaky Test Analysis</title>
      <dc:creator>Haadiyah Zafar</dc:creator>
      <pubDate>Tue, 18 Aug 2026 15:56:28 +0000</pubDate>
      <link>https://dev.to/haadiyahzafar/from-logs-to-issues-an-agentic-pipeline-for-flaky-test-analysis-480k</link>
      <guid>https://dev.to/haadiyahzafar/from-logs-to-issues-an-agentic-pipeline-for-flaky-test-analysis-480k</guid>
      <description>&lt;p&gt;Flaky tests are the silent killer of developer velocity. They fail randomly, waste hours of debugging, and erode trust in CI. As a developer, I've spent too many mornings scrolling through GitHub Actions logs trying to figure out if a failure is a real bug or just a network blip.&lt;br&gt;
So I built FlakeFixer — a tool that monitors GitHub Actions workflows, extracts failure logs, uses an AI agent to classify the root cause (race condition, network timeout, infrastructure blip, dependency flake), and automatically creates a GitHub Issue with a plain-English analysis and suggested mitigation.&lt;br&gt;
In this post, I'll walk you through the architecture, the tech stack, and how you can build your own.&lt;br&gt;
&lt;strong&gt;The Problem&lt;/strong&gt;&lt;br&gt;
Flaky tests are tests that pass and fail randomly without any code changes. They're common in CI pipelines, especially when tests rely on external services, timing, or shared state.&lt;br&gt;
Manually debugging flaky tests is painful because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logs are huge and noisy.&lt;/li&gt;
&lt;li&gt;The actual error is often buried in a stack trace.&lt;/li&gt;
&lt;li&gt;The root cause might be an infrastructure hiccup, not a code bug.&lt;/li&gt;
&lt;li&gt;I wanted a system that could automatically:&lt;/li&gt;
&lt;li&gt;Fetch failed CI runs.&lt;/li&gt;
&lt;li&gt;Extract the relevant log snippet.&lt;/li&gt;
&lt;li&gt;Use an LLM to reason about the failure and categorize it.&lt;/li&gt;
&lt;li&gt;Post a concise analysis as a GitHub Issue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's exactly what FlakeFixer does.&lt;br&gt;
&lt;strong&gt;High-Level Architecture&lt;/strong&gt;&lt;br&gt;
The system has three main components:&lt;br&gt;
&lt;strong&gt;Data Ingestion&lt;/strong&gt; — Python script that uses PyGithub to list failed workflow runs and download logs.&lt;br&gt;
&lt;strong&gt;Agentic Analysis&lt;/strong&gt;— LangChain + Pydantic output parser that classifies the failure and generates a mitigation suggestion.&lt;br&gt;
&lt;strong&gt;Reporting&lt;/strong&gt; — Automatically creates GitHub Issues with the analysis, labeled by flake category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building the Flaky Test Lab&lt;/strong&gt;&lt;br&gt;
Before building the tool, I needed a way to generate flaky failures on demand. I created a companion repo called ci-flake-lab that intentionally produces several types of flaky failures:&lt;br&gt;
&lt;strong&gt;Race condition&lt;/strong&gt; — a test that fails when threads access a shared list.&lt;br&gt;
&lt;strong&gt;Network timeout&lt;/strong&gt; — a test that tries to connect to a non-routable IP.&lt;br&gt;
&lt;strong&gt;Infrastructure blip&lt;/strong&gt; — a test that randomly raises OSError: No space left on device.&lt;br&gt;
&lt;strong&gt;Unknown&lt;/strong&gt;— failures that don't cleanly fit a known category, to test how the agent handles ambiguity.&lt;/p&gt;

&lt;p&gt;I used GitHub Actions environment variables to switch the flake type on each run, with a matrix strategy so multiple flake types run in parallel. This gave me a steady stream of failed logs to test the pipeline against- you can see the resulting auto-filed issues in the repo, each tagged with flaky-test and its specific category.&lt;br&gt;
[Insert your screenshot of the ci-flake-lab issues list here]&lt;br&gt;
Here's the workflow YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Flake Farm
on: [push, workflow_dispatch]

jobs:
  flaky-tests:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        flake-type: [RACE, NETWORK, INFRA, UNKNOWN]
      fail-fast: false
    env:
      FLAKE_${{ matrix.flake-type }}: 1
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.10'
      - run: pip install pytest
      - run: pytest test_app.py -v

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Data Ingestion with PyGithub&lt;/strong&gt;&lt;br&gt;
The ingestion module uses PyGithub to authenticate and list failed workflow runs, then downloads the log archive and extracts the raw text.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
from github import Github
import requests, zipfile, io

class IngestionPipeline:
    def __init__(self, token, repo_name):
        self.gh = Github(token)
        self.repo = self.gh.get_repo(repo_name)

    def get_failed_runs(self, workflow_id, max_runs=20):
        workflow = self.repo.get_workflow(workflow_id)
        return list(workflow.get_runs(status="failure"))[:max_runs]

    def download_logs(self, run_id):
        run = self.repo.get_workflow_run(run_id)
        headers = {"Authorization": f"token {token}"}
        resp = requests.get(run.logs_url, headers=headers, allow_redirects=True)
        with zipfile.ZipFile(io.BytesIO(resp.content)) as zf:
            full_log = ""
            for name in zf.namelist():
                if name.endswith(".txt"):
                    full_log += zf.read(name).decode("utf-8", errors="replace") + "\n"
        return full_log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives me the raw log text. In the full version, I also trim the log to the most relevant parts (the last 100 lines and lines containing FAILED or ERROR) to keep the LLM prompt within token limits.&lt;br&gt;
&lt;strong&gt;The Agentic Analysis Engine&lt;/strong&gt;&lt;br&gt;
For the AI agent, I used LangChain with a Pydantic output parser to force the LLM to return structured JSON. This ensures I can reliably parse the output and use it to create issues.&lt;br&gt;
The prompt asks the LLM to classify the failure and provide an explanation and mitigation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

class FlakeAnalysis(BaseModel):
    flake_category: str = Field(description="race_condition, network_timeout, infrastructure_blip, unknown")
    explanation: str
    mitigation: str
    confidence: float

class FlakeAnalyzer:
    def __init__(self):
        self.llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1)
        self.parser = PydanticOutputParser(pydantic_object=FlakeAnalysis)

    def analyze(self, log_snippet):
        prompt = ChatPromptTemplate.from_template("""
You are a flaky test analyst. Given the CI failure log, classify the root cause.
Log:
{log}

{format_instructions}
""")
        formatted = prompt.format_prompt(
            log=log_snippet[:6000],
            format_instructions=self.parser.get_format_instructions()
        )
        response = self.llm.invoke(formatted.to_messages())
        return self.parser.parse(response.content)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's an example of what the agent returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "flake_category": "network_timeout",
  "explanation": "The test attempted to connect to an external service but timed out. This is likely an infrastructure or network issue, not a code bug.",
  "mitigation": "Add retry logic with exponential backoff and increase the timeout. Consider mocking external services in unit tests.",
  "confidence": 0.9
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Automated Reporting — Creating GitHub Issues&lt;/strong&gt;&lt;br&gt;
The final piece is the reporting layer. Once the agent returns the analysis, I use PyGithub to create a GitHub Issue with the analysis, labels, and a log snippet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
from github import Github
class Reporter:
    def __init__(self, token, repo_name):
        self.gh = Github(token)
        self.repo = self.gh.get_repo(repo_name)

    def create_flake_issue(self, analysis, run_id, log_snippet):
        title = f"[Flake] {analysis.flake_category.replace('_',' ').title()} (run #{run_id})"
        body = f"""## Flaky Test Analysis
**Category:** `{analysis.flake_category}`
**Confidence:** {analysis.confidence:.2f}

### Root Cause
{analysis.explanation}

### Suggested Mitigation
{analysis.mitigation}

&amp;lt;details&amp;gt;&amp;lt;summary&amp;gt;Log snippet&amp;lt;/summary&amp;gt;


{log_snippet[:1500]}

&amp;lt;/details&amp;gt;

*Auto-generated by FlakeFixer agent.*
"""
        labels = ["flaky-test", analysis.flake_category]
        issue = self.repo.create_issue(title=title, body=body, labels=labels)
        return issue.html_url



&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see the real output of this in the ci-flake-lab issues tab — every one of those [Flake] ... issues was filed automatically by FlakeFixer, not by hand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2For7bekn8raahxpxp3sq7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2For7bekn8raahxpxp3sq7.png" alt="Logs showing the issues annotated by the FlakeFixer" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open Source Contribution&lt;/strong&gt;&lt;br&gt;
While building FlakeFixer, I noticed that pytest-github-actions-annotate-failures did not truncate long annotation messages, causing clutter in PRs. I opened a PR to add truncation for annotation output. This was my first contribution to that project, and it reinforced the importance of clean, readable CI output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next steps for FlakeFixer:&lt;/strong&gt;&lt;br&gt;
Add a vector database to store and retrieve similar historical failures.&lt;br&gt;
Fine-tune a small language model on my labeled flake dataset.&lt;br&gt;
Generate weekly flake summary reports.&lt;br&gt;
Conclusion&lt;br&gt;
Flaky tests don't have to be a black hole of developer time. By combining GitHub Actions, LLMs, and automation, we can automatically triage and document flaky failures, giving maintainers back their time and sanity.&lt;br&gt;
If you're interested in the code or want to see a demo, check out the repos:&lt;br&gt;
&lt;a href="https://dev.tourl"&gt;https://github.com/Haadiyah-Zafar/FlakeFixer&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.tourl"&gt;https://github.com/Haadiyah-Zafar/ci-flake-lab&lt;/a&gt;&lt;br&gt;
Let me know in the comments if you've tackled similar problems or have ideas for improvement!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
    </item>
  </channel>
</rss>
