Skip to content

Dependabot PR Helper #454

Dependabot PR Helper

Dependabot PR Helper #454

# Dependabot PR Helper - Auto-approve and auto-merge low-risk updates
#
# SECURITY NOTES:
# - Uses DEPENDABOT_PAT (fine-grained PAT scoped to this repo only)
# - Alternative: GitHub App tokens provide short-lived credentials but add complexity
# - Double-verifies PR author is dependabot[bot] to prevent spoofing
#
# COOLING PERIOD:
# - Uses PR creation time (created_at), not update time
# - Rebases by Dependabot don't reset the 48-hour timer (intentional)
# - This tracks how long the update has been available in the ecosystem
#
# GROUPED DEPENDENCIES:
# - dependabot/fetch-metadata returns "highest risk" values for groups
# - update-type: highest semver (major > minor > patch)
# - dependency-type: production takes precedence over development
# - This makes groups conservative: if ANY dep is risky, whole group needs review
# - Example: (prod patch + dev minor) → minor + production → manual review
#
# ALTERNATIVE: Consider Renovate Bot for built-in stability days and grouping
# https://docs.renovatebot.com/
name: Dependabot PR Helper
on:
pull_request_target:
types: [opened, synchronize]
# Re-check PRs twice daily to process those past cooling period
schedule:
- cron: '0 9,21 * * *' # 9 AM and 9 PM UTC
# Allow manual trigger
workflow_dispatch:
permissions:
contents: write
pull-requests: write
issues: write
jobs:
# Job for PR events (single PR)
dependabot:
runs-on: ubuntu-latest
# Only run on Dependabot PRs (double-check both actor and PR author)
if: github.actor == 'dependabot[bot]' && github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v3
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
- name: Check PR age for cooling period
id: age-check
run: |
CREATED_AT="${{ github.event.pull_request.created_at }}"
CREATED_TIMESTAMP=$(date -d "$CREATED_AT" +%s)
CURRENT_TIMESTAMP=$(date +%s)
AGE_HOURS=$(( (CURRENT_TIMESTAMP - CREATED_TIMESTAMP) / 3600 ))
echo "PR created at: $CREATED_AT"
echo "PR age: $AGE_HOURS hours"
if [ $AGE_HOURS -ge 48 ]; then
echo "passed_cooling=true" >> $GITHUB_OUTPUT
echo "✅ PR has passed 48-hour cooling period"
else
REMAINING=$(( 48 - AGE_HOURS ))
echo "passed_cooling=false" >> $GITHUB_OUTPUT
echo "⏳ PR needs $REMAINING more hours before auto-merge"
fi
- name: Add labels to Dependabot PRs
run: |
# Add dependency label
gh pr edit "$PR_URL" --add-label "dependencies"
# Add update type label
case "${{ steps.metadata.outputs.update-type }}" in
"version-update:semver-patch")
gh pr edit "$PR_URL" --add-label "patch-update"
;;
"version-update:semver-minor")
gh pr edit "$PR_URL" --add-label "minor-update"
;;
"version-update:semver-major")
gh pr edit "$PR_URL" --add-label "major-update"
;;
esac
# Add dependency type label (dev vs production)
case "${{ steps.metadata.outputs.dependency-type }}" in
"direct:development")
gh pr edit "$PR_URL" --add-label "dev-dependency"
;;
"direct:production")
gh pr edit "$PR_URL" --add-label "prod-dependency"
;;
esac
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Comment with update details - Patch
if: steps.metadata.outputs.update-type == 'version-update:semver-patch'
run: |
gh pr comment "$PR_URL" --body "🔧 **Patch Update**
This is a patch-level dependency update that typically includes:
- Bug fixes
- Security patches
- Performance improvements
**Dependencies updated:** ${{ steps.metadata.outputs.dependency-names }}
✅ Generally safe to merge after CI passes."
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Comment with update details - Minor
if: steps.metadata.outputs.update-type == 'version-update:semver-minor'
run: |
gh pr comment "$PR_URL" --body "⬆️ **Minor Update**
This is a minor-level dependency update that typically includes:
- New features (backward compatible)
- Enhancements
- Non-breaking API additions
**Dependencies updated:** ${{ steps.metadata.outputs.dependency-names }}
⚠️ Review changes and test functionality before merging."
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Comment with update details - Major
if: steps.metadata.outputs.update-type == 'version-update:semver-major'
run: |
gh pr comment "$PR_URL" --body "🚨 **Major Update - Manual Review Required**
This is a major-level dependency update that may include:
- Breaking changes
- API modifications
- Removed or changed functionality
**Dependencies updated:** ${{ steps.metadata.outputs.dependency-names }}
❗ **Action Required:**
1. Review the changelog/release notes
2. Test thoroughly for breaking changes
3. Update code if necessary
4. Manual approval and merge required"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Comment on security updates
if: steps.metadata.outputs.update-type == 'version-update:semver-patch' && contains(github.event.pull_request.title, 'security')
run: |
gh pr comment "$PR_URL" --body "🔒 **Security Update Detected**
This appears to be a security-related update.
**Dependencies updated:** ${{ steps.metadata.outputs.dependency-names }}
🚀 **Recommended:** Review and merge promptly after CI passes to address potential vulnerabilities."
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Add reviewers for major updates
if: steps.metadata.outputs.update-type == 'version-update:semver-major'
run: |
# Add yourself as reviewer for major updates (replace with your GitHub username)
gh pr edit "$PR_URL" --add-reviewer "daedalist"
echo "Major update detected - consider adding reviewers manually"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ========================================
# AUTO-APPROVE AND AUTO-MERGE SECTION
# ========================================
- name: Determine if eligible for auto-merge
id: auto-merge-check
run: |
UPDATE_TYPE="${{ steps.metadata.outputs.update-type }}"
DEP_TYPE="${{ steps.metadata.outputs.dependency-type }}"
PASSED_COOLING="${{ steps.age-check.outputs.passed_cooling }}"
echo "Update type: $UPDATE_TYPE"
echo "Dependency type: $DEP_TYPE"
echo "Passed cooling period: $PASSED_COOLING"
ELIGIBLE="false"
REASON=""
# Check cooling period first
if [ "$PASSED_COOLING" != "true" ]; then
REASON="Waiting for 48-hour cooling period"
# Patch updates: always eligible (after cooling)
elif [ "$UPDATE_TYPE" == "version-update:semver-patch" ]; then
ELIGIBLE="true"
REASON="Patch update"
# Minor updates: only dev dependencies
elif [ "$UPDATE_TYPE" == "version-update:semver-minor" ]; then
if [ "$DEP_TYPE" == "direct:development" ]; then
ELIGIBLE="true"
REASON="Minor update for dev dependency"
else
REASON="Minor update for production dependency (requires manual review)"
fi
else
REASON="Major update (requires manual review)"
fi
echo "eligible=$ELIGIBLE" >> $GITHUB_OUTPUT
echo "reason=$REASON" >> $GITHUB_OUTPUT
echo "Decision: eligible=$ELIGIBLE, reason=$REASON"
- name: Auto-approve eligible PRs
if: steps.auto-merge-check.outputs.eligible == 'true'
run: |
echo "✅ Auto-approving: ${{ steps.auto-merge-check.outputs.reason }}"
gh pr review "$PR_URL" --approve --body "🤖 Auto-approved: ${{ steps.auto-merge-check.outputs.reason }}
This PR passed the 48-hour cooling period and all CI checks.
**Update type:** ${{ steps.metadata.outputs.update-type }}
**Dependency type:** ${{ steps.metadata.outputs.dependency-type }}
**Dependencies:** ${{ steps.metadata.outputs.dependency-names }}"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.DEPENDABOT_PAT }}
- name: Enable auto-merge for eligible PRs
if: steps.auto-merge-check.outputs.eligible == 'true'
run: |
echo "🔀 Enabling auto-merge (squash) for PR"
gh pr merge "$PR_URL" --auto --squash
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.DEPENDABOT_PAT }}
- name: Comment on ineligible PRs
if: steps.auto-merge-check.outputs.eligible == 'false' && steps.age-check.outputs.passed_cooling == 'true'
run: |
gh pr comment "$PR_URL" --body "ℹ️ **Auto-merge not enabled**
Reason: ${{ steps.auto-merge-check.outputs.reason }}
This PR requires manual review and approval."
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ========================================
# SCHEDULED JOB - Check all open Dependabot PRs
# ========================================
scheduled-check:
runs-on: ubuntu-latest
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
steps:
- name: Check out repository
uses: actions/checkout@v5
- name: Process open Dependabot PRs
env:
GITHUB_TOKEN: ${{ secrets.DEPENDABOT_PAT }}
run: |
echo "🔍 Finding open Dependabot PRs..."
# Get all open PRs from dependabot
PRS=$(gh pr list --author "dependabot[bot]" --state open --json number,createdAt,title --limit 20)
if [ "$PRS" == "[]" ]; then
echo "No open Dependabot PRs found"
exit 0
fi
echo "$PRS" | jq -c '.[]' | while read -r pr; do
PR_NUMBER=$(echo "$pr" | jq -r '.number')
CREATED_AT=$(echo "$pr" | jq -r '.createdAt')
TITLE=$(echo "$pr" | jq -r '.title')
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "PR #$PR_NUMBER: $TITLE"
# Calculate age
CREATED_TIMESTAMP=$(date -d "$CREATED_AT" +%s)
CURRENT_TIMESTAMP=$(date +%s)
AGE_HOURS=$(( (CURRENT_TIMESTAMP - CREATED_TIMESTAMP) / 3600 ))
echo "Age: $AGE_HOURS hours"
if [ $AGE_HOURS -lt 48 ]; then
REMAINING=$(( 48 - AGE_HOURS ))
echo "⏳ Needs $REMAINING more hours for cooling period"
continue
fi
# Check if already has auto-merge enabled
MERGE_STATE=$(gh pr view "$PR_NUMBER" --json autoMergeRequest --jq '.autoMergeRequest')
if [ "$MERGE_STATE" != "null" ] && [ -n "$MERGE_STATE" ]; then
echo "✅ Auto-merge already enabled"
continue
fi
# Get metadata using labels (set by the PR event job)
LABELS=$(gh pr view "$PR_NUMBER" --json labels --jq '.labels[].name' | tr '\n' ' ')
echo "Labels: $LABELS"
ELIGIBLE="false"
REASON=""
# Check eligibility based on labels
if echo "$LABELS" | grep -q "patch-update"; then
ELIGIBLE="true"
REASON="Patch update"
elif echo "$LABELS" | grep -q "minor-update"; then
# Minor updates need dev-dependency check - only auto-merge if labeled
if echo "$LABELS" | grep -q "dev-dependency"; then
ELIGIBLE="true"
REASON="Minor update for dev dependency"
else
REASON="Minor update for production dependency (requires manual review)"
fi
elif echo "$LABELS" | grep -q "major-update"; then
REASON="Major update (requires manual review)"
else
REASON="Unknown update type - missing labels"
fi
if [ "$ELIGIBLE" == "true" ]; then
echo "✅ Eligible for auto-merge: $REASON"
# Approve if not already approved
REVIEW_STATE=$(gh pr view "$PR_NUMBER" --json reviewDecision --jq '.reviewDecision')
if [ "$REVIEW_STATE" != "APPROVED" ]; then
echo "Approving PR..."
gh pr review "$PR_NUMBER" --approve --body "🤖 Auto-approved (scheduled check): $REASON
This PR passed the 48-hour cooling period."
fi
# Enable auto-merge
echo "Enabling auto-merge..."
gh pr merge "$PR_NUMBER" --auto --squash
else
echo "❌ Not eligible: $REASON"
fi
done