Skip to content
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
54 changes: 22 additions & 32 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
# Pull Request Description

## What
<!-- Describe what the change is doing, including behavior before and after the change -->
## What and why?
<!-- Briefly describe what changed and why — include behavior before and after if helpful -->
<!-- Frontend: Add screenshots or videos showing before/after -->

## Why
<!-- Explain why the change is being made and needed -->

## How to Test
## How to test
<!-- Describe how the change has been tested, and how a reviewer can test the change locally -->

## What needs special review?
<!-- Optional: Call out specific areas needing detailed review -->

## Dependencies, breaking changes, and deployment notes
<!-- A list of links to pull requests that are depend on or are dependencies of this PR -->
<!-- Any special deployment considerations? -->
<!-- List any breaking changes -->

## Release notes
<!--- REPLACE THIS COMMENT WITH YOUR DESCRIPTION --->

<!--
PR instructions for release notes:

Expand All @@ -21,40 +30,21 @@ PR instructions for release notes:
- `breaking-change`
- `deprecation`
- `documentation`
- `environment-variables`

2. In the next section, describe the changes so that an external user can understand them. Keep it simple and link to the docs with [Learn more ...](<relative-link>), if available.
-->

## Pull Request Dependencies

<!-- A list of links to pull requests that are depend on or are dependencies of this PR -->

## External Release Notes
<!--- REPLACE THIS COMMENT WITH YOUR DESCRIPTION --->

## Deployment Notes
<!-- Any special deployment considerations? -->

## Breaking Changes
<!-- List any breaking changes -->

## Screenshots/Videos (Frontend Only)
<!-- Add screenshots or videos showing before/after -->

## Checklist
- [ ] PR body describes what, why, and how to test
- [ ] Release notes written
- [ ] Deployment notes written
- [ ] Breaking changes identified
- [ ] What and why
- [ ] Screenshots or videos (Frontend)
- [ ] How to test
- [ ] What needs special review
- [ ] Dependencies, breaking changes, and deployment notes
- [ ] Labels applied
- [ ] PR linked to Shortcut
- [ ] Screenshots/videos added (Frontend)
- [ ] Unit tests added (Backend)
- [ ] Tested locally
- [ ] Documentation updated (if required)
- [ ] Environment variable additions/changes documented (if required)

## Areas Needing Special Review
<!-- Optional: Call out specific areas needing detailed review -->

## Additional Notes
<!-- Any other information that would be helpful to reviewers -->
52 changes: 31 additions & 21 deletions .github/scripts/release_notes_check.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
import json
import os
import re
import json

import requests
from github import Github


def get_pull_request_number(pr_url):
response = requests.get(pr_url)
response = requests.get(pr_url, timeout=10)
if response.status_code == 200:
pr_number = int(re.search(r'pull/(\d+)', response.url).group(1))
pr_number = int(re.search(r"pull/(\d+)", response.url).group(1))
return pr_number
else:
raise ValueError("Failed to fetch pull request details.")


def ci_check(pr_number, access_token):
g = Github(access_token)
# Get repository, pull request, and labels
repo = g.get_repo(os.environ['GITHUB_REPOSITORY'])
repo = g.get_repo(os.environ["GITHUB_REPOSITORY"])
pr = repo.get_pull(pr_number)
labels = [label.name for label in pr.labels]
description = pr.body

# Check for the presence of 'internal' label
if 'internal' in labels:
return True

required_labels = ['highlight', 'enhancement', 'bug', 'deprecation', 'documentation']
# Check for the presence of 'internal' or 'dependencies' label
if "internal" in labels or "dependencies" in labels:
return True

required_labels = [
"highlight",
"enhancement",
"bug",
"deprecation",
"documentation",
]

# Check if root comment is empty
if description is None or not description.strip():
Expand All @@ -39,38 +48,39 @@ def ci_check(pr_number, access_token):
# Check for the presence of at least one label
if not any(label in labels for label in required_labels):
# Check for description of external change
release_notes_pattern = r'## External Release Notes[\n\r]+(.*?)(?:\n##|\Z)'
release_notes_pattern = r"## External Release Notes[\n\r]+(.*?)(?:\n##|\Z)"
release_notes_match = re.search(release_notes_pattern, description, re.DOTALL)
if release_notes_match:
release_notes_text = release_notes_match.group(1).strip()
if release_notes_text and release_notes_text != '<!--- REPLACE THIS COMMENT WITH YOUR DESCRIPTION --->':
if release_notes_text and release_notes_text != "<!--- REPLACE THIS COMMENT WITH YOUR DESCRIPTION --->":
comment = "Pull requests must include at least one of the required labels: `internal` (no release notes required), `highlight`, `enhancement`, `bug`, `deprecation`, `documentation`."
pr.create_issue_comment(comment)
return False
# Pull request has neither a label nor a description
# Pull request has neither a label nor a description
comment = "Pull requests must include at least one of the required labels: `internal`, `highlight`, `enhancement`, `bug`, `deprecation`, `documentation`. Except for `internal`, pull requests must also include a description in the release notes section."
pr.create_issue_comment(comment)
return False

# Check for description of external change
release_notes_pattern = r'## External Release Notes[\n\r]+(.*?)(?:\n##|\Z)'
release_notes_pattern = r"## External Release Notes[\n\r]+(.*?)(?:\n##|\Z)"
release_notes_match = re.search(release_notes_pattern, description, re.DOTALL)
if release_notes_match:
release_notes_text = release_notes_match.group(1).strip()
if release_notes_text and release_notes_text != '<!--- REPLACE THIS COMMENT WITH YOUR DESCRIPTION --->':
if release_notes_text and release_notes_text != "<!--- REPLACE THIS COMMENT WITH YOUR DESCRIPTION --->":
return True

comment = "Pull requests must include a description in the release notes section."
pr.create_issue_comment(comment)
return False

if __name__ == '__main__':
event_path = os.environ['GITHUB_EVENT_PATH']
with open(event_path, 'r') as f:

if __name__ == "__main__":
event_path = os.environ["GITHUB_EVENT_PATH"]
with open(event_path) as f:
event_payload = json.load(f)
pr_number = event_payload['pull_request']['number']
access_token = os.environ['GITHUB_TOKEN']
pr_number = event_payload["pull_request"]["number"]

access_token = os.environ["GITHUB_TOKEN"]

result = ci_check(pr_number, access_token)

Expand Down
82 changes: 43 additions & 39 deletions .github/workflows/ai_explain.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import os
import json
import os
import sys

from openai import OpenAI
from github import Github
from tiktoken import encoding_for_model
from openai import APIConnectionError, OpenAI, OpenAIError


# Initialize GitHub and OpenAI clients
github_token = os.getenv("GITHUB_TOKEN")
Expand All @@ -28,13 +29,15 @@

diff = "\n\n".join(diffs)

# Add a unique marker at the start of the comment to find comments by the bot
COMMENT_MARKER = "<!-- AI-EXPLAIN-COMMENT -->"

# Fetch existing AI explanation comment
existing_explanation_comment = None
comments = sorted(pr.get_issue_comments(), key=lambda x: x.created_at, reverse=True)
existing_explanation_comments = []
comments = pr.get_issue_comments()
for comment in comments:
if comment.user.login == "github-actions[bot]":
existing_explanation_comment = comment
break
if comment.user.login == "github-actions[bot]" and COMMENT_MARKER in comment.body:
existing_explanation_comments.append(comment)

# OpenAI prompt template
prompt_template = """
Expand Down Expand Up @@ -70,52 +73,53 @@
# Prepare OpenAI prompt
prompt = prompt_template.format(diff=diff)

# check number of tokens
encoding = encoding_for_model("gpt-4o-mini")
tokens = encoding.encode(prompt)
print(f"Number of tokens: {len(tokens)}")
# 128k is max tokens for gpt-4o-mini
num_output_tokens = 1000
if len(tokens) > 128000 - num_output_tokens:
tokens = tokens[: 128000 - num_output_tokens]
prompt = encoding.decode(tokens)

# Call OpenAI API
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
response_format={"type": "json_object"},
messages=[
{
"role": "user",
"content": prompt,
}
],
temperature=0,
)
try:
# Call OpenAI API
client = OpenAI()
response = client.chat.completions.create(
model="o3-mini",
response_format={"type": "json_object"},
messages=[
{
"role": "user",
"content": prompt,
}
],
)
except APIConnectionError as e:
print(f"OpenAI API connection error: {e}")
sys.exit(0) # happy exit so that the workflow will continue
except OpenAIError as e:
print(f"OpenAI API error: {e}")
sys.exit(0) # happy exit so that the workflow will continue
except Exception as e:
print(f"Unexpected error: {e}")
sys.exit(0) # happy exit so that the workflow will continue

# Parse OpenAI response
ai_response = json.loads(response.choices[0].message.content.strip())

# Create a new comment and delete the existing explanation comment
new_comment = pr.create_issue_comment(
f"{COMMENT_MARKER}\n"
f"{ai_response['summary']}\n\n"
f"## Test Suggestions\n"
f"- " + "\n- ".join(ai_response["test_suggestions"])
if ai_response.get("test_suggestions", None)
else (
"n/a" + "\n\n"
f"## Code Quality Assessment\n"
f"- " + "\n- ".join(ai_response["code_quality_assessment"])
"n/a" + "\n\n## Code Quality Assessment\n- " + "\n- ".join(ai_response["code_quality_assessment"])
if ai_response.get("code_quality_assessment", None)
else (
"n/a" + "\n\n"
f"## Security Assessment\n"
f"- " + "\n- ".join(ai_response["security_assessment"])
"n/a" + "\n\n## Security Assessment\n- " + "\n- ".join(ai_response["security_assessment"])
if ai_response.get("security_assessment", None)
else "n/a" + "\n\n"
)
)
)
if existing_explanation_comment:
existing_explanation_comment.delete()

# Delete all previous AI explain comments
for comment in existing_explanation_comments:
try:
comment.delete()
except Exception as e:
print(f"Failed to delete comment: {e!s}")
7 changes: 3 additions & 4 deletions .github/workflows/ai_explain.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,20 @@ jobs:
uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: '3.x'
python-version: "3.11"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install openai
pip install tiktoken
pip install PyGithub

- name: Explain PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_PR_SUMMARY_KEY }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_REF: ${{ github.ref }}
run: python .github/workflows/ai_explain.py
2 changes: 1 addition & 1 deletion .github/workflows/release_notes_check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:

permissions:
contents: read
pull-requests: write
pull-requests: read

jobs:
ci_check:
Expand Down