forked from lowRISC/opentitan
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
239 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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,48 @@ | ||
name: 'Testing GH Actions' | ||
|
||
# **What it does**: Renders the content of every page and check all internal links. | ||
# **Why we have it**: To make sure all links connect correctly. | ||
# **Who does it impact**: Docs content. | ||
|
||
on: | ||
workflow_dispatch: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
|
||
permissions: | ||
contents: read | ||
# Needed for the 'trilom/file-changes-action' action | ||
pull-requests: read | ||
|
||
jobs: | ||
check-links: | ||
runs-on: "ubuntu-latest" | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v3 | ||
|
||
- name: Install python requirements | ||
run: | | ||
python -m ensurepip --upgrade | ||
audo apt update | ||
sudo apt install -y python3-pip | ||
pip install requests | ||
# Creates file "$/files.json", among others | ||
- name: Gather files changed | ||
uses: trilom/file-changes-action@a6ca26c14274c33b15e6499323aac178af06ad4b | ||
with: | ||
fileOutput: 'json' | ||
|
||
# For verification | ||
- name: Show files changed | ||
run: cat $HOME/files.json | ||
|
||
- name: Check for blocked changs | ||
run: | | ||
./ci/scripts/check-pr-changes-allowed.py $HOME/files.json \ | ||
--gh-repo ${{ github.action_repository }} \ | ||
--gh-token ${{ secrets.GITHUB_TOKEN }} \ | ||
--pr-ref ${{ github.ref }} |
This file contains 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,8 @@ | ||
# A test block file | ||
|
||
hw/ip/*/rtl/* | ||
hw/ip_templates/*/rtl/* | ||
hw/top_earlgrey/rtl/* | ||
hw/top_earlgrey/ip/*/rtl/* | ||
hw/top_earlgrey/ip_autogen/*/rtl/* | ||
|
This file contains 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,183 @@ | ||
#!/usr/bin/env python3 | ||
# Copyright lowRISC contributors. | ||
# Licensed under the Apache License, Version 2.0, see LICENSE for details. | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
from fnmatch import fnmatch | ||
import json | ||
import argparse | ||
import sys | ||
import re | ||
import requests | ||
|
||
# Number of authorizations required from committers to override a block | ||
NUM_AUTHS_REQD = 2 | ||
|
||
GH_API_URL = 'https://api.github.com/repos/' | ||
|
||
def load_blockfile(blockfile_name): | ||
blocklist = [] | ||
with open(blockfile_name, 'r') as blockfile: | ||
for line in blockfile.readlines(): | ||
# Remove everything following a '#' for comments | ||
line = line.partition('#')[0] | ||
# Remove leading and trailing whitespace | ||
line = line.strip() | ||
|
||
# If anything remains after the above add it to the list | ||
if line != '': | ||
blocklist.append(line) | ||
|
||
return blocklist | ||
|
||
def load_and_validate_changes(changes_json_file): | ||
with open(changes_json_file, 'r') as changes_json: | ||
changes = json.load(changes_json) | ||
|
||
if type(changes) != list: | ||
raise ValueError('Changes JSON must be a list at top-level') | ||
|
||
for change in changes: | ||
if type(change) != str: | ||
raise ValueError(f'Saw value {change} in changes JSON which is not ' | ||
'a string. Changes JSON must be a single list of ' | ||
'strings') | ||
|
||
return changes | ||
|
||
def check_changes_against_blocklist(changes, blocklist): | ||
blocked_changes = {} | ||
|
||
for change in changes: | ||
# Create list of patterns from blocklist that match change | ||
blocked = list(filter(lambda x: x is not None, | ||
map(lambda b: b if fnmatch(change, b) else None, blocklist))) | ||
|
||
# If any match patterns exist add them to the blocked_changes dictionary | ||
if blocked: | ||
blocked_changes[change] = blocked | ||
|
||
return blocked_changes | ||
|
||
|
||
pr_ref_re = re.compile(r'refs\/pull\/(\d+)\/merge') | ||
def get_pr_number_from_ref(pr_ref): | ||
pr_ref_match = pr_ref_re.match(pr_ref) | ||
|
||
if pr_ref_match: | ||
return int(pr_ref_match.group(1)) | ||
|
||
raise ValueError(f'{pr_ref} is not a valid PR ref') | ||
|
||
def fetch_pr_comments(gh_token, repo_name, pr_number): | ||
headers = { | ||
'Accept': 'application/vnd.github+json', | ||
'Authorization': f'Bearer {gh_token}', | ||
'X-GitHub-Api-Version': '2022-11-28' | ||
} | ||
|
||
pr_comment_url = f'{GH_API_URL}{repo_name}/issues/{pr_number}/comments' | ||
pr_comment_request = requests.get(pr_comment_url, headers=headers) | ||
|
||
comments_json_list = pr_comment_request.json() | ||
# Return pairs with commenter handle as the first element and the comment | ||
# body as the second | ||
return [(c['user']['login'], c['body']) for c in comments_json_list] | ||
|
||
committer_re = re.compile(r'\* ([\w\s-]+) \(([\w-]+)\)') | ||
|
||
def load_committers(committers_filename): | ||
with open(committers_filename, 'r') as committers_file: | ||
committer_tuples = committer_re.findall(committers_file.read()) | ||
|
||
committers_dict = {ct[1].lower(): ct[0] for ct in committer_tuples} | ||
|
||
if 'githubid' in committers_dict: | ||
del committers_dict['githubid'] | ||
|
||
return committers_dict | ||
|
||
authorize_re = re.compile(r'^CHANGE AUTHORIZED: (.*)$', re.MULTILINE) | ||
|
||
def get_authorized_changes(comments, committers): | ||
file_change_authorizations = {} | ||
|
||
for comment_author, comment_body in comments: | ||
comment_author = comment_author.lower() | ||
|
||
if comment_author not in committers: | ||
continue | ||
|
||
file_authorizations = authorize_re.findall(comment_body) | ||
for filename in file_authorizations: | ||
filename = filename.strip() | ||
|
||
if filename not in file_change_authorizations: | ||
file_change_authorizations[filename] = [comment_author] | ||
else: | ||
file_change_authorizations[filename].append(comment_author) | ||
|
||
return file_change_authorizations | ||
|
||
def main(): | ||
arg_parser = argparse.ArgumentParser( | ||
prog='check-pr-changes-allowed', | ||
description='''Checks a list of changed files supplied as a json | ||
list against blocked file patterns. If any match | ||
exits with code 1 and the PR should be blocked.''') | ||
|
||
arg_parser.add_argument('changes_json_file', metavar='changes-json-file', | ||
help='''JSON file containing a list of changed files''') | ||
|
||
arg_parser.add_argument('--block-file', default='BLOCKFILE', | ||
help='''Plain text file containing path patterns that when matched | ||
indicate a blocked file. One pattern per line''') | ||
|
||
arg_parser.add_argument('--pr-ref', | ||
help='git ref of the PR requesting the change') | ||
|
||
arg_parser.add_argument('--gh-token', | ||
help='''A github access token used to read PR comments for override | ||
commands''') | ||
|
||
arg_parser.add_argument('--gh-repo', | ||
help='Name of the repository on github to read PR comments from', | ||
default='lowrisc/opentitan') | ||
|
||
args = arg_parser.parse_args() | ||
|
||
blocklist = load_blockfile(args.block_file) | ||
changes = load_and_validate_changes(args.changes_json_file) | ||
blocked_changes = check_changes_against_blocklist(changes, blocklist) | ||
|
||
if (args.gh_token is not None and args.pr_ref is not None): | ||
committers = load_committers('COMMITTERS') | ||
pr_number = get_pr_number_from_ref(args.pr_ref) | ||
pr_comments = fetch_pr_comments(args.gh_token, args.gh_repo, | ||
pr_number) | ||
authorized_changes = get_authorized_changes(pr_comments, committers) | ||
|
||
block_changes_files = list(blocked_changes.keys()) | ||
for change in block_changes_files: | ||
if (change in authorized_changes | ||
and len(authorized_changes[change]) >= NUM_AUTHS_REQD): | ||
del blocked_changes[change] | ||
|
||
authorizers = ', '.join([f'{committers[handle]} ({handle})' | ||
for handle in authorized_changes[change]]) | ||
print(f'{change} change is authorized by {authorizers}') | ||
|
||
if blocked_changes: | ||
for change, block_patterns in blocked_changes.items(): | ||
patterns_str = ' '.join(block_patterns) | ||
print(f'{change} blocked by pattern(s): {patterns_str}') | ||
|
||
print('UNAUTHORIZED CHANGES PRESENT, PR cannot be merged!') | ||
return 1 | ||
|
||
print('No unauthorized changes, clear to merge') | ||
return 0 | ||
|
||
if __name__ == '__main__': | ||
sys.exit(main()) | ||
|