Skip to content

Commit 373c10b

Browse files
feat: add auto-pr-description reusable action (#106)
- Create reusable GitHub Action for AI-powered PR description generation - Support for Google Gemini API integration - JIRA ticket integration with configurable URL prefix - Image preservation in existing PR descriptions - Comprehensive documentation and usage examples - Structured output format with Description, Changes, and Verification sections This action can be used across multiple repositories to automatically generate meaningful pull request descriptions based on git diff analysis.
1 parent 854f54c commit 373c10b

4 files changed

Lines changed: 401 additions & 0 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Auto PR Description Generator
2+
3+
A reusable GitHub Action that automatically generates pull request descriptions using AI (Google Gemini) based on the git diff of changes.
4+
5+
## Features
6+
7+
- 🤖 **AI-Powered**: Uses Google Gemini to analyze code changes and generate meaningful descriptions
8+
- 🎯 **Smart Formatting**: Generates structured descriptions with Description, Changes, and Verification sections
9+
- 🖼️ **Image Preservation**: Maintains existing images at the top of PR descriptions
10+
- 🎫 **JIRA Integration**: Automatically extracts JIRA ticket IDs and adds ticket links
11+
-**Fast & Lightweight**: Minimal dependencies and quick execution
12+
13+
## Usage
14+
15+
### Basic Usage
16+
17+
```yaml
18+
- name: Generate PR Description
19+
uses: ./.github/actions/auto-pr-description
20+
with:
21+
gemini-api-key: ${{ secrets.GEMINI_API_KEY }}
22+
github-token: ${{ secrets.GITHUB_TOKEN }}
23+
pr-number: ${{ github.event.pull_request.number }}
24+
base-sha: ${{ github.event.pull_request.base.sha }}
25+
head-sha: ${{ github.event.pull_request.head.sha }}
26+
```
27+
28+
### Complete Workflow Example
29+
30+
```yaml
31+
name: Auto PR Description
32+
on:
33+
pull_request:
34+
types: [labeled]
35+
36+
jobs:
37+
update-pr-description:
38+
name: Update PR Description
39+
runs-on: ubuntu-latest
40+
if: |
41+
github.event_name == 'pull_request' &&
42+
github.base_ref == 'main' &&
43+
(github.event.pull_request.draft == false || github.event.action == 'labeled') &&
44+
(contains(github.event.pull_request.labels.*.name, 'auto-pr-description') ||
45+
contains(github.event.pull_request.labels.*.name, 'test'))
46+
permissions:
47+
pull-requests: write
48+
contents: read
49+
steps:
50+
- uses: actions/checkout@v4
51+
with:
52+
fetch-depth: 0
53+
54+
- name: Generate PR Description
55+
uses: ./.github/actions/auto-pr-description
56+
with:
57+
gemini-api-key: ${{ secrets.GEMINI_API_KEY }}
58+
github-token: ${{ secrets.GITHUB_TOKEN }}
59+
pr-number: ${{ github.event.pull_request.number }}
60+
base-sha: ${{ github.event.pull_request.base.sha }}
61+
head-sha: ${{ github.event.pull_request.head.sha }}
62+
jira-ticket-url-prefix: 'https://yourcompany.atlassian.net/browse/'
63+
```
64+
65+
## Inputs
66+
67+
| Input | Description | Required | Default |
68+
|-------|-------------|----------|---------|
69+
| `gemini-api-key` | The API key for the Gemini API | ✅ | - |
70+
| `github-token` | GitHub token for PR operations | ✅ | - |
71+
| `pr-number` | Pull request number | ✅ | - |
72+
| `base-sha` | Base commit SHA for diff comparison | ✅ | - |
73+
| `head-sha` | Head commit SHA for diff comparison | ✅ | - |
74+
| `jira-ticket-url-prefix` | JIRA ticket URL prefix | ❌ | `https://virdocs.atlassian.net/browse/` |
75+
76+
## Outputs
77+
78+
| Output | Description |
79+
|--------|-------------|
80+
| `description` | The generated PR description |
81+
| `updated` | Whether the PR description was updated |
82+
83+
## Generated Description Format
84+
85+
The action generates PR descriptions in this structured format:
86+
87+
```markdown
88+
## Description
89+
A concise summary of what the changes accomplish.
90+
91+
## Changes
92+
- [ ] Specific change or feature added
93+
- [ ] Another modification made
94+
- [ ] Bug fix or improvement
95+
96+
## Verification
97+
- [ ] Test that should be performed
98+
- [ ] Verification step to confirm functionality
99+
- [ ] Additional checks recommended
100+
101+
## Ticket
102+
https://yourcompany.atlassian.net/browse/TICKET-123
103+
```
104+
105+
## JIRA Integration
106+
107+
The action automatically detects JIRA ticket IDs from:
108+
1. **PR Title**: Extracts patterns like `CORE-1234`, `PAR-567`, etc.
109+
2. **Branch Name**: Falls back to branch name if not found in title
110+
111+
Example branch names that work:
112+
- `CORE-1234-feature-description`
113+
- `PAR-567-bug-fix`
114+
- `feature/CORE-1234-new-feature`
115+
116+
## Prerequisites
117+
118+
### Required Secrets
119+
120+
1. **GEMINI_API_KEY**: Get your API key from [Google AI Studio](https://makersuite.google.com/app/apikey)
121+
2. **GITHUB_TOKEN**: Automatically provided by GitHub Actions
122+
123+
### Required Permissions
124+
125+
The workflow must have these permissions:
126+
```yaml
127+
permissions:
128+
pull-requests: write
129+
contents: read
130+
```
131+
132+
## Trigger Patterns
133+
134+
### Label-Based Triggering
135+
Add these labels to trigger the action:
136+
- `auto-pr-description`: Specific label for PR description generation
137+
- `test`: Dual-purpose label that can trigger both testing and description generation
138+
139+
### Draft Mode Handling
140+
- **Draft PRs**: Action doesn't run automatically to save CI resources
141+
- **Label Override**: Adding trigger labels to draft PRs will run the action
142+
- **Ready for Review**: Converting draft to ready automatically triggers the action
143+
144+
## Error Handling
145+
146+
The action handles various error scenarios:
147+
- Missing or invalid Gemini API key
148+
- API rate limits and timeouts
149+
- Large diffs that exceed API limits
150+
- Network connectivity issues
151+
- Invalid PR numbers or SHAs
152+
153+
## Customization
154+
155+
### Custom JIRA URL
156+
```yaml
157+
- uses: ./.github/actions/auto-pr-description
158+
with:
159+
jira-ticket-url-prefix: 'https://mycompany.atlassian.net/browse/'
160+
# ... other inputs
161+
```
162+
163+
### Using Outputs
164+
```yaml
165+
- name: Generate PR Description
166+
id: pr-desc
167+
uses: ./.github/actions/auto-pr-description
168+
with:
169+
# ... inputs
170+
171+
- name: Use generated description
172+
run: |
173+
echo "Generated description: ${{ steps.pr-desc.outputs.description }}"
174+
echo "Was updated: ${{ steps.pr-desc.outputs.updated }}"
175+
```
176+
177+
## Troubleshooting
178+
179+
### Common Issues
180+
181+
1. **Missing API Key**: Ensure `GEMINI_API_KEY` is set in repository secrets
182+
2. **Permission Denied**: Check that workflow has `pull-requests: write` permission
183+
3. **Large Diffs**: Very large changes might exceed API limits - consider smaller PRs
184+
4. **Rate Limits**: Gemini API has rate limits - add delays between calls if needed
185+
186+
### Debug Mode
187+
188+
Enable debug logging by setting:
189+
```yaml
190+
env:
191+
ACTIONS_STEP_DEBUG: true
192+
```
193+
194+
## License
195+
196+
MIT License - see LICENSE file for details.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: Auto PR Description Generator
2+
description: Automatically generate pull request descriptions using AI based on git diff
3+
inputs:
4+
gemini-api-key:
5+
description: 'The API key for the Gemini API'
6+
required: true
7+
github-token:
8+
description: 'GitHub token for PR operations'
9+
required: true
10+
pr-number:
11+
description: 'Pull request number'
12+
required: true
13+
base-sha:
14+
description: 'Base commit SHA for diff comparison'
15+
required: true
16+
head-sha:
17+
description: 'Head commit SHA for diff comparison'
18+
required: true
19+
jira-ticket-url-prefix:
20+
description: 'JIRA ticket URL prefix (e.g., https://company.atlassian.net/browse/)'
21+
required: false
22+
default: 'https://virdocs.atlassian.net/browse/'
23+
outputs:
24+
description:
25+
description: 'The generated PR description'
26+
value: ${{ steps.generate_description.outputs.description }}
27+
updated:
28+
description: 'Whether the PR description was updated'
29+
value: ${{ steps.update_pr.outputs.updated }}
30+
31+
runs:
32+
using: 'composite'
33+
steps:
34+
- name: Setup Node.js
35+
uses: actions/setup-node@v4
36+
with:
37+
node-version: '22.x'
38+
39+
- name: Install dependencies
40+
shell: bash
41+
run: |
42+
npm install
43+
working-directory: ${{ github.action_path }}
44+
45+
- name: Generate git diff
46+
shell: bash
47+
run: |
48+
git fetch origin ${{ inputs.base-sha }} --depth=1
49+
git diff ${{ inputs.base-sha }}...${{ inputs.head-sha }} > pr.diff
50+
echo "Generated diff file with $(wc -l < pr.diff) lines"
51+
52+
- name: Generate PR description
53+
id: generate_description
54+
shell: bash
55+
env:
56+
GEMINI_API_KEY: ${{ inputs.gemini-api-key }}
57+
GH_TOKEN: ${{ inputs.github-token }}
58+
PR_NUMBER: ${{ inputs.pr-number }}
59+
JIRA_TICKET_URL_PREFIX: ${{ inputs.jira-ticket-url-prefix }}
60+
run: |
61+
# Generate description using AI
62+
DESCRIPTION=$(node ${{ github.action_path }}/generate_pr_description.js pr.diff)
63+
64+
# Get existing PR body to check for images
65+
FIRST_LINE=$(gh pr view ${{ inputs.pr-number }} --json body --jq '.body' | head -n 1)
66+
67+
# Preserve images if they exist at the beginning
68+
if echo "$FIRST_LINE" | grep -qE '^(<img[^>]*>[[:space:]]*|!\[[^]]*\]\([^)]*\))$'; then
69+
printf '%s\n\n%s\n' "$FIRST_LINE" "$DESCRIPTION" > pr_body.md
70+
else
71+
printf '%s\n' "$DESCRIPTION" > pr_body.md
72+
fi
73+
74+
# Add JIRA ticket link if found
75+
PR_TITLE=$(gh pr view ${{ inputs.pr-number }} --json title --jq '.title')
76+
TICKET_ID=$(echo "$PR_TITLE" | grep -oE '[A-Z]+-[0-9]+' || true)
77+
if [ -z "$TICKET_ID" ]; then
78+
TICKET_ID=$(echo "$GITHUB_HEAD_REF" | grep -oE '[A-Z]+-[0-9]+' || true)
79+
fi
80+
if [ -n "$TICKET_ID" ]; then
81+
TICKET_URL="${{ inputs.jira-ticket-url-prefix }}${TICKET_ID}"
82+
printf '\n## Ticket\n%s\n' "$TICKET_URL" >> pr_body.md
83+
fi
84+
85+
# Output the description for other steps to use
86+
echo "description<<EOF" >> $GITHUB_OUTPUT
87+
cat pr_body.md >> $GITHUB_OUTPUT
88+
echo "EOF" >> $GITHUB_OUTPUT
89+
90+
- name: Update PR description
91+
id: update_pr
92+
shell: bash
93+
env:
94+
GH_TOKEN: ${{ inputs.github-token }}
95+
PR_NUMBER: ${{ inputs.pr-number }}
96+
run: |
97+
gh pr edit ${{ inputs.pr-number }} --body-file pr_body.md
98+
echo "updated=true" >> $GITHUB_OUTPUT
99+
echo "Successfully updated PR #${{ inputs.pr-number }} description"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
6+
(async () => {
7+
const [, , diffFile] = process.argv;
8+
if (!diffFile) {
9+
console.error('Usage: generate_pr_description.js <diff_file>');
10+
process.exit(1);
11+
}
12+
13+
if (!fs.existsSync(diffFile)) {
14+
console.error(`Error: Diff file not found at ${diffFile}`);
15+
process.exit(1);
16+
}
17+
18+
const apiKey = process.env.GEMINI_API_KEY;
19+
if (!apiKey) {
20+
console.error('Error: GEMINI_API_KEY environment variable is required');
21+
process.exit(1);
22+
}
23+
24+
// Create prompt for PR description generation
25+
const promptTemplate = `You are an expert programmer who is tasked with writing a pull request description.
26+
You will be given the git diff of the changes and you must write a markdown pull request description.
27+
Do not include the git diff in the pull request description. Do not include any other text in the pull request description.
28+
The pull request description should follow the following format:
29+
30+
## Description
31+
A short description of the changes.
32+
33+
## Changes
34+
- [ ] Change 1
35+
- [ ] Change 2
36+
37+
## Verification
38+
- [ ] Verification step 1
39+
- [ ] Verification step 2
40+
`;
41+
42+
const diffContent = fs.readFileSync(diffFile, 'utf8');
43+
const combinedPrompt = `${promptTemplate}\n\nHere is the git diff:\n\n${diffContent}`;
44+
45+
try {
46+
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`, {
47+
method: 'POST',
48+
headers: { 'Content-Type': 'application/json' },
49+
body: JSON.stringify({
50+
contents: [{
51+
parts: [{
52+
text: combinedPrompt
53+
}]
54+
}],
55+
generationConfig: {
56+
temperature: 0.7,
57+
topK: 40,
58+
topP: 0.95,
59+
maxOutputTokens: 2048,
60+
}
61+
})
62+
});
63+
64+
if (!response.ok) {
65+
const errorText = await response.text();
66+
console.error(`Error: Gemini API request failed with status ${response.status}`);
67+
console.error(`Response: ${errorText}`);
68+
process.exit(1);
69+
}
70+
71+
const json = await response.json();
72+
73+
if (!json.candidates || !json.candidates[0] || !json.candidates[0].content) {
74+
console.error('Error: Invalid response from Gemini API');
75+
console.error(JSON.stringify(json, null, 2));
76+
process.exit(1);
77+
}
78+
79+
const result = json.candidates[0].content.parts[0].text;
80+
process.stdout.write(result);
81+
} catch (error) {
82+
console.error(`Error: Failed to generate pull request description: ${error.message}`);
83+
process.exit(1);
84+
}
85+
})();

0 commit comments

Comments
 (0)