gemini-pr-review.yml
1 name: AI Code Review 2 3 on: 4 pull_request: 5 types: [opened, synchronize, reopened] 6 7 permissions: 8 contents: write # For committing .mem file, review.json, and auto-formatting/linting 9 pull-requests: write # For creating reviews and comments 10 11 jobs: 12 gemini-code-review: 13 runs-on: ubuntu-latest 14 steps: 15 - name: PR Info 16 env: 17 PR_NUMBER: ${{ github.event.pull_request.number }} 18 REPO: ${{ github.repository }} 19 run: | 20 echo "Pull Request Number: $PR_NUMBER" 21 echo "Repository: $REPO" 22 echo "Event type: ${{ github.event.action }}" 23 echo "PR Head SHA: ${{ github.event.pull_request.head.sha }}" 24 echo "PR Base SHA: ${{ github.event.pull_request.base.sha }}" 25 26 - name: ๐ Checkout Repo 27 uses: actions/checkout@v4 28 with: 29 fetch-depth: 0 # Needed for diffing against base or last run SHA 30 31 - name: ๐ง Setup Node.js 32 uses: actions/setup-node@v4 33 with: 34 node-version: '20' 35 36 - name: Set PR SHA variables 37 id: sha 38 run: | 39 echo "head_sha=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT 40 echo "base_sha=${{ github.event.pull_request.base.sha }}" >> $GITHUB_OUTPUT 41 # CURRENT_SHA represents the state of the PR branch being reviewed 42 echo "current_sha=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT 43 44 - name: Read last run SHA from reviews/gemini-pr-review.mem 45 id: last_run 46 run: | 47 mkdir -p reviews # Ensure the reviews directory exists 48 if [ -f "reviews/gemini-pr-review.mem" ]; then 49 LAST_RUN_SHA_VALUE=$(cat reviews/gemini-pr-review.mem) 50 echo "last_run_sha=$LAST_RUN_SHA_VALUE" >> $GITHUB_OUTPUT 51 echo "Found last run SHA: $LAST_RUN_SHA_VALUE" 52 else 53 echo "last_run_sha=" >> $GITHUB_OUTPUT # Ensure it's an empty string if file not found 54 echo "No previous run SHA found (reviews/gemini-pr-review.mem does not exist)." 55 fi 56 57 - name: ๐ Set up Python and Install Dependencies 58 uses: actions/setup-python@v5 59 with: 60 python-version: '3.10' 61 - run: | 62 python -m pip install --upgrade pip 63 pip install flake8 google-generativeai PyGithub unidiff "google-ai-generativelanguage>=0.6.0" github3.py requests 64 65 - name: ๐ JS Syntax Check 66 run: | 67 echo "Running Node.js syntax validation on app/ and tests/ directories..." 68 set -e 69 errors=0 70 summary="" 71 js_files=$(find app tests -type f -name "*.js" 2>/dev/null) 72 if [ -z "$js_files" ]; then 73 echo "No JavaScript files found in app/ or tests/ to check." 74 else 75 for file in $js_files; do 76 echo "Checking $file" 77 if ! output=$(node --check "$file" 2>&1); then 78 errors=$((errors+1)) 79 summary+="$file:\n$output\n\n" 80 fi 81 done 82 fi 83 if [ "$errors" -gt 0 ]; then 84 echo -e "โ ๏ธ Syntax errors found in $errors JavaScript files:\n" 85 echo -e "$summary" 86 exit 1 87 else 88 echo "โ No syntax errors detected in JavaScript files." 89 fi 90 91 - name: ๐ Python Syntax Check 92 run: | 93 echo "Running Python validation on .github/ and other specified .py files..." 94 set -e 95 errors=0 96 summary="" 97 py_files_to_check=$(find .github -type f -name "*.py" 2>/dev/null) 98 99 if [ -z "$py_files_to_check" ]; then 100 echo "No Python files found in specified paths to check." 101 else 102 echo "Step 1: Basic syntax check with py_compile" 103 for file in $py_files_to_check; do 104 echo "Checking $file with py_compile" 105 if ! output=$(python -m py_compile "$file" 2>&1); then 106 errors=$((errors+1)) # Increment for each file with py_compile errors 107 summary+="$file (py_compile syntax error):\n$output\n\n" 108 fi 109 done 110 111 echo "\nStep 2: Checking for AST parsing issues (e.g., f-string errors)" 112 # Reset errors for this step if you want to count them separately, or use a different counter 113 ast_errors=0 114 ast_summary="" 115 for file in $py_files_to_check; do 116 echo "Checking $file with ast.parse" 117 if ! python -c "import ast; ast.parse(open('$file', encoding='utf-8').read())" 2>&1 >/dev/null; then 118 # Check if py_compile already flagged this file to avoid redundant general error messages 119 # This is a heuristic, ast.parse might catch different things. 120 if ! grep -q "$file (py_compile syntax error)" <<< "$summary"; then 121 ast_errors=$((ast_errors+1)) 122 ast_summary+="$file (AST parsing error, e.g., f-string):\nPython's AST parser failed. Check syntax, especially f-strings.\n\n" 123 fi 124 fi 125 done 126 if [ "$ast_errors" -gt 0 ]; then 127 summary+=$ast_summary 128 errors=$((errors + ast_errors)) # Add to total errors if counting distinctly 129 fi 130 131 132 echo "\nStep 3: Checking for critical errors with flake8" 133 flake8_errors=0 134 flake8_summary="" 135 for file in $py_files_to_check; do 136 echo "Running flake8 on $file" 137 output=$(flake8 --select=E9,F821 "$file" 2>&1) 138 if [ -n "$output" ]; then 139 # Check if similar errors already reported to avoid too much noise 140 if ! grep -q "$file" <<< "$summary"; then # Simple check 141 flake8_errors=$((flake8_errors+1)) 142 flake8_summary+="$file (flake8 critical errors E9/F821):\n$output\n\n" 143 fi 144 fi 145 done 146 if [ "$flake8_errors" -gt 0 ]; then 147 summary+=$flake8_summary 148 errors=$((errors + flake8_errors)) # Add to total errors 149 fi 150 fi 151 152 if [ "$errors" -gt 0 ]; then # Use the master 'errors' counter 153 echo -e "\nโ ๏ธ Issues found in Python files:\n" 154 echo -e "$summary" 155 exit 1 156 else 157 echo "\nโ No critical issues detected in Python files." 158 fi 159 160 - name: โ Verify Biome Setup 161 run: | 162 echo "Biome version:" 163 npx --no-update-notifier -y @biomejs/biome --version 164 if [ -f "biome.json" ]; then 165 echo "Found biome.json configuration. Validating..." 166 echo "// Test file for Biome config validation" > biome-test.js 167 npx --no-update-notifier -y @biomejs/biome check biome-test.js || echo "Warning: Biome config validation check encountered an issue, but continuing." 168 rm biome-test.js 169 else 170 echo "No biome.json found. Biome will use default settings." 171 fi 172 173 - name: ๐ ๐ป Biome Format and Commit 174 run: | 175 echo "Running Biome formatter..." 176 npx --no-update-notifier -y @biomejs/biome format --write app/ tests/ || true 177 178 export GIT_AUTHOR_NAME="dtub[bot]" 179 export GIT_AUTHOR_EMAIL="209926867+dtub[bot]@users.noreply.github.com" 180 export GIT_COMMITTER_NAME="dtub[bot]" 181 export GIT_COMMITTER_EMAIL="209926867+dtub[bot]@users.noreply.github.com" 182 183 git checkout -- package-lock.json 2>/dev/null || true 184 git checkout -- package.json 2>/dev/null || true 185 git add -A app/ tests/ || true 186 187 if [[ -n $(git diff --cached --name-only) ]]; then 188 echo "Formatting changes detected, committing..." 189 git commit -m "๐ ๐ป Auto-format with Biome" || echo "Commit failed (format), but continuing" 190 git push origin HEAD:${{ github.event.pull_request.head.ref }} || echo "Push failed (format), but continuing" 191 echo "Formatting changes committed and pushed." 192 else 193 echo "No changes to commit after formatting." 194 fi 195 196 - name: ๐ชฎ Biome Lint and Commit 197 run: | 198 echo "Running Biome linter with --apply --unsafe ..." 199 npx --no-update-notifier -y @biomejs/biome lint --apply --unsafe app/ tests/ || true 200 201 export GIT_AUTHOR_NAME="dtub[bot]" 202 export GIT_AUTHOR_EMAIL="209926867+dtub[bot]@users.noreply.github.com" 203 export GIT_COMMITTER_NAME="dtub[bot]" 204 export GIT_COMMITTER_EMAIL="209926867+dtub[bot]@users.noreply.github.com" 205 206 git checkout -- package-lock.json 2>/dev/null || true 207 git checkout -- package.json 2>/dev/null || true 208 git add -A app/ tests/ || true 209 210 if [[ -n $(git diff --cached --name-only) ]]; then 211 echo "Linting changes detected, committing..." 212 git commit -m "๐ชฎ Auto-lint with Biome" || echo "Commit failed (lint), but continuing" 213 git push origin HEAD:${{ github.event.pull_request.head.ref }} || echo "Push failed (lint), but continuing" 214 echo "Linting changes committed and pushed." 215 else 216 echo "No changes to commit after linting." 217 fi 218 219 - name: ๐ Checkout latest code after auto-fixes 220 uses: actions/checkout@v4 221 with: 222 ref: ${{ github.event.pull_request.head.ref }} 223 repository: ${{ github.event.pull_request.head.repo.full_name }} 224 fetch-depth: 0 225 226 - name: Run code review on final code 227 env: 228 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 229 GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} 230 GEMINI_MODEL: gemini-2.5-flash-preview-04-17 # As per your request 231 INPUT_EXCLUDE: "*.md,*.txt,*.json,*.lock,*.yml,*.yaml,*.svg,*.png,*.jpg,*.jpeg,*.gif,*.ico,*.woff,*.woff2,*.ttf,*.eot,*.otf,*.map,*.min.js,*.min.css,reviews/gemini-pr-review.mem,reviews/gemini-pr-review.json,*.test.js,*.d.ts,vendor/**,node_modules/**,dist/**,build/**" 232 LAST_RUN_SHA: ${{ steps.last_run.outputs.last_run_sha }} 233 CURRENT_SHA: ${{ steps.sha.outputs.head_sha }} 234 GITHUB_EVENT_PATH: ${{ github.event_path }} 235 GITHUB_EVENT_NAME: ${{ github.event_name }} 236 GITHUB_REPOSITORY: ${{ github.repository }} 237 GITHUB_SERVER_URL: ${{ github.server_url }} 238 GITHUB_HEAD_REF: ${{ github.head_ref }} 239 run: | 240 max_attempts=3 241 attempt=1 242 while [ $attempt -le $max_attempts ]; do 243 echo "Attempt $attempt of $max_attempts to run code review script" 244 if python .github/workflows/gemini-pr-review.py; then 245 echo "Code review script completed successfully" 246 break 247 else 248 exit_code=$? 249 echo "Code review script failed with exit code $exit_code" 250 if [ $attempt -lt $max_attempts ]; then 251 sleep_time=$((60 * attempt)) 252 echo "Waiting $sleep_time seconds before retrying..." 253 sleep $sleep_time 254 else 255 echo "All attempts for code review script failed." 256 # Allow workflow to continue to save SHA and commit review.json even if script failed 257 echo "Continuing workflow despite script failure after retries to commit artifacts." 258 fi 259 fi 260 attempt=$((attempt + 1)) 261 done 262 263 - name: ๐งช Run Tests with Coverage 264 continue-on-error: true 265 run: | 266 mkdir -p coverage 267 if [ -f "tests/index.js" ]; then 268 npx --no-update-notifier -y c8 node tests/index.js 269 [ -d "coverage" ] || echo "Warning: Coverage directory not found after running tests." 270 else 271 echo "Warning: Test entry point tests/index.js not found. Skipping tests." 272 fi 273 274 - name: Commit review artifacts (SHA mem file and review JSON) 275 run: | 276 mkdir -p reviews 277 # Save the SHA of the PR's head that was just reviewed 278 echo "Saving current head SHA for next run: ${{ steps.sha.outputs.head_sha }}" 279 echo "${{ steps.sha.outputs.head_sha }}" > reviews/gemini-pr-review.mem 280 281 export GIT_AUTHOR_NAME="dtub[bot]" 282 export GIT_AUTHOR_EMAIL="209926867+dtub[bot]@users.noreply.github.com" 283 export GIT_COMMITTER_NAME="dtub[bot]" 284 export GIT_COMMITTER_EMAIL="209926867+dtub[bot]@users.noreply.github.com" 285 286 # Add both .mem and .json files. Script generates .json in reviews/ directory. 287 git add reviews/gemini-pr-review.mem reviews/gemini-pr-review.json 2>/dev/null || true 288 289 if git diff --staged --quiet --exit-code; then 290 echo "No changes to review artifacts (gemini-pr-review.mem, gemini-pr-review.json) to commit." 291 else 292 git commit -m "๐๏ธโ๐จ๏ธ Update Gemini PR review artifacts (SHA & results)" 293 # Push to the PR branch. Retry push a few times in case of temporary conflict. 294 for i in 1 2 3; do 295 git pull --rebase origin ${{ github.event.pull_request.head.ref }} || true # Try to rebase before push 296 if git push origin HEAD:${{ github.event.pull_request.head.ref }}; then 297 echo "Push of review artifacts succeeded." 298 break 299 fi 300 if [ "$i" -eq 3 ]; then 301 echo "Push of review artifacts failed after multiple attempts." 302 else 303 echo "Push failed, retrying in 10s..." 304 sleep 10 305 fi 306 done 307 fi