# 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@v9 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}`);