diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f298bfe24..c24b67bf5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,14 +1,23 @@ # Pull Request Description -## What - +## What and why? + + -## Why - - -## How to Test +## How to test +## What needs special review? + + +## Dependencies, breaking changes, and deployment notes + + + + +## Release notes + + -## Pull Request Dependencies - - - -## External Release Notes - - -## Deployment Notes - - -## Breaking Changes - - -## Screenshots/Videos (Frontend Only) - - ## 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 - - -## Additional Notes - diff --git a/.github/scripts/release_notes_check.py b/.github/scripts/release_notes_check.py index 62b91c0c3..6473dbe11 100644 --- a/.github/scripts/release_notes_check.py +++ b/.github/scripts/release_notes_check.py @@ -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(): @@ -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 != '': + if release_notes_text and release_notes_text != "": 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 != '': + if release_notes_text and release_notes_text != "": 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) diff --git a/.github/workflows/ai_explain.py b/.github/workflows/ai_explain.py index 2e6150c7a..4aa79fa98 100644 --- a/.github/workflows/ai_explain.py +++ b/.github/workflows/ai_explain.py @@ -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") @@ -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 = "" + # 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 = """ @@ -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}") diff --git a/.github/workflows/ai_explain.yaml b/.github/workflows/ai_explain.yaml index 80209abec..587ff35ac 100644 --- a/.github/workflows/ai_explain.yaml +++ b/.github/workflows/ai_explain.yaml @@ -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 diff --git a/.github/workflows/release_notes_check.yaml b/.github/workflows/release_notes_check.yaml index 38bb167e6..792697140 100644 --- a/.github/workflows/release_notes_check.yaml +++ b/.github/workflows/release_notes_check.yaml @@ -6,7 +6,7 @@ on: permissions: contents: read - pull-requests: write + pull-requests: read jobs: ci_check: