Monitor Organization Bot PRs #27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Monitor Organization Bot PRs | |
| on: | |
| schedule: | |
| # Run every 6 hours | |
| - cron: '0 */6 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| target_repo: | |
| description: 'Specific repo to check (e.g., VectorInstitute/repo-name)' | |
| required: false | |
| pr_number: | |
| description: 'Specific PR number to process' | |
| required: false | |
| permissions: | |
| contents: read | |
| issues: write | |
| id-token: write | |
| env: | |
| ORG_NAME: VectorInstitute | |
| jobs: | |
| discover-bot-prs: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| prs: ${{ steps.find-prs.outputs.prs }} | |
| steps: | |
| - name: Checkout bot repository | |
| uses: actions/checkout@v6 | |
| - name: Find bot PRs across organization | |
| id: find-prs | |
| run: | | |
| # Search for all open bot PRs (Dependabot and pre-commit-ci) in the organization | |
| if [ -n "${{ github.event.inputs.target_repo }}" ]; then | |
| # Manual trigger - specific repo | |
| REPOS="${{ github.event.inputs.target_repo }}" | |
| else | |
| # Scheduled - read repos from CSV file | |
| if [ -f repos-to-monitor.csv ]; then | |
| REPOS=$(tail -n +2 repos-to-monitor.csv | grep -v '^$' | tr '\n' ' ') | |
| echo "Reading repos from repos-to-monitor.csv" | |
| else | |
| echo "Error: repos-to-monitor.csv not found" | |
| exit 1 | |
| fi | |
| fi | |
| # Array to store PR information | |
| PRS_JSON="[]" | |
| # Bot authors to check | |
| BOT_AUTHORS=("app/dependabot" "pre-commit-ci[bot]") | |
| for REPO in $REPOS; do | |
| echo "Checking $REPO for bot PRs..." | |
| # Get all open PRs from each bot author | |
| for BOT_AUTHOR in "${BOT_AUTHORS[@]}"; do | |
| REPO_PRS=$(gh pr list --repo "$REPO" \ | |
| --author "$BOT_AUTHOR" \ | |
| --state open \ | |
| --json number,title,url,headRefName,statusCheckRollup 2>/dev/null || echo "[]") | |
| if [ "$REPO_PRS" != "[]" ] && [ -n "$REPO_PRS" ]; then | |
| # Add repo name to each PR object in the array | |
| REPO_PRS=$(echo "$REPO_PRS" | jq -c "map(. + {repo: \"$REPO\"})") | |
| # Append to our collection | |
| PRS_JSON=$(echo "$PRS_JSON" | jq -c ". + $REPO_PRS") | |
| fi | |
| done | |
| done | |
| # Output the collected PRs | |
| echo "Found $(echo "$PRS_JSON" | jq 'length') bot PRs" | |
| echo "$PRS_JSON" | jq -c '.' | |
| # Save to output (handle GitHub Actions output size limits) | |
| echo "prs=$(echo "$PRS_JSON" | jq -c '.')" >> $GITHUB_OUTPUT | |
| env: | |
| GH_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }} | |
| process-passing-prs: | |
| runs-on: ubuntu-latest | |
| needs: discover-bot-prs | |
| if: needs.discover-bot-prs.outputs.prs != '[]' | |
| strategy: | |
| matrix: | |
| pr: ${{ fromJson(needs.discover-bot-prs.outputs.prs) }} | |
| max-parallel: 5 | |
| fail-fast: false | |
| steps: | |
| - name: Checkout bot repository | |
| uses: actions/checkout@v6 | |
| - name: Check if this is the oldest PR from this repo | |
| id: check-order | |
| run: | | |
| CURRENT_REPO="${{ matrix.pr.repo }}" | |
| CURRENT_PR="${{ matrix.pr.number }}" | |
| # Get all PRs from the matrix for the same repo | |
| ALL_PRS='${{ toJson(fromJson(needs.discover-bot-prs.outputs.prs)) }}' | |
| # Find all PRs from the same repo | |
| SAME_REPO_PRS=$(echo "$ALL_PRS" | jq -r --arg repo "$CURRENT_REPO" ' | |
| map(select(.repo == $repo)) | map(.number) | sort | .[] | |
| ') | |
| # Get the oldest (lowest number) PR | |
| OLDEST_PR=$(echo "$SAME_REPO_PRS" | head -1) | |
| # Count how many PRs from this repo | |
| PR_COUNT=$(echo "$SAME_REPO_PRS" | wc -l | tr -d ' ') | |
| echo "Repo: $CURRENT_REPO has $PR_COUNT PR(s)" | |
| echo "Current PR: #$CURRENT_PR" | |
| echo "Oldest PR: #$OLDEST_PR" | |
| echo "other_prs=$(echo "$SAME_REPO_PRS" | grep -v "^$CURRENT_PR$" | tr '\n' ',' | sed 's/,$//')" >> $GITHUB_OUTPUT | |
| if [ "$CURRENT_PR" = "$OLDEST_PR" ]; then | |
| echo "is_oldest=true" >> $GITHUB_OUTPUT | |
| echo " This is the oldest PR from $CURRENT_REPO - will process" | |
| else | |
| echo "is_oldest=false" >> $GITHUB_OUTPUT | |
| echo " Skipping - PR #$OLDEST_PR should be merged first" | |
| fi | |
| continue-on-error: true | |
| - name: Analyze PR status | |
| if: steps.check-order.outputs.is_oldest == 'true' | |
| id: analyze | |
| run: | | |
| REPO="${{ matrix.pr.repo }}" | |
| PR_NUMBER="${{ matrix.pr.number }}" | |
| echo "Analyzing $REPO#$PR_NUMBER" | |
| # Initial delay to allow GitHub to compute merge status after checks complete | |
| echo " Waiting 15 seconds for GitHub to compute merge status..." | |
| sleep 15 | |
| # Retry logic for mergeable status (GitHub sometimes needs time to compute) | |
| MAX_RETRIES=5 | |
| RETRY_DELAY=10 | |
| ATTEMPT=1 | |
| while [ $ATTEMPT -le $MAX_RETRIES ]; do | |
| echo " Attempt $ATTEMPT/$MAX_RETRIES: Checking PR status..." | |
| # Get detailed status | |
| STATUS_JSON=$(gh pr view $PR_NUMBER --repo "$REPO" --json statusCheckRollup,reviewDecision,mergeable) | |
| # Check if all checks passed | |
| ALL_PASSED=$(echo "$STATUS_JSON" | jq -r ' | |
| .statusCheckRollup | |
| | if . == null then false | |
| else map(select(.name != "Monitor Organization Bot PRs")) | |
| | all(.conclusion == "SUCCESS" or .conclusion == "NEUTRAL" or .conclusion == "SKIPPED" or .conclusion == null) | |
| end | |
| ') | |
| # Check if any checks failed | |
| HAS_FAILURES=$(echo "$STATUS_JSON" | jq -r ' | |
| .statusCheckRollup | |
| | if . == null then false | |
| else any(.conclusion == "FAILURE") | |
| end | |
| ') | |
| # Check if mergeable | |
| MERGEABLE=$(echo "$STATUS_JSON" | jq -r '.mergeable') | |
| echo "Status: all_passed=$ALL_PASSED, has_failures=$HAS_FAILURES, mergeable=$MERGEABLE" | |
| # If mergeable status is computed (not UNKNOWN), we're done | |
| if [ "$MERGEABLE" != "UNKNOWN" ]; then | |
| echo " Mergeable status determined: $MERGEABLE" | |
| break | |
| fi | |
| # If this was the last attempt, warn but continue | |
| if [ $ATTEMPT -eq $MAX_RETRIES ]; then | |
| echo " Mergeable status still UNKNOWN after $MAX_RETRIES attempts. Proceeding anyway." | |
| break | |
| fi | |
| # Wait before retry with exponential backoff | |
| WAIT_TIME=$((RETRY_DELAY * ATTEMPT)) | |
| echo " Mergeable status is UNKNOWN. Waiting ${WAIT_TIME}s before retry..." | |
| sleep $WAIT_TIME | |
| ATTEMPT=$((ATTEMPT + 1)) | |
| done | |
| # Set outputs | |
| echo "all-passed=$ALL_PASSED" >> $GITHUB_OUTPUT | |
| echo "has-failures=$HAS_FAILURES" >> $GITHUB_OUTPUT | |
| echo "mergeable=$MERGEABLE" >> $GITHUB_OUTPUT | |
| echo " Final status: all_passed=$ALL_PASSED, has_failures=$HAS_FAILURES, mergeable=$MERGEABLE" | |
| env: | |
| GH_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }} | |
| - name: Auto-merge passing PRs | |
| if: steps.check-order.outputs.is_oldest == 'true' && steps.analyze.outputs.all-passed == 'true' && steps.analyze.outputs.mergeable == 'MERGEABLE' | |
| id: auto-merge | |
| run: | | |
| REPO="${{ matrix.pr.repo }}" | |
| PR_NUMBER="${{ matrix.pr.number }}" | |
| echo " All checks passed for $REPO#$PR_NUMBER - proceeding with auto-merge" | |
| # Check if already approved | |
| REVIEW_DECISION=$(gh pr view $PR_NUMBER --repo "$REPO" --json reviewDecision --jq '.reviewDecision') | |
| if [ "$REVIEW_DECISION" != "APPROVED" ]; then | |
| # Approve the PR | |
| cat > /tmp/approve-msg.txt <<'EOF' | |
| All checks passed. Auto-approving bot PR. | |
| *AI Engineering Maintenance Bot - Maintaining Vector Institute Repositories built by AI Engineering* | |
| EOF | |
| gh pr review $PR_NUMBER --repo "$REPO" --approve --body-file /tmp/approve-msg.txt | |
| fi | |
| # Enable auto-merge | |
| gh pr merge $PR_NUMBER --repo "$REPO" --auto --squash || { | |
| echo " Auto-merge failed, may already be enabled or blocked by branch protection" | |
| } | |
| # Comment on success | |
| cat > /tmp/success-msg.txt <<'EOF' | |
| All checks passed! This PR will be automatically merged. | |
| *AI Engineering Maintenance Bot - Maintaining Vector Institute Repositories built by AI Engineering* | |
| EOF | |
| gh pr comment $PR_NUMBER --repo "$REPO" --body-file /tmp/success-msg.txt | |
| echo " Auto-merge configured for $REPO#$PR_NUMBER" | |
| env: | |
| GH_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }} | |
| continue-on-error: true | |
| - name: Attempt merge despite UNKNOWN status (fallback) | |
| if: steps.check-order.outputs.is_oldest == 'true' && steps.analyze.outputs.all-passed == 'true' && steps.analyze.outputs.mergeable == 'UNKNOWN' && steps.analyze.outputs.has-failures == 'false' | |
| run: | | |
| REPO="${{ matrix.pr.repo }}" | |
| PR_NUMBER="${{ matrix.pr.number }}" | |
| echo " Mergeable status is UNKNOWN but all checks passed for $REPO#$PR_NUMBER" | |
| echo " Attempting auto-merge anyway (will fail gracefully if conflicts exist)..." | |
| # Check if already approved | |
| REVIEW_DECISION=$(gh pr view $PR_NUMBER --repo "$REPO" --json reviewDecision --jq '.reviewDecision') | |
| if [ "$REVIEW_DECISION" != "APPROVED" ]; then | |
| # Approve the PR | |
| cat > /tmp/approve-msg.txt <<'EOF' | |
| All checks passed. Auto-approving bot PR. | |
| Note: Mergeable status was UNKNOWN, but attempting merge anyway. | |
| *AI Engineering Maintenance Bot - Maintaining Vector Institute Repositories built by AI Engineering* | |
| EOF | |
| gh pr review $PR_NUMBER --repo "$REPO" --approve --body-file /tmp/approve-msg.txt | |
| fi | |
| # Try to enable auto-merge (will fail if there are actual conflicts) | |
| if gh pr merge $PR_NUMBER --repo "$REPO" --auto --squash 2>&1; then | |
| echo " Auto-merge configured successfully despite UNKNOWN status" | |
| # Comment on success | |
| cat > /tmp/success-msg.txt <<'EOF' | |
| All checks passed! This PR will be automatically merged. | |
| Note: GitHub's mergeable status was UNKNOWN, but no conflicts were detected. | |
| *AI Engineering Maintenance Bot - Maintaining Vector Institute Repositories built by AI Engineering* | |
| EOF | |
| gh pr comment $PR_NUMBER --repo "$REPO" --body-file /tmp/success-msg.txt | |
| else | |
| echo " Auto-merge failed - there may be merge conflicts or branch protection rules" | |
| # Comment about the issue | |
| cat > /tmp/warning-msg.txt <<'EOF' | |
| All checks passed, but auto-merge could not be enabled. | |
| This might be due to: | |
| - Merge conflicts that need manual resolution | |
| - Branch protection rules requiring additional reviews | |
| - GitHub still computing mergeable status | |
| Please check the PR and merge manually if appropriate. | |
| *AI Engineering Maintenance Bot - Maintaining Vector Institute Repositories built by AI Engineering* | |
| EOF | |
| gh pr comment $PR_NUMBER --repo "$REPO" --body-file /tmp/warning-msg.txt | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }} | |
| continue-on-error: true | |
| - name: Setup Google Cloud SDK | |
| if: steps.check-order.outputs.is_oldest == 'true' && (steps.auto-merge.conclusion == 'success' || steps.auto-merge.outcome == 'success') | |
| uses: google-github-actions/setup-gcloud@v2 | |
| with: | |
| version: 'latest' | |
| - name: Authenticate to Google Cloud | |
| if: steps.check-order.outputs.is_oldest == 'true' && (steps.auto-merge.conclusion == 'success' || steps.auto-merge.outcome == 'success') | |
| uses: google-github-actions/auth@v3 | |
| with: | |
| workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} | |
| service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} | |
| - name: Record auto-merge to GCS | |
| if: steps.check-order.outputs.is_oldest == 'true' && (steps.auto-merge.conclusion == 'success' || steps.auto-merge.outcome == 'success') | |
| run: | | |
| REPO="${{ matrix.pr.repo }}" | |
| PR_NUMBER="${{ matrix.pr.number }}" | |
| echo "📝 Recording auto-merge to GCS for $REPO#$PR_NUMBER" | |
| # Generate date-based path | |
| DATE_PATH=$(date -u +"%Y/%m/%d") | |
| # Generate filename | |
| REPO_NAME=$(echo "$REPO" | sed 's/\//-/g') | |
| RECORD_FILE="$REPO_NAME-pr-$PR_NUMBER-automerge-${{ github.run_id }}.json" | |
| DEST_PATH="traces/$DATE_PATH/$RECORD_FILE" | |
| # Get PR details | |
| PR_DETAILS=$(gh pr view $PR_NUMBER --repo "$REPO" --json title,author,mergedAt,url) | |
| PR_TITLE=$(echo "$PR_DETAILS" | jq -r '.title') | |
| PR_AUTHOR=$(echo "$PR_DETAILS" | jq -r '.author.login') | |
| PR_URL=$(echo "$PR_DETAILS" | jq -r '.url') | |
| # Create auto-merge record | |
| cat > /tmp/automerge-record.json << EOF | |
| { | |
| "metadata": { | |
| "pr": { | |
| "repo": "$REPO", | |
| "number": $PR_NUMBER, | |
| "title": "$PR_TITLE", | |
| "url": "$PR_URL", | |
| "author": "$PR_AUTHOR" | |
| }, | |
| "merge_type": "auto_merge", | |
| "workflow_run_id": "${{ github.run_id }}", | |
| "workflow_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| }, | |
| "execution": { | |
| "start_time": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")", | |
| "duration_seconds": 0, | |
| "model": null | |
| }, | |
| "events": [ | |
| { | |
| "seq": 1, | |
| "timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")", | |
| "type": "INFO", | |
| "content": "All checks passed. PR auto-merged without requiring bot fixes." | |
| } | |
| ], | |
| "result": { | |
| "status": "SUCCESS", | |
| "changes_made": 0, | |
| "files_modified": [], | |
| "commit_sha": null, | |
| "commit_url": null, | |
| "merge_method": "auto_merge" | |
| } | |
| } | |
| EOF | |
| # Upload to GCS | |
| gcloud storage cp /tmp/automerge-record.json \ | |
| "gs://bot-dashboard-vectorinstitute/$DEST_PATH" \ | |
| --content-type="application/json" \ | |
| --cache-control="no-cache, no-store, must-revalidate" || { | |
| echo " Failed to upload auto-merge record to GCS" | |
| } | |
| # Update traces index | |
| if gcloud storage cp gs://bot-dashboard-vectorinstitute/data/traces_index.json /tmp/traces_index.json 2>/dev/null; then | |
| echo "Downloaded existing traces index" | |
| else | |
| echo '{"traces": [], "last_updated": ""}' > /tmp/traces_index.json | |
| echo "Created new traces index" | |
| fi | |
| # Create index entry | |
| cat > /tmp/trace-index-entry.json << EOF | |
| { | |
| "repo": "$REPO", | |
| "pr_number": $PR_NUMBER, | |
| "trace_path": "$DEST_PATH", | |
| "workflow_run_id": "${{ github.run_id }}", | |
| "timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")", | |
| "merge_type": "auto_merge" | |
| } | |
| EOF | |
| # Append to index | |
| jq --slurpfile entry /tmp/trace-index-entry.json \ | |
| '.traces += $entry | .last_updated = "'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'"' \ | |
| /tmp/traces_index.json > /tmp/traces_index_updated.json | |
| # Upload updated index | |
| gcloud storage cp /tmp/traces_index_updated.json \ | |
| gs://bot-dashboard-vectorinstitute/data/traces_index.json \ | |
| --content-type="application/json" \ | |
| --cache-control="no-cache, no-store, must-revalidate" || { | |
| echo " Failed to update traces index" | |
| } | |
| echo " Auto-merge recorded to GCS: $DEST_PATH" | |
| env: | |
| GH_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }} | |
| continue-on-error: true | |
| - name: Trigger branch updates for remaining PRs | |
| if: steps.check-order.outputs.is_oldest == 'true' && steps.check-order.outputs.other_prs != '' && (steps.auto-merge.conclusion == 'success' || steps.auto-merge.outcome == 'success') | |
| run: | | |
| REPO="${{ matrix.pr.repo }}" | |
| OTHER_PRS="${{ steps.check-order.outputs.other_prs }}" | |
| if [ -n "$OTHER_PRS" ]; then | |
| echo " Triggering branch updates for remaining PRs: $OTHER_PRS" | |
| IFS=',' read -ra PR_ARRAY <<< "$OTHER_PRS" | |
| for OTHER_PR in "${PR_ARRAY[@]}"; do | |
| echo "Requesting rebase for #$OTHER_PR..." | |
| # Comment @dependabot rebase to trigger branch update | |
| gh pr comment $OTHER_PR --repo "$REPO" --body "@dependabot rebase" || { | |
| echo " Failed to trigger rebase for #$OTHER_PR" | |
| } | |
| sleep 2 # Small delay between comments | |
| done | |
| echo " Branch update requests sent to remaining PRs" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }} | |
| continue-on-error: true | |
| - name: Trigger fix for failing PRs | |
| if: steps.analyze.outputs.has-failures == 'true' | |
| run: | | |
| REPO="${{ matrix.pr.repo }}" | |
| PR_NUMBER="${{ matrix.pr.number }}" | |
| echo "❌ Failures detected in $REPO#$PR_NUMBER - triggering fix workflow" | |
| # Trigger the fix workflow via repository dispatch | |
| gh workflow run fix-remote-pr.yml \ | |
| --repo ${{ github.repository }} \ | |
| --field target_repo="$REPO" \ | |
| --field pr_number="$PR_NUMBER" | |
| echo " Fix workflow triggered for $REPO#$PR_NUMBER" | |
| env: | |
| GH_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }} | |
| continue-on-error: true | |
| report-summary: | |
| runs-on: ubuntu-latest | |
| needs: [discover-bot-prs, process-passing-prs] | |
| if: always() | |
| steps: | |
| - name: Generate summary | |
| run: | | |
| PR_COUNT=$(echo '${{ needs.discover-bot-prs.outputs.prs }}' | jq 'length') | |
| echo "## Bot PR Monitor Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Organization**: ${{ env.ORG_NAME }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Bot PRs Found**: $PR_COUNT" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Run Time**: $(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "$PR_COUNT" -gt 0 ]; then | |
| echo "### PRs Processed" >> $GITHUB_STEP_SUMMARY | |
| echo '${{ needs.discover-bot-prs.outputs.prs }}' | \ | |
| jq -r '.[] | "- [\(.repo)#\(.number)](\(.url)) - \(.title)"' >> $GITHUB_STEP_SUMMARY | |
| fi |