projects/unit3/github-actions-integration/starter/server.py (135 lines of code) (raw):
#!/usr/bin/env python3
"""
Module 2: GitHub Actions Integration - STARTER CODE
Extend your PR Agent with webhook handling and MCP Prompts for CI/CD workflows.
"""
import json
import os
import subprocess
from typing import Optional
from pathlib import Path
from datetime import datetime
from mcp.server.fastmcp import FastMCP
# Initialize the FastMCP server
mcp = FastMCP("pr-agent-actions")
# PR template directory (shared between starter and solution)
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates"
# Default PR templates
DEFAULT_TEMPLATES = {
"bug.md": "Bug Fix",
"feature.md": "Feature",
"docs.md": "Documentation",
"refactor.md": "Refactor",
"test.md": "Test",
"performance.md": "Performance",
"security.md": "Security"
}
# TODO: Add path to events file where webhook_server.py stores events
# Hint: EVENTS_FILE = Path(__file__).parent / "github_events.json"
# Type mapping for PR templates
TYPE_MAPPING = {
"bug": "bug.md",
"fix": "bug.md",
"feature": "feature.md",
"enhancement": "feature.md",
"docs": "docs.md",
"documentation": "docs.md",
"refactor": "refactor.md",
"cleanup": "refactor.md",
"test": "test.md",
"testing": "test.md",
"performance": "performance.md",
"optimization": "performance.md",
"security": "security.md"
}
# ===== Module 1 Tools (Already includes output limiting fix from Module 1) =====
@mcp.tool()
async def analyze_file_changes(
base_branch: str = "main",
include_diff: bool = True,
max_diff_lines: int = 500
) -> str:
"""Get the full diff and list of changed files in the current git repository.
Args:
base_branch: Base branch to compare against (default: main)
include_diff: Include the full diff content (default: true)
max_diff_lines: Maximum number of diff lines to include (default: 500)
"""
try:
# Get list of changed files
files_result = subprocess.run(
["git", "diff", "--name-status", f"{base_branch}...HEAD"],
capture_output=True,
text=True,
check=True
)
# Get diff statistics
stat_result = subprocess.run(
["git", "diff", "--stat", f"{base_branch}...HEAD"],
capture_output=True,
text=True
)
# Get the actual diff if requested
diff_content = ""
truncated = False
if include_diff:
diff_result = subprocess.run(
["git", "diff", f"{base_branch}...HEAD"],
capture_output=True,
text=True
)
diff_lines = diff_result.stdout.split('\n')
# Check if we need to truncate (learned from Module 1)
if len(diff_lines) > max_diff_lines:
diff_content = '\n'.join(diff_lines[:max_diff_lines])
diff_content += f"\n\n... Output truncated. Showing {max_diff_lines} of {len(diff_lines)} lines ..."
diff_content += "\n... Use max_diff_lines parameter to see more ..."
truncated = True
else:
diff_content = diff_result.stdout
# Get commit messages for context
commits_result = subprocess.run(
["git", "log", "--oneline", f"{base_branch}..HEAD"],
capture_output=True,
text=True
)
analysis = {
"base_branch": base_branch,
"files_changed": files_result.stdout,
"statistics": stat_result.stdout,
"commits": commits_result.stdout,
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)",
"truncated": truncated,
"total_diff_lines": len(diff_lines) if include_diff else 0
}
return json.dumps(analysis, indent=2)
except subprocess.CalledProcessError as e:
return json.dumps({"error": f"Git error: {e.stderr}"})
except Exception as e:
return json.dumps({"error": str(e)})
@mcp.tool()
async def get_pr_templates() -> str:
"""List available PR templates with their content."""
templates = [
{
"filename": filename,
"type": template_type,
"content": (TEMPLATES_DIR / filename).read_text()
}
for filename, template_type in DEFAULT_TEMPLATES.items()
]
return json.dumps(templates, indent=2)
@mcp.tool()
async def suggest_template(changes_summary: str, change_type: str) -> str:
"""Let Claude analyze the changes and suggest the most appropriate PR template.
Args:
changes_summary: Your analysis of what the changes do
change_type: The type of change you've identified (bug, feature, docs, refactor, test, etc.)
"""
# Get available templates
templates_response = await get_pr_templates()
templates = json.loads(templates_response)
# Find matching template
template_file = TYPE_MAPPING.get(change_type.lower(), "feature.md")
selected_template = next(
(t for t in templates if t["filename"] == template_file),
templates[0] # Default to first template if no match
)
suggestion = {
"recommended_template": selected_template,
"reasoning": f"Based on your analysis: '{changes_summary}', this appears to be a {change_type} change.",
"template_content": selected_template["content"],
"usage_hint": "Claude can help you fill out this template based on the specific changes in your PR."
}
return json.dumps(suggestion, indent=2)
# ===== Module 2: New GitHub Actions Tools =====
@mcp.tool()
async def get_recent_actions_events(limit: int = 10) -> str:
"""Get recent GitHub Actions events received via webhook.
Args:
limit: Maximum number of events to return (default: 10)
"""
# TODO: Implement this function
# 1. Check if EVENTS_FILE exists
# 2. Read the JSON file
# 3. Return the most recent events (up to limit)
# 4. Return empty list if file doesn't exist
return json.dumps({"message": "TODO: Implement get_recent_actions_events"})
@mcp.tool()
async def get_workflow_status(workflow_name: Optional[str] = None) -> str:
"""Get the current status of GitHub Actions workflows.
Args:
workflow_name: Optional specific workflow name to filter by
"""
# TODO: Implement this function
# 1. Read events from EVENTS_FILE
# 2. Filter events for workflow_run events
# 3. If workflow_name provided, filter by that name
# 4. Group by workflow and show latest status
# 5. Return formatted workflow status information
return json.dumps({"message": "TODO: Implement get_workflow_status"})
# ===== Module 2: MCP Prompts =====
@mcp.prompt()
async def analyze_ci_results():
"""Analyze recent CI/CD results and provide insights."""
# TODO: Implement this prompt
# Return a string with instructions for Claude to:
# 1. Use get_recent_actions_events()
# 2. Use get_workflow_status()
# 3. Analyze results and provide insights
return "TODO: Implement analyze_ci_results prompt"
@mcp.prompt()
async def create_deployment_summary():
"""Generate a deployment summary for team communication."""
# TODO: Implement this prompt
# Return a string that guides Claude to create a deployment summary
return "TODO: Implement create_deployment_summary prompt"
@mcp.prompt()
async def generate_pr_status_report():
"""Generate a comprehensive PR status report including CI/CD results."""
# TODO: Implement this prompt
# Return a string that guides Claude to combine code changes with CI/CD status
return "TODO: Implement generate_pr_status_report prompt"
@mcp.prompt()
async def troubleshoot_workflow_failure():
"""Help troubleshoot a failing GitHub Actions workflow."""
# TODO: Implement this prompt
# Return a string that guides Claude through troubleshooting steps
return "TODO: Implement troubleshoot_workflow_failure prompt"
if __name__ == "__main__":
print("Starting PR Agent MCP server...")
print("NOTE: Run webhook_server.py in a separate terminal to receive GitHub events")
mcp.run()