Skip to content

Monitor Organization Bot PRs #27

Monitor Organization Bot PRs

Monitor Organization Bot PRs #27

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