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:
- Failure triage: Reading failed build logs, classifying the root cause, and suggesting or applying a fix.
- Flaky test handling: Detecting non-deterministic test failures and re-running them with diagnostics enabled.
- Configuration maintenance: Detecting outdated action versions, deprecated images, or insecure dependencies in pipeline definitions.
- Deployment coordination: Drafting release notes, requesting approvals, and rolling back when health checks fail.
- Observability: Summarizing pipeline trends and surfacing recurring issues to the team.
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:
- Reduced mean-time-to-resolution (MTTR): The agent can read a 5,000-line log file in seconds and propose a fix before a human even opens the page.
- Consistent triage: Every failure is classified with the same rigor, eliminating the "only Sarah knows why the deploy breaks on Tuesdays" problem.
- Knowledge capture: The agent's reasoning traces become a searchable record of how issues were resolved.
- Human-in-the-loop safety: Agents can draft changes but require approval, blending automation speed with human judgment.
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:
- Event source: A webhook receiver (FastAPI, AWS Lambda, GitHub App) that ingests pipeline events.
- Context retriever: Tools that fetch logs, git diffs, test reports, and runbook documents.
- Agent team: Multiple AutoGen agents with distinct roles — a Triage Agent, a Fixer Agent, a Reviewer Agent, and a Coordinator.
- Action layer: Tools that perform side effects: opening PRs, commenting on issues, triggering reruns, or rolling back deployments.
- Guardrails: Allow-lists of permitted actions, approval gates, and audit logging.
- State store: A database or object store for conversation history and run artifacts.
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:
- Slack integration: Add a tool that posts triage summaries to a Slack channel and accepts approval reactions as signals.
- Kubernetes rollback: Add a tool that checks deployment health and rolls back via
kubectlor the Argo CD API when error budgets are exceeded. - Dependency scanning: Wire in Dependabot or Snyk alerts so the agent can open upgrade PRs for vulnerable dependencies.
- Multi-repo awareness: Let the agent correlate failures across microservices by indexing recent deploys and their health metrics.
- RAG over runbooks: Embed your incident runbooks in a vector store and give the TriageAgent a retrieval tool so it grounds its diagnoses in documented procedures.
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.