Auto Update PR #131
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
# Copyright 2025 Google LLC. | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
name: Auto Update PR | |
on: | |
push: | |
branches: [main] | |
schedule: | |
# Run daily at 2 AM UTC to catch stale PRs | |
- cron: '0 2 * * *' | |
workflow_dispatch: | |
inputs: | |
pr_number: | |
description: 'PR number to update (optional, updates all if not specified)' | |
required: false | |
type: string | |
permissions: | |
contents: write # Required for updateBranch API | |
pull-requests: write | |
issues: write | |
jobs: | |
update-prs: | |
runs-on: ubuntu-latest | |
concurrency: | |
group: auto-update-pr-${{ github.event_name }} | |
cancel-in-progress: true | |
steps: | |
- name: Update PRs that are behind main | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const prNumber = context.payload.inputs?.pr_number; | |
// Get list of open PRs | |
const prs = prNumber | |
? [(await github.rest.pulls.get({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
pull_number: parseInt(prNumber) | |
})).data] | |
: await github.paginate(github.rest.pulls.list, { | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
state: 'open', | |
sort: 'updated', | |
direction: 'desc' | |
}); | |
console.log(`Found ${prs.length} open PRs to check`); | |
// Constants for comment flood control | |
const UPDATE_COMMENT_COOLDOWN_DAYS = 7; | |
const COOLDOWN_MS = UPDATE_COMMENT_COOLDOWN_DAYS * 24 * 60 * 60 * 1000; | |
for (const pr of prs) { | |
// Skip bot PRs and drafts | |
if (pr.user.login.includes('[bot]')) { | |
console.log(`Skipping bot PR #${pr.number} from ${pr.user.login}`); | |
continue; | |
} | |
if (pr.draft) { | |
console.log(`Skipping draft PR #${pr.number}`); | |
continue; | |
} | |
try { | |
// Check if PR is behind main (base...head comparison) | |
const { data: comparison } = await github.rest.repos.compareCommits({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
base: pr.base.ref, // main branch | |
head: `${pr.head.repo.owner.login}:${pr.head.ref}` // Fully qualified ref for forks | |
}); | |
if (comparison.behind_by > 0) { | |
console.log(`PR #${pr.number} is ${comparison.behind_by} commits behind ${pr.base.ref}`); | |
// Check if the PR allows maintainer edits | |
if (pr.maintainer_can_modify) { | |
// Try to update the branch | |
try { | |
await github.rest.pulls.updateBranch({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
pull_number: pr.number | |
}); | |
console.log(`✅ Updated PR #${pr.number}`); | |
// Add a comment | |
await github.rest.issues.createComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: pr.number, | |
body: `🔄 **Branch Updated**\n\nYour branch was ${comparison.behind_by} commits behind \`${pr.base.ref}\` and has been automatically updated. CI checks will re-run shortly.` | |
}); | |
} catch (updateError) { | |
console.log(`Could not auto-update PR #${pr.number}: ${updateError.message}`); | |
// Determine the reason for failure | |
let failureReason = ''; | |
if (updateError.status === 409 || updateError.message.includes('merge conflict')) { | |
failureReason = '\n\n**Note:** Automatic update failed due to merge conflicts. Please resolve them manually.'; | |
} else if (updateError.status === 422) { | |
failureReason = '\n\n**Note:** Cannot push to fork. Please update manually.'; | |
} | |
// Notify the contributor to update manually | |
await github.rest.issues.createComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: pr.number, | |
body: `⚠️ **Branch Update Required**\n\nYour branch is ${comparison.behind_by} commits behind \`${pr.base.ref}\`.${failureReason}\n\nPlease update your branch:\n\n\`\`\`bash\ngit fetch origin ${pr.base.ref}\ngit merge origin/${pr.base.ref}\ngit push\n\`\`\`\n\nOr use GitHub's "Update branch" button if available.` | |
}); | |
} | |
} else { | |
// Can't modify, just notify | |
console.log(`PR #${pr.number} doesn't allow maintainer edits`); | |
// Check if we already commented recently (within last 7 days) | |
const { data: comments } = await github.rest.issues.listComments({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: pr.number, | |
since: new Date(Date.now() - COOLDOWN_MS).toISOString() | |
}); | |
const hasRecentUpdateComment = comments.some(c => | |
c.body?.includes('Branch Update Required') && | |
c.user?.login === 'github-actions[bot]' | |
); | |
if (!hasRecentUpdateComment) { | |
await github.rest.issues.createComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: pr.number, | |
body: `⚠️ **Branch Update Required**\n\nYour branch is ${comparison.behind_by} commits behind \`${pr.base.ref}\`. Please update your branch to ensure CI checks run with the latest code:\n\n\`\`\`bash\ngit fetch origin ${pr.base.ref}\ngit merge origin/${pr.base.ref}\ngit push\n\`\`\`\n\nNote: Enable "Allow edits by maintainers" to allow automatic updates.` | |
}); | |
} | |
} | |
} else { | |
console.log(`PR #${pr.number} is up to date`); | |
} | |
} catch (error) { | |
console.error(`Error processing PR #${pr.number}:`, error.message); | |
} | |
} | |
// Log rate limit status | |
const { data: rateLimit } = await github.rest.rateLimit.get(); | |
console.log(`API rate limit remaining: ${rateLimit.rate.remaining}/${rateLimit.rate.limit}`); |