Skip to content

Commit cc6e44a

Browse files
authored
Allow code owners to self-approve pull requests (#166)
This action runs when auto-merge is enabled, and approves the PR using the alibuild user if the submitter of the PR is the code owner of all code in the PR.
1 parent 2afc4cb commit cc6e44a

File tree

1 file changed

+97
-0
lines changed

1 file changed

+97
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: PR self-approval
2+
3+
on:
4+
pull_request:
5+
types:
6+
- auto_merge_enabled
7+
8+
jobs:
9+
approve:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
# Check out in order to get CODEOWNERS.
14+
- uses: actions/checkout@v2
15+
with:
16+
# Always use the latest CODEOWNERS file, *not* the one from the PR!
17+
ref: ${{ github.event.repository.default_branch }}
18+
persist-credentials: false
19+
20+
- name: Install dependencies
21+
run: pip install PyGithub
22+
23+
# First, parse filename patterns in CODEOWNERS using git-check-ignore(1)
24+
# to get the applicable code owners for this PR.
25+
- name: Parse CODEOWNERS
26+
id: owners
27+
shell: bash -exo pipefail {0}
28+
run: |
29+
git config --global init.defaultBranch master # avoid annoying warning
30+
git init ../tmp # temporary repo for .gitignore parsing
31+
awk '!/^[[:space:]]*(#|$)/ {print $1}' CODEOWNERS > ../tmp/.gitignore
32+
# Find changed files in PR and which CODEOWNERS entries match them.
33+
curl -fsSL "${{ github.event.pull_request.diff_url }}" |
34+
sed -rn 's,^diff --git a/(.*) b/(.*)$,\1\n\2,p' | uniq |
35+
git -C ../tmp check-ignore -v --no-index --stdin |
36+
cut -d: -f2 |
37+
sed 's/$/p/' > extract-lines.sed
38+
# Extract user names applicable to this PR. Same format as CODEOWNERS,
39+
# but without the leading pattern, so we just have usernames separated
40+
# by spaces and newlines.
41+
echo "::set-output name=owners::$(
42+
grep -vE '^[[:space:]]*(#|$)' CODEOWNERS |
43+
sed -nf extract-lines.sed |
44+
sed 's/^[^[:space:]]*[[:space:]]*//' |
45+
tr '\n' ';'
46+
)"
47+
48+
# Finally, approve, if the author is only editing files owned by themselves.
49+
- name: Check author is allowed to self-approve
50+
shell: python
51+
env:
52+
submitter: ${{ github.event.sender.login }}
53+
pr: ${{ github.event.pull_request.number }}
54+
repo: ${{ github.event.repository.full_name }}
55+
owners: ${{ steps.owners.outputs.owners }}
56+
github_token: ${{ secrets.ALIBUILD_GITHUB_TOKEN }}
57+
run: |
58+
import functools, github, os
59+
60+
gh = github.Github(os.environ['github_token'])
61+
submitter = os.environ['submitter']
62+
63+
@functools.lru_cache(maxsize=None)
64+
def matches_owner(user_or_team):
65+
user_or_team = user_or_team.lstrip('@')
66+
org, is_team, team_name = user_or_team.partition('/')
67+
if not is_team:
68+
return user_or_team == submitter
69+
try:
70+
gh.get_organization(org) \
71+
.get_team_by_slug(team_name) \
72+
.get_team_membership(submitter)
73+
except github.UnknownObjectException:
74+
return False
75+
return True
76+
77+
def check_lines():
78+
auto_approve = True
79+
# owners is a string containing semicolon-separated records of
80+
# space-separated usernames. At least one username per record must
81+
# match the submitter (taking teams into account), and all lines
82+
# must have a matching username.
83+
for line in os.environ['owners'].strip(';').split(';'):
84+
line_owners = line.split()
85+
assert all(o.startswith('@') for o in line_owners), \
86+
'failed to parse CODEOWNERS'
87+
if not any(map(matches_owner, line_owners)):
88+
print('::warning::Not auto-approving as you are not one of',
89+
', '.join(line_owners))
90+
auto_approve = False
91+
return auto_approve
92+
93+
if check_lines():
94+
gh.get_repo(os.environ['repo']) \
95+
.get_pull(int(os.environ['pr'])) \
96+
.create_review(event='APPROVE',
97+
body=f'Auto-approving on behalf of @{submitter}.')

0 commit comments

Comments
 (0)