]> git.xonotic.org Git - xonotic/xonotic.git/blob - misc/infrastructure/prepare_releasenotes.py
0532a7e41054e8656f85f151f20ecbe5d62c78d1
[xonotic/xonotic.git] / misc / infrastructure / prepare_releasenotes.py
1 from enum import Enum
2 import logging
3 import requests
4 from typing import NamedTuple, TextIO
5
6 # TODO: remove after testing
7 import os
8 import json
9 # end remove after testing
10
11
12 MR_TYPE = Enum("MR_TYPE", {"Feature(s)": 1,
13                            "Fix(es)": 2,
14                            "Refactoring": 3,
15                            "NO_TYPE_GIVEN": 9999})
16
17 # for ordering
18 MR_SIZE = Enum("MR_SIZE", {"Enormous": 1,
19                            "Large": 2,
20                            "Medium": 3,
21                            "Small": 4,
22                            "Tiny": 5,
23                            "UNKNOWN": 6})
24
25 TOPIC_PREFIX = "Topic: "
26 CHANGELOG_PREFIX = "RN::"
27 MR_TYPE_PREFIX = "MR Content: "
28 MR_SIZE_PREFIX = "MR Size::"
29
30 MAIN_PROJECT_ID = 73434
31 EXCLUDED_PROJECT_IDS = []
32 TARGET_BRANCHES = ["master", "develop", "pending-release"]
33
34 GROUP_NAME = "xonotic"
35 BASEURL = "https://gitlab.com/api/v4"
36 MAIN_PROJECT_BASEURL = BASEURL + f"/projects/{MAIN_PROJECT_ID}/repository"
37 GROUP_BASEURL = BASEURL + f"/groups/{GROUP_NAME}"
38
39
40 class MergeRequestInfo(NamedTuple):
41     iid: int
42     size: MR_SIZE
43     author: str
44     short_desc: str
45     web_url: str
46
47
48 def get_time_of_latest_release() -> str:
49     response = requests.get(MAIN_PROJECT_BASEURL + "/tags")
50     latest = response.json()[0]
51     return latest["commit"]["created_at"]
52
53
54 def get_merge_requests(timestamp: str) -> list[dict]:
55     if os.path.isfile("testdata.json"):
56         with open("testdata.json") as f:
57             return json.load(f)
58     page_len = 10
59     MAX_PAGES = 100
60     url = GROUP_BASEURL + "/merge_requests?state=merged&updated_after=" +\
61         f"{timestamp}&per_page={page_len}&page="
62     current_page = 1
63     data = []
64     while True:
65         response = requests.get(url + str(current_page))
66         new_data = response.json()
67         if not new_data:
68             break
69         data.extend(new_data)
70         if len(new_data) < page_len:
71             break
72         if current_page == MAX_PAGES:
73             break
74         current_page += 1
75     return data
76
77
78 def process_description(description: str) -> str:
79     if not description:
80         raise ValueError("Empty description")
81     lines = description.splitlines()
82     if not lines[0].strip() == "Summary for release notes:":
83         raise ValueError("Unexpected description format: Summary missing")
84     summary = ""
85     for line in lines[1:]:
86         if line.startswith("---"):
87             continue
88         if not line:
89             break
90         summary += line + " " # add space
91     return summary.strip()
92
93
94
95 def process(data: list[dict]) -> dict[MR_TYPE, dict[str, MergeRequestInfo]]:
96     # extract type, size and topic from labels for easier filtering/ordering
97     # extract short description from description
98     # extract author->name
99     processed_data = {mr_type: {} for mr_type in MR_TYPE}
100     for item in data:
101         if item["project_id"] in EXCLUDED_PROJECT_IDS:
102             continue
103         if item["target_branch"] not in TARGET_BRANCHES:
104             continue
105         mr_type = MR_TYPE.NO_TYPE_GIVEN
106         size = MR_SIZE.UNKNOWN
107         section = "UNKNOWN SECTION"
108         for label in item["labels"]:
109             if label.startswith(MR_TYPE_PREFIX):
110                 try:
111                     new_mr_type = MR_TYPE[label.removeprefix(MR_TYPE_PREFIX)]
112                 except KeyError:
113                     logging.warning(f"Unexpected label: {label}, skipping")
114                     continue
115                 if new_mr_type.value < mr_type.value:
116                     mr_type = new_mr_type
117                 continue
118             if label.startswith(MR_SIZE_PREFIX):
119                 try:
120                     new_size = MR_SIZE[label.removeprefix(MR_SIZE_PREFIX)]
121                 except KeyError:
122                     logging.warning(f"Unexpected label: {label}, skipping")
123                     continue
124                 if new_size.value < size.value:
125                     size = new_size
126                 continue
127             if label.startswith(CHANGELOG_PREFIX):
128                 section = label.removeprefix(CHANGELOG_PREFIX)
129                 continue
130         try:
131             short_desc = process_description(item["description"])
132         except ValueError as e:
133             logging.warning(f"Error processing the description for "
134                             f"{item['iid']}: {e}")
135             short_desc = item["title"]
136         author = item["author"]["name"]
137         if section not in processed_data[mr_type]:
138             processed_data[mr_type][section] = []
139         processed_data[mr_type][section].append(MergeRequestInfo(
140             iid=item["iid"], size=size, author=author,
141             short_desc=short_desc, web_url=item["web_url"]))
142     return processed_data
143
144
145 def draft_releasenotes(fp: TextIO, data: dict[MR_TYPE, dict[str, MergeRequestInfo]]) -> None:
146     fp.writelines(["Release Notes\n", "===\n", "\n"])
147     for mr_type, sectioned_mr_data in data.items():
148         type_written = False
149         for section, merge_requests in sectioned_mr_data.items():
150             formatted_items = []
151             merge_requests.sort(key=lambda x: x.size.value)
152             for item in merge_requests:
153                 authors = item.author
154                 formatted_items.append(f"- {item.short_desc} by {authors} "
155                                        f"([{item.iid}]({item.web_url}))\n")
156             if formatted_items:
157                 if not type_written:
158                     fp.writelines([f"{mr_type.name}\n", "---\n"])
159                     type_written = True
160                 fp.writelines([f"### {section}\n", *formatted_items])
161                 fp.write("\n")
162
163
164 def main() -> None:
165     release_timestamp = get_time_of_latest_release()
166     merge_requests = get_merge_requests(release_timestamp)
167     processed_data = process(merge_requests)
168     with open(f"RN_draft_since_{release_timestamp}.md", "w") as f:
169         draft_releasenotes(f, processed_data)
170
171
172 if __name__ == "__main__":
173     main()