Skip to content

Commit d98faa5

Browse files
authored
Ticket to PR example (#592)
# Motivation <!-- Why is this change necessary? --> # Content Adding an example of using the codegen sdk and event decorators to build a quick Linear to PR bot integration <!-- Please include a summary of the change --> # Testing <!-- How was the change tested? --> # Please check the following before marking your PR as ready for review - [ ] I have added tests for my changes - [ ] I have updated the documentation or added new documentation as needed --------- Co-authored-by: rushilpatel0 <[email protected]>
1 parent cac3ba7 commit d98faa5

File tree

7 files changed

+2958
-0
lines changed

7 files changed

+2958
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
LINEAR_ACCESS_TOKEN="..."
2+
LINEAR_SIGNING_SECRET="..."
3+
LINEAR_TEAM_ID="..."
4+
GITHUB_TOKEN="..."
5+
ANTHROPIC_API_KEY="..."
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Linear Ticket to GitHub PR Bot
2+
3+
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.
4+
5+
## Prerequisites
6+
7+
Before running this application, you'll need the following API tokens and credentials:
8+
9+
- GitHub API Token
10+
- Linear API Token
11+
- Anthropic API Token
12+
- Linear Signing Key
13+
- Linear Team ID
14+
15+
## Setup
16+
17+
1. Clone the repository
18+
1. Set up your environment variables in a `.env` file:
19+
20+
```env
21+
GITHUB_TOKEN=your_github_token
22+
LINEAR_API_TOKEN=your_linear_token
23+
ANTHROPIC_API_KEY=your_anthropic_token
24+
LINEAR_SIGNING_KEY=your_linear_signing_key
25+
LINEAR_TEAM_ID=your_team_id
26+
```
27+
28+
## Features
29+
30+
- Automatic PR creation from Linear tickets
31+
- AI-powered code generation using an agentic approach
32+
- Webhook integration with Linear
33+
34+
## Usage
35+
36+
1. uv sync
37+
1. uv run modal deploy app.py
38+
- At this point you should have a modal app with an endpoint that is auto registered to linear as a webhook callback url.
39+
1. Try making a ticket and adding the `Codegen` label to trigger the agent
40+
41+
## Contributing
42+
43+
Contributions are welcome! Please feel free to submit a Pull Request.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from codegen import Codebase, CodeAgent
2+
from codegen.extensions.clients.linear import LinearClient
3+
from codegen.extensions.events.app import CodegenApp
4+
from codegen.extensions.tools.github.create_pr import create_pr
5+
from codegen.shared.enums.programming_language import ProgrammingLanguage
6+
from helpers import create_codebase, format_linear_message, has_codegen_label, process_update_event
7+
8+
from fastapi import Request
9+
10+
import os
11+
import modal
12+
import logging
13+
14+
15+
logging.basicConfig(level=logging.INFO)
16+
logger = logging.getLogger(__name__)
17+
18+
image = modal.Image.debian_slim(python_version="3.13").apt_install("git").pip_install("fastapi[standard]", "codegen>=v0.26.3")
19+
20+
app = CodegenApp("linear-bot", image=image, modal_api_key="")
21+
22+
23+
@app.cls(secrets=[modal.Secret.from_dotenv()], keep_warm=1)
24+
class LinearApp:
25+
codebase: Codebase
26+
27+
@modal.enter()
28+
def run_this_on_container_startup(self):
29+
self.codebase = create_codebase("codegen-sh/codegen-sdk", ProgrammingLanguage.PYTHON)
30+
31+
# Subscribe web endpoints as linear webhook callbacks
32+
app.linear.subscribe_all_handlers()
33+
34+
@modal.exit()
35+
def run_this_on_container_exit(self):
36+
app.linear.unsubscribe_all_handlers()
37+
38+
@modal.web_endpoint(method="POST")
39+
@app.linear.event("Issue", should_handle=has_codegen_label)
40+
def handle_webhook(self, data: dict, request: Request):
41+
""""Handle incoming webhook events from Linear""" ""
42+
linear_client = LinearClient(access_token=os.environ["LINEAR_ACCESS_TOKEN"])
43+
44+
event = process_update_event(data)
45+
linear_client.comment_on_issue(event.issue_id, "I'm on it 👍")
46+
47+
query = format_linear_message(event.title, event.description)
48+
agent = CodeAgent(self.codebase)
49+
50+
agent.run(query)
51+
52+
pr_title = f"[{event.identifier}] " + event.title
53+
pr_body = "Codegen generated PR for issue: " + event.issue_url
54+
create_pr_result = create_pr(self.codebase, pr_title, pr_body)
55+
56+
logger.info(f"PR created: {create_pr_result.model_dump_json()}")
57+
58+
linear_client.comment_on_issue(event.issue_id, f"I've finished running, please review the PR: {create_pr_result.url}")
59+
self.codebase.reset()
60+
61+
return {"status": "success"}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Any, Dict, List, Optional
2+
from pydantic import BaseModel
3+
4+
5+
class LinearLabels(BaseModel):
6+
id: str
7+
color: str # hex color
8+
name: str
9+
10+
11+
class LinearIssueUpdateEvent(BaseModel):
12+
action: str
13+
issue_id: str
14+
actor: Dict[str, Any]
15+
created_at: str
16+
issue_url: str
17+
data: Dict[str, Any]
18+
labels: List[LinearLabels]
19+
updated_from: Dict[str, Any]
20+
title: str
21+
description: Optional[str] = None
22+
identifier: Optional[str] = None
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from codegen import Codebase, ProgrammingLanguage
2+
from typing import List, Dict, Any
3+
from codegen.sdk.codebase.config import CodebaseConfig
4+
from data import LinearLabels, LinearIssueUpdateEvent
5+
import os
6+
import logging
7+
8+
9+
logging.basicConfig(level=logging.INFO)
10+
logger = logging.getLogger(__name__)
11+
12+
13+
def process_update_event(event_data: dict[str, Any]):
14+
print("processing update event")
15+
16+
action = event_data.get("action")
17+
actor = event_data.get("actor")
18+
created_at = event_data.get("createdAt")
19+
issue_url = event_data.get("url")
20+
data: Dict[str, Any] = event_data.get("data")
21+
issue_id = data.get("id")
22+
title = data.get("title")
23+
description = data.get("description")
24+
identifier = data.get("identifier")
25+
26+
labels: List[LinearLabels] = data.get("labels")
27+
updated_from: Dict[str, Any] = event_data.get("updatedFrom")
28+
29+
update_event = LinearIssueUpdateEvent(
30+
issue_id=issue_id,
31+
action=action,
32+
actor=actor,
33+
created_at=created_at,
34+
issue_url=issue_url,
35+
data=data,
36+
labels=labels,
37+
updated_from=updated_from,
38+
title=title,
39+
description=description,
40+
identifier=identifier,
41+
)
42+
return update_event
43+
44+
45+
def format_linear_message(title: str, description: str | None = "") -> str:
46+
"""Format a Linear update event into a message for the agent"""
47+
48+
return f"""
49+
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.
50+
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.
51+
"""
52+
53+
54+
def has_codegen_label(*args, **kwargs):
55+
body = kwargs.get("data")
56+
type = body.get("type")
57+
action = body.get("action")
58+
59+
if type == "Issue" and action == "update":
60+
# handle issue update (label updates)
61+
update_event = process_update_event(body)
62+
63+
has_codegen_label = any(label.name == "Codegen" for label in update_event.labels)
64+
codegen_label_id = next((label.id for label in update_event.labels if label.name == "Codegen"), None)
65+
had_codegen_label = codegen_label_id in update_event.updated_from.get("labels", []) if codegen_label_id else False
66+
previous_labels = update_event.updated_from.get("labelIds", None)
67+
68+
if previous_labels is None or not has_codegen_label:
69+
logger.info("No labels updated, skipping codegen bot response")
70+
return False
71+
72+
if has_codegen_label and not had_codegen_label:
73+
logger.info("Codegen label added, codegen bot will respond")
74+
return True
75+
76+
logger.info("Codegen label removed or already existed, codegen bot will not respond")
77+
return False
78+
79+
80+
def create_codebase(repo_name: str, language: ProgrammingLanguage):
81+
config = CodebaseConfig()
82+
config.secrets.github_token = os.environ["GITHUB_TOKEN"]
83+
84+
return Codebase.from_repo(repo_name, language=language, tmp_dir="/root", config=config)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[project]
2+
name = "ticket-to-pr"
3+
version = "0.1.0"
4+
description = "A example implementation of a agentic linear bot that can create PRs from linear tickets"
5+
readme = "README.md"
6+
requires-python = ">=3.12, <3.14"
7+
dependencies = [
8+
"codegen==0.26.3",
9+
"fastapi>=0.115.8",
10+
"modal>=0.73.51",
11+
"pydantic>=2.10.6",
12+
]

0 commit comments

Comments
 (0)