mirror of
				https://github.com/Frooodle/Stirling-PDF.git
				synced 2025-10-25 11:17:28 +02:00 
			
		
		
		
	feat: add automated PR title review using GitHub Actions and AI (#3784)
# Description of Changes - Added a new GitHub Actions workflow `.github/workflows/ai_pr_title_review.yml` to perform AI-powered PR title evaluations - Introduced configuration files: - `.github/config/repo_devs.json` to define trusted developers - `.github/config/system-prompt.txt` as the system prompt for the AI model - Workflow checks the PR actor against the `repo_devs.json` list and evaluates the PR title if the actor is a listed developer - Integrates GPT-4o via `actions/ai-inference` to analyze diffs and suggest improved PR titles in JSON - Posts a suggestion comment or praise, depending on AI rating, using `github-script` - Supports secure repo setup with hardened runners and tokenized GitHub App bot access --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									fe553c7173
								
							
						
					
					
						commit
						21875d7052
					
				
							
								
								
									
										12
									
								
								.github/config/repo_devs.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.github/config/repo_devs.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| { | ||||
|   "repo_devs": [ | ||||
|     "frooodle", | ||||
|     "sf298", | ||||
|     "Ludy87", | ||||
|     "LaserKaspar", | ||||
|     "sbplat", | ||||
|     "reecebrowne", | ||||
|     "DarioGii", | ||||
|     "ConnorYoh" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										13
									
								
								.github/config/system-prompt.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.github/config/system-prompt.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| You are a professional software engineer specializing in reviewing pull request titles. | ||||
| 
 | ||||
| Your job is to analyze a git diff and an existing PR title, then evaluate and improve the PR title. | ||||
| 
 | ||||
| You must: | ||||
| - Always return valid JSON | ||||
| - Only return the JSON response (no Markdown, no formatting) | ||||
| - Use one of these conventional commit types at the beginning of the title: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test | ||||
| - Use lowercase only, no emojis, no trailing period | ||||
| - Ensure the title is between 5 and 72 printable ASCII characters | ||||
| - Never let spelling or grammar errors affect the rating | ||||
| - If the PR title is rated 6 or higher and only contains spelling or grammar mistakes, correct it - do not rephrase it | ||||
| - If the PR title is rated below 6, generate a new, better title based on the diff | ||||
							
								
								
									
										228
									
								
								.github/workflows/ai_pr_title_review.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								.github/workflows/ai_pr_title_review.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,228 @@ | ||||
| name: AI - PR Title Review | ||||
| 
 | ||||
| on: | ||||
|   pull_request: | ||||
|     types: [opened, edited] | ||||
|     branches: [main] | ||||
| 
 | ||||
| permissions:  # required for secure-repo hardening | ||||
|   contents: read | ||||
| 
 | ||||
| jobs: | ||||
|   ai-title-review: | ||||
|     permissions: | ||||
|       contents: read | ||||
|       pull-requests: write | ||||
|       models: read | ||||
| 
 | ||||
|     runs-on: ubuntu-latest | ||||
| 
 | ||||
|     steps: | ||||
|       - name: Harden Runner | ||||
|         uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 | ||||
|         with: | ||||
|           egress-policy: audit | ||||
| 
 | ||||
|       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
| 
 | ||||
|       - name: Configure Git to suppress detached HEAD warning | ||||
|         run: git config --global advice.detachedHead false | ||||
| 
 | ||||
|       - name: Setup GitHub App Bot | ||||
|         if: github.actor != 'dependabot[bot]' | ||||
|         id: setup-bot | ||||
|         uses: ./.github/actions/setup-bot | ||||
|         continue-on-error: true | ||||
|         with: | ||||
|           app-id: ${{ secrets.GH_APP_ID }} | ||||
|           private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} | ||||
| 
 | ||||
|       - name: Check if actor is repo developer | ||||
|         id: actor | ||||
|         run: | | ||||
|           if [[ "${{ github.actor }}" == *"[bot]" ]]; then | ||||
|             echo "PR opened by a bot – skipping AI title review." | ||||
|             echo "is_repo_dev=false" >> $GITHUB_OUTPUT | ||||
|             exit 0 | ||||
|           fi | ||||
|           if [ ! -f .github/config/repo_devs.json ]; then | ||||
|             echo "Error: .github/config/repo_devs.json not found" >&2 | ||||
|             exit 1 | ||||
|           fi | ||||
|           # Validate JSON and extract repo_devs | ||||
|           REPO_DEVS=$(jq -r '.repo_devs[]' .github/config/repo_devs.json 2>/dev/null || { echo "Error: Invalid JSON in repo_devs.json" >&2; exit 1; }) | ||||
|           # Convert developer list into Bash array | ||||
|           mapfile -t DEVS_ARRAY <<< "$REPO_DEVS" | ||||
|           if [[ " ${DEVS_ARRAY[*]} " == *" ${{ github.actor }} "* ]]; then | ||||
|             echo "is_repo_dev=true" >> $GITHUB_OUTPUT | ||||
|           else | ||||
|             echo "is_repo_dev=false" >> $GITHUB_OUTPUT | ||||
|           fi | ||||
| 
 | ||||
|       - name: Get PR diff | ||||
|         if: steps.actor.outputs.is_repo_dev == 'true' | ||||
|         id: get_diff | ||||
|         run: | | ||||
|           git fetch origin ${{ github.base_ref }} | ||||
|           git diff origin/${{ github.base_ref }}...HEAD | head -n 10000 | grep -vP '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x{202E}\x{200B}]' > pr.diff | ||||
|           echo "diff<<EOF" >> $GITHUB_OUTPUT | ||||
|           cat pr.diff >> $GITHUB_OUTPUT | ||||
|           echo "EOF" >> $GITHUB_OUTPUT | ||||
| 
 | ||||
|       - name: Check and sanitize PR title | ||||
|         if: steps.actor.outputs.is_repo_dev == 'true' | ||||
|         id: sanitize_pr_title | ||||
|         env: | ||||
|           PR_TITLE_RAW: ${{ github.event.pull_request.title }} | ||||
|         run: | | ||||
|           # Sanitize PR title: max 72 characters, only printable characters | ||||
|           PR_TITLE=$(echo "$PR_TITLE_RAW" | tr -d '\n\r' | head -c 72 | sed 's/[^[:print:]]//g') | ||||
|           if [[ ${#PR_TITLE} -lt 5 ]]; then | ||||
|             echo "PR title is too short. Must be at least 5 characters." >&2 | ||||
|           fi | ||||
|           echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT | ||||
| 
 | ||||
|       - name: AI PR Title Analysis | ||||
|         if: steps.actor.outputs.is_repo_dev == 'true' | ||||
|         id: ai-title-analysis | ||||
|         uses: actions/ai-inference@d645f067d89ee1d5d736a5990e327e504d1c5a4a # v1.1.0 | ||||
|         with: | ||||
|           model: openai/gpt-4o | ||||
|           system-prompt-file: ".github/config/system-prompt.txt" | ||||
|           prompt: | | ||||
|             Based on the following input data: | ||||
| 
 | ||||
|             { | ||||
|               "diff": "${{ steps.get_diff.outputs.diff }}", | ||||
|               "pr_title": "${{ steps.sanitize_pr_title.outputs.pr_title }}" | ||||
|             } | ||||
| 
 | ||||
|             Respond ONLY with valid JSON in the format: | ||||
|             { | ||||
|               "improved_rating": <0-10>, | ||||
|               "improved_ai_title_rating": <0-10>, | ||||
|               "improved_title": "<ai generated title>" | ||||
|             } | ||||
| 
 | ||||
|       - name: Validate and set SCRIPT_OUTPUT | ||||
|         if: steps.actor.outputs.is_repo_dev == 'true' | ||||
|         run: | | ||||
|           cat <<EOF > ai_response.json | ||||
|           ${{ steps.ai-title-analysis.outputs.response }} | ||||
|           EOF | ||||
| 
 | ||||
|           # Validate JSON structure | ||||
|           jq -e ' | ||||
|             (keys | sort) == ["improved_ai_title_rating", "improved_rating", "improved_title"] and | ||||
|             (.improved_rating | type == "number" and . >= 0 and . <= 10) and | ||||
|             (.improved_ai_title_rating | type == "number" and . >= 0 and . <= 10) and | ||||
|             (.improved_title | type == "string") | ||||
|           ' ai_response.json | ||||
|           if [ $? -ne 0 ]; then | ||||
|             echo "Invalid AI response format" >&2 | ||||
|             cat ai_response.json >&2 | ||||
|             exit 1 | ||||
|           fi | ||||
|           # Parse JSON fields | ||||
|           IMPROVED_RATING=$(jq -r '.improved_rating' ai_response.json) | ||||
|           IMPROVED_TITLE=$(jq -r '.improved_title' ai_response.json) | ||||
|           # Limit comment length to 1000 characters | ||||
|           COMMENT=$(cat <<EOF | ||||
|           ## 🤖 AI PR Title Suggestion | ||||
| 
 | ||||
|           **PR-Title Rating**: $IMPROVED_RATING/10 | ||||
| 
 | ||||
|           ### ⬇️ Suggested Title (copy & paste): | ||||
| 
 | ||||
|           \`\`\` | ||||
|           $IMPROVED_TITLE | ||||
|           \`\`\` | ||||
| 
 | ||||
|           --- | ||||
|           *Generated by GitHub Models AI* | ||||
|           EOF | ||||
|           ) | ||||
|           echo "$COMMENT" > /tmp/ai-title-comment.md | ||||
|           # Log input and output to the GitHub Step Summary | ||||
|           echo "### 🤖 AI PR Title Analysis" >> $GITHUB_STEP_SUMMARY | ||||
|           echo "### Input PR Title" >> $GITHUB_STEP_SUMMARY | ||||
|           echo '```bash' >> $GITHUB_STEP_SUMMARY | ||||
|           echo "${{ steps.sanitize_pr_title.outputs.pr_title }}" >> $GITHUB_STEP_SUMMARY | ||||
|           echo '```' >> $GITHUB_STEP_SUMMARY | ||||
|           echo '### AI Response (raw JSON)' >> $GITHUB_STEP_SUMMARY | ||||
|           echo '```json' >> $GITHUB_STEP_SUMMARY | ||||
|           cat ai_response.json >> $GITHUB_STEP_SUMMARY | ||||
|           echo '```' >> $GITHUB_STEP_SUMMARY | ||||
| 
 | ||||
|       - name: Post comment on PR if needed | ||||
|         if: steps.actor.outputs.is_repo_dev == 'true' | ||||
|         uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | ||||
|         continue-on-error: true | ||||
|         with: | ||||
|           github-token: ${{ steps.setup-bot.outputs.token }} | ||||
|           script: | | ||||
|             const fs = require('fs'); | ||||
|             const body = fs.readFileSync('/tmp/ai-title-comment.md', 'utf8'); | ||||
|             const { GITHUB_REPOSITORY } = process.env; | ||||
|             const [owner, repo] = GITHUB_REPOSITORY.split('/'); | ||||
|             const issue_number = context.issue.number; | ||||
| 
 | ||||
|             const ratingMatch = body.match(/\*\*PR-Title Rating\*\*: (\d+)\/10/); | ||||
|             const rating = ratingMatch ? parseInt(ratingMatch[1], 10) : null; | ||||
| 
 | ||||
|             const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]"; | ||||
|             const comments = await github.rest.issues.listComments({ owner, repo, issue_number }); | ||||
| 
 | ||||
|             const existing = comments.data.find(c => | ||||
|               c.user?.login === expectedActor && | ||||
|               c.body.includes("## 🤖 AI PR Title Suggestion") | ||||
|             ); | ||||
| 
 | ||||
|             if (rating === null) { | ||||
|               console.log("No rating found in AI response – skipping."); | ||||
|               return; | ||||
|             } | ||||
| 
 | ||||
|             if (rating <= 5) { | ||||
|               if (existing) { | ||||
|                 await github.rest.issues.updateComment({ | ||||
|                   owner, repo, | ||||
|                   comment_id: existing.id, | ||||
|                   body | ||||
|                 }); | ||||
|                 console.log("Updated existing suggestion comment."); | ||||
|               } else { | ||||
|                 await github.rest.issues.createComment({ | ||||
|                   owner, repo, issue_number, | ||||
|                   body | ||||
|                 }); | ||||
|                 console.log("Created new suggestion comment."); | ||||
|               } | ||||
|             } else { | ||||
|               const praise = `## 🤖 AI PR Title Suggestion\n\nGreat job! The current PR title is clear and well-structured.\n\n✅ No suggestions needed.\n\n---\n*Generated by GitHub Models AI*`; | ||||
| 
 | ||||
|               if (existing) { | ||||
|                 await github.rest.issues.updateComment({ | ||||
|                   owner, repo, | ||||
|                   comment_id: existing.id, | ||||
|                   body: praise | ||||
|                 }); | ||||
|                 console.log("Replaced suggestion with praise."); | ||||
|               } else { | ||||
|                 console.log("Rating > 5 and no existing comment – skipping comment."); | ||||
|               } | ||||
|             } | ||||
| 
 | ||||
|       - name: is not repo dev | ||||
|         if: steps.actor.outputs.is_repo_dev != 'true' | ||||
|         run: | | ||||
|           exit 0 # Skip the AI title review for non-repo developers | ||||
| 
 | ||||
|       - name: Clean up | ||||
|         if: always() | ||||
|         run: | | ||||
|           rm -f pr.diff ai_response.json /tmp/ai-title-comment.md | ||||
|           echo "Cleaned up temporary files." | ||||
|         continue-on-error: true # Ensure cleanup runs even if previous steps fail | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user