← Back to DevBytes

Building a CI/CD Automation Agent with AutoGen: Complete Guide

Introduction to Building a CI/CD Automation Agent with AutoGen

Modern software development teams ship code dozens of times per day, and the pipelines that make this possible — Continuous Integration and Continuous Delivery (CI/CD) — have become the circulatory system of any healthy engineering organization. However, managing these pipelines is tedious: failed builds need triaging, flaky tests need re-running, configuration drift needs fixing, and deployment approvals need routing. This is where an autonomous CI/CD agent built on top of Microsoft's AutoGen framework can dramatically reduce toil.

AutoGen is a multi-agent conversation framework that lets you orchestrate Large Language Model (LLM) powered agents capable of writing code, executing it, retrieving context, and collaborating with one another. By combining AutoGen with your existing CI/CD tooling — GitHub Actions, GitLab CI, Jenkins, Argo CD, or Kubernetes — you can build an agent that triages failures, proposes fixes, opens pull requests, and even coordinates deployments under human supervision.

In this complete guide, will walk through what a CI/CD automation agent is, why it matters, how to architect one with AutoGen, and the best practices you should follow when running it in production.

What Is a CI/CD Automation Agent?

A CI/CD automation agent is an LLM-backed system that observes events in your delivery pipeline and takes actions on your behalf. Unlike a static script or a templated bot, an agent reasons about the situation, retrieves relevant context, and chooses an appropriate response. Typical responsibilities include:

The key distinction from a traditional automation tool is agency. The agent does not follow a fixed decision tree; it interprets context, plans, and acts — while still operating within guardrails you define.

Why It Matters

Engineering teams spend a surprising amount of time on pipeline maintenance. Industry surveys consistently show that developers lose several hours per week to broken builds, environment drift, and deployment troubleshooting. An agent-based approach helps in four concrete ways:

AutoGen is particularly well-suited for this because it natively supports tool use, code execution, group conversations between specialized agents, and structured termination conditions — all of which map cleanly onto CI/CD workflows.

Architecture Overview

Before writing code, let's define the architecture. A robust CI/CD agent built with AutoGen typically has the following components:

The flow is straightforward: an event arrives, the Coordinator decides which agents to engage, those agents retrieve context and reason, propose actions, and a human (or automated policy) approves execution.

Prerequisites and Setup

You will need Python 3.10 or later, an OpenAI API key (or an alternative LLM provider supported by AutoGen), and a GitHub repository where you can safely experiment. Install AutoGen and supporting libraries:

pip install "autogen-agentchat==0.4.2" \
            "autogen-ext[openai]==0.4.2" \
            fastapi uvicorn httpx pydantic

Create a project layout:

cicd-agent/
├── agents/
│   ├── __init__.py
│   ├── coordinator.py
│   ├── triage.py
│   ├── fixer.py
│   └── reviewer.py
├── tools/
│   ├── __init__.py
│   ├── github_tools.py
│   ├── log_tools.py
│   └── ci_tools.py
├── server.py
├── config.py
└── .env

Store secrets in .env:

OPENAI_API_KEY=sk-...
GITHUB_TOKEN=ghp_...
GITHUB_REPO=yourorg/yourrepo
WEBHOOK_SECRET=supersecret

Defining Tools

AutoGen agents are only as capable as the tools they can call. Let's start with the most important one: fetching CI logs from GitHub Actions.

Log Retrieval Tool

# tools/github_tools.py
import os
import httpx
from autogen_core.tools import FunctionTool

GITHUB_API = "https://api.github.com"
TOKEN = os.environ["GITHUB_TOKEN"]
REPO = os.environ["GITHUB_REPO"]

async def get_failed_workflow_logs(run_id: int) -> str:
    """Fetch the logs of a failed GitHub Actions workflow run.

    Args:
        run_id: The numeric ID of the workflow run.

    Returns:
        A string containing the most relevant log lines (truncated to 8000 chars).
    """
    headers = {
        "Authorization": f"Bearer {TOKEN}",
        "Accept": "application/vnd.github+json",
    }
    async with httpx.AsyncClient(timeout=30) as client:
        # Get the list of jobs in the run
        jobs_resp = await client.get(
            f"{GITHUB_API}/repos/{REPO}/actions/runs/{run_id}/jobs",
            headers=headers,
        )
        jobs_resp.raise_for_status()
        jobs = jobs_resp.json().get("jobs", [])

        failed = [j for j in jobs if j.get("conclusion") == "failure"]
        if not failed:
            return "No failed jobs found in this run."

        # Download logs zip endpoint
        logs_resp = await client.get(
            f"{GITHUB_API}/repos/{REPO}/actions/runs/{run_id}/logs",
            headers=headers,
            follow_redirects=True,
        )
        # GitHub returns a zip; for simplicity we return the raw text preview.
        text = logs_resp.text[:8000]
        return f"Failed jobs: {[j['name'] for j in failed]}\n\nLog preview:\n{text}"

get_failed_workflow_logs_tool = FunctionTool(
    get_failed_workflow_logs,
    name="get_failed_workflow_logs",
    description="Retrieve logs from a failed GitHub Actions workflow run.",
)

Git Diff and PR Tools

# tools/github_tools.py (continued)
async def get_pull_request_diff(pr_number: int) -> str:
    """Fetch the diff of a pull request."""
    headers = {
        "Authorization": f"Bearer {TOKEN}",
        "Accept": "application/vnd.github.v3.diff",
    }
    async with httpx.AsyncClient(timeout=30) as client:
        resp = await client.get(
            f"{GITHUB_API}/repos/{REPO}/pulls/{pr_number}",
            headers=headers,
        )
        resp.raise_for_status()
        return resp.text[:12000]

get_pull_request_diff_tool = FunctionTool(
    get_pull_request_diff,
    name="get_pull_request_diff",
    description="Fetch the unified diff of a pull request.",
)

async def comment_on_pr(pr_number: int, body: str) -> str:
    """Post a comment on a pull request."""
    headers = {
        "Authorization": f"Bearer {TOKEN}",
        "Accept": "application/vnd.github+json",
    }
    async with httpx.AsyncClient(timeout=30) as client:
        resp = await client.post(
            f"{GITHUB_API}/repos/{REPO}/issues/{pr_number}/comments",
            headers=headers,
            json={"body": body},
        )
        resp.raise_for_status()
        return f"Comment posted to PR #{pr_number}."

comment_on_pr_tool = FunctionTool(
    comment_on_pr,
    name="comment_on_pr",
    description="Post a comment on a pull request.",
)

Re-run Tool

# tools/ci_tools.py
import os
import httpx
from autogen_core.tools import FunctionTool

GITHUB_API = "https://api.github.com"
TOKEN = os.environ["GITHUB_TOKEN"]
REPO = os.environ["GITHUB_REPO"]

async def rerun_failed_jobs(run_id: int) -> str:
    """Re-run only the failed jobs of a workflow run."""
    headers = {
        "Authorization": f"Bearer {TOKEN}",
        "Accept": "application/vnd.github+json",
    }
    async with httpx.AsyncClient(timeout=30) as client:
        resp = await client.post(
            f"{GITHUB_API}/repos/{REPO}/actions/runs/{run_id}/rerun-failed-jobs",
            headers=headers,
        )
        if resp.status_code == 201:
            return f"Re-queued failed jobs for run {run_id}."
        return f"Failed to re-run: {resp.status_code} {resp.text}"

rerun_failed_jobs_tool = FunctionTool(
    rerun_failed_jobs,
    name="rerun_failed_jobs",
    description="Re-run only the failed jobs of a GitHub Actions workflow run.",
)

Building the Agent Team

With tools in place, we now define the agents. AutoGen 0.4 uses an actor-based model where agents exchange messages. We'll create four agents and wire them into a GroupChat-style orchestration using a RoundRobinGroupChat or a custom selector.

The Triage Agent

# agents/triage.py
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
from tools.github_tools import get_failed_workflow_logs_tool, get_pull_request_diff_tool

def build_triage_agent(model_client: OpenAIChatCompletionClient) -> AssistantAgent:
    return AssistantAgent(
        name="TriageAgent",
        model_client=model_client,
        tools=[get_failed_workflow_logs_tool, get_pull_request_diff_tool],
        system_message=(
            "You are a CI/CD triage specialist. When given a failed workflow run ID "
            "or a pull request number, retrieve the relevant logs and diff, then "
            "classify the failure into one of: "
            "(a) code_error, (b) flaky_test, (c) config_issue, (d) infra_issue, (e) dependency_issue. "
            "Provide a one-paragraph root cause summary and a recommended next action. "
            "Do not modify code yourself — hand off to the FixerAgent with a clear description."
        ),
    )

The Fixer Agent

# agents/fixer.py
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient

def build_fixer_agent(model_client: OpenAIChatCompletionClient) -> AssistantAgent:
    return AssistantAgent(
        name="FixerAgent",
        model_client=model_client,
        system_message=(
            "You are a senior engineer who proposes code or config fixes. "
            "Given a triage report, produce a concrete patch in unified diff format. "
            "Explain each change in plain English. Do not invent files — only modify "
            "files referenced in the triage. Always end with the line 'PATCH READY' "
            "so the ReviewerAgent knows to evaluate."
        ),
    )

The Reviewer Agent

# agents/reviewer.py
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
from tools.github_tools import comment_on_pr_tool

def build_reviewer_agent(model_client: OpenAIChatCompletionClient) -> AssistantAgent:
    return AssistantAgent(
        name="ReviewerAgent",
        model_client=model_client,
        tools=[comment_on_pr_tool],
        system_message=(
            "You are a cautious code reviewer. Evaluate the FixerAgent's patch for "
            "correctness, security, and style. If approved, post a summary comment "
            "on the PR using the comment_on_pr tool and end with 'APPROVED'. "
            "If rejected, end with 'CHANGES_REQUESTED' and list required changes."
        ),
    )

The Coordinator

# agents/coordinator.py
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
from tools.ci_tools import rerun_failed_jobs_tool

def build_coordinator_agent(model_client: OpenAIChatCompletionClient) -> AssistantAgent:
    return AssistantAgent(
        name="Coordinator",
        model_client=model_client,
        tools=[rerun_failed_jobs_tool],
        system_message=(
            "You are the CI/CD Coordinator. You receive webhook events and decide "
            "which agents to involve. For 'workflow_run' failures, ask TriageAgent "
            "to investigate. For flaky tests, you may use rerun_failed_jobs directly. "
            "Always summarize the final outcome for the human operator. "
            "End the conversation with 'DONE' when the issue is resolved or escalated."
        ),
    )

Orchestrating the Team

AutoGen's GroupChat lets multiple agents collaborate in a shared conversation. We use a SelectorGroupChat so the Coordinator can dynamically choose the next speaker based on context.

# server.py
import os
import json
import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_agentchat.teams import SelectorGroupChat
from autogen_agentchat.conditions import TextMentionTermination, MaxMessageTermination

from agents.coordinator import build_coordinator_agent
from agents.triage import build_triage_agent
from agents.fixer import build_fixer_agent
from agents.reviewer import build_reviewer_agent

app = FastAPI()

def build_team():
    model_client = OpenAIChatCompletionClient(
        model="gpt-4o-mini",
        api_key=os.environ["OPENAI_API_KEY"],
    )
    coordinator = build_coordinator_agent(model_client)
    triage = build_triage_agent(model_client)
    fixer = build_fixer_agent(model_client)
    reviewer = build_reviewer_agent(model_client)

    termination = TextMentionTermination("DONE") | MaxMessageTermination(12)
    team = SelectorGroupChat(
        participants=[coordinator, triage, fixer, reviewer],
        model_client=model_client,
        termination_condition=termination,
        selector_prompt=(
            "You are selecting the next agent to speak. Available agents: {roles}. "
            "Recent history:\n{history}\n"
            "Pick the single most appropriate agent to advance the task."
        ),
    )
    return team, model_client

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    if not signature.startswith("sha256="):
        return False
    expected = "sha256=" + hmac.new(
        secret.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.post("/webhook")
async def webhook(request: Request):
    body = await request.body()
    signature = request.headers.get("X-Hub-Signature-256", "")
    if not verify_signature(body, signature, os.environ["WEBHOOK_SECRET"]):
        raise HTTPException(status_code=401, detail="Invalid signature")

    event = json.loads(body)
    event_name = request.headers.get("X-GitHub-Event", "")

    if event_name == "workflow_run" and event.get("action") == "completed":
        run = event["workflow_run"]
        if run["conclusion"] != "failure":
            return {"status": "ignored", "reason": "not a failure"}

        team, model_client = build_team()
        task = (
            f"A GitHub Actions workflow run failed.\n"
            f"Run ID: {run['id']}\n"
            f"Workflow: {run['name']}\n"
            f"Branch: {run['head_branch']}\n"
            f"Commit: {run['head_sha']}\n"
            f"Please triage the failure, propose a fix if appropriate, "
            f"and post a summary. End with DONE when finished."
        )
        try:
            result = await team.run(task=task)
        finally:
            await model_client.close()

        return {
            "status": "completed",
            "messages": [m.to_dict() for m in result.messages],
            "stop_reason": result.stop_reason,
        }

    return {"status": "ignored", "event": event_name}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Start the server locally with python server.py and use a tool like ngrok to expose it to GitHub as a webhook endpoint. Configure your GitHub repository to send workflow_run events to https://your-tunnel/webhook.

Adding Human-in-the-Loop Approval

Allowing an agent to post comments is low-risk, but allowing it to commit code or trigger production deployments requires a human gate. AutoGen supports a UserProxyAgent pattern, but for webhook-driven flows a simpler approach is to require explicit approval via a GitHub PR review.

Modify the FixerAgent to produce a patch as a PR comment rather than committing directly, and let a human click "Approve" before any further action runs. Here is an example approval-aware action tool:

# tools/github_tools.py (continued)
async def create_pull_request(
    head_branch: str,
    title: str,
    body: str,
    base_branch: str = "main",
) -> str:
    """Open a pull request. Requires a human to review and merge."""
    headers = {
        "Authorization": f"Bearer {TOKEN}",
        "Accept": "application/vnd.github+json",
    }
    async with httpx.AsyncClient(timeout=30) as client:
        resp = await client.post(
            f"{GITHUB_API}/repos/{REPO}/pulls",
            headers=headers,
            json={
                "title": title,
                "body": body,
                "head": head_branch,
                "base": base_branch,
            },
        )
        resp.raise_for_status()
        pr = resp.json()
        return f"Opened PR #{pr['number']}: {pr['html_url']}"

create_pull_request_tool = FunctionTool(
    create_pull_request,
    name="create_pull_request",
    description="Open a draft pull request for human review. Never auto-merge.",
)

Wire this tool into the FixerAgent and update its system message to require opening a PR rather than pushing directly. The ReviewerAgent then comments on the PR, and a human reviewer makes the final merge decision.

Testing the Agent Locally

Before connecting real webhooks, test the team with a synthetic task. This lets you iterate on prompts without waiting for a real failure.

# test_team.py
import asyncio
from server import build_team

async def main():
    team, model_client = build_team()
    task = (
        "Simulated failure: workflow run ID 987654321 on branch 'feature/login' "
        "failed at the 'test' job. The error appears to be a missing import in "
        "src/auth.py. Triage, propose a fix, and review it. End with DONE."
    )
    result = await team.run(task=task)
    for msg in result.messages:
        print(f"[{msg.source}] {msg.content}\n")
    await model_client.close()

asyncio.run(main())

Run it with python test_team.py and inspect the conversation. You should see the Coordinator delegate to TriageAgent, TriageAgent call the log tool, FixerAgent propose a patch, and ReviewerAgent either approve or request changes.

Best Practices

1. Scope Tools Tightly

Every tool is an attack surface. Avoid giving agents generic "run shell command" tools in production. Instead, expose narrowly-scoped functions like rerun_failed_jobs or comment_on_pr that perform validation internally. Log every tool invocation with arguments and results.

2. Enforce Allow-lists

Restrict which repositories, branches, and environments the agent may touch. For example, never let the agent operate on main directly or trigger production deployments without a separate approval workflow.

3. Use Cheap Models for Routing

The Coordinator and TriageAgent often perform classification tasks that smaller models handle well. Use gpt-4o-mini for routing and reserve a larger model like gpt-4o for the FixerAgent, where reasoning quality matters most. This keeps costs predictable.

4. Cap Conversation Length

Always combine a semantic termination condition (like TextMentionTermination("DONE")) with a hard cap (MaxMessageTermination). Without a cap, a confused agent loop can burn through tokens indefinitely.

5. Persist Conversation State

For long-running incidents, serialize the team's message history to a database so you can resume, audit, and learn from past runs. AutoGen messages are Pydantic models and serialize cleanly with msg.model_dump().

6. Treat LLM Output as Untrusted

Never execute code the agent generates without sandboxing. If the FixerAgent proposes a patch, render it as a diff in a PR for human review rather than applying it automatically. The agent is a co-pilot, not an autopilot.

7. Monitor and Evaluate

Track metrics: percentage of failures auto-triaged correctly, percentage of fixes merged without modification, average tokens per run, and false-positive rate. Use these to refine prompts and tool boundaries over time.

8. Version Your Prompts

System prompts are code. Store them in version control, review changes in pull requests, and run regression tests against a fixed set of historical incidents so prompt changes don't silently regress behavior.

Extending the Agent

Once the core loop works, you can extend it in several directions:

Conclusion

Building a CI/CD automation agent with AutoGen turns the repetitive, context-heavy work of pipeline triage into a structured, auditable, and increasingly autonomous workflow. By composing specialized agents — a Coordinator, a Triage specialist, a Fixer, and a Reviewer — and equipping them with tightly scoped tools, you create a system that can read logs, diagnose failures, propose patches, and coordinate human approval without ever bypassing safety gates. The result is faster mean-time-to-resolution, more consistent incident handling, and a durable record of how your team resolves delivery problems. Start small with a single failure class, instrument everything, and expand the agent's responsibilities only as confidence and guardrails grow — that incremental path is the surest way to a CI/CD agent your team can trust in production.

— Ad —

Google AdSense will appear here after approval

← Back to all articles