4 from typing import NamedTuple, TextIO
5 from datetime import datetime
7 # TODO: remove after testing
10 # end remove after testing
13 MR_TYPE = Enum("MR_TYPE", {"Feature(s)": 1,
16 "NO_TYPE_GIVEN": 9999})
19 MR_SIZE = Enum("MR_SIZE", {"Enormous": 1,
26 TOPIC_PREFIX = "Topic: "
27 CHANGELOG_PREFIX = "RN::"
28 MR_TYPE_PREFIX = "MR Content: "
29 MR_SIZE_PREFIX = "MR Size::"
31 MAIN_PROJECT_ID = 73434
32 EXCLUDED_PROJECT_IDS = []
33 TARGET_BRANCHES = ["master", "develop", "pending-release"]
35 GROUP_NAME = "xonotic"
36 BASEURL = "https://gitlab.com/api/v4"
37 MAIN_PROJECT_BASEURL = BASEURL + f"/projects/{MAIN_PROJECT_ID}/repository"
38 GROUP_BASEURL = BASEURL + f"/groups/{GROUP_NAME}"
41 class MergeRequestInfo(NamedTuple):
49 def get_time_of_latest_release() -> str:
50 response = requests.get(MAIN_PROJECT_BASEURL + "/tags")
51 latest = response.json()[0]
52 return latest["commit"]["created_at"]
55 def get_merge_requests(timestamp: str) -> list[dict]:
56 if os.path.isfile("testdata.json"):
57 with open("testdata.json") as f:
61 url = GROUP_BASEURL + "/merge_requests?state=merged&updated_after=" +\
62 f"{timestamp}&per_page={page_len}&page="
66 response = requests.get(url + str(current_page))
67 new_data = response.json()
71 if len(new_data) < page_len:
73 if current_page == MAX_PAGES:
79 def process_description(description: str) -> str:
81 raise ValueError("Empty description")
82 lines = description.splitlines()
83 if not lines[0].strip() == "Summary for release notes:":
84 raise ValueError("Unexpected description format: Summary missing")
86 for line in lines[1:]:
87 if line.startswith("---"):
91 summary += line + " " # add space
92 return summary.strip()
96 def process(timestamp: datetime, data: list[dict]) -> dict[MR_TYPE, dict[str, MergeRequestInfo]]:
97 # extract type, size and topic from labels for easier filtering/ordering
98 # extract short description from description
99 # extract author->name
100 processed_data = {mr_type: {} for mr_type in MR_TYPE}
102 if item["project_id"] in EXCLUDED_PROJECT_IDS:
104 if item["target_branch"] not in TARGET_BRANCHES:
106 # Workaround for missing merge information
107 if "merged_at" not in item or not isinstance(item["merged_at"], str):
108 logging.warning(f"Invalid merge information for {item['iid']} "
109 f"(project: {item['project_id']})")
111 # GitLab's rest API doesn't offer a way to filter by "merged_after", so
112 # check the "merge_at" field
113 if datetime.fromisoformat(item["merged_at"]) < timestamp:
115 mr_type = MR_TYPE.NO_TYPE_GIVEN
116 size = MR_SIZE.UNKNOWN
117 section = "UNKNOWN SECTION"
118 for label in item["labels"]:
119 if label.startswith(MR_TYPE_PREFIX):
121 new_mr_type = MR_TYPE[label.removeprefix(MR_TYPE_PREFIX)]
123 logging.warning(f"Unexpected label: {label}, skipping")
125 if new_mr_type.value < mr_type.value:
126 mr_type = new_mr_type
128 if label.startswith(MR_SIZE_PREFIX):
130 new_size = MR_SIZE[label.removeprefix(MR_SIZE_PREFIX)]
132 logging.warning(f"Unexpected label: {label}, skipping")
134 if new_size.value < size.value:
137 if label.startswith(CHANGELOG_PREFIX):
138 section = label.removeprefix(CHANGELOG_PREFIX)
141 short_desc = process_description(item["description"])
142 except ValueError as e:
143 logging.warning(f"Error processing the description for "
144 f"{item['iid']}: {e}")
145 short_desc = item["title"]
146 author = item["author"]["name"]
147 if section not in processed_data[mr_type]:
148 processed_data[mr_type][section] = []
149 processed_data[mr_type][section].append(MergeRequestInfo(
150 iid=item["iid"], size=size, author=author,
151 short_desc=short_desc, web_url=item["web_url"]))
152 return processed_data
155 def draft_releasenotes(fp: TextIO, data: dict[MR_TYPE, dict[str, MergeRequestInfo]]) -> None:
156 fp.writelines(["Release Notes\n", "===\n", "\n"])
157 for mr_type, sectioned_mr_data in data.items():
159 for section, merge_requests in sectioned_mr_data.items():
161 merge_requests.sort(key=lambda x: x.size.value)
162 for item in merge_requests:
163 authors = item.author
164 formatted_items.append(f"- {item.short_desc} by {authors} "
165 f"([{item.iid}]({item.web_url}))\n")
168 fp.writelines([f"{mr_type.name}\n", "---\n"])
170 fp.writelines([f"### {section}\n", *formatted_items])
175 release_timestamp_str = get_time_of_latest_release()
176 release_timestamp = datetime.fromisoformat(release_timestamp_str)
177 merge_requests = get_merge_requests(release_timestamp_str)
178 processed_data = process(release_timestamp, merge_requests)
179 with open(f"RN_draft_since_{release_timestamp_str}.md", "w") as f:
180 draft_releasenotes(f, processed_data)
183 if __name__ == "__main__":