| 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()}")
 |