/ .github / workflows / gemini-pr-review.yml
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