update_mute_issues.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. import os
  2. import re
  3. import requests
  4. from github import Github
  5. from urllib.parse import quote, urlencode
  6. ORG_NAME = 'ydb-platform'
  7. REPO_NAME = 'ydb'
  8. PROJECT_ID = '45'
  9. TEST_HISTORY_DASHBOARD = "https://datalens.yandex/4un3zdm0zcnyr"
  10. CURRENT_TEST_HISTORY_DASHBOARD = "https://datalens.yandex/34xnbsom67hcq?"
  11. # Github api (personal access token (classic)) token shoud have permitions to
  12. # repo
  13. # - repo:status
  14. # - repo_deployment
  15. # - public_repo
  16. # admin:org
  17. # project
  18. def run_query(query, variables=None):
  19. GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
  20. HEADERS = {"Authorization": f"Bearer {GITHUB_TOKEN}", "Content-Type": "application/json"}
  21. request = requests.post(
  22. 'https://api.github.com/graphql', json={'query': query, 'variables': variables}, headers=HEADERS
  23. )
  24. if request.status_code == 200:
  25. return request.json()
  26. else:
  27. raise Exception(f"Query failed to run by returning code of {request.status_code}. {query}")
  28. def get_repository(org_name=ORG_NAME, repo_name=REPO_NAME):
  29. query = """
  30. {
  31. organization(login: "%s") {
  32. repository(name: "%s") {
  33. id
  34. }
  35. }
  36. }
  37. """ % (
  38. org_name,
  39. repo_name,
  40. )
  41. result = run_query(query)
  42. return result['data']['organization']['repository']
  43. def get_project_v2_fields(org_name=ORG_NAME, project_id=PROJECT_ID):
  44. query_template = """
  45. {
  46. organization(login: "%s") {
  47. projectV2(number: %s) {
  48. id
  49. fields(first: 100) {
  50. nodes {
  51. ... on ProjectV2Field {
  52. id
  53. name
  54. }
  55. ... on ProjectV2SingleSelectField {
  56. id
  57. name
  58. options {
  59. id
  60. name
  61. }
  62. }
  63. }
  64. }
  65. }
  66. }
  67. }
  68. """
  69. query = query_template % (org_name, project_id)
  70. result = run_query(query)
  71. return (
  72. result['data']['organization']['projectV2']['id'],
  73. result['data']['organization']['projectV2']['fields']['nodes'],
  74. )
  75. def create_and_add_issue_to_project(title, body, project_id=PROJECT_ID, org_name=ORG_NAME, state=None, owner=None):
  76. """Добавляет issue в проект.
  77. Args:
  78. title (str): Название issue.
  79. body (str): Содержимое issue.
  80. project_id (int): ID проекта.
  81. org_name (str): Имя организации.
  82. Returns:
  83. None
  84. """
  85. result = None
  86. # Получаем ID полей "State" и "Owner"
  87. inner_project_id, project_fields = get_project_v2_fields(org_name, project_id)
  88. state_field_id = None
  89. owner_field_id = None
  90. for field in project_fields:
  91. if field.get('name'):
  92. if field['name'].lower() == "status":
  93. state_field_id = field['id']
  94. state_option_id = None
  95. if state:
  96. for option in field['options']:
  97. if option['name'].lower() == state.lower():
  98. state_option_id = option['id']
  99. break
  100. if field['name'].lower() == "owner":
  101. owner_field_id = field['id']
  102. owner_option_id = None
  103. if owner:
  104. for option in field['options']:
  105. if option['name'].lower() == owner.lower():
  106. owner_option_id = option['id']
  107. break
  108. if not state_field_id or not owner_field_id:
  109. raise Exception(f"Не найдены поля 'State' или 'Owner' в проекте {project_id}")
  110. # get repo
  111. repo = get_repository()
  112. # create issue
  113. query = """
  114. mutation ($repositoryId: ID!, $title: String!, $body: String!) {
  115. createIssue(input: {repositoryId: $repositoryId, title: $title, body: $body}) {
  116. issue {
  117. id,
  118. url
  119. }
  120. }
  121. }
  122. """
  123. variables = {"repositoryId": repo['id'], "title": title, "body": body}
  124. issue = run_query(query, variables)
  125. if not issue.get('errors'):
  126. print(f"Issue {title} created ")
  127. else:
  128. print(f"Error: Issue {title} not created ")
  129. return result
  130. issue_id = issue['data']['createIssue']['issue']['id']
  131. issue_url = issue['data']['createIssue']['issue']['url']
  132. query_add_to_project = """
  133. mutation ($projectId: ID!, $issueId: ID!) {
  134. addProjectV2ItemById(input: {projectId: $projectId, contentId: $issueId}) {
  135. item {
  136. id
  137. }
  138. }
  139. }
  140. """
  141. variables = {
  142. "projectId": inner_project_id,
  143. "issueId": issue_id,
  144. }
  145. result_add_to_project = run_query(query_add_to_project, variables)
  146. item_id = result_add_to_project['data']['addProjectV2ItemById']['item']['id']
  147. if not result_add_to_project.get('errors'):
  148. print(f"Issue {issue_url} added to project.")
  149. else:
  150. print(f"Error: Issue {title}: {issue_url} not added to project.")
  151. return result
  152. for field_name, filed_value, field_id, value_id in [
  153. ['state', state, state_field_id, state_option_id],
  154. ['owner', owner, owner_field_id, owner_option_id],
  155. ]:
  156. query_modify_fields = """
  157. mutation ($projectId: ID!, $itemId: ID!, $FieldId: ID!, $OptionId: String) {
  158. updateProjectV2ItemFieldValue(input: {
  159. projectId: $projectId,
  160. itemId: $itemId,
  161. fieldId: $FieldId,
  162. value: {
  163. singleSelectOptionId: $OptionId
  164. }
  165. }) {
  166. projectV2Item {
  167. id
  168. }
  169. }
  170. }
  171. """
  172. variables = {
  173. "projectId": inner_project_id,
  174. "itemId": item_id,
  175. "FieldId": field_id,
  176. "OptionId": value_id,
  177. }
  178. result_modify_field = run_query(query_modify_fields, variables)
  179. if not result_modify_field.get('errors'):
  180. print(f"Issue {title}: {issue_url} modified :{field_name} = {filed_value}")
  181. else:
  182. print(f"Error: Issue {title}: {issue_url} not modified")
  183. return result
  184. result = {'issue_url': issue_url, 'owner': owner, 'title': title}
  185. return result
  186. def fetch_all_issues(org_name=ORG_NAME, project_id=PROJECT_ID):
  187. issues = []
  188. has_next_page = True
  189. end_cursor = "null"
  190. project_issues_query = """
  191. {
  192. organization(login: "%s") {
  193. projectV2(number: %s) {
  194. id
  195. title
  196. items(first: 100, after: %s) {
  197. nodes {
  198. content {
  199. ... on Issue {
  200. id
  201. title
  202. url
  203. state
  204. body
  205. createdAt
  206. }
  207. }
  208. fieldValues(first: 20) {
  209. nodes {
  210. ... on ProjectV2ItemFieldSingleSelectValue {
  211. field {
  212. ... on ProjectV2SingleSelectField {
  213. name
  214. }
  215. }
  216. name
  217. id
  218. updatedAt
  219. }
  220. ... on ProjectV2ItemFieldLabelValue {
  221. labels(first: 20) {
  222. nodes {
  223. id
  224. name
  225. }
  226. }
  227. }
  228. ... on ProjectV2ItemFieldTextValue {
  229. text
  230. id
  231. updatedAt
  232. creator {
  233. url
  234. }
  235. }
  236. ... on ProjectV2ItemFieldMilestoneValue {
  237. milestone {
  238. id
  239. }
  240. }
  241. ... on ProjectV2ItemFieldRepositoryValue {
  242. repository {
  243. id
  244. url
  245. }
  246. }
  247. }
  248. }
  249. }
  250. pageInfo {
  251. hasNextPage
  252. endCursor
  253. }
  254. }
  255. }
  256. }
  257. }
  258. """
  259. while has_next_page:
  260. query = project_issues_query % (org_name, project_id, end_cursor)
  261. result = run_query(query)
  262. if result:
  263. project_items = result['data']['organization']['projectV2']['items']
  264. issues.extend(project_items['nodes'])
  265. page_info = project_items['pageInfo']
  266. has_next_page = page_info['hasNextPage']
  267. end_cursor = f"\"{page_info['endCursor']}\"" if page_info['endCursor'] else "null"
  268. else:
  269. has_next_page = False
  270. return issues
  271. def generate_github_issue_title_and_body(test_data):
  272. owner = test_data[0]['owner']
  273. branch = test_data[0]['branch']
  274. test_full_names = [f"{d['full_name']}" for d in test_data]
  275. test_mute_strings = [f"{d['mute_string']}" for d in test_data]
  276. summary = [
  277. f"{d['test_name']}: {d['state']} last {d['days_in_state']} days, at {d['date_window']}: success_rate {d['success_rate']}%, {d['summary']}"
  278. for d in test_data
  279. ]
  280. # Title
  281. if len(test_full_names) > 1:
  282. title = f'Mute {test_data[0]["suite_folder"]} {len(test_full_names)} tests'
  283. else:
  284. title = f'Mute {test_data[0]["full_name"]}'
  285. # Преобразование списка тестов в строку и кодирование
  286. test_string = "\n".join(test_full_names)
  287. test_mute_strings_string = "\n".join(test_mute_strings)
  288. summary_string = "\n".join(summary)
  289. # Создаем ссылку на историю тестов, кодируя параметры
  290. test_run_history_params = "&".join(
  291. urlencode({"full_name": f"__in_{test}"})
  292. for test in test_full_names
  293. )
  294. test_run_history_link = f"{CURRENT_TEST_HISTORY_DASHBOARD}{test_run_history_params}"
  295. # owner
  296. # Тело сообщения и кодирование
  297. body_template = (
  298. f"Mute:<!--mute_list_start-->\n"
  299. f"{test_string}\n"
  300. f"<!--mute_list_end-->\n\n"
  301. f"Branch:<!--branch_list_start-->\n"
  302. f"{branch}\n"
  303. f"<!--branch_list_end-->\n\n"
  304. f"**Add line to [muted_ya.txt](https://github.com/ydb-platform/ydb/blob/main/.github/config/muted_ya.txt):**\n"
  305. "```\n"
  306. f"{test_mute_strings_string}\n"
  307. "```\n\n"
  308. f"Owner: {owner}\n\n"
  309. "**Read more in [mute_rules.md](https://github.com/ydb-platform/ydb/blob/main/.github/config/mute_rules.md)**\n\n"
  310. f"**Summary history:** \n {summary_string}\n"
  311. "\n\n"
  312. f"**Test run history:** [link]({test_run_history_link})\n\n"
  313. f"More info in [dashboard]({TEST_HISTORY_DASHBOARD})"
  314. )
  315. return (
  316. title,
  317. body_template,
  318. )
  319. def parse_body(body):
  320. tests = []
  321. branches = []
  322. prepared_body = ''
  323. start_mute_list = "<!--mute_list_start-->"
  324. end_mute_list = "<!--mute_list_end-->"
  325. start_branch_list = "<!--branch_list_start-->"
  326. end_branch_list = "<!--branch_list_end-->"
  327. # tests
  328. if all(x in body for x in [start_mute_list, end_mute_list]):
  329. idx1 = body.find(start_mute_list)
  330. idx2 = body.find(end_mute_list)
  331. lines = body[idx1 + len(start_mute_list) + 1 : idx2].split('\n')
  332. else:
  333. if body.startswith('Mute:'):
  334. prepared_body = body.split('Mute:', 1)[1].strip()
  335. elif body.startswith('Mute'):
  336. prepared_body = body.split('Mute', 1)[1].strip()
  337. elif body.startswith('ydb'):
  338. prepared_body = body
  339. lines = prepared_body.split('**Add line to')[0].split('\n')
  340. tests = [line.strip() for line in lines if line.strip().startswith('ydb/')]
  341. # branch
  342. if all(x in body for x in [start_branch_list, end_branch_list]):
  343. idx1 = body.find(start_branch_list)
  344. idx2 = body.find(end_branch_list)
  345. branches = body[idx1 + len(start_branch_list) + 1 : idx2].split('\n')
  346. else:
  347. branches = ['main']
  348. return tests, branches
  349. def get_issues_and_tests_from_project(ORG_NAME, PROJECT_ID):
  350. issues = fetch_all_issues(ORG_NAME, PROJECT_ID)
  351. all_issues_with_contet = {}
  352. for issue in issues:
  353. content = issue['content']
  354. if content:
  355. body = content['body']
  356. # for debug
  357. if content['id'] == 'I_kwDOGzZjoM6V3BoE':
  358. print(1)
  359. #
  360. tests, branches = parse_body(body)
  361. field_values = issue.get('fieldValues', {}).get('nodes', [])
  362. for field_value in field_values:
  363. field_name = field_value.get('field', {}).get('name', '').lower()
  364. if field_name == "status" and 'name' in field_value:
  365. status = field_value.get('name', 'N/A')
  366. status_updated = field_value.get('updatedAt', '1970-01-0901T00:00:01Z')
  367. elif field_name == "owner" and 'name' in field_value:
  368. owner = field_value.get('name', 'N/A')
  369. print(f"Issue ID: {content['id']}")
  370. print(f"Title: {content['title']}")
  371. print(f"URL: {content['url']}")
  372. print(f"State: {content['state']}")
  373. print(f"CreatedAt: {content['createdAt']}")
  374. print(f"Status: {status}")
  375. print(f"Status updated: {status_updated}")
  376. print(f"Owner: {owner}")
  377. print("Tests:")
  378. all_issues_with_contet[content['id']] = {}
  379. all_issues_with_contet[content['id']]['title'] = content['title']
  380. all_issues_with_contet[content['id']]['url'] = content['url']
  381. all_issues_with_contet[content['id']]['state'] = content['state']
  382. all_issues_with_contet[content['id']]['createdAt'] = content['createdAt']
  383. all_issues_with_contet[content['id']]['status_updated'] = status_updated
  384. all_issues_with_contet[content['id']]['status'] = status
  385. all_issues_with_contet[content['id']]['owner'] = owner
  386. all_issues_with_contet[content['id']]['tests'] = []
  387. all_issues_with_contet[content['id']]['branches'] = branches
  388. for test in tests:
  389. all_issues_with_contet[content['id']]['tests'].append(test)
  390. print(f"- {test}")
  391. print('\n')
  392. return all_issues_with_contet
  393. def get_muted_tests_from_issues():
  394. issues = get_issues_and_tests_from_project(ORG_NAME, PROJECT_ID)
  395. muted_tests = {}
  396. for issue in issues:
  397. if issues[issue]["status"] == "Muted" and issues[issue]["state"] != 'CLOSED':
  398. for test in issues[issue]['tests']:
  399. if test not in muted_tests:
  400. muted_tests[test] = []
  401. muted_tests[test].append(
  402. {
  403. 'url': issues[issue]['url'],
  404. 'createdAt': issues[issue]['createdAt'],
  405. 'status_updated': issues[issue]['status_updated'],
  406. 'status': issues[issue]['status'],
  407. 'state': issues[issue]['state'],
  408. 'branches': issues[issue]['branches'],
  409. }
  410. )
  411. return muted_tests
  412. def main():
  413. if "GITHUB_TOKEN" not in os.environ:
  414. print("Error: Env variable GITHUB_TOKEN is missing, skipping")
  415. return 1
  416. else:
  417. github_token = os.environ["GITHUB_TOKEN"]
  418. # muted_tests = get_muted_tests_from_issues()
  419. # create_github_issues(tests)
  420. # create_and_add_issue_to_project('test issue','test_issue_body', state = 'Muted', owner = 'fq')
  421. # print(1)
  422. # update_issue_state(muted_tests, github_token, "closed")
  423. if __name__ == "__main__":
  424. main()