-
Notifications
You must be signed in to change notification settings - Fork 3.4k
131 lines (114 loc) · 5.68 KB
/
validate-pr-target-branch.yml
File metadata and controls
131 lines (114 loc) · 5.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# 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@v8
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) => {
const lower = username.toLowerCase();
return lower === 'copilot' ||
lower === 'dotnet-bot' ||
lower.startsWith('app/') ||
lower.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;
}
// If the PR is from a branch (not a fork), it must be from an internal contributor
if (pr.head.repo && pr.head.repo.id === pr.base.repo.id) {
console.log('PR is from a branch in the same repo - skipping validation');
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) {
// If the user cannot be found (e.g. non-user accounts like Copilot), skip
if (error.status === 404) {
console.log(`User ${triggeredBy} not found - skipping`);
return;
}
console.error('Error checking permissions:', error);
// If we can't determine permissions for other reasons, skip to avoid false positives
return;
}
// 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);
}