-
Notifications
You must be signed in to change notification settings - Fork 359
/
update_changelog.py
170 lines (140 loc) · 6.7 KB
/
update_changelog.py
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
# Script to update `CHANGELOG.md` in order to release any of the mamba packages
# Steps:
# 1. Run this script to update the root `CHANGELOG.md` file by giving the date of
# the last release as input (cf. last date shown at the top of the file for reference)
# or any other starting date that may be relevant for the release,
# and the release version to be made.
# 2. If you are happy with the changes, run `releaser.py` to update the versions and
# corresponding nested `CHANGELOG.md` files.
# N.B If the release is to be a pre-release (alpha,...), the versions should not be updated in `.py` and `.h` files.
# Only the `CHANGELOG.md` files should be modified.
# If otherwise, please revert the corresponding files if modified by the script.
# 3. Follow the steps described in the `releaser.py` output.
from datetime import date
import json
import re
import subprocess
def validate_date(date_str):
try:
date.fromisoformat(date_str)
except ValueError:
raise ValueError("Incorrect date format, should be YYYY-MM-DD")
def subprocess_run(*args: str, **kwargs) -> str:
"""Execute a command in a subprocess while properly capturing stderr in exceptions."""
try:
p = subprocess.run(
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, **kwargs
)
except subprocess.CalledProcessError as e:
print(f"Command {args} failed with stderr: {e.stderr.decode()}")
print(f"Command {args} failed with stdout: {e.stdout.decode()}")
raise e
return p.stdout
def append_to_file(ctgr_name, prs, out_file):
out_file.write("\n{}:\n\n".format(ctgr_name))
for pr in prs:
# Author
pr_author_cmd = "gh pr view {} --json author".format(pr)
author_login = dict(json.loads(subprocess_run(*pr_author_cmd.split()).decode("utf-8")))[
"author"
]["login"]
# Title
pr_title_cmd = "gh pr view {} --json title".format(pr)
title = dict(json.loads(subprocess_run(*pr_title_cmd.split()).decode("utf-8")))["title"]
# URL
pr_url_cmd = "gh pr view {} --json url".format(pr)
url = dict(json.loads(subprocess_run(*pr_url_cmd.split()).decode("utf-8")))["url"]
# Files
# Use a different command with graphql allowing pagination
# (since number of files retrieved with 'gh pr view {pr} files' is limited to 100)
# cf. https://github.com/cli/cli/issues/5368
graphql_files_cmd = f"gh api graphql -f query='query($owner: String!, $repo: String!, $pr: Int!, $endCursor: String) {{repository(owner: $owner, name: $repo) {{pullRequest(number: $pr) {{files(first: 100, after: $endCursor) {{pageInfo{{ hasNextPage, endCursor }} nodes {{path}}}}}}}}}}' -F owner='mamba-org' -F repo='mamba' -F pr={pr} --paginate --jq '.data.repository.pullRequest.files.nodes.[].path'"
files = subprocess.run(
graphql_files_cmd, shell=True, capture_output=True, text=True
).stdout.split("\n")
ref_mamba_pkgs = ["libmamba/", "libmambapy/", "micromamba/"]
concerned_pkgs = set()
for f in files:
for ref_pkg in ref_mamba_pkgs:
if f.startswith(ref_pkg):
concerned_pkgs.add(ref_pkg)
if (sorted(ref_mamba_pkgs) == sorted(concerned_pkgs)) or (len(concerned_pkgs) == 0):
concerned_pkgs = ["all/"]
# Write in file
out_file.write(
"- [{}] {} by @{} in {}\n".format(
(", ".join([pkg[:-1] for pkg in concerned_pkgs])), title, author_login, url
)
)
def main():
commits_starting_date = input(
"Enter the starting date of commits to be included in the release in the format YYYY-MM-DD: "
)
validate_date(commits_starting_date)
release_version = input("Enter the version to be released: ")
# Get commits to include in the release
log_cmd = "git log --since=" + commits_starting_date
commits = subprocess_run(*log_cmd.split()).decode("utf-8")
# Create the regular expression pattern
opening_char = "(#"
closing_char = ")"
pattern = re.compile(re.escape(opening_char) + "(.*?)" + re.escape(closing_char))
# Get the PRs numbers
prs_nbrs = re.findall(pattern, commits)
# Make three lists to categorize PRs: "Enhancements", "Bug fixes" and "CI fixes and doc"
enhancements_prs = [] # release::enhancements
bug_fixes_prs = [] # release::bug_fixes
ci_docs_prs = [] # release::ci_docs
for pr in prs_nbrs:
# Get labels
pr_labels_cmd = "gh pr view {} --json labels".format(pr)
labels = dict(json.loads(subprocess_run(*pr_labels_cmd.split()).decode("utf-8")))["labels"]
nb_rls_lbls_types = 0
label = ""
for lab in labels:
if lab["name"].startswith("release::"):
nb_rls_lbls_types = nb_rls_lbls_types + 1
label = lab["name"]
# Only one release label should be set
if nb_rls_lbls_types == 0:
raise ValueError("No release label is set for PR #{}".format(pr))
elif nb_rls_lbls_types > 1:
raise ValueError(
"Only one release label should be set. PR #{} has {} labels.".format(
pr, nb_rls_lbls_types
)
)
# Dispatch PRs with their corresponding label
if label == "release::enhancements":
enhancements_prs.append(pr)
elif label == "release::bug_fixes":
bug_fixes_prs.append(pr)
elif label == "release::ci_docs":
ci_docs_prs.append(pr)
else:
raise ValueError("Unknown release label {} for PR #{}".format(label, pr))
with open("CHANGELOG.md", "r+") as changelog_file:
# Make sure we're appending at the beginning of the file
content_to_restore = changelog_file.read()
changelog_file.seek(0)
# Append new info
# Release date and version
changelog_file.write("{}\n".format(date.today().strftime("%Y.%m.%d")))
changelog_file.write("==========\n")
changelog_file.write(
"\nReleases: libmamba {0}, libmambapy {0}, micromamba {0}\n".format(release_version)
)
# PRs info
if enhancements_prs:
append_to_file("Enhancements", enhancements_prs, changelog_file)
if bug_fixes_prs:
append_to_file("Bug fixes", bug_fixes_prs, changelog_file)
if ci_docs_prs:
append_to_file("CI fixes and doc", ci_docs_prs, changelog_file)
# Write back old content of CHANGELOG file
changelog_file.write("\n" + content_to_restore)
print(
"'CHANGELOG.md' was successfully updated.\nPlease run 'releaser.py' if you agree with the changes applied."
)
if __name__ == "__main__":
main()