GitHub Actions to Auto-Write PR Descriptions
Crafting a good pull request description is crucial. It’s the primary way you communicate your changes to reviewers, explain the "why" behind your code, outline a test plan, and flag potential risks. A well-written description speeds up reviews, improves code quality, and serves as valuable documentation for future reference.
Yet, let's be honest: writing detailed PR descriptions often feels like a chore. In the rush to merge, it's easy to default to a single line or even leave it blank. This isn't ideal for anyone involved in the development process.
What if you could automate this process? What if your PR description could be generated automatically, summarizing your changes, suggesting a test plan, and highlighting risks, all before you even type a single character? This isn't science fiction; it's entirely achievable using GitHub Actions and the power of large language models (LLMs).
The Core Idea: Automating with Diffs and AI
The fundamental concept is straightforward:
1. Trigger: A GitHub Action listens for pull_request events (e.g., when a PR is opened or updated).
2. Extract Diff: The action fetches the complete diff for the pull request. This diff is the source of truth for all changes.
3. Process with AI: The diff is then sent to an LLM, which is prompted to generate a structured PR description based on the code changes.
4. Update PR: The generated description is then used to update the pull request's body via the GitHub API.
This approach leverages the strengths of both GitHub Actions for automation and LLMs for intelligent content generation.
Setting Up Your GitHub Action
Let's walk through setting up a basic GitHub Action. You'll need a .github/workflows/auto-pr-description.yml file in your repository.
Here's the initial structure for an action that triggers on PR creation and updates, and simply fetches the diff:
name: Auto-Write PR Description
on:
pull_request:
types: [opened, synchronize] # Trigger on PR open and new commits
jobs:
generate_description:
runs-on: ubuntu-latest
permissions:
pull-requests: write # Grant permission to write to PRs
contents: read # Grant permission to read repository contents
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for accurate diffing
- name: Get PR diff
id: get_diff
run: |
# Get the diff between the base branch and the head branch of the PR
PR_DIFF=$(git diff ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }})
echo "PR_DIFF_RAW<<EOF" >> $GITHUB_OUTPUT
echo "$PR_DIFF" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
shell: bash
In this example:
* We use actions/checkout@v4 to get your repository's code. fetch-depth: 0 is important to ensure git diff can correctly compare against the base branch, even if the base has a long history.
* The Get PR diff step uses git diff to extract the changes. github.event.pull_request.base.sha and github.event.pull_request.head.sha provide the exact commit SHAs for the comparison.
* We store the diff in a multi-line output variable PR_DIFF_RAW using a delimiter (EOF) to handle its potentially large size and special characters.
Integrating with an LLM
Now for the interesting part: sending this diff to an LLM. For this example, we'll use a curl command to interact with the OpenAI API. You'd typically use a dedicated action or a more robust script for production, but curl demonstrates the core principle.
You'll need an OpenAI API key, which should be stored securely as a GitHub Secret (e.g., OPENAI_API_KEY).
Let's modify the previous action to include an LLM call:
# ... (previous YAML for name, on, jobs, runs-on, permissions) ...
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get PR diff
id: get_diff
run: |
PR_DIFF=$(git diff ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }})
echo "PR_DIFF_RAW<<EOF" >> $GITHUB_OUTPUT
echo "$PR_DIFF" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
shell: bash
- name: Generate PR description with LLM
id: generate_description
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PR_DIFF: ${{ steps.get_diff.outputs.PR_DIFF_RAW }}
run: |
# Prepare the prompt for the LLM
PROMPT="You are an expert software engineer. Review the following code changes (diff) and generate a concise, structured pull request description. Include a summary of the changes, a suggested test plan, and any potential risks or breaking changes. Format it as Markdown.
Diff:
\`\`\`diff
$PR_DIFF
\`\`\`
Please provide the description in the following format:
## Summary
[Your summary here]
## Test Plan
[Steps to test the changes]
## Risks & Breaking Changes
[Any risks or breaking changes]"
# Call the OpenAI API
LLM_RESPONSE=$(curl -s -X POST "https://api.openai.com/v1/chat/completions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-3.5-turbo",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "'"${PROMPT}"'"}
],
"temperature": 0.7
}')
# Extract the content from the LLM response
GENERATED_DESCRIPTION=$(echo "$LLM_RESPONSE" | jq -r '.choices[0].message.content')
# Handle cases where the LLM might not return content or has an error
if [ -z "$GENERATED_DESCRIPTION" ] || echo "$GENERATED_DESCRIPTION" | grep -q "error"; then
echo "LLM failed to generate description or returned an error."
GENERATED_DESCRIPTION="Automated description failed. Please write manually."
fi
echo "GENERATED_DESCRIPTION<<EOF" >> $GITHUB_OUTPUT
echo "$GENERATED_DESCRIPTION" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
shell: bash
In this new step:
* We pass the OPENAI_API_KEY and the PR_DIFF as environment variables.
* The PROMPT is carefully crafted to instruct the LLM on what kind of output we expect, including the desired Markdown structure. Good prompting is key to getting useful results.
* We use curl to send a POST request to the OpenAI chat/completions endpoint, specifying gpt-3.5-turbo (or gpt-4 for better quality/cost).
* jq is used to parse the JSON response and extract the content of the LLM's message.
* Basic error handling is included for when the LLM might fail or return an empty response.
Updating the PR Description
Finally, we need to take the generated description and apply it to the pull request. The gh CLI (GitHub CLI) is perfect for this, as it's pre-installed on GitHub Actions runners and handles authentication automatically when the pull-requests: write permission is granted.
Add this final step to your workflow:
```yaml
... (previous YAML for name, on, jobs, runs-on, permissions, and previous steps) ...
- name: Update PR description
env:
GENERATED_DESCRIPTION: ${{ steps.generate_description.outputs.GENERATED_DESCRIPTION }}
run: |
PR_BODY_CURRENT=$(gh pr view ${{ github.event.pull_request.number }} --json body --jq .body)
# Only update if the current PR body is empty or contains the default GitHub template
# This prevents overwriting a description a developer might have already started writing.
if [ -z "$PR_BODY_CURRENT" ]