1import json
2import re
3import sys
4import subprocess
5import urllib.request
6
7from typing import Iterable, Optional, Tuple
8from urllib.request import urlopen
9from datetime import datetime
10
11# Number of spaces used for each indentation level
12JSON_INDENT = 4
13
14releases_json = None
15
16# Releases that have reached end-of-life no longer receive any updates
17# and it is rather pointless trying to update those.
18#
19# https://endoflife.date/electron
20def supported_version_range() -> range:
21 """Returns a range of electron releases that have not reached end-of-life yet"""
22 global releases_json
23 if releases_json is None:
24 releases_json = json.loads(
25 urlopen("https://endoflife.date/api/electron.json").read()
26 )
27 supported_releases = [
28 int(x["cycle"])
29 for x in releases_json
30 if x["eol"] == False
31 or datetime.strptime(x["eol"], "%Y-%m-%d") > datetime.today()
32 ]
33
34 return range(
35 min(supported_releases), # incl.
36 # We have also packaged the beta release in nixpkgs,
37 # but it is not tracked by endoflife.date
38 max(supported_releases) + 2, # excl.
39 1,
40 )
41
42def get_latest_version(major_version: str) -> Tuple[str, str]:
43 """Returns the latest version for a given major version"""
44 electron_releases: dict = json.loads(
45 urlopen("https://releases.electronjs.org/releases.json").read()
46 )
47 major_version_releases = filter(
48 lambda item: item["version"].startswith(f"{major_version}."), electron_releases
49 )
50 m = max(major_version_releases, key=lambda item: item["date"])
51
52 rev = f"v{m['version']}"
53 return (m, rev)
54
55
56def load_info_json(path: str) -> dict:
57 """Load the contents of a JSON file
58
59 Args:
60 path: The path to the JSON file
61
62 Returns: An empty dict if the path does not exist, otherwise the contents of the JSON file.
63 """
64 try:
65 with open(path, "r") as f:
66 return json.loads(f.read())
67 except:
68 return {}
69
70
71def save_info_json(path: str, content: dict) -> None:
72 """Saves the given info to a JSON file
73
74 Args:
75 path: The path where the info should be saved
76 content: The content to be saved as JSON.
77 """
78 with open(path, "w") as f:
79 f.write(json.dumps(content, indent=JSON_INDENT, default=vars, sort_keys=True))
80 f.write("\n")
81
82
83def parse_cve_numbers(tag_name: str) -> Iterable[str]:
84 """Returns mentioned CVE numbers from a given release tag"""
85 cve_pattern = r"CVE-\d{4}-\d+"
86 url = f"https://api.github.com/repos/electron/electron/releases/tags/{tag_name}"
87 headers = {
88 "Accept": "application/vnd.github+json",
89 "X-GitHub-Api-Version": "2022-11-28",
90 }
91 request = urllib.request.Request(url=url, headers=headers)
92 release_note = ""
93 try:
94 with urlopen(request) as response:
95 release_note = json.loads(response.read().decode("utf-8"))["body"]
96 except:
97 print(
98 f"WARN: Fetching release note for {tag_name} from GitHub failed!",
99 file=sys.stderr,
100 )
101
102 return sorted(re.findall(cve_pattern, release_note))
103
104
105def commit_result(
106 package_name: str, old_version: Optional[str], new_version: str, path: str
107) -> None:
108 """Creates a git commit with a short description of the change
109
110 Args:
111 package_name: The package name, e.g. `electron-source.electron-{major_version}`
112 or `electron_{major_version}-bin`
113
114 old_version: Version number before the update.
115 Can be left empty when initializing a new release.
116
117 new_version: Version number after the update.
118
119 path: Path to the lockfile to be committed
120 """
121 assert (
122 isinstance(package_name, str) and len(package_name) > 0
123 ), "Argument `package_name` cannot be empty"
124 assert (
125 isinstance(new_version, str) and len(new_version) > 0
126 ), "Argument `new_version` cannot be empty"
127
128 if old_version != new_version:
129 major_version = new_version.split(".")[0]
130 cve_fixes_text = "\n".join(
131 list(
132 map(lambda cve: f"- Fixes {cve}", parse_cve_numbers(f"v{new_version}"))
133 )
134 )
135 init_msg = f"init at {new_version}"
136 update_msg = f"{old_version} -> {new_version}"
137 diff = (
138 f"- Diff: https://github.com/electron/electron/compare/refs/tags/v{old_version}...v{new_version}\n"
139 if old_version != None
140 else ""
141 )
142 commit_message = f"""{package_name}: {update_msg if old_version != None else init_msg}
143
144- Changelog: https://github.com/electron/electron/releases/tag/v{new_version}
145{diff}{cve_fixes_text}
146"""
147 subprocess.run(
148 [
149 "git",
150 "add",
151 path,
152 ]
153 )
154 subprocess.run(
155 [
156 "git",
157 "commit",
158 "-m",
159 commit_message,
160 ]
161 )