Skip to content

Commit

Permalink
Merge pull request #391 from gitautoai/wes
Browse files Browse the repository at this point in the history
Enable GitAuto to open pull requests from Jira tickets
  • Loading branch information
hiroshinishio authored Dec 10, 2024
2 parents 2c9e7a5 + b77f8a4 commit a15967c
Show file tree
Hide file tree
Showing 17 changed files with 428 additions and 247 deletions.
2 changes: 1 addition & 1 deletion config.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,6 @@ def get_env_var(name: str) -> str:
EXCEPTION_OWNERS = ["gitautoai", "hiroshinishio"]
OWNER_TYPE = "Organization"
TEST_EMAIL = "test@gitauto.ai"
UNIQUE_ISSUE_ID = "O/gitautoai/test#1"
TEST_REPO_NAME = "test-repo"
USER_ID = -1
USER_NAME = "username-test"
9 changes: 9 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
# Local imports
from config import GITHUB_WEBHOOK_SECRET, ENV, PRODUCT_NAME, SENTRY_DSN, UTF8
from scheduler import schedule_handler
from services.gitauto_handler import handle_gitauto
from services.github.github_manager import verify_webhook_signature
from services.webhook_handler import handle_webhook_event
from services.jira.jira_manager import verify_jira_webhook

if ENV != "local":
sentry_sdk.init(
Expand Down Expand Up @@ -69,6 +71,13 @@ async def handle_webhook(request: Request) -> dict[str, str]:
return {"message": "Webhook processed successfully"}


@app.post(path="/jira-webhook")
async def handle_jira_webhook(request: Request):
payload = await verify_jira_webhook(request)
await handle_gitauto(payload=payload, trigger_type="checkbox", input_from="jira")
return {"message": "Jira webhook processed successfully"}


@app.get(path="/")
async def root() -> dict[str, str]:
return {"message": PRODUCT_NAME}
8 changes: 2 additions & 6 deletions scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import logging
import time
from config import GITHUB_APP_USER_ID, GITHUB_APP_USER_NAME, PRODUCT_ID, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_URL
from config import PRODUCT_ID, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_URL
from services.github.github_manager import (
add_label_to_issue,
get_installation_access_token,
Expand Down Expand Up @@ -59,11 +59,7 @@ def schedule_handler(_event, _context) -> dict[str, int]:
# Check the remaining available usage count, continue if it's less than 1.
requests_left, _request_count, _end_date = (
supabase_manager.get_how_many_requests_left_and_cycle(
user_id=GITHUB_APP_USER_ID,
installation_id=installation_id,
user_name=GITHUB_APP_USER_NAME,
owner_id=owner_id,
owner_name=owner,
installation_id=installation_id, owner_id=owner_id, owner_name=owner
)
)
if requests_left < 1:
Expand Down
5 changes: 2 additions & 3 deletions services/check_run_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def handle_check_run(payload: CheckRunCompletedPayload) -> None:
"owner": owner_name,
"repo": repo_name,
"is_fork": is_fork,
"issue_number": pull_number,
"new_branch": head_branch,
"base_branch": head_branch, # Yes, intentionally set head_branch to base_branch because get_remote_file_tree requires the base branch
"sender_id": sender_id,
Expand Down Expand Up @@ -127,9 +128,7 @@ def handle_check_run(payload: CheckRunCompletedPayload) -> None:
# Create a first comment to inform the user that GitAuto is trying to fix the Check Run error
msg = "Oops! Check run stumbled. Digging into logs... 🕵️"
comment_body = create_progress_bar(p=0, msg=msg)
comment_url = create_comment(
issue_number=pull_number, body=comment_body, base_args=base_args
)
comment_url: str | None = create_comment(body=comment_body, base_args=base_args)
base_args["comment_url"] = comment_url

# Get title, body, and code changes in the PR
Expand Down
138 changes: 58 additions & 80 deletions services/gitauto_handler.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,36 @@
# Standard imports
import json
import logging
import time
from uuid import uuid4
from typing import Literal

# Local imports
from config import (
EXCEPTION_OWNERS,
GITHUB_APP_USER_ID,
IS_PRD,
PRODUCT_ID,
PRODUCT_NAME,
SUPABASE_URL,
SUPABASE_SERVICE_ROLE_KEY,
PR_BODY_STARTS_WITH,
ISSUE_NUMBER_FORMAT,
)
from services.github.github_manager import (
create_pull_request,
create_remote_branch,
get_installation_access_token,
get_issue_comments,
get_latest_remote_commit_sha,
get_remote_file_content_by_url,
get_remote_file_tree,
create_comment,
update_comment,
add_reaction_to_issue,
get_user_public_email,
)
from services.github.github_types import (
BaseArgs,
GitHubLabeledPayload,
IssueInfo,
RepositoryInfo,
)
from services.github.github_types import GitHubLabeledPayload
from services.github.utilities import deconstruct_github_payload
from services.jira.jira_manager import deconstruct_jira_payload
from services.openai.commit_changes import chat_with_agent
from services.openai.instructions.write_pr_body import WRITE_PR_BODY
from services.openai.chat import chat_with_ai
from services.supabase import SupabaseManager
from utils.extract_urls import extract_urls
from utils.progress_bar import create_progress_bar
from utils.text_copy import (
UPDATE_COMMENT_FOR_422,
Expand All @@ -51,104 +42,88 @@
supabase_manager = SupabaseManager(url=SUPABASE_URL, key=SUPABASE_SERVICE_ROLE_KEY)


async def handle_gitauto(payload: GitHubLabeledPayload, trigger_type: str) -> None:
async def handle_gitauto(
payload: GitHubLabeledPayload,
trigger_type: str,
input_from: Literal["github", "jira"],
) -> None:
"""Core functionality to create comments on issue, create PRs, and update progress."""
current_time: float = time.time()

# Extract label and validate it
if trigger_type == "label" and payload["label"]["name"] != PRODUCT_ID:
return

# Extract issue related variables
issue: IssueInfo = payload["issue"]
issue_title: str = issue["title"]
issue_number: int = issue["number"]
issue_body: str = issue["body"] or ""
issuer_name: str = issue["user"]["login"]

# Extract repository related variables
repo: RepositoryInfo = payload["repository"]
repo_name: str = repo["name"]
is_fork: bool = repo.get("fork", False)

# Extract owner related variables
owner_type: str = repo["owner"]["type"]
owner_name: str = repo["owner"]["login"]
owner_id: int = repo["owner"]["id"]

# Extract branch related variables
base_branch_name: str = repo["default_branch"]
uuid: str = str(object=uuid4())
new_branch_name: str = f"{PRODUCT_ID}{ISSUE_NUMBER_FORMAT}{issue['number']}-{uuid}"

# Extract sender related variables
sender_id: int = payload["sender"]["id"]
is_automation: bool = sender_id == GITHUB_APP_USER_ID
sender_name: str = payload["sender"]["login"]
reviewers: list[str] = list(
set(name for name in (sender_name, issuer_name) if "[bot]" not in name)
)

# Extract other information
github_urls, other_urls = extract_urls(text=issue_body)
installation_id: int = payload["installation"]["id"]
token: str = get_installation_access_token(installation_id=installation_id)
sender_email = get_user_public_email(username=sender_name, token=token)

base_args: BaseArgs = {
"owner": owner_name,
"repo": repo_name,
"is_fork": is_fork,
"base_branch": base_branch_name,
"new_branch": new_branch_name,
"token": token,
"reviewers": reviewers,
}
# Deconstruct payload based on input_from
base_args = None
if input_from == "github":
base_args = deconstruct_github_payload(payload=payload)
elif input_from == "jira":
base_args = deconstruct_jira_payload(payload=payload)

# Get some base args
installation_id = base_args["installation_id"]
owner_id = base_args["owner_id"]
owner_name = base_args["owner"]
owner_type = base_args["owner_type"]
repo_name = base_args["repo"]
issue_number = base_args["issue_number"]
issue_title = base_args["issue_title"]
issue_body = base_args["issue_body"]
issuer_name = base_args["issuer_name"]
new_branch_name = base_args["new_branch"]
sender_id = base_args["sender_id"]
sender_name = base_args["sender_name"]
sender_email = base_args["sender_email"]
github_urls = base_args["github_urls"]
token = base_args["token"]
is_automation = base_args["is_automation"]
# Check if the user has reached the request limit
requests_left, request_count, end_date = (
supabase_manager.get_how_many_requests_left_and_cycle(
user_id=sender_id,
installation_id=installation_id,
user_name=sender_name,
owner_id=owner_id,
owner_name=owner_name,
installation_id=installation_id, owner_id=owner_id, owner_name=owner_name
)
)
print(f"{requests_left=}")

# Notify the user if the request limit is reached and early return
if requests_left <= 0 and IS_PRD and owner_name not in EXCEPTION_OWNERS:
logging.info("\nRequest limit reached for user %s.", sender_name)
body = request_limit_reached(
user_name=sender_name, request_count=request_count, end_date=end_date
)
create_comment(issue_number=issue_number, body=body, base_args=base_args)
create_comment(body=body, base_args=base_args)
return

msg = "Got your request. Alright, let's get to it..."
comment_body = create_progress_bar(p=0, msg=msg)
comment_url = create_comment(
issue_number=issue_number, body=comment_body, base_args=base_args
)
comment_url: str | None = create_comment(body=comment_body, base_args=base_args)
base_args["comment_url"] = comment_url
unique_issue_id = f"{owner_type}/{owner_name}/{repo_name}#{issue_number}"
unique_issue_id = f"{owner_type}/{owner_name}/{repo_name}"
if input_from == "github":
unique_issue_id = unique_issue_id + f"#{issue_number}"
elif input_from == "jira":
unique_issue_id = unique_issue_id + f"#jira-{issue_number}"
usage_record_id = supabase_manager.create_user_request(
user_id=sender_id,
user_id=sender_id if input_from == "github" else 0,
user_name=sender_name,
installation_id=installation_id,
unique_issue_id=unique_issue_id,
email=sender_email,
)
add_reaction_to_issue(
issue_number=issue_number, content="eyes", base_args=base_args
)
if input_from == "github":
add_reaction_to_issue(
issue_number=issue_number, content="eyes", base_args=base_args
)

# Check out the issue comments, and root files/directories list
comment_body = "Checking the issue title, body, comments, and root files list..."
update_comment(body=comment_body, base_args=base_args, p=10)
root_files_and_dirs: list[str] = get_remote_file_tree(base_args=base_args)
issue_comments = get_issue_comments(issue_number=issue_number, base_args=base_args)
if input_from == "github":
issue_comments = get_issue_comments(
issue_number=issue_number, base_args=base_args
)
elif input_from == "jira":
issue_comments = base_args["issue_comments"]

# Check out the URLs in the issue body
reference_contents: list[str] = []
Expand Down Expand Up @@ -197,10 +172,13 @@ async def handle_gitauto(payload: GitHubLabeledPayload, trigger_type: str) -> No
# Create a remote branch
comment_body = "Looks like it's doable. Creating the remote branch..."
update_comment(body=comment_body, base_args=base_args, p=30)
latest_commit_sha: str = get_latest_remote_commit_sha(
clone_url=repo["clone_url"],
base_args=base_args,
)
latest_commit_sha: str = ""
if input_from == "github":
latest_commit_sha = get_latest_remote_commit_sha(
clone_url=base_args["clone_url"], base_args=base_args
)
elif input_from == "jira":
latest_commit_sha = base_args["latest_commit_sha"]
create_remote_branch(sha=latest_commit_sha, base_args=base_args)

# Loop a process explore repo and commit changes until the ticket is resolved
Expand Down
17 changes: 17 additions & 0 deletions services/github/branch_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import requests
from config import GITHUB_API_URL, TIMEOUT
from services.github.create_headers import create_headers
from utils.handle_exceptions import handle_exceptions


@handle_exceptions(default_return_value="main", raise_on_error=False)
def get_default_branch(owner: str, repo: str, token: str):
"""https://docs.github.com/en/rest/branches/branches?apiVersion=2022-11-28#list-branches"""
url = f"{GITHUB_API_URL}/repos/{owner}/{repo}/branches"
headers = create_headers(token=token)
response = requests.get(url=url, headers=headers, timeout=TIMEOUT)
response.raise_for_status()
data = response.json()
default_branch_name: str = data[0]["name"]
latest_commit_sha: str = data[0]["commit"]["sha"]
return default_branch_name, latest_commit_sha
38 changes: 21 additions & 17 deletions services/github/github_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,18 +215,24 @@ def commit_changes_to_remote_branch(
return f"diff applied to the file: {file_path} successfully by {commit_changes_to_remote_branch.__name__}()."


@handle_exceptions(raise_on_error=True)
def create_comment(issue_number: int, body: str, base_args: BaseArgs) -> str:
@handle_exceptions(default_return_value=None, raise_on_error=False)
def create_comment(body: str, base_args: BaseArgs):
"""https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#create-an-issue-comment"""
owner, repo, token = base_args["owner"], base_args["repo"], base_args["token"]
response: requests.Response = requests.post(
url=f"{GITHUB_API_URL}/repos/{owner}/{repo}/issues/{issue_number}/comments",
headers=create_headers(token=token),
json={"body": body},
timeout=TIMEOUT,
)
response.raise_for_status()
return response.json()["url"]
issue_number = base_args["issue_number"]
input_from = base_args["input_from"]
if input_from == "github":
response: requests.Response = requests.post(
url=f"{GITHUB_API_URL}/repos/{owner}/{repo}/issues/{issue_number}/comments",
headers=create_headers(token=token),
json={"body": body},
timeout=TIMEOUT,
)
response.raise_for_status()
url: str = response.json()["url"]
return url
if input_from == "jira":
return None


@handle_exceptions(default_return_value=None, raise_on_error=False)
Expand Down Expand Up @@ -257,11 +263,7 @@ def create_comment_on_issue_with_gitauto_button(payload: GitHubLabeledPayload) -

requests_left, request_count, end_date = (
supabase_manager.get_how_many_requests_left_and_cycle(
user_id=user_id,
installation_id=installation_id,
user_name=user_name,
owner_id=owner_id,
owner_name=owner_name,
installation_id=installation_id, owner_id=owner_id, owner_name=owner_name
)
)

Expand Down Expand Up @@ -373,7 +375,7 @@ def initialize_repo(repo_path: str, remote_url: str) -> None:


@handle_exceptions(default_return_value=None, raise_on_error=False)
def get_installation_access_token(installation_id: int) -> str | None:
def get_installation_access_token(installation_id: int) -> str:
"""https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app"""
jwt_token: str = create_jwt()
response: requests.Response = requests.post(
Expand Down Expand Up @@ -752,9 +754,11 @@ async def verify_webhook_signature(request: Request, secret: str) -> None:
@handle_exceptions(default_return_value=None, raise_on_error=False)
def update_comment(
body: str, base_args: BaseArgs, p: int | None = None
) -> dict[str, Any]:
):
"""https://docs.github.com/en/rest/issues/comments#update-an-issue-comment"""
comment_url, token = base_args["comment_url"], base_args["token"]
if comment_url is None:
return None
if p is not None:
body = create_progress_bar(p=p, msg=body)
print(body + "\n")
Expand Down
Loading

0 comments on commit a15967c

Please sign in to comment.