340 lines
12 KiB
Python
340 lines
12 KiB
Python
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
import httpx
|
|
import requests
|
|
|
|
PR_checkTemplate = ['Paddle']
|
|
|
|
BRANCH = os.environ['BRANCH']
|
|
if BRANCH.startswith("develop"):
|
|
REPO_TEMPLATE = {
|
|
"Paddle": r'''### PR Category(.*[^\s].*)### PR Types(.*[^\s].*)### Description(.*[^\s].*)### 是否引起精度变化(.*[^\s].*)'''
|
|
}
|
|
elif BRANCH.startswith("release"):
|
|
REPO_TEMPLATE = {
|
|
"Paddle": r'''### PR Category(.*[^\s].*)### PR Types(.*[^\s].*)### Description(.*?devPR:https://github\.com/PaddlePaddle/Paddle/pull/.*?)(?:\n###|\Z)'''
|
|
}
|
|
else:
|
|
REPO_TEMPLATE = {
|
|
"Paddle": r'''### PR Category(.*[^\s].*)### PR Types(.*[^\s].*)### Description(.*[^\s].*)'''
|
|
}
|
|
|
|
|
|
def re_rule(body, CHECK_TEMPLATE):
|
|
PR_RE = re.compile(CHECK_TEMPLATE, re.DOTALL)
|
|
result = PR_RE.search(body)
|
|
return result
|
|
|
|
|
|
def extract_pr_links(description_text):
|
|
pattern = r'(?:https://github\.com/PaddlePaddle/Paddle/pull/|#)(\d+)'
|
|
return re.findall(pattern, description_text)
|
|
|
|
|
|
def check_link_accessible(url):
|
|
try:
|
|
response = requests.head(url, allow_redirects=True, timeout=5)
|
|
return response.status_code == 200
|
|
except requests.RequestException:
|
|
return False
|
|
|
|
|
|
def parameter_accuracy(body):
|
|
PR_dic = {}
|
|
PR_Category = [
|
|
'User Experience',
|
|
'Execute Infrastructure',
|
|
'Operator Mechanism',
|
|
'CINN',
|
|
'Custom Device',
|
|
'Performance Optimization',
|
|
'Distributed Strategy',
|
|
'Parameter Server',
|
|
'Communication Library',
|
|
'Auto Parallel',
|
|
'Inference',
|
|
'Environment Adaptation',
|
|
]
|
|
PR_Types = [
|
|
'New features',
|
|
'Bug fixes',
|
|
'Improvements',
|
|
'Performance',
|
|
'BC Breaking',
|
|
'Deprecations',
|
|
'Docs',
|
|
'Devs',
|
|
'Not User Facing',
|
|
'Security',
|
|
'Others',
|
|
]
|
|
|
|
Accuracy_Change = [
|
|
'是',
|
|
'否',
|
|
]
|
|
body = re.sub("\r\n", "", body)
|
|
type_end = body.find('### PR Types')
|
|
changes_end = body.find('### Description')
|
|
PR_dic['PR Category'] = body[len('### PR Category') : type_end]
|
|
PR_dic['PR Types'] = body[type_end + len('### PR Types') : changes_end]
|
|
message = ''
|
|
for key in PR_dic:
|
|
test_list = PR_Category if key == 'PR Category' else PR_Types
|
|
test_list_lower = [l.lower() for l in test_list]
|
|
value = PR_dic[key].strip().split(',')
|
|
single_mess = ''
|
|
if len(value) == 1 and value[0] == '':
|
|
message += f'{key} should be in {test_list}. but now is None.'
|
|
else:
|
|
for i in value:
|
|
i = i.strip().lower()
|
|
if i not in test_list_lower:
|
|
single_mess += f'{i}.'
|
|
if len(single_mess) != 0:
|
|
message += f'{key} should be in {test_list}. but now is [{single_mess}].'
|
|
if BRANCH.startswith("release"):
|
|
PR_dic['Description'] = body[changes_end + len('### Description') :]
|
|
des_pr_id = extract_pr_links(PR_dic['Description'])
|
|
if len(des_pr_id) == 0 or not check_link_accessible(
|
|
"https://github.com/PaddlePaddle/Paddle/pull/" + str(des_pr_id[0])
|
|
):
|
|
message += 'The PR link does not exist. To merge into the release branch, you need to merge into the develop branch first and then cherry-pick it to the release branch. Please merge into develop first and fill in the PR link in the Description. Use this format: devPR:https://github.com/PaddlePaddle/Paddle/pull/xxxx'
|
|
|
|
if BRANCH.startswith("develop"):
|
|
accuracy_start = body.find('### 是否引起精度变化')
|
|
if accuracy_start != -1:
|
|
# 确保description_start在accuracy_start之后
|
|
PR_dic['Precision Change Impact'] = body[
|
|
accuracy_start + len('### 是否引起精度变化') :
|
|
]
|
|
else:
|
|
PR_dic['Precision Change Impact'] = ''
|
|
accuracy_value = PR_dic.get('Precision Change Impact', '').strip()
|
|
print(f'Extracted Precision Change Impact: "{accuracy_value}"')
|
|
if not accuracy_value:
|
|
message += '必须填写是否引起精度变化'
|
|
else:
|
|
found_valid = False
|
|
for option in Accuracy_Change:
|
|
if option in accuracy_value:
|
|
found_valid = True
|
|
break
|
|
if not found_valid:
|
|
message += f'精度变化必须填写为:{Accuracy_Change}. 现在是 {accuracy_value}.'
|
|
return message
|
|
|
|
|
|
def check_precision_change_approval(body, pr_number, pr_user):
|
|
"""Check if PR with precision change has sufficient approval"""
|
|
# Only check for develop branch
|
|
if not BRANCH.startswith("develop"):
|
|
return True, "Not develop branch, skip precision change approval check"
|
|
|
|
# Check if involves precision change
|
|
body_without_comments = re.sub(r'<!--.*?-->', '', body, flags=re.DOTALL)
|
|
accuracy_start = body_without_comments.find('### 是否引起精度变化')
|
|
if accuracy_start != -1:
|
|
precision_text = body_without_comments[
|
|
accuracy_start + len('### 是否引起精度变化') :
|
|
]
|
|
else:
|
|
precision_text = ''
|
|
return False, '未匹配到是否引起精度变化字段,无法判断是否涉及精度变化'
|
|
precision_text = precision_text.strip()
|
|
|
|
print(f'Extracted precision text: "{precision_text}"')
|
|
has_precision_change = '是' in precision_text
|
|
|
|
if not has_precision_change:
|
|
return True, "不涉及精度变化,无需检查"
|
|
REQUIRED_APPROVERS = ['sneaxiy']
|
|
REQUIRED_APPROVERS_LOWER = [user.lower() for user in REQUIRED_APPROVERS]
|
|
print(
|
|
f"PR {pr_number} involves precision change, checking approvals from required approvers: {REQUIRED_APPROVERS}"
|
|
)
|
|
|
|
# If has precision change, check approval status
|
|
reviews_url = f"https://api.github.com/repos/PaddlePaddle/Paddle/pulls/{pr_number}/reviews"
|
|
headers = {
|
|
'Authorization': 'token ' + GITHUB_API_TOKEN,
|
|
'Accept': 'application/vnd.github+json',
|
|
}
|
|
|
|
try:
|
|
response = httpx.get(reviews_url, headers=headers, timeout=10)
|
|
if response.status_code != 200:
|
|
return (
|
|
False,
|
|
f"Cannot get review information: {response.status_code}",
|
|
)
|
|
|
|
reviews = response.json()
|
|
|
|
# Check for approved reviews
|
|
approved_by = {}
|
|
for approver in REQUIRED_APPROVERS:
|
|
approved_by[approver] = False
|
|
|
|
if pr_user.lower() in REQUIRED_APPROVERS_LOWER:
|
|
approved_by[pr_user] = True
|
|
print(f" ✓ Approved by PR author {pr_user}")
|
|
|
|
# Check all reviews
|
|
for review in reviews:
|
|
if review['state'] == 'APPROVED':
|
|
reviewer = review['user']['login']
|
|
if reviewer.lower() in REQUIRED_APPROVERS_LOWER:
|
|
# Find the correct casing for this reviewer
|
|
for approver in REQUIRED_APPROVERS:
|
|
if approver.lower() == reviewer.lower():
|
|
approved_by[approver] = True
|
|
print(f" ✓ Approved by {approver}")
|
|
break
|
|
# Check if all required approvers have approved
|
|
missing_approvers = [
|
|
approver
|
|
for approver, has_approved in approved_by.items()
|
|
if not has_approved
|
|
]
|
|
if len(missing_approvers) == 0:
|
|
approvers_list = ", ".join(REQUIRED_APPROVERS)
|
|
return (
|
|
True,
|
|
f"✅ All required approvers have approved: {approvers_list}",
|
|
)
|
|
else:
|
|
approved_list = [a for a in REQUIRED_APPROVERS if approved_by[a]]
|
|
missing_list = ", ".join(missing_approvers)
|
|
|
|
if len(approved_list) > 0:
|
|
approved_str = ", ".join(approved_list)
|
|
return (
|
|
False,
|
|
f"❌ Missing approvals from: {missing_list}. Approved by: {approved_str}",
|
|
)
|
|
else:
|
|
return (
|
|
False,
|
|
f"❌ No approvals from required approvers. Missing: {missing_list}",
|
|
)
|
|
except Exception as e:
|
|
return False, f"Error checking approval: {e!s}"
|
|
|
|
|
|
def checkComments(url):
|
|
headers = {
|
|
'Authorization': 'token ' + GITHUB_API_TOKEN,
|
|
}
|
|
response = httpx.get(
|
|
url, headers=headers, timeout=None, follow_redirects=True
|
|
).json()
|
|
return response
|
|
|
|
|
|
def checkPRTemplate(repo, body, CHECK_TEMPLATE):
|
|
"""
|
|
Check if PR's description meet the standard of template
|
|
Args:
|
|
body: PR's Body.
|
|
CHECK_TEMPLATE: check template str.
|
|
Returns:
|
|
res: True or False
|
|
"""
|
|
res = False
|
|
comment_pattern = re.compile(r'<!--.*?-->', re.DOTALL)
|
|
if body is None:
|
|
body = ''
|
|
body = comment_pattern.sub('', body)
|
|
result = re_rule(body, CHECK_TEMPLATE)
|
|
message = ''
|
|
if len(CHECK_TEMPLATE) == 0 and len(body) == 0:
|
|
res = False
|
|
elif result is not None:
|
|
message = parameter_accuracy(body)
|
|
res = True if message == '' else False
|
|
elif result is None:
|
|
res = False
|
|
message = parameter_accuracy(body)
|
|
if BRANCH.startswith("release") and len(message) == 0:
|
|
message = 'The PR link does not exist. To merge into the release branch, you need to merge into the develop branch first and then cherry-pick it to the release branch. Please merge into develop first and fill in the PR link in the Description. Use this format: devPR:https://github.com/PaddlePaddle/Paddle/pull/xxxx'
|
|
return res, message
|
|
|
|
|
|
def pull_request_event_template(event, repo, *args, **kwargs):
|
|
pr_effect_repos = PR_checkTemplate
|
|
pr_num = event['number']
|
|
url = event["comments_url"]
|
|
BODY = event["body"]
|
|
sha = event["head"]["sha"]
|
|
title = event["title"]
|
|
pr_user = event["user"]["login"]
|
|
print(f'receive data : pr_num: {pr_num}, title: {title}, user: {pr_user}')
|
|
if repo in pr_effect_repos:
|
|
CHECK_TEMPLATE = REPO_TEMPLATE[repo]
|
|
global check_pr_template
|
|
global check_pr_template_message
|
|
check_pr_template, check_pr_template_message = checkPRTemplate(
|
|
repo, BODY, CHECK_TEMPLATE
|
|
)
|
|
print(f"check_pr_template: {check_pr_template} pr: {pr_num}")
|
|
if check_pr_template is False:
|
|
print("ERROR MESSAGE:", check_pr_template_message)
|
|
sys.exit(7)
|
|
else:
|
|
print("check approve")
|
|
approval_ok, approval_msg = check_precision_change_approval(
|
|
BODY, pr_num, pr_user
|
|
)
|
|
print(
|
|
f"Approval check result: {approval_ok}, message: {approval_msg}"
|
|
)
|
|
if not approval_ok:
|
|
check_pr_template_message = approval_msg
|
|
print("ERROR MESSAGE:", check_pr_template_message)
|
|
sys.exit(8)
|
|
sys.exit(0)
|
|
|
|
|
|
def get_a_pull(pull_id):
|
|
url = "https://api.github.com/repos/PaddlePaddle/Paddle/pulls/ " + str(
|
|
pull_id
|
|
)
|
|
|
|
payload = {}
|
|
headers = {
|
|
'Authorization': 'token ' + GITHUB_API_TOKEN,
|
|
'Accept': 'application/vnd.github+json',
|
|
}
|
|
response = httpx.request(
|
|
"GET", url, headers=headers, data=payload, follow_redirects=True
|
|
)
|
|
return response.json()
|
|
|
|
|
|
def main(org, repo, pull_id):
|
|
pull_info = get_a_pull(pull_id)
|
|
pull_request_event_template(pull_info, repo)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
AGILE_PULL_ID = os.getenv("AGILE_PULL_ID")
|
|
GITHUB_API_TOKEN = os.getenv("GITHUB_API_TOKEN")
|
|
main("PaddlePaddle", "Paddle", AGILE_PULL_ID)
|