-
Notifications
You must be signed in to change notification settings - Fork 506
116 lines (106 loc) · 5.25 KB
/
codeowner-self-approval.yml
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
---
name: PR self-approval
'on':
# We need pull_request_target so we can use ${{ secrets.* }}.
pull_request_target:
types:
- auto_merge_enabled
permissions:
contents: read
pull-requests: write
jobs:
approve:
runs-on: ubuntu-22.04
# Only run if the PR author enabled auto-merge, not someone else.
# Also run if a new approval was created, as this affects whether we can
# auto-approve. There is a risk of infinite loops here, though -- when we
# submit our review, that'll trigger this workflow again, so only run if
# someone other than us (i.e. alibuild) reviewed.
if: >-
(github.event.action == 'auto_merge_enabled' &&
github.event.sender.login == github.event.pull_request.user.login)
steps:
- name: Install dependencies
run: pip install codeowners PyGithub
# Approve the PR, if the author is only editing files owned by themselves.
- name: Auto-approve PR if permitted
shell: python
env:
submitter: ${{ github.event.pull_request.user.login }}
pr: ${{ github.event.pull_request.number }}
repo: ${{ github.event.repository.full_name }}
github_token: ${{ secrets.ALIBUILD_GITHUB_TOKEN }}
run: |
import os
import sys
from functools import lru_cache
from codeowners import CodeOwners
from github import Github, UnknownObjectException
# Cache results of this function so we don't have to look up team
# membership multiple times.
@lru_cache(maxsize=None)
def matches_owner(owner):
'''Return whether the PR's submitter matches the given owner.
owner is a (type, name) tuple, as returned by the CodeOwners.of
function. EMAIL owners cannot be handled currently, and will raise
a ValueError.
'''
owner_type, owner_name = owner
if owner_type == 'USERNAME':
return owner_name.lstrip('@') == os.environ['submitter']
elif owner_type == 'TEAM':
org, _, team_name = owner_name.lstrip('@').partition('/')
try:
gh.get_organization(org) \
.get_team_by_slug(team_name) \
.get_team_membership(os.environ['submitter'])
except UnknownObjectException:
return False # submitter is not a member of this team
return True
elif owner_type == 'EMAIL':
# We can't resolve email addresses to GitHub usernames using
# GitHub's API.
raise ValueError("can't handle email addresses in CODEOWNERS")
else:
raise ValueError(f'unknown owner type {owner_type}')
gh = Github(os.environ['github_token'])
repo = gh.get_repo(os.environ['repo'])
pr = repo.get_pull(int(os.environ['pr']))
owners = CodeOwners(repo.get_contents('CODEOWNERS')
.decoded_content.decode('utf-8'))
approvals_from = {review.user.login for review in pr.get_reviews()
if review.state == 'APPROVED'}
# At least one username per CODEOWNERS line must match the submitter
# (taking teams into account), and all lines must have a matching
# username. If the PR author is not the codeowner for a file, then if
# a codeowner of that file approved this PR, we'll still auto-approve.
auto_approve = True
for filename in (f.filename for f in pr.get_files()):
file_owners, line, *_ = owners.matching_line(filename)
file_owners_names = {name.lstrip('@') for _, name in file_owners}
if approvals_from & file_owners_names:
print(f'{filename}: OK: you have approval from the code'
' owners of this file, specifically:',
', '.join(approvals_from & file_owners_names),
f' (CODEOWNERS line {line})', file=sys.stderr)
elif any(map(matches_owner, file_owners)):
print(f'{filename}: OK: you are a code owner of this file'
f' (CODEOWNERS line {line})', file=sys.stderr)
else:
print('::warning::Not auto-approving as none of',
', '.join(file_owners_names),
f'have approved this PR as owners of {filename}'
f' (CODEOWNERS line {line})', file=sys.stderr)
# Don't break out of the loop, so that we print every
# non-matching CODEOWNERS rule for information.
auto_approve = False
if auto_approve:
print('Approving PR', file=sys.stderr)
pr.create_review(event='APPROVE', body=(
f'Auto-approving on behalf of @{os.environ["submitter"]}.'
))
else:
print('::warning::Not approving PR. You can see whose approval'
' you need in the messages above. This check will run again'
" when the PR's author disables and reenables auto-merge.",
file=sys.stderr)