mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-23 08:02:02 +00:00
Merge pull request #49475 from ClickHouse/check-description-in-lambda
Check the PRs body directly in lambda, without rerun
This commit is contained in:
commit
f7470e1453
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -2,7 +2,7 @@
|
|||||||
A technical comment, you are free to remove or leave it as it is when PR is created
|
A technical comment, you are free to remove or leave it as it is when PR is created
|
||||||
The following categories are used in the next scripts, update them accordingly
|
The following categories are used in the next scripts, update them accordingly
|
||||||
utils/changelog/changelog.py
|
utils/changelog/changelog.py
|
||||||
tests/ci/run_check.py
|
tests/ci/cancel_and_rerun_workflow_lambda/app.py
|
||||||
-->
|
-->
|
||||||
### Changelog category (leave one):
|
### Changelog category (leave one):
|
||||||
- New Feature
|
- New Feature
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
import json
|
import json
|
||||||
import os
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
@ -27,6 +27,123 @@ MAX_RETRY = 5
|
|||||||
|
|
||||||
DEBUG_INFO = {} # type: Dict[str, Any]
|
DEBUG_INFO = {} # type: Dict[str, Any]
|
||||||
|
|
||||||
|
# Descriptions are used in .github/PULL_REQUEST_TEMPLATE.md, keep comments there
|
||||||
|
# updated accordingly
|
||||||
|
# The following lists are append only, try to avoid editing them
|
||||||
|
# They still could be cleaned out after the decent time though.
|
||||||
|
LABELS = {
|
||||||
|
"pr-backward-incompatible": ["Backward Incompatible Change"],
|
||||||
|
"pr-bugfix": [
|
||||||
|
"Bug Fix",
|
||||||
|
"Bug Fix (user-visible misbehavior in an official stable release)",
|
||||||
|
"Bug Fix (user-visible misbehaviour in official stable or prestable release)",
|
||||||
|
"Bug Fix (user-visible misbehavior in official stable or prestable release)",
|
||||||
|
],
|
||||||
|
"pr-build": [
|
||||||
|
"Build/Testing/Packaging Improvement",
|
||||||
|
"Build Improvement",
|
||||||
|
"Build/Testing Improvement",
|
||||||
|
"Build",
|
||||||
|
"Packaging Improvement",
|
||||||
|
],
|
||||||
|
"pr-documentation": [
|
||||||
|
"Documentation (changelog entry is not required)",
|
||||||
|
"Documentation",
|
||||||
|
],
|
||||||
|
"pr-feature": ["New Feature"],
|
||||||
|
"pr-improvement": ["Improvement"],
|
||||||
|
"pr-not-for-changelog": [
|
||||||
|
"Not for changelog (changelog entry is not required)",
|
||||||
|
"Not for changelog",
|
||||||
|
],
|
||||||
|
"pr-performance": ["Performance Improvement"],
|
||||||
|
}
|
||||||
|
|
||||||
|
CATEGORY_TO_LABEL = {c: lb for lb, categories in LABELS.items() for c in categories}
|
||||||
|
|
||||||
|
|
||||||
|
def check_pr_description(pr_body: str) -> Tuple[str, str]:
|
||||||
|
"""The function checks the body to being properly formatted according to
|
||||||
|
.github/PULL_REQUEST_TEMPLATE.md, if the first returned string is not empty,
|
||||||
|
then there is an error."""
|
||||||
|
lines = list(map(lambda x: x.strip(), pr_body.split("\n") if pr_body else []))
|
||||||
|
lines = [re.sub(r"\s+", " ", line) for line in lines]
|
||||||
|
|
||||||
|
# Check if body contains "Reverts ClickHouse/ClickHouse#36337"
|
||||||
|
if [
|
||||||
|
True
|
||||||
|
for line in lines
|
||||||
|
if re.match(r"\AReverts {GITHUB_REPOSITORY}#[\d]+\Z", line)
|
||||||
|
]:
|
||||||
|
return "", LABELS["pr-not-for-changelog"][0]
|
||||||
|
|
||||||
|
category = ""
|
||||||
|
entry = ""
|
||||||
|
description_error = ""
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
if re.match(r"(?i)^[#>*_ ]*change\s*log\s*category", lines[i]):
|
||||||
|
i += 1
|
||||||
|
if i >= len(lines):
|
||||||
|
break
|
||||||
|
# Can have one empty line between header and the category
|
||||||
|
# itself. Filter it out.
|
||||||
|
if not lines[i]:
|
||||||
|
i += 1
|
||||||
|
if i >= len(lines):
|
||||||
|
break
|
||||||
|
category = re.sub(r"^[-*\s]*", "", lines[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Should not have more than one category. Require empty line
|
||||||
|
# after the first found category.
|
||||||
|
if i >= len(lines):
|
||||||
|
break
|
||||||
|
if lines[i]:
|
||||||
|
second_category = re.sub(r"^[-*\s]*", "", lines[i])
|
||||||
|
description_error = (
|
||||||
|
"More than one changelog category specified: "
|
||||||
|
f"'{category}', '{second_category}'"
|
||||||
|
)
|
||||||
|
return description_error, category
|
||||||
|
|
||||||
|
elif re.match(
|
||||||
|
r"(?i)^[#>*_ ]*(short\s*description|change\s*log\s*entry)", lines[i]
|
||||||
|
):
|
||||||
|
i += 1
|
||||||
|
# Can have one empty line between header and the entry itself.
|
||||||
|
# Filter it out.
|
||||||
|
if i < len(lines) and not lines[i]:
|
||||||
|
i += 1
|
||||||
|
# All following lines until empty one are the changelog entry.
|
||||||
|
entry_lines = []
|
||||||
|
while i < len(lines) and lines[i]:
|
||||||
|
entry_lines.append(lines[i])
|
||||||
|
i += 1
|
||||||
|
entry = " ".join(entry_lines)
|
||||||
|
# Don't accept changelog entries like '...'.
|
||||||
|
entry = re.sub(r"[#>*_.\- ]", "", entry)
|
||||||
|
# Don't accept changelog entries like 'Close #12345'.
|
||||||
|
entry = re.sub(r"^[\w\-\s]{0,10}#?\d{5,6}\.?$", "", entry)
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if not category:
|
||||||
|
description_error = "Changelog category is empty"
|
||||||
|
# Filter out the PR categories that are not for changelog.
|
||||||
|
elif re.match(
|
||||||
|
r"(?i)doc|((non|in|not|un)[-\s]*significant)|(not[ ]*for[ ]*changelog)",
|
||||||
|
category,
|
||||||
|
):
|
||||||
|
pass # to not check the rest of the conditions
|
||||||
|
elif category not in CATEGORY_TO_LABEL:
|
||||||
|
description_error, category = f"Category '{category}' is not valid", ""
|
||||||
|
elif not entry:
|
||||||
|
description_error = f"Changelog entry required for category '{category}'"
|
||||||
|
|
||||||
|
return description_error, category
|
||||||
|
|
||||||
|
|
||||||
class Worker(Thread):
|
class Worker(Thread):
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -268,11 +385,11 @@ def get_workflow_description(workflow_url: str, token: str) -> WorkflowDescripti
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _exec_post_with_retry(url, token):
|
def _exec_post_with_retry(url: str, token: str, json: Optional[Any] = None) -> Any:
|
||||||
headers = {"Authorization": f"token {token}"}
|
headers = {"Authorization": f"token {token}"}
|
||||||
for i in range(MAX_RETRY):
|
for i in range(MAX_RETRY):
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, headers=headers)
|
response = requests.post(url, headers=headers, json=json)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@ -373,27 +490,20 @@ def main(event):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if action == "edited":
|
if action == "edited":
|
||||||
print("PR is edited, check if it needs to rerun")
|
print("PR is edited, check if the body is correct")
|
||||||
workflow_descriptions = get_workflows_description_for_pull_request(
|
error, category = check_pr_description(pull_request["body"])
|
||||||
pull_request, token
|
if error:
|
||||||
)
|
|
||||||
workflow_descriptions = (
|
|
||||||
workflow_descriptions
|
|
||||||
or get_workflow_description_fallback(pull_request, token)
|
|
||||||
)
|
|
||||||
workflow_descriptions.sort(key=lambda x: x.run_id) # type: ignore
|
|
||||||
most_recent_workflow = workflow_descriptions[-1]
|
|
||||||
if (
|
|
||||||
most_recent_workflow.status == "completed"
|
|
||||||
and most_recent_workflow.name in NEED_RERUN_ON_EDITED
|
|
||||||
):
|
|
||||||
print(
|
print(
|
||||||
"The PR's body is changed and workflow is finished. "
|
f"The PR's body is wrong, is going to comment it. The error is: {error}"
|
||||||
"Rerun to check the description"
|
|
||||||
)
|
)
|
||||||
exec_workflow_url([most_recent_workflow.rerun_url], token)
|
post_json = {
|
||||||
print("Rerun finished, exiting")
|
"body": "This is an automatic comment. The PR descriptions does not "
|
||||||
return
|
f"match the [template]({pull_request['base']['repo']['html_url']}/"
|
||||||
|
"blob/master/.github/PULL_REQUEST_TEMPLATE.md?plain=1).\n\n"
|
||||||
|
f"Please, edit it accordingly.\n\nThe error is: {error}"
|
||||||
|
}
|
||||||
|
_exec_post_with_retry(pull_request["comments_url"], token, json=post_json)
|
||||||
|
return
|
||||||
|
|
||||||
if action == "synchronize":
|
if action == "synchronize":
|
||||||
print("PR is synchronized, going to stop old actions")
|
print("PR is synchronized, going to stop old actions")
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from github import Github
|
from github import Github
|
||||||
@ -21,6 +20,8 @@ from docs_check import NAME as DOCS_NAME
|
|||||||
from env_helper import GITHUB_REPOSITORY, GITHUB_SERVER_URL
|
from env_helper import GITHUB_REPOSITORY, GITHUB_SERVER_URL
|
||||||
from get_robot_token import get_best_robot_token
|
from get_robot_token import get_best_robot_token
|
||||||
from pr_info import FORCE_TESTS_LABEL, PRInfo
|
from pr_info import FORCE_TESTS_LABEL, PRInfo
|
||||||
|
|
||||||
|
from cancel_and_rerun_workflow_lambda.app import CATEGORY_TO_LABEL, check_pr_description
|
||||||
from workflow_approve_rerun_lambda.app import TRUSTED_CONTRIBUTORS
|
from workflow_approve_rerun_lambda.app import TRUSTED_CONTRIBUTORS
|
||||||
|
|
||||||
TRUSTED_ORG_IDS = {
|
TRUSTED_ORG_IDS = {
|
||||||
@ -33,40 +34,6 @@ DO_NOT_TEST_LABEL = "do not test"
|
|||||||
FEATURE_LABEL = "pr-feature"
|
FEATURE_LABEL = "pr-feature"
|
||||||
SUBMODULE_CHANGED_LABEL = "submodule changed"
|
SUBMODULE_CHANGED_LABEL = "submodule changed"
|
||||||
|
|
||||||
# They are used in .github/PULL_REQUEST_TEMPLATE.md, keep comments there
|
|
||||||
# updated accordingly
|
|
||||||
# The following lists are append only, try to avoid editing them
|
|
||||||
# They atill could be cleaned out after the decent time though.
|
|
||||||
LABELS = {
|
|
||||||
"pr-backward-incompatible": ["Backward Incompatible Change"],
|
|
||||||
"pr-bugfix": [
|
|
||||||
"Bug Fix",
|
|
||||||
"Bug Fix (user-visible misbehavior in an official stable release)",
|
|
||||||
"Bug Fix (user-visible misbehaviour in official stable or prestable release)",
|
|
||||||
"Bug Fix (user-visible misbehavior in official stable or prestable release)",
|
|
||||||
],
|
|
||||||
"pr-build": [
|
|
||||||
"Build/Testing/Packaging Improvement",
|
|
||||||
"Build Improvement",
|
|
||||||
"Build/Testing Improvement",
|
|
||||||
"Build",
|
|
||||||
"Packaging Improvement",
|
|
||||||
],
|
|
||||||
"pr-documentation": [
|
|
||||||
"Documentation (changelog entry is not required)",
|
|
||||||
"Documentation",
|
|
||||||
],
|
|
||||||
"pr-feature": ["New Feature"],
|
|
||||||
"pr-improvement": ["Improvement"],
|
|
||||||
"pr-not-for-changelog": [
|
|
||||||
"Not for changelog (changelog entry is not required)",
|
|
||||||
"Not for changelog",
|
|
||||||
],
|
|
||||||
"pr-performance": ["Performance Improvement"],
|
|
||||||
}
|
|
||||||
|
|
||||||
CATEGORY_TO_LABEL = {c: lb for lb, categories in LABELS.items() for c in categories}
|
|
||||||
|
|
||||||
|
|
||||||
def pr_is_by_trusted_user(pr_user_login, pr_user_orgs):
|
def pr_is_by_trusted_user(pr_user_login, pr_user_orgs):
|
||||||
if pr_user_login.lower() in TRUSTED_CONTRIBUTORS:
|
if pr_user_login.lower() in TRUSTED_CONTRIBUTORS:
|
||||||
@ -120,91 +87,6 @@ def should_run_ci_for_pr(pr_info: PRInfo) -> Tuple[bool, str, str]:
|
|||||||
return True, "No special conditions apply", "pending"
|
return True, "No special conditions apply", "pending"
|
||||||
|
|
||||||
|
|
||||||
def check_pr_description(pr_info: PRInfo) -> Tuple[str, str]:
|
|
||||||
lines = list(
|
|
||||||
map(lambda x: x.strip(), pr_info.body.split("\n") if pr_info.body else [])
|
|
||||||
)
|
|
||||||
lines = [re.sub(r"\s+", " ", line) for line in lines]
|
|
||||||
|
|
||||||
# Check if body contains "Reverts ClickHouse/ClickHouse#36337"
|
|
||||||
if [
|
|
||||||
True
|
|
||||||
for line in lines
|
|
||||||
if re.match(rf"\AReverts {GITHUB_REPOSITORY}#[\d]+\Z", line)
|
|
||||||
]:
|
|
||||||
return "", LABELS["pr-not-for-changelog"][0]
|
|
||||||
|
|
||||||
category = ""
|
|
||||||
entry = ""
|
|
||||||
description_error = ""
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
while i < len(lines):
|
|
||||||
if re.match(r"(?i)^[#>*_ ]*change\s*log\s*category", lines[i]):
|
|
||||||
i += 1
|
|
||||||
if i >= len(lines):
|
|
||||||
break
|
|
||||||
# Can have one empty line between header and the category
|
|
||||||
# itself. Filter it out.
|
|
||||||
if not lines[i]:
|
|
||||||
i += 1
|
|
||||||
if i >= len(lines):
|
|
||||||
break
|
|
||||||
category = re.sub(r"^[-*\s]*", "", lines[i])
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
# Should not have more than one category. Require empty line
|
|
||||||
# after the first found category.
|
|
||||||
if i >= len(lines):
|
|
||||||
break
|
|
||||||
if lines[i]:
|
|
||||||
second_category = re.sub(r"^[-*\s]*", "", lines[i])
|
|
||||||
result_status = (
|
|
||||||
"More than one changelog category specified: '"
|
|
||||||
+ category
|
|
||||||
+ "', '"
|
|
||||||
+ second_category
|
|
||||||
+ "'"
|
|
||||||
)
|
|
||||||
return result_status, category
|
|
||||||
|
|
||||||
elif re.match(
|
|
||||||
r"(?i)^[#>*_ ]*(short\s*description|change\s*log\s*entry)", lines[i]
|
|
||||||
):
|
|
||||||
i += 1
|
|
||||||
# Can have one empty line between header and the entry itself.
|
|
||||||
# Filter it out.
|
|
||||||
if i < len(lines) and not lines[i]:
|
|
||||||
i += 1
|
|
||||||
# All following lines until empty one are the changelog entry.
|
|
||||||
entry_lines = []
|
|
||||||
while i < len(lines) and lines[i]:
|
|
||||||
entry_lines.append(lines[i])
|
|
||||||
i += 1
|
|
||||||
entry = " ".join(entry_lines)
|
|
||||||
# Don't accept changelog entries like '...'.
|
|
||||||
entry = re.sub(r"[#>*_.\- ]", "", entry)
|
|
||||||
# Don't accept changelog entries like 'Close #12345'.
|
|
||||||
entry = re.sub(r"^[\w\-\s]{0,10}#?\d{5,6}\.?$", "", entry)
|
|
||||||
else:
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
if not category:
|
|
||||||
description_error = "Changelog category is empty"
|
|
||||||
# Filter out the PR categories that are not for changelog.
|
|
||||||
elif re.match(
|
|
||||||
r"(?i)doc|((non|in|not|un)[-\s]*significant)|(not[ ]*for[ ]*changelog)",
|
|
||||||
category,
|
|
||||||
):
|
|
||||||
pass # to not check the rest of the conditions
|
|
||||||
elif category not in CATEGORY_TO_LABEL:
|
|
||||||
description_error, category = f"Category '{category}' is not valid", ""
|
|
||||||
elif not entry:
|
|
||||||
description_error = f"Changelog entry required for category '{category}'"
|
|
||||||
|
|
||||||
return description_error, category
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
@ -224,7 +106,7 @@ def main():
|
|||||||
gh = Github(get_best_robot_token(), per_page=100)
|
gh = Github(get_best_robot_token(), per_page=100)
|
||||||
commit = get_commit(gh, pr_info.sha)
|
commit = get_commit(gh, pr_info.sha)
|
||||||
|
|
||||||
description_error, category = check_pr_description(pr_info)
|
description_error, category = check_pr_description(pr_info.body)
|
||||||
pr_labels_to_add = []
|
pr_labels_to_add = []
|
||||||
pr_labels_to_remove = []
|
pr_labels_to_remove = []
|
||||||
if (
|
if (
|
||||||
@ -262,7 +144,7 @@ def main():
|
|||||||
f"expect adding docs for {FEATURE_LABEL}",
|
f"expect adding docs for {FEATURE_LABEL}",
|
||||||
DOCS_NAME,
|
DOCS_NAME,
|
||||||
)
|
)
|
||||||
else:
|
elif not description_error:
|
||||||
set_mergeable_check(commit, "skipped")
|
set_mergeable_check(commit, "skipped")
|
||||||
|
|
||||||
if description_error:
|
if description_error:
|
||||||
|
Loading…
Reference in New Issue
Block a user