Skip to content
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

Add generate PR description workflow #3042

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 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
70 changes: 70 additions & 0 deletions .github/workflows/generate-pr-description.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Auto PR Description

on:
pull_request:
types: [opened, edited]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
auto-describe:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I open the PR as a draft and then later change it to "Ready for Review", this code will not run again.

permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Checkout code
uses: actions/[email protected]

- name: Set up Python
uses: actions/[email protected]
with:
python-version: '3.11'

- name: Install dependencies
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.cargo/env
uv pip install --system requests openai
htahir1 marked this conversation as resolved.
Show resolved Hide resolved

- name: Check for previous successful run
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow would run only when the PR is opened. So, it will only be run once. That's why the step here does not make much sense. When it runs for the first and only time, the comment won't be there, and thus the code will execute.

id: check_comment
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
COMMENT=$(gh api -X GET "/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" | jq '.[] | select(.body | contains("Auto PR description generated successfully")) | .id')
if [ -n "$COMMENT" ]; then
echo "Workflow has already run successfully for this PR."
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Wait for potential edits
if: steps.check_comment.outputs.skip == 'false'
run: sleep 60 # Wait for 1 min

- name: Generate PR description
if: steps.check_comment.outputs.skip == 'false'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: python scripts/generate_pr_description.py

- name: Add success comment
if: steps.check_comment.outputs.skip == 'false'
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
gh issue comment ${PR_NUMBER} --body "Auto PR description generated successfully"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Check for errors
if: failure()
run: |
echo "The PR description generation failed. Please check the logs for more information."
htahir1 marked this conversation as resolved.
Show resolved Hide resolved
100 changes: 100 additions & 0 deletions scripts/generate_pr_description.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import json
import os
import requests
import openai

MAX_CHARS = 400000 # Maximum characters for changes summary

def truncate_changes(changes_summary):
"""Truncates the changes summary to fit within MAX_CHARS."""
total_chars = 0
truncated_summary = []
for change in changes_summary:
change_chars = len(change)
if total_chars + change_chars > MAX_CHARS:
remaining_chars = MAX_CHARS - total_chars
if remaining_chars > 50: # Ensure we're not adding just a few characters
truncated_change = change[:remaining_chars]
truncated_summary.append(truncated_change + "...")
break
total_chars += change_chars
truncated_summary.append(change)
return truncated_summary

def generate_pr_description():
# GitHub API setup
htahir1 marked this conversation as resolved.
Show resolved Hide resolved
token = os.environ['GITHUB_TOKEN']
repo = os.environ['GITHUB_REPOSITORY']

# Get PR number from the event payload
with open(os.environ['GITHUB_EVENT_PATH']) as f:
event = json.load(f)
pr_number = event['pull_request']['number']

headers = {'Authorization': f'token {token}'}
api_url = f'https://api.github.com/repos/{repo}/pulls/{pr_number}'

# Get current PR description
pr_info = requests.get(api_url, headers=headers).json()
current_description = pr_info['body'] or ''
htahir1 marked this conversation as resolved.
Show resolved Hide resolved

# Check if description matches the default template
default_template_indicator = "I implemented/fixed _ to achieve _."

if default_template_indicator in current_description:
# Get PR files
files_url = f'{api_url}/files'
files = requests.get(files_url, headers=headers).json()

# Process files
changes_summary = []
for file in files:
filename = file['filename']
status = file['status']

if status == 'added':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually, if there is a new file, there is some meaningful code in there. That's why it might be nice to add the patch for the new files as well.

changes_summary.append(f"Added new file: {filename}")
elif status == 'removed':
changes_summary.append(f"Removed file: {filename}")
elif status == 'modified':
if file['binary']:
changes_summary.append(f"Modified binary file: {filename}")
else:
patch = file.get('patch', '')
if patch:
changes_summary.append(f"Modified {filename}:")
changes_summary.append(patch)
elif status == 'renamed':
changes_summary.append(f"Renamed file from {file['previous_filename']} to {filename}")

# Truncate changes summary if it's too long
truncated_changes = truncate_changes(changes_summary)
changes_text = "\n".join(truncated_changes)

# Generate description using OpenAI
openai.api_key = os.environ['OPENAI_API_KEY']
response = openai.OpenAI().chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant that generates concise pull request descriptions based on changes to files."},
{"role": "user", "content": f"Generate a brief, informative pull request description based on these changes:\n\n{changes_text}"}
],
max_tokens=1000
)
htahir1 marked this conversation as resolved.
Show resolved Hide resolved

generated_description = response.choices[0].message.content.strip()

# Replace the placeholder in the template with the generated description
updated_description = current_description.replace(default_template_indicator, generated_description)

# Update PR description
data = {'body': updated_description}
requests.patch(api_url, json=data, headers=headers)
print(f"Updated PR description with generated content")
return True
else:
print("PR already has a non-default description. No action taken.")
return False

if __name__ == "__main__":
generate_pr_description()
Loading