1
|
1
|
#!/usr/bin/env python3
|
|
2
|
+import argparse
|
2
|
3
|
from datetime import datetime
|
3
|
4
|
import enum
|
4
|
5
|
from pathlib import Path
|
... |
... |
@@ -23,6 +24,11 @@ project_order = { |
23
|
24
|
}
|
24
|
25
|
|
25
|
26
|
|
|
27
|
+class EntryType(enum.IntFlag):
|
|
28
|
+ UPDATE = 0
|
|
29
|
+ ISSUE = 1
|
|
30
|
+
|
|
31
|
+
|
26
|
32
|
class Platform(enum.IntFlag):
|
27
|
33
|
WINDOWS = 8
|
28
|
34
|
MACOS = 4
|
... |
... |
@@ -32,40 +38,12 @@ class Platform(enum.IntFlag): |
32
|
38
|
ALL_PLATFORMS = 8 | 4 | 2 | 1
|
33
|
39
|
|
34
|
40
|
|
35
|
|
-class Issue:
|
36
|
|
- def __init__(self, j):
|
37
|
|
- self.title = j["title"]
|
38
|
|
- self.project, self.number = (
|
39
|
|
- j["references"]["full"].rsplit("/", 2)[-1].split("#")
|
40
|
|
- )
|
41
|
|
- self.number = int(self.number)
|
42
|
|
- self.platform = 0
|
43
|
|
- self.num_platforms = 0
|
44
|
|
- if "Desktop" in j["labels"]:
|
45
|
|
- self.platform = Platform.DESKTOP
|
46
|
|
- self.num_platforms += 3
|
47
|
|
- else:
|
48
|
|
- if "Windows" in j["labels"]:
|
49
|
|
- self.platform |= Platform.WINDOWS
|
50
|
|
- self.num_platforms += 1
|
51
|
|
- if "MacOS" in j["labels"]:
|
52
|
|
- self.platform |= Platform.MACOS
|
53
|
|
- self.num_platforms += 1
|
54
|
|
- if "Linux" in j["labels"]:
|
55
|
|
- self.platform |= Platform.LINUX
|
56
|
|
- self.num_platforms += 1
|
57
|
|
- if "Android" in j["labels"]:
|
58
|
|
- if is_mb and self.num_platforms == 0:
|
59
|
|
- raise Exception(
|
60
|
|
- f"Android-only issue on Mullvad Browser: {j['references']['full']}!"
|
61
|
|
- )
|
62
|
|
- elif not is_mb:
|
63
|
|
- self.platform |= Platform.ANDROID
|
64
|
|
- self.num_platforms += 1
|
65
|
|
- if not self.platform or (is_mb and self.platform == Platform.DESKTOP):
|
66
|
|
- self.platform = Platform.ALL_PLATFORMS
|
67
|
|
- self.num_platforms = 4
|
68
|
|
- self.is_build = "Build System" in j["labels"]
|
|
41
|
+class ChangelogEntry:
|
|
42
|
+ def __init__(self, type_, platform, num_platforms, is_build):
|
|
43
|
+ self.type = type_
|
|
44
|
+ self.platform = platform
|
|
45
|
+ self.num_platforms = num_platforms
|
|
46
|
+ self.is_build = is_build
|
69
|
47
|
|
70
|
48
|
def get_platforms(self):
|
71
|
49
|
if self.platform == Platform.ALL_PLATFORMS:
|
... |
... |
@@ -81,15 +59,78 @@ class Issue: |
81
|
59
|
platforms.append("Android")
|
82
|
60
|
return " + ".join(platforms)
|
83
|
61
|
|
84
|
|
- def __str__(self):
|
85
|
|
- return f"Bug {self.number}: {self.title} [{self.project}]"
|
86
|
|
-
|
87
|
62
|
def __lt__(self, other):
|
|
63
|
+ if self.type != other.type:
|
|
64
|
+ return self.type < other.type
|
|
65
|
+ if self.type == EntryType.UPDATE:
|
|
66
|
+ # Rely on sorting being stable on Python
|
|
67
|
+ return False
|
88
|
68
|
if self.project == other.project:
|
89
|
69
|
return self.number < other.number
|
90
|
70
|
return project_order[self.project] < project_order[other.project]
|
91
|
71
|
|
92
|
72
|
|
|
73
|
+class UpdateEntry(ChangelogEntry):
|
|
74
|
+ def __init__(self, name, version):
|
|
75
|
+ if name == "Firefox" and not is_mb:
|
|
76
|
+ platform = Platform.DESKTOP
|
|
77
|
+ num_platforms = 3
|
|
78
|
+ elif name == "GeckoView":
|
|
79
|
+ platform = Platform.ANDROID
|
|
80
|
+ num_platforms = 3
|
|
81
|
+ else:
|
|
82
|
+ platform = Platform.ALL_PLATFORMS
|
|
83
|
+ num_platforms = 4
|
|
84
|
+ super().__init__(
|
|
85
|
+ EntryType.UPDATE, platform, num_platforms, name == "Go"
|
|
86
|
+ )
|
|
87
|
+ self.name = name
|
|
88
|
+ self.version = version
|
|
89
|
+
|
|
90
|
+ def __str__(self):
|
|
91
|
+ return f"Updated {self.name} to {self.version}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+class Issue(ChangelogEntry):
|
|
95
|
+ def __init__(self, j):
|
|
96
|
+ self.title = j["title"]
|
|
97
|
+ self.project, self.number = (
|
|
98
|
+ j["references"]["full"].rsplit("/", 2)[-1].split("#")
|
|
99
|
+ )
|
|
100
|
+ self.number = int(self.number)
|
|
101
|
+ platform = 0
|
|
102
|
+ num_platforms = 0
|
|
103
|
+ if "Desktop" in j["labels"]:
|
|
104
|
+ platform = Platform.DESKTOP
|
|
105
|
+ num_platforms += 3
|
|
106
|
+ else:
|
|
107
|
+ if "Windows" in j["labels"]:
|
|
108
|
+ platform |= Platform.WINDOWS
|
|
109
|
+ num_platforms += 1
|
|
110
|
+ if "MacOS" in j["labels"]:
|
|
111
|
+ platform |= Platform.MACOS
|
|
112
|
+ num_platforms += 1
|
|
113
|
+ if "Linux" in j["labels"]:
|
|
114
|
+ platform |= Platform.LINUX
|
|
115
|
+ num_platforms += 1
|
|
116
|
+ if "Android" in j["labels"]:
|
|
117
|
+ if is_mb and num_platforms == 0:
|
|
118
|
+ raise Exception(
|
|
119
|
+ f"Android-only issue on Mullvad Browser: {j['references']['full']}!"
|
|
120
|
+ )
|
|
121
|
+ elif not is_mb:
|
|
122
|
+ platform |= Platform.ANDROID
|
|
123
|
+ num_platforms += 1
|
|
124
|
+ if not platform or (is_mb and platform == Platform.DESKTOP):
|
|
125
|
+ platform = Platform.ALL_PLATFORMS
|
|
126
|
+ num_platforms = 4
|
|
127
|
+ is_build = "Build System" in j["labels"]
|
|
128
|
+ super().__init__(EntryType.ISSUE, platform, num_platforms, is_build)
|
|
129
|
+
|
|
130
|
+ def __str__(self):
|
|
131
|
+ return f"Bug {self.number}: {self.title} [{self.project}]"
|
|
132
|
+
|
|
133
|
+
|
93
|
134
|
def sorted_issues(issues):
|
94
|
135
|
issues = [sorted(v) for v in issues.values()]
|
95
|
136
|
return sorted(
|
... |
... |
@@ -99,8 +140,20 @@ def sorted_issues(issues): |
99
|
140
|
)
|
100
|
141
|
|
101
|
142
|
|
102
|
|
-if len(sys.argv) < 2:
|
103
|
|
- print(f"Usage: {sys.argv[0]} version-to-release or #issue-id")
|
|
143
|
+parser = argparse.ArgumentParser()
|
|
144
|
+parser.add_argument("issue_version")
|
|
145
|
+parser.add_argument("--date", help="The date of the release")
|
|
146
|
+parser.add_argument("--firefox", help="New Firefox version (if we rebased)")
|
|
147
|
+parser.add_argument("--tor", help="New Tor version (if updated)")
|
|
148
|
+parser.add_argument("--no-script", help="New NoScript version (if updated)")
|
|
149
|
+parser.add_argument("--openssl", help="New OpenSSL version (if updated)")
|
|
150
|
+parser.add_argument("--ublock", help="New uBlock version (if updated)")
|
|
151
|
+parser.add_argument("--zlib", help="New zlib version (if updated)")
|
|
152
|
+parser.add_argument("--go", help="New Go version (if updated)")
|
|
153
|
+args = parser.parse_args()
|
|
154
|
+
|
|
155
|
+if not args.issue_version:
|
|
156
|
+ parser.print_help()
|
104
|
157
|
sys.exit(1)
|
105
|
158
|
|
106
|
159
|
token_file = Path(__file__).parent / ".changelogs_token"
|
... |
... |
@@ -121,7 +174,7 @@ with token_file.open() as f: |
121
|
174
|
token = f.read().strip()
|
122
|
175
|
headers = {"PRIVATE-TOKEN": token}
|
123
|
176
|
|
124
|
|
-version = sys.argv[1]
|
|
177
|
+version = args.issue_version
|
125
|
178
|
r = requests.get(
|
126
|
179
|
f"{API_URL}/projects/{PROJECT_ID}/issues?labels=Release Prep",
|
127
|
180
|
headers=headers,
|
... |
... |
@@ -132,7 +185,7 @@ if r.status_code == 401: |
132
|
185
|
issue = None
|
133
|
186
|
issues = []
|
134
|
187
|
for i in r.json():
|
135
|
|
- if i["title"].find(sys.argv[1]) != -1:
|
|
188
|
+ if i["title"].find(version) != -1:
|
136
|
189
|
issues.append(i)
|
137
|
190
|
if len(issues) == 1:
|
138
|
191
|
issue = issues[0]
|
... |
... |
@@ -172,20 +225,44 @@ iid = issue["iid"] |
172
|
225
|
|
173
|
226
|
linked = {}
|
174
|
227
|
linked_build = {}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+def add_entry(entry):
|
|
231
|
+ target = linked_build if entry.is_build else linked
|
|
232
|
+ if entry.platform not in target:
|
|
233
|
+ target[entry.platform] = []
|
|
234
|
+ target[entry.platform].append(entry)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+if args.firefox:
|
|
238
|
+ add_entry(UpdateEntry("Firefox", args.firefox))
|
|
239
|
+ if not is_mb:
|
|
240
|
+ add_entry(UpdateEntry("GeckoView", args.firefox))
|
|
241
|
+if args.tor and not is_mb:
|
|
242
|
+ add_entry(UpdateEntry("Tor", args.tor))
|
|
243
|
+if args.no_script:
|
|
244
|
+ add_entry(UpdateEntry("NoScript", args.no_script))
|
|
245
|
+if not is_mb:
|
|
246
|
+ if args.openssl:
|
|
247
|
+ add_entry(UpdateEntry("OpenSSL", args.openssl))
|
|
248
|
+ if args.zlib:
|
|
249
|
+ add_entry(UpdateEntry("zlib", args.zlib))
|
|
250
|
+ if args.go:
|
|
251
|
+ add_entry(UpdateEntry("Go", args.go))
|
|
252
|
+elif args.ublock:
|
|
253
|
+ add_entry(UpdateEntry("uBlock Origin", args.ublock))
|
|
254
|
+
|
175
|
255
|
r = requests.get(
|
176
|
256
|
f"{API_URL}/projects/{PROJECT_ID}/issues/{iid}/links", headers=headers
|
177
|
257
|
)
|
178
|
258
|
for i in r.json():
|
179
|
|
- i = Issue(i)
|
180
|
|
- target = linked_build if i.is_build else linked
|
181
|
|
- if i.platform not in target:
|
182
|
|
- target[i.platform] = []
|
183
|
|
- target[i.platform].append(i)
|
|
259
|
+ add_entry(Issue(i))
|
|
260
|
+
|
184
|
261
|
linked = sorted_issues(linked)
|
185
|
262
|
linked_build = sorted_issues(linked_build)
|
186
|
263
|
|
187
|
264
|
name = "Mullvad" if is_mb else "Tor"
|
188
|
|
-date = datetime.now().strftime("%B %d %Y")
|
|
265
|
+date = args.date if args.date else datetime.now().strftime("%B %d %Y")
|
189
|
266
|
print(f"{name} Browser {version} - {date}")
|
190
|
267
|
for issues in linked:
|
191
|
268
|
print(f" * {issues[0].get_platforms()}")
|