-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Add workflow to prevent external contributors from targeting release branches #37635
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
52dc60e
Initial plan
Copilot 6cce5ec
Add GitHub Action to validate PR target branches for external contrib…
Copilot bfa662d
Address code review feedback: optimize workflow triggers and use dyna…
Copilot e95c414
Update .github/workflows/validate-pr-target-branch.yml
AndriySvyryd 8382dba
Address PR feedback: forbid release/* branches, check triggering user…
Copilot cf79cd1
Improve bot detection, clarify message, and consolidate logic
Copilot b6ff4fe
Add reopened trigger back per feedback
Copilot 301b069
Refactor: extract hasWriteAccess helper to avoid code duplication
Copilot a8b5f50
Optimize: avoid duplicate permission checks and inline hasWriteAccess
Copilot 6243e5d
Refactor: move permission check before branch logic to eliminate dupl…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| # This workflow automatically closes PRs from external contributors (those without push permissions) | ||
| # that target release/* branches. It posts a comment asking them to resubmit with the correct target branch. | ||
| # | ||
| # External contributors should not target release/* branches per our contribution guidelines. | ||
| # Internal contributors (with push permissions) can target any branch as needed. | ||
|
|
||
| name: Validate PR Target Branch | ||
|
|
||
| on: | ||
| pull_request_target: | ||
| types: [opened, edited, reopened] | ||
|
|
||
| permissions: | ||
| pull-requests: write | ||
| issues: write | ||
|
|
||
| jobs: | ||
| validate: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Check PR target branch and author permissions | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const pr = context.payload.pull_request; | ||
| const targetBranch = pr.base.ref; | ||
| const prNumber = pr.number; | ||
| const prAuthor = pr.user.login; | ||
| const action = context.payload.action; | ||
| const triggeredBy = context.actor; | ||
|
|
||
| console.log(`PR #${prNumber} by ${prAuthor} targets branch: ${targetBranch}`); | ||
| console.log(`Action: ${action}, triggered by: ${triggeredBy}`); | ||
|
|
||
| // Helper function to check if a user is a bot | ||
| const isBot = (username) => { | ||
| return username === 'copilot' || | ||
| username === 'dotnet-bot' || | ||
| username.startsWith('app/') || | ||
| username.includes('[bot]'); | ||
| }; | ||
|
|
||
| // If action is 'edited', check if the base branch was actually changed | ||
| if (action === 'edited' && !context.payload.changes?.base) { | ||
| console.log('PR was edited but base branch was not changed - skipping'); | ||
| return; | ||
| } | ||
|
|
||
| // Skip if triggered by a bot or PR is authored by a bot | ||
| if (isBot(triggeredBy) || isBot(prAuthor)) { | ||
| console.log('Bot detected - skipping'); | ||
| return; | ||
| } | ||
|
|
||
| // Check if the user who triggered the action has push permissions | ||
| let hasWriteAccess = false; | ||
| try { | ||
| const { data: permissions } = await github.rest.repos.getCollaboratorPermissionLevel({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| username: triggeredBy | ||
| }); | ||
|
|
||
| hasWriteAccess = ['admin', 'write'].includes(permissions.permission); | ||
| console.log(`User ${triggeredBy} has permission level: ${permissions.permission}`); | ||
| } catch (error) { | ||
| console.error('Error checking permissions:', error); | ||
| // If we can't determine permissions, assume external contributor | ||
| } | ||
|
|
||
| // Check if target branch is a release branch | ||
| if (!targetBranch.startsWith('release/')) { | ||
| // For new PRs by external contributors, add community-contribution label | ||
| if (action === 'opened' && !hasWriteAccess) { | ||
| try { | ||
| console.log('Adding community-contribution label'); | ||
| await github.rest.issues.addLabels({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: prNumber, | ||
| labels: ['community-contribution'] | ||
| }); | ||
| } catch (error) { | ||
| console.error('Error adding label:', error); | ||
| } | ||
| } | ||
| console.log('PR does not target a release branch - allowed'); | ||
| return; | ||
| } | ||
|
|
||
| // If user has write access, allow PR to release branch | ||
| if (hasWriteAccess) { | ||
| console.log('User has write access - allowed'); | ||
| return; | ||
| } | ||
|
|
||
| // External contributor targeting release branch - close and comment | ||
| console.log('External contributor targeting release branch - closing PR'); | ||
|
|
||
| try { | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: prNumber, | ||
| body: `Thank you for your contribution! However, this PR targets the \`${targetBranch}\` branch.\n\nExternal contributions should not target release branches. This pull request has been closed automatically; please open a new pull request targeting \`main\` or another non-release branch.\n\nFor more information, see our [contribution guidelines](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/.github/CONTRIBUTING.md).` | ||
| }); | ||
|
|
||
| await github.rest.pulls.update({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: prNumber, | ||
| state: 'closed' | ||
| }); | ||
|
|
||
| console.log('PR closed successfully'); | ||
| } catch (error) { | ||
| console.error('Error closing PR:', error); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.