Skip to content

Ticket to PR example #592

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions codegen-examples/examples/ticket-to-pr/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
LINEAR_ACCESS_TOKEN="..."
LINEAR_SIGNING_SECRET="..."
LINEAR_TEAM_ID="..."
GITHUB_TOKEN="..."
ANTHROPIC_API_KEY="..."
43 changes: 43 additions & 0 deletions codegen-examples/examples/ticket-to-pr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Linear Ticket to GitHub PR Bot

This example project demonstrates how to deploy an agentic bot that automatically creates GitHub Pull Requests from Linear tickets. The bot leverages Linear's webhook system to listen for ticket updates and uses AI to generate corresponding GitHub PRs with appropriate code changes.

## Prerequisites

Before running this application, you'll need the following API tokens and credentials:

- GitHub API Token
- Linear API Token
- Anthropic API Token
- Linear Signing Key
- Linear Team ID

## Setup

1. Clone the repository
1. Set up your environment variables in a `.env` file:

```env
GITHUB_TOKEN=your_github_token
LINEAR_API_TOKEN=your_linear_token
ANTHROPIC_API_KEY=your_anthropic_token
LINEAR_SIGNING_KEY=your_linear_signing_key
LINEAR_TEAM_ID=your_team_id
```

## Features

- Automatic PR creation from Linear tickets
- AI-powered code generation using an agentic approach
- Webhook integration with Linear

## Usage

1. uv sync
1. uv run modal deploy app.py
- At this point you should have a modal app with an endpoint that is auto registered to linear as a webhook callback url.
1. Try making a ticket and adding the `Codegen` label to trigger the agent

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
61 changes: 61 additions & 0 deletions codegen-examples/examples/ticket-to-pr/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from codegen import Codebase, CodeAgent
from codegen.extensions.clients.linear import LinearClient
from codegen.extensions.events.app import CodegenApp
from codegen.extensions.tools.github.create_pr import create_pr
from codegen.shared.enums.programming_language import ProgrammingLanguage
from helpers import create_codebase, format_linear_message, has_codegen_label, process_update_event

from fastapi import Request

import os
import modal
import logging


logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

image = modal.Image.debian_slim(python_version="3.13").apt_install("git").pip_install("fastapi[standard]", "codegen>=v0.26.3")

app = CodegenApp("linear-bot", image=image, modal_api_key="")


@app.cls(secrets=[modal.Secret.from_dotenv()], keep_warm=1)
class LinearApp:
codebase: Codebase

@modal.enter()
def run_this_on_container_startup(self):
self.codebase = create_codebase("codegen-sh/codegen-sdk", ProgrammingLanguage.PYTHON)

# Subscribe web endpoints as linear webhook callbacks
app.linear.subscribe_all_handlers()

@modal.exit()
def run_this_on_container_exit(self):
app.linear.unsubscribe_all_handlers()

@modal.web_endpoint(method="POST")
@app.linear.event("Issue", should_handle=has_codegen_label)
def handle_webhook(self, data: dict, request: Request):
""""Handle incoming webhook events from Linear""" ""
linear_client = LinearClient(access_token=os.environ["LINEAR_ACCESS_TOKEN"])

event = process_update_event(data)
linear_client.comment_on_issue(event.issue_id, "I'm on it 👍")

query = format_linear_message(event.title, event.description)
agent = CodeAgent(self.codebase)

agent.run(query)

pr_title = f"[{event.identifier}] " + event.title
pr_body = "Codegen generated PR for issue: " + event.issue_url
create_pr_result = create_pr(self.codebase, pr_title, pr_body)

logger.info(f"PR created: {create_pr_result.model_dump_json()}")

linear_client.comment_on_issue(event.issue_id, f"I've finished running, please review the PR: {create_pr_result.url}")
self.codebase.reset()

return {"status": "success"}
22 changes: 22 additions & 0 deletions codegen-examples/examples/ticket-to-pr/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import Any, Dict, List, Optional
from pydantic import BaseModel


class LinearLabels(BaseModel):
id: str
color: str # hex color
name: str


class LinearIssueUpdateEvent(BaseModel):
action: str
issue_id: str
actor: Dict[str, Any]
created_at: str
issue_url: str
data: Dict[str, Any]
labels: List[LinearLabels]
updated_from: Dict[str, Any]
title: str
description: Optional[str] = None
identifier: Optional[str] = None
84 changes: 84 additions & 0 deletions codegen-examples/examples/ticket-to-pr/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from codegen import Codebase, ProgrammingLanguage
from typing import List, Dict, Any
from codegen.sdk.codebase.config import CodebaseConfig
from data import LinearLabels, LinearIssueUpdateEvent
import os
import logging


logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def process_update_event(event_data: dict[str, Any]):
print("processing update event")

action = event_data.get("action")
actor = event_data.get("actor")
created_at = event_data.get("createdAt")
issue_url = event_data.get("url")
data: Dict[str, Any] = event_data.get("data")

Check failure on line 20 in codegen-examples/examples/ticket-to-pr/helpers.py

View workflow job for this annotation

GitHub Actions / mypy

error: Incompatible types in assignment (expression has type "Any | None", variable has type "dict[str, Any]") [assignment]
issue_id = data.get("id")
title = data.get("title")
description = data.get("description")
identifier = data.get("identifier")

labels: List[LinearLabels] = data.get("labels")

Check failure on line 26 in codegen-examples/examples/ticket-to-pr/helpers.py

View workflow job for this annotation

GitHub Actions / mypy

error: Incompatible types in assignment (expression has type "Any | None", variable has type "list[LinearLabels]") [assignment]
updated_from: Dict[str, Any] = event_data.get("updatedFrom")

Check failure on line 27 in codegen-examples/examples/ticket-to-pr/helpers.py

View workflow job for this annotation

GitHub Actions / mypy

error: Incompatible types in assignment (expression has type "Any | None", variable has type "dict[str, Any]") [assignment]

update_event = LinearIssueUpdateEvent(
issue_id=issue_id,
action=action,
actor=actor,
created_at=created_at,
issue_url=issue_url,
data=data,
labels=labels,
updated_from=updated_from,
title=title,
description=description,
identifier=identifier,
)
return update_event


def format_linear_message(title: str, description: str | None = "") -> str:
"""Format a Linear update event into a message for the agent"""

return f"""
Here is a new issue titled '{title}' and with the description '{description}'. Continue to respond to this query. Use your tools to query the codebase for more context.
When applicable include references to files and line numbers, code snippets are also encouraged. Don't forget to create a pull request with your changes, use the appropriate tool to do so.
"""


def has_codegen_label(*args, **kwargs):
body = kwargs.get("data")
type = body.get("type")
action = body.get("action")

if type == "Issue" and action == "update":
# handle issue update (label updates)
update_event = process_update_event(body)

has_codegen_label = any(label.name == "Codegen" for label in update_event.labels)
codegen_label_id = next((label.id for label in update_event.labels if label.name == "Codegen"), None)
had_codegen_label = codegen_label_id in update_event.updated_from.get("labels", []) if codegen_label_id else False
previous_labels = update_event.updated_from.get("labelIds", None)

if previous_labels is None or not has_codegen_label:
logger.info("No labels updated, skipping codegen bot response")
return False

if has_codegen_label and not had_codegen_label:
logger.info("Codegen label added, codegen bot will respond")
return True

logger.info("Codegen label removed or already existed, codegen bot will not respond")
return False


def create_codebase(repo_name: str, language: ProgrammingLanguage):
config = CodebaseConfig()
config.secrets.github_token = os.environ["GITHUB_TOKEN"]

return Codebase.from_repo(repo_name, language=language, tmp_dir="/root", config=config)
12 changes: 12 additions & 0 deletions codegen-examples/examples/ticket-to-pr/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[project]
name = "ticket-to-pr"
version = "0.1.0"
description = "A example implementation of a agentic linear bot that can create PRs from linear tickets"
readme = "README.md"
requires-python = ">=3.12, <3.14"
dependencies = [
"codegen==0.26.3",
"fastapi>=0.115.8",
"modal>=0.73.51",
"pydantic>=2.10.6",
]
Loading
Loading