1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
|
from datetime import datetime
from collections import Counter
import re
import yaml
from typing import Any, List, Dict, Set
from github.Repository import Repository
from common import IssueProcess, Common, get_origin_link_and_tag, IssuePackage
from utils import AUTO_CLOSE_LABEL, get_last_released_date, record_release, get_python_release_pipeline, run_pipeline
# assignee dict which will be assigned to handle issues
_PYTHON_OWNER = {'azure-sdk', 'msyyc'}
_PYTHON_ASSIGNEE = {'ChenxiJiang333'}
# labels
_CONFIGURED = 'Configured'
_AUTO_ASK_FOR_CHECK = 'auto-ask-check'
_BRANCH_ATTENTION = 'base-branch-attention'
_MultiAPI = 'MultiAPI'
# record published issues
_FILE_OUT = 'published_issues_python.csv'
class IssueProcessPython(IssueProcess):
def __init__(self, issue_package: IssuePackage, request_repo: Repository,
assignee_candidates: Set[str], language_owner: Set[str]):
IssueProcess.__init__(self, issue_package, request_repo, assignee_candidates, language_owner)
self.output_folder = '' # network of sdk/network/azure-mgmt-XXX
self.delay_time = self.get_delay_time()
self.python_tag = ''
self.rest_repo_hash = ''
self.language_name = 'python'
def get_delay_time(self):
q = [comment.updated_at
for comment in self.issue_package.issue.get_comments() if comment.user.login not in self.language_owner]
q.sort()
return (datetime.now() - (self.created_time if not q else q[-1])).days
@staticmethod
def get_specefied_param(pattern: str, issue_body_list: List[str]) -> str:
for line in issue_body_list:
if pattern in line:
return line.split(":", 1)[-1].strip()
return ""
def multi_api_policy(self) -> None:
if self.has_label(_MultiAPI) and not self.has_label(_AUTO_ASK_FOR_CHECK):
self.bot_advice.append(_MultiAPI)
def get_edit_content(self) -> None:
self.edit_content = f'\n{self.readme_link.replace("/readme.md", "")}\nReadme Tag: {self.target_readme_tag}'
@property
def is_multiapi(self):
return self.has_label(_MultiAPI)
@property
def readme_comparison(self) -> bool:
# to see whether need change readme
if self.has_label(_CONFIGURED):
return False
if 'package-' not in self.target_readme_tag:
return True
contents = self.get_local_file_content()
pattern_tag = re.compile(r'tag: package-[\w+-.]+')
package_tags = pattern_tag.findall(contents)
whether_same_tag = self.target_readme_tag in package_tags[0]
whether_change_readme = not whether_same_tag or self.is_multiapi
return whether_change_readme
def auto_reply(self) -> None:
if not self.has_label(_AUTO_ASK_FOR_CHECK) or self.has_label(_CONFIGURED):
issue_number = self.issue_package.issue.number
if not self.readme_comparison:
try:
spec_readme = self.readme_link + ('' if self.has_typespec_folder else '/readme.md')
issue_link = self.issue_package.issue.html_url
release_pipeline_url = get_python_release_pipeline(self.output_folder)
res_run = run_pipeline(issue_link=issue_link,
pipeline_url=release_pipeline_url,
spec_readme=spec_readme,
python_tag=self.python_tag,
rest_repo_hash=self.rest_repo_hash,
target_date=self.target_date,
issue_owner=self.owner,
)
if res_run:
self.log(f'{issue_number} run pipeline successfully')
else:
self.log(f'{issue_number} run pipeline fail')
except Exception as e:
self.comment(f'hi @{self.assignee}, please check release-helper: `{e}`')
self.add_label(_AUTO_ASK_FOR_CHECK)
else:
self.log(f'issue {issue_number} need config readme')
if self.has_label(_CONFIGURED):
self.issue_package.issue.remove_from_labels(_CONFIGURED)
def attention_policy(self):
if self.has_label(_BRANCH_ATTENTION):
self.bot_advice.append('new version is 0.0.0, please check base branch!')
def auto_bot_advice(self):
super().auto_bot_advice()
self.multi_api_policy()
self.attention_policy()
def auto_close(self) -> None:
if self.has_label(AUTO_CLOSE_LABEL):
return
last_version, last_time = get_last_released_date(self.package_name)
if last_version and last_time > self.created_time:
comment = f'Hi @{self.owner}, pypi link: https://pypi.org/project/{self.package_name}/{last_version}/'
self.issue_package.issue.create_comment(body=comment)
self.issue_package.issue.edit(state='closed')
self.add_label(AUTO_CLOSE_LABEL)
self.is_open = False
self.log(f"{self.issue_package.issue.number} has been closed!")
record_release(self.package_name, self.issue_package.issue, _FILE_OUT, last_version)
def package_name_output_folder_from_readme(self):
try:
contents = self.get_local_file_content('readme.python.md')
except Exception as e: # pylint: disable=too-broad-except
print(f"fail to read readme.python.md: {e}")
return
pattern_package = re.compile(r'package-name: [\w+-.]+')
pattern_output = re.compile(r'\$\(python-sdks-folder\)/(.*?)/azure-')
self.package_name = pattern_package.search(contents).group().split(':')[-1].strip()
self.output_folder = pattern_output.search(contents).group().split('/')[1]
if 'multi-api' in contents:
self.add_label(_MultiAPI)
def package_name_output_folder_from_tspconfig(self):
try:
contents = self.get_local_file_content('tspconfig.yaml')
except Exception as e: # pylint: disable=too-broad-except
print(f"fail to read tspconfig.yaml: {e}")
return
yaml_contents = yaml.safe_load(contents)
# tspconfig.yaml example: https://github.com/Azure/azure-rest-api-specs/blob/main/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml
self.output_folder = yaml_contents.get("parameters", {}).get("service-dir", {}).get("default", "").split('/')[-1]
emitters = yaml_contents.get("options", {})
for emitter_name in emitters:
if "/typespec-python" in emitter_name:
self.package_name = emitters[emitter_name].get("package-dir", "")
break
@property
def has_typespec_folder(self) -> bool:
return self.local_file("tspconfig.yaml").exists()
def auto_parse(self):
super().auto_parse()
issue_body_list = self.get_issue_body()
self.readme_link = issue_body_list[0].strip("\r\n ")
if not self.package_name and self.has_typespec_folder:
self.package_name_output_folder_from_tspconfig()
if not self.package_name and re.findall(".+/Azure/azure-rest-api-specs/.+/resource-manager", self.readme_link):
self.package_name_output_folder_from_readme()
if not self.package_name:
raise Exception(f"package name not found in readme.python.md or tspconfig.yaml")
# Get the specified tag and rest repo hash in issue body
self.rest_repo_hash = self.get_specefied_param("->hash:", issue_body_list[:5])
self.python_tag = self.get_specefied_param("->Readme Tag:", issue_body_list[:5])
def run(self) -> None:
super().run()
self.auto_reply()
self.auto_close()
class Python(Common):
def __init__(self, issues, language_owner, sdk_assignees):
super(Python, self).__init__(issues, language_owner, sdk_assignees)
self.issue_process_function = IssueProcessPython
if not self.for_test():
self.file_out_name = 'release_python_status.md'
def duplicated_policy(self):
counter = Counter([item.package_name for item in self.result])
for item in self.result:
if counter[item.package_name] > 1:
item.bot_advice.insert(0, 'duplicated issue <br>')
def run(self):
self.proc_issue()
self.duplicated_policy()
self.output()
def python_process(issues: List[Any]) -> Python:
return Python(issues, _PYTHON_OWNER, _PYTHON_ASSIGNEE)
|