Files
2026-07-13 12:40:42 +08:00

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)