1#!/usr/bin/env nix-shell
2#!nix-shell -I nixpkgs=./. -i python3 -p common-updater-scripts gnused nix coreutils python312
3"""
4Updater script for the ocis_5-bin package.
5
6This script fetches an HTML table from a specified URL and parses it to determine the release type
7(either "Rolling" or "Production") of a given software version. It uses the built-in urllib.request
8for fetching the HTML content and the built-in html.parser for parsing the HTML. By relying only on
9standard library modules, we avoid dependencies on third-party libraries, which simplifies deployment
10and improves portability.
11"""
12import urllib.request
13import os
14import subprocess
15import json
16import sys
17from datetime import datetime
18from html.parser import HTMLParser
19
20TRACKING_CHANNEL = "Production" # Either Rolling or Production
21
22GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", None)
23
24MAJOR_VERSION = 5
25PKG_NAME = f"ocis_{MAJOR_VERSION}-5"
26
27class TableParser(HTMLParser):
28 def __init__(self, version):
29 super().__init__()
30 self.version = version
31 self.in_td = False
32 self.current_row = []
33 self.release_type = None
34 self.in_target_row = False
35
36 def handle_starttag(self, tag, attrs):
37 if tag == "td":
38 self.in_td = True
39
40 if tag == "a":
41 href = dict(attrs).get("href", "")
42 if self.version in href:
43 self.in_target_row = True
44
45 def handle_endtag(self, tag):
46 if tag == "td":
47 self.in_td = False
48
49 if tag == "tr" and self.in_target_row:
50 self.release_type = self.current_row[1]
51 self.in_target_row = False
52
53 if tag == "tr":
54 self.current_row = []
55
56 def handle_data(self, data):
57 if self.in_td:
58 self.current_row.append(data.strip())
59
60
61def get_release_type(content, version):
62 parser = TableParser(version)
63 parser.feed(content)
64 return parser.release_type
65
66
67def get_all_versions():
68 """Get versions from GitHub releases with pagination (up to 10 pages)."""
69 versions = []
70 page = 1
71 max_pages = 10
72 per_page = 30
73
74 while page <= max_pages:
75 url = f"https://api.github.com/repos/owncloud/ocis/releases?page={page}&per_page={per_page}"
76 req = urllib.request.Request(url)
77
78 if GITHUB_TOKEN:
79 req.add_header("Authorization", f"Bearer {GITHUB_TOKEN}")
80
81 req.add_header("Accept", "application/vnd.github.v3+json")
82 req.add_header("User-Agent", "ocis-bin-updater-script")
83
84 with urllib.request.urlopen(req) as response:
85 if response.status != 200:
86 raise Exception(f"HTTP request failed with status {response.status}")
87
88 data = response.read()
89 releases = json.loads(data)
90
91 if not releases:
92 break
93
94 for release in releases:
95 version = release["tag_name"].lstrip("v")
96 published_date = datetime.strptime(
97 release["published_at"], "%Y-%m-%dT%H:%M:%SZ"
98 )
99 versions.append({"version": version, "published_date": published_date})
100
101 page += 1
102
103 if len(releases) < per_page:
104 break
105
106 if not versions:
107 raise Exception("No releases found in GitHub API response")
108
109 return versions
110
111
112def get_current_version():
113 result = subprocess.run(
114 [
115 "nix-instantiate",
116 "--eval",
117 "-E",
118 f"with import ./. {{}}; {PKG_NAME}.version or (lib.getVersion {PKG_NAME})",
119 ],
120 capture_output=True,
121 text=True,
122 )
123 result.check_returncode()
124 return result.stdout.strip().strip('"')
125
126
127def get_hash(os_name, arch, version):
128 url = f"https://github.com/owncloud/ocis/releases/download/v{version}/ocis-{version}-{os_name}-{arch}"
129 result = subprocess.run(
130 ["nix-prefetch-url", "--type", "sha256", url], capture_output=True, text=True
131 )
132 result.check_returncode()
133 pkg_hash = result.stdout.strip()
134 result = subprocess.run(
135 ["nix", "hash", "to-sri", f"sha256:{pkg_hash}"], capture_output=True, text=True
136 )
137 result.check_returncode()
138 return result.stdout.strip()
139
140
141def update_source_version(pkg_name, version, hash_value, system):
142 subprocess.run(
143 [
144 "update-source-version",
145 pkg_name,
146 version,
147 hash_value,
148 f"--system={system}",
149 "--ignore-same-version",
150 ],
151 check=True,
152 )
153
154
155def main():
156 print("Fetching all versions from GitHub API (with pagination)...")
157 all_versions = get_all_versions()
158 print(f"Found {len(all_versions)} versions across multiple pages")
159
160 if not all_versions:
161 print("Error: No versions fetched from GitHub API")
162 sys.exit(1)
163
164 # We depend on the fact that versions are sorted reverse chronologically
165 for version in all_versions:
166 if version["version"].startswith(str(MAJOR_VERSION)):
167 latest_version = version
168 break
169 print(f"Latest version from GitHub: {latest_version['version']}")
170
171 nix_current_version = get_current_version()
172 print(f"Current nix version: {nix_current_version}")
173
174 current_version = None
175 for version in all_versions:
176 if nix_current_version == version["version"]:
177 current_version = version
178 break
179
180 if not current_version:
181 available_versions = [v["version"] for v in all_versions]
182 print(
183 f"Error: Cannot find GitHub release for current nix version {nix_current_version}"
184 )
185 print(
186 f"Available versions (searched {len(available_versions)} across multiple pages): {', '.join(available_versions[:10])}..."
187 )
188 sys.exit(1)
189
190 print(f"Found current version {current_version['version']} in GitHub releases")
191
192 if current_version == latest_version:
193 print(f"{PKG_NAME} is already up-to-date: {current_version['version']}")
194 return
195
196 print("Fetching release roadmap information...")
197 roadmap_url = "https://owncloud.dev/ocis/release_roadmap/"
198 try:
199 response = urllib.request.urlopen(roadmap_url)
200 content = response.read().decode("utf-8")
201
202 latest_version_channel = get_release_type(content, latest_version["version"])
203 current_version_channel = get_release_type(content, current_version["version"])
204
205 print(
206 f"Latest version {latest_version['version']} is in channel: {latest_version_channel}"
207 )
208 print(
209 f"Current version {current_version['version']} is in channel: {current_version_channel}"
210 )
211 except Exception as e:
212 print(f"Warning: Failed to fetch release roadmap information: {e}")
213 print("Proceeding with update using latest version")
214 latest_version_channel = TRACKING_CHANNEL
215 current_version_channel = TRACKING_CHANNEL
216
217 target_version = None
218 if latest_version_channel == TRACKING_CHANNEL:
219 target_version = latest_version
220 print(
221 f"Using latest version {latest_version['version']} as it is in the {TRACKING_CHANNEL} channel"
222 )
223 elif latest_version_channel != TRACKING_CHANNEL:
224 print(f"Looking for a newer version in the {TRACKING_CHANNEL} channel...")
225 for version in all_versions:
226 try:
227 channel = get_release_type(content, version["version"])
228 if (
229 channel == TRACKING_CHANNEL
230 and version["published_date"] > current_version["published_date"]
231 ):
232 target_version = version
233 print(
234 f"{PKG_NAME} found newer version {version['version']} in channel {TRACKING_CHANNEL}"
235 )
236 break
237 except Exception as e:
238 print(
239 f"Warning: Failed to determine channel for version {version['version']}: {e}"
240 )
241
242 if not target_version:
243 print(
244 f"{PKG_NAME} could not find newer version in {TRACKING_CHANNEL} than the current {current_version['version']}"
245 )
246 return
247
248 print(
249 f"Updating {PKG_NAME} from {current_version['version']} to {target_version['version']}"
250 )
251
252 systems = [
253 ("darwin", "arm64", "aarch64-darwin"),
254 ("darwin", "amd64", "x86_64-darwin"),
255 ("linux", "arm64", "aarch64-linux"),
256 ("linux", "arm", "armv7l-linux"),
257 ("linux", "amd64", "x86_64-linux"),
258 ("linux", "386", "i686-linux"),
259 ]
260
261 for os_name, arch, system in systems:
262 print(f"Calculating hash for {os_name}-{arch}...")
263 hash_value = get_hash(os_name, arch, target_version["version"])
264 print(f"Updating package for {system}...")
265 update_source_version(PKG_NAME, target_version["version"], hash_value, system)
266
267 print(f"Successfully updated {PKG_NAME} to version {target_version['version']}")
268
269
270if __name__ == "__main__":
271 main()