
|
81 |
82 | | **Browser** | **Version** | **CFBundle Identifier** | **Download** |
83 | |------------|-------------------|---------------------|------------|
84 | | **Safari Technology Preview
(Tahoe)**
Post Date:December 19, 2025 | `234` | `com.apple.SafariTechnologyPreview` |
|
85 | | **Safari Technology Preview
(Sequoia)**
Post Date:December 19, 2025 | `234` | `com.apple.SafariTechnologyPreview` |
|
86 |
87 |
88 | ## Browser Settings Management
89 |
90 | View your current browser policies and explore available policy options:
91 |
92 | ###

Chrome
93 | 1. **View Current Policies**: Enter `chrome://policy` in your address bar to see active policies
94 | 2. **Available Options**: [Chrome Enterprise Policy Documentation](https://chromeenterprise.google/policies/)
95 |
96 | ###

Firefox
97 | 1. **View Current Policies**: Enter `about:policies` in your address bar to see active policies
98 | 2. **Available Options**: [Firefox Policy Documentation](https://mozilla.github.io/policy-templates/)
99 |
100 | ###

Edge
101 | 1. **View Current Policies**: Enter `edge://policy` in your address bar to see active policies
102 | 2. **Available Options**: [Edge Policy Documentation](https://learn.microsoft.com/en-us/deployedge/microsoft-edge-policies)
103 |
104 | ###

Safari
105 | 1. **View Current Policies**: Open System Settings > Profiles & Device Management
106 | 2. **Available Options**: [Safari Configuration Profile Reference](https://support.apple.com/guide/deployment/welcome/web)
107 |
108 |
--------------------------------------------------------------------------------
/.github/actions/generate_chrome_latest.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | import xml.etree.ElementTree as ET
3 | from xml.dom import minidom
4 | import json
5 | from datetime import datetime
6 | import os
7 | import yaml
8 | from pytz import timezone
9 |
10 | def fetch_chrome_versions(channel):
11 | """
12 | Fetch Chrome version history for a given channel using the Google Version History API.
13 | """
14 | print(f"Fetching Chrome version history for channel: {channel}")
15 | url = f"https://versionhistory.googleapis.com/v1/chrome/platforms/mac/channels/{channel}/versions"
16 | result = subprocess.run(["curl", url], capture_output=True, text=True)
17 | return result.stdout
18 |
19 | def fetch_mac_version(channel):
20 | """
21 | Fetch the latest Mac Chrome version for a given channel.
22 | Returns a dict with version, formatted release time, and timestamp.
23 | """
24 | print(f"Fetching latest Mac version for channel: {channel}")
25 | url = f"https://versionhistory.googleapis.com/v1/chrome/platforms/mac/channels/{channel.lower()}/versions/all/releases?filter=endtime=none"
26 | result = subprocess.run(["curl", "-s", url], capture_output=True, text=True)
27 | if not result.stdout:
28 | return {"version": "N/A", "time": "N/A", "timestamp": "N/A"}
29 | try:
30 | data = json.loads(result.stdout)
31 | releases = data.get("releases", [])
32 | if not releases:
33 | return {"version": "N/A", "time": "N/A", "timestamp": "N/A"}
34 | # Find the release with the highest fraction, or the most recent startTime
35 | latest_release = max(
36 | releases,
37 | key=lambda r: (
38 | r.get("fraction", 0),
39 | r.get("serving", {}).get("startTime", "")
40 | )
41 | )
42 | version = latest_release.get("version", "N/A")
43 | start_time_str = latest_release.get("serving", {}).get("startTime", None)
44 | if start_time_str:
45 | dt = datetime.strptime(start_time_str, "%Y-%m-%dT%H:%M:%S.%fZ")
46 | dt = timezone('US/Eastern').localize(dt)
47 | release_time = dt.strftime("%B %d, %Y %I:%M %p %Z")
48 | timestamp = int(dt.timestamp() * 1000)
49 | else:
50 | release_time = "N/A"
51 | timestamp = "N/A"
52 | return {"version": version, "time": release_time, "timestamp": timestamp}
53 | except Exception as e:
54 | print(f"Error fetching mac version for channel {channel}: {e}")
55 | return {"version": "N/A", "time": "N/A", "timestamp": "N/A"}
56 |
57 | def convert_to_xml(json_data):
58 | """
59 | Convert Chrome version data from JSON to XML format.
60 | """
61 | root = ET.Element("versions")
62 | last_updated = ET.SubElement(root, "last_updated")
63 | last_updated.text = datetime.now(timezone('US/Eastern')).strftime("%B %d, %Y %I:%M %p %Z")
64 | for version in json_data["versions"]:
65 | version_element = ET.SubElement(root, "version")
66 | name_element = ET.SubElement(version_element, "name")
67 | name_element.text = version["name"]
68 | version_element = ET.SubElement(version_element, "version")
69 | version_element.text = version["version"]
70 | return minidom.parseString(ET.tostring(root)).toprettyxml(indent=" ")
71 |
72 | def convert_to_yaml(json_data):
73 | """
74 | Convert Chrome version data from JSON to YAML format.
75 | """
76 | last_updated = {"last_updated": datetime.now(timezone('US/Eastern')).strftime("%B %d, %Y %I:%M %p %Z")}
77 | json_data.update(last_updated)
78 | if "nextPageToken" in json_data and not json_data["nextPageToken"]:
79 | del json_data["nextPageToken"]
80 | return yaml.dump(json_data, default_flow_style=False)
81 |
82 | def convert_to_json(json_data):
83 | """
84 | Convert Chrome version data to JSON string with last_updated.
85 | """
86 | last_updated = {"last_updated": datetime.now(timezone('US/Eastern')).strftime("%B %d, %Y %I:%M %p %Z")}
87 | json_data = {**last_updated, **json_data}
88 | if "nextPageToken" in json_data and not json_data["nextPageToken"]:
89 | del json_data["nextPageToken"]
90 | return json.dumps(json_data, indent=2)
91 |
92 | def convert_mac_versions_to_xml(stable, extended, beta, dev, canary, canary_asan):
93 | """
94 | Convert Mac Chrome channel versions to XML format.
95 | """
96 | root = ET.Element("mac_versions")
97 | last_updated = ET.SubElement(root, "last_updated")
98 | last_updated.text = datetime.now(timezone('US/Eastern')).strftime("%B %d, %Y %I:%M %p %Z")
99 |
100 | stable_element = ET.SubElement(root, "stable")
101 | version_element = ET.SubElement(stable_element, "version")
102 | version_element.text = stable["version"]
103 | time_element = ET.SubElement(stable_element, "release_time")
104 | time_element.text = stable["time"]
105 | download_url = ET.SubElement(stable_element, "download_link")
106 | download_url.text = "https://dl.google.com/chrome/mac/stable/accept_tos%3Dhttps%253A%252F%252Fwww.google.com%252Fintl%252Fen_ph%252Fchrome%252Fterms%252F%26_and_accept_tos%3Dhttps%253A%252F%252Fpolicies.google.com%252Fterms/googlechrome.pkg"
107 |
108 | extended_element = ET.SubElement(root, "extended")
109 | version_element = ET.SubElement(extended_element, "version")
110 | version_element.text = extended["version"]
111 | time_element = ET.SubElement(extended_element, "release_time")
112 | time_element.text = extended["time"]
113 | download_url = ET.SubElement(extended_element, "download_link")
114 | download_url.text = "https://dl.google.com/chrome/mac/stable/accept_tos%3Dhttps%253A%252F%252Fwww.google.com%252Fintl%252Fen_ph%252Fchrome%252Fterms%252F%26_and_accept_tos%3Dhttps%253A%252F%252Fpolicies.google.com%252Fterms/googlechrome.pkg"
115 |
116 | beta_element = ET.SubElement(root, "beta")
117 | version_element = ET.SubElement(beta_element, "version")
118 | version_element.text = beta["version"]
119 | time_element = ET.SubElement(beta_element, "release_time")
120 | time_element.text = beta["time"]
121 | download_url = ET.SubElement(beta_element, "download_link")
122 | download_url.text = "https://dl.google.com/chrome/mac/beta/accept_tos%3Dhttps%253A%252F%252Fwww.google.com%252Fintl%252Fen_ph%252Fchrome%252Fterms%252F%26_and_accept_tos%3Dhttps%253A%252F%252Fpolicies.google.com%252Fterms/googlechrome.pkg"
123 |
124 | dev_element = ET.SubElement(root, "dev")
125 | version_element = ET.SubElement(dev_element, "version")
126 | version_element.text = dev["version"]
127 | time_element = ET.SubElement(dev_element, "release_time")
128 | time_element.text = dev["time"]
129 | download_url = ET.SubElement(dev_element, "download_link")
130 | download_url.text = "https://dl.google.com/chrome/mac/universal/dev/googlechromedev.dmg"
131 |
132 | canary_element = ET.SubElement(root, "canary")
133 | version_element = ET.SubElement(canary_element, "version")
134 | version_element.text = canary["version"]
135 | time_element = ET.SubElement(canary_element, "release_time")
136 | time_element.text = canary["time"]
137 | download_url = ET.SubElement(canary_element, "download_link")
138 | download_url.text = "https://dl.google.com/chrome/mac/universal/canary/googlechromecanary.dmg"
139 |
140 | canary_asan_element = ET.SubElement(root, "canary_asan")
141 | version_element = ET.SubElement(canary_asan_element, "version")
142 | version_element.text = canary_asan["version"]
143 | time_element = ET.SubElement(canary_asan_element, "release_time")
144 | time_element.text = canary_asan["time"]
145 | download_url = ET.SubElement(canary_asan_element, "download_link")
146 | download_url.text = "https://dl.google.com/chrome/mac/universal/canary/googlechromecanary.dmg"
147 |
148 | return minidom.parseString(ET.tostring(root)).toprettyxml(indent=" ")
149 |
150 | def convert_mac_versions_to_yaml(stable, extended, beta, dev, canary, canary_asan):
151 | """
152 | Convert Mac Chrome channel versions to YAML format.
153 | """
154 | last_updated = {"last_updated": datetime.now(timezone('US/Eastern')).strftime("%B %d, %Y %I:%M %p %Z")}
155 | mac_versions = {
156 | "stable": {
157 | "version": stable["version"],
158 | "time": stable["time"],
159 | "download_link": "https://dl.google.com/chrome/mac/stable/accept_tos%3Dhttps%253A%252F%252Fwww.google.com%252Fintl%252Fen_ph%252Fchrome%252Fterms%252F%26_and_accept_tos%3Dhttps%253A%252F%252Fpolicies.google.com%252Fterms/googlechrome.pkg"
160 | },
161 | "extended": {
162 | "version": extended["version"],
163 | "time": extended["time"],
164 | "download_link": "https://dl.google.com/chrome/mac/stable/accept_tos%3Dhttps%253A%252F%252Fwww.google.com%252Fintl%252Fen_ph%252Fchrome%252Fterms%252F%26_and_accept_tos%3Dhttps%253A%252F%252Fpolicies.google.com%252Fterms/googlechrome.pkg"
165 | },
166 | "beta": {
167 | "version": beta["version"],
168 | "time": beta["time"],
169 | "download_link": "https://dl.google.com/chrome/mac/beta/accept_tos%3Dhttps%253A%252F%252Fwww.google.com%252Fintl%252Fen_ph%252Fchrome%252Fterms%252F%26_and_accept_tos%3Dhttps%253A%252F%252Fpolicies.google.com%252Fterms/googlechrome.pkg"
170 | },
171 | "dev": {
172 | "version": dev["version"],
173 | "time": dev["time"],
174 | "download_link": "https://dl.google.com/chrome/mac/universal/dev/googlechromedev.dmg"
175 | },
176 | "canary": {
177 | "version": canary["version"],
178 | "time": canary["time"],
179 | "download_link": "https://dl.google.com/chrome/mac/universal/canary/googlechromecanary.dmg"
180 | },
181 | "canary_asan": {
182 | "version": canary_asan["version"],
183 | "time": canary_asan["time"],
184 | "download_link": "https://dl.google.com/chrome/mac/universal/canary/googlechromecanary.dmg"
185 | }
186 | }
187 | mac_versions = {**last_updated, **mac_versions}
188 | return yaml.dump(mac_versions, default_flow_style=False)
189 |
190 | def convert_mac_versions_to_json(stable, extended, beta, dev, canary, canary_asan):
191 | """
192 | Convert Mac Chrome channel versions to JSON format.
193 | """
194 | mac_versions = {
195 | "stable": {
196 | "version": stable["version"],
197 | "time": stable["time"],
198 | "download_link": "https://dl.google.com/chrome/mac/stable/accept_tos%3Dhttps%253A%252F%252Fwww.google.com%252Fintl%252Fen_ph%252Fchrome%252Fterms%252F%26_and_accept_tos%3Dhttps%253A%252F%252Fpolicies.google.com%252Fterms/googlechrome.pkg"
199 | },
200 | "extended": {
201 | "version": extended["version"],
202 | "time": extended["time"],
203 | "download_link": "https://dl.google.com/chrome/mac/stable/accept_tos%3Dhttps%253A%252F%252Fwww.google.com%252Fintl%252Fen_ph%252Fchrome%252Fterms%252F%26_and_accept_tos%3Dhttps%253A%252F%252Fpolicies.google.com%252Fterms/googlechrome.pkg"
204 | },
205 | "beta": {
206 | "version": beta["version"],
207 | "time": beta["time"],
208 | "download_link": "https://dl.google.com/chrome/mac/beta/accept_tos%3Dhttps%253A%252F%252Fwww.google.com%252Fintl%252Fen_ph%252Fchrome%252Fterms%252F%26_and_accept_tos%3Dhttps%253A%252F%252Fpolicies.google.com%252Fterms/googlechrome.pkg"
209 | },
210 | "dev": {
211 | "version": dev["version"],
212 | "time": dev["time"],
213 | "download_link": "https://dl.google.com/chrome/mac/universal/dev/googlechromedev.dmg"
214 | },
215 | "canary": {
216 | "version": canary["version"],
217 | "time": canary["time"],
218 | "download_link": "https://dl.google.com/chrome/mac/universal/canary/googlechromecanary.dmg"
219 | },
220 | "canary_asan": {
221 | "version": canary_asan["version"],
222 | "time": canary_asan["time"],
223 | "download_link": "https://dl.google.com/chrome/mac/universal/canary/googlechromecanary.dmg"
224 | }
225 | }
226 | last_updated = {"last_updated": datetime.now(timezone('US/Eastern')).strftime("%B %d, %Y %I:%M %p %Z")}
227 | mac_versions = {**last_updated, **mac_versions}
228 | return json.dumps(mac_versions, indent=2)
229 |
230 | def fetch_chrome_history(channel):
231 | """
232 | Fetch Chrome release history for a given channel.
233 | Returns a list of dicts with version, release date, end date, fraction, and fraction group.
234 | """
235 | url = f"https://versionhistory.googleapis.com/v1/chrome/platforms/mac/channels/{channel}/versions/all/releases"
236 | result = subprocess.run(["curl", "-s", url], capture_output=True, text=True)
237 | if not result.stdout:
238 | return []
239 | try:
240 | data = json.loads(result.stdout)
241 | releases = data.get("releases", [])
242 | history = []
243 | for release in releases:
244 | version = release.get("version")
245 | start_time = format_time(release.get("serving", {}).get("startTime"))
246 | end_time = format_time(release.get("serving", {}).get("endTime"))
247 | fraction_val = release.get("fraction", 0)
248 | # Format fraction as a percentage string, up to two decimals
249 | fraction_pct = fraction_val * 100
250 | if fraction_pct == int(fraction_pct):
251 | fraction = f"{int(fraction_pct)}%"
252 | else:
253 | fraction = f"{fraction_pct:.2f}".rstrip('0').rstrip('.') + "%"
254 | fraction_group = release.get("fractionGroup", "N/A")
255 | history.append({
256 | "version": version,
257 | "release_date": start_time,
258 | "end_date": end_time,
259 | "fraction": fraction,
260 | "fraction_group": fraction_group
261 | })
262 | return history
263 | except Exception as e:
264 | print(f"Error processing history: {e}")
265 | return []
266 |
267 | def format_time(iso_time):
268 | """
269 | Convert ISO time string to formatted date string.
270 | """
271 | if not iso_time:
272 | return "N/A"
273 | try:
274 | dt = datetime.strptime(iso_time, "%Y-%m-%dT%H:%M:%S.%fZ")
275 | return dt.strftime("%B %d, %Y %I:%M %p")
276 | except ValueError:
277 | return "Invalid Time"
278 |
279 | def convert_history_to_json(history):
280 | """
281 | Convert Chrome release history to JSON string.
282 | """
283 | return json.dumps({"releases": history}, indent=2)
284 |
285 | def convert_history_to_yaml(history):
286 | """
287 | Convert Chrome release history to YAML string.
288 | """
289 | return yaml.dump({"releases": history}, default_flow_style=False)
290 |
291 | def convert_history_to_xml(history):
292 | """
293 | Convert Chrome release history to XML format.
294 | """
295 | root = ET.Element("releases")
296 | last_updated = ET.SubElement(root, "last_updated")
297 | last_updated.text = datetime.now(timezone('US/Eastern')).strftime("%B %d, %Y %I:%M %p %Z")
298 | for entry in history:
299 | release_element = ET.SubElement(root, "release")
300 | version_element = ET.SubElement(release_element, "version")
301 | version_element.text = entry["version"]
302 | start_time_element = ET.SubElement(release_element, "release_date")
303 | start_time_element.text = entry["release_date"]
304 | end_time_element = ET.SubElement(release_element, "end_date")
305 | end_time_element.text = entry["end_date"]
306 | fraction_element = ET.SubElement(release_element, "fraction")
307 | fraction_element.text = entry["fraction"]
308 | fraction_group_element = ET.SubElement(release_element, "fraction_group")
309 | fraction_group_element.text = str(entry["fraction_group"])
310 | return minidom.parseString(ET.tostring(root)).toprettyxml(indent=" ")
311 |
312 | def main():
313 | """
314 | Main function to fetch, convert, and save Chrome version and history data for all channels.
315 | """
316 | # Create the output directory if it doesn't exist
317 | output_dir = "latest_chrome_files"
318 | os.makedirs(output_dir, exist_ok=True)
319 | print(f"Output directory: {output_dir}")
320 |
321 | channels = [
322 | {"name": "extended", "channelType": "EXTENDED"},
323 | {"name": "stable", "channelType": "STABLE"},
324 | {"name": "beta", "channelType": "BETA"},
325 | {"name": "dev", "channelType": "DEV"},
326 | {"name": "canary", "channelType": "CANARY"},
327 | {"name": "canary_asan", "channelType": "CANARY_ASAN"}
328 | ]
329 |
330 | # Fetch and save version history for each channel in XML, YAML, and JSON
331 | for channel in channels:
332 | print(f"Processing channel: {channel['channelType']}")
333 | try:
334 | json_data = json.loads(fetch_chrome_versions(channel["name"]))
335 | except Exception as e:
336 | print(f"Error fetching versions for {channel['name']}: {e}")
337 | continue
338 | xml_data = convert_to_xml(json_data)
339 | yaml_data = convert_to_yaml(json_data)
340 | json_data_str = convert_to_json(json_data)
341 |
342 | xml_filename = os.path.join(output_dir, f"chrome_{channel['channelType'].lower()}_history.xml")
343 | yaml_filename = os.path.join(output_dir, f"chrome_{channel['channelType'].lower()}_history.yaml")
344 | json_filename = os.path.join(output_dir, f"chrome_{channel['channelType'].lower()}_history.json")
345 |
346 | with open(xml_filename, "w") as xml_file:
347 | xml_file.write(xml_data)
348 | print(f"Wrote XML: {xml_filename}")
349 | with open(yaml_filename, "w") as yaml_file:
350 | yaml_file.write(yaml_data)
351 | print(f"Wrote YAML: {yaml_filename}")
352 | with open(json_filename, "w") as json_file:
353 | json_file.write(json_data_str)
354 | print(f"Wrote JSON: {json_filename}")
355 |
356 | # Fetch and save Mac Stable, Beta, Dev, and Canary versions
357 | mac_channels = ["Stable", "Extended", "Beta", "Dev", "Canary", "Canary_ASAN"]
358 | print("Fetching Mac channel versions...")
359 | mac_versions = {}
360 | for channel in mac_channels:
361 | mac_versions[channel.lower()] = fetch_mac_version(channel)
362 | mac_versions_xml = convert_mac_versions_to_xml(
363 | mac_versions["stable"], mac_versions["extended"], mac_versions["beta"], mac_versions["dev"], mac_versions["canary"], mac_versions["canary_asan"]
364 | )
365 | mac_versions_yaml = convert_mac_versions_to_yaml(
366 | mac_versions["stable"], mac_versions["extended"], mac_versions["beta"], mac_versions["dev"], mac_versions["canary"], mac_versions["canary_asan"]
367 | )
368 | mac_versions_json = convert_mac_versions_to_json(
369 | mac_versions["stable"], mac_versions["extended"], mac_versions["beta"], mac_versions["dev"], mac_versions["canary"], mac_versions["canary_asan"]
370 | )
371 |
372 | xml_filename = os.path.join(output_dir, "chrome_latest_versions.xml")
373 | yaml_filename = os.path.join(output_dir, "chrome_latest_versions.yaml")
374 | json_filename = os.path.join(output_dir, "chrome_latest_versions.json")
375 |
376 | with open(xml_filename, "w") as xml_file:
377 | xml_file.write(mac_versions_xml)
378 | print(f"Wrote Mac XML: {xml_filename}")
379 | with open(yaml_filename, "w") as yaml_file:
380 | yaml_file.write(mac_versions_yaml)
381 | print(f"Wrote Mac YAML: {yaml_filename}")
382 | with open(json_filename, "w") as json_file:
383 | json_file.write(mac_versions_json)
384 | print(f"Wrote Mac JSON: {json_filename}")
385 |
386 | # Fetch and save Chrome release history for all channels
387 | print("Fetching Chrome history for all channels...")
388 | channels = ["stable", "extended", "beta", "dev", "canary", "canary_asan"]
389 | for channel in channels:
390 | print(f"Processing channel: {channel}")
391 | history = fetch_chrome_history(channel)
392 | history_json = convert_history_to_json(history)
393 | history_yaml = convert_history_to_yaml(history)
394 | history_xml = convert_history_to_xml(history)
395 |
396 | json_filename = os.path.join(output_dir, f"chrome_{channel}_history.json")
397 | yaml_filename = os.path.join(output_dir, f"chrome_{channel}_history.yaml")
398 | xml_filename = os.path.join(output_dir, f"chrome_{channel}_history.xml")
399 |
400 | with open(json_filename, "w") as json_file:
401 | json_file.write(history_json)
402 | print(f"Wrote JSON: {json_filename}")
403 | with open(yaml_filename, "w") as yaml_file:
404 | yaml_file.write(history_yaml)
405 | print(f"Wrote YAML: {yaml_filename}")
406 | with open(xml_filename, "w") as xml_file:
407 | xml_file.write(history_xml)
408 | print(f"Wrote XML: {xml_filename}")
409 |
410 | # Convert all *_history.json files to YAML in the output directory
411 | for filename in os.listdir(output_dir):
412 | if filename.endswith("_history.json"):
413 | json_path = os.path.join(output_dir, filename)
414 | yaml_path = os.path.join(output_dir, filename.replace(".json", ".yaml"))
415 | try:
416 | with open(json_path, "r") as f:
417 | data = json.load(f)
418 | with open(yaml_path, "w") as f:
419 | yaml.dump(data, f, sort_keys=False, allow_unicode=True)
420 | except Exception as e:
421 | print(f"Error converting {json_path} to YAML: {e}")
422 |
423 | # Ensure all *_history.json and *_history.yaml files have last_updated at the top level
424 | now_str = datetime.now(timezone('US/Eastern')).strftime("%B %d, %Y %I:%M %p %Z")
425 | for filename in os.listdir(output_dir):
426 | if filename.endswith("_history.json"):
427 | json_path = os.path.join(output_dir, filename)
428 | try:
429 | with open(json_path, "r") as f:
430 | data = json.load(f)
431 | if "last_updated" not in data:
432 | data = {"last_updated": now_str, **data}
433 | with open(json_path, "w") as f:
434 | json.dump(data, f, indent=2)
435 | except Exception as e:
436 | print(f"Error updating last_updated in {json_path}: {e}")
437 | if filename.endswith("_history.yaml"):
438 | yaml_path = os.path.join(output_dir, filename)
439 | try:
440 | with open(yaml_path, "r") as f:
441 | data = yaml.safe_load(f)
442 | if data is not None and "last_updated" not in data:
443 | data = {"last_updated": now_str, **data}
444 | with open(yaml_path, "w") as f:
445 | yaml.dump(data, f, sort_keys=False, allow_unicode=True)
446 | except Exception as e:
447 | print(f"Error updating last_updated in {yaml_path}: {e}")
448 |
449 | if __name__ == "__main__":
450 | main()
451 |
--------------------------------------------------------------------------------
/.github/actions/generate_readme.py:
--------------------------------------------------------------------------------
1 | import xml.etree.ElementTree as ET
2 | import os
3 | from datetime import datetime
4 | from pytz import timezone
5 |
6 | def parse_xml_file(file_path):
7 | """Parse XML file robustly, handling encoding and BOM issues."""
8 | if not os.path.exists(file_path):
9 | raise FileNotFoundError(file_path)
10 | try:
11 | # Try normal parse first
12 | return ET.parse(file_path)
13 | except ET.ParseError:
14 | # Try reading as utf-8-sig to remove BOM if present
15 | with open(file_path, 'r', encoding='utf-8-sig') as f:
16 | content = f.read()
17 | return ET.ElementTree(ET.fromstring(content))
18 |
19 | def read_xml_value(file_path, xpath):
20 | """Generic function to read any value from XML using xpath"""
21 | if not os.path.exists(file_path):
22 | return "N/A"
23 | try:
24 | tree = parse_xml_file(file_path)
25 | root = tree.getroot()
26 |
27 | # For Edge specific handling
28 | if 'edge' in file_path.lower():
29 | # Updated channel map to match new XML format
30 | channel_map = {
31 | 'stable': 'current',
32 | 'dev': 'dev',
33 | 'beta': 'beta',
34 | 'canary': 'canary'
35 | }
36 | base_channel = xpath.split('/')[0]
37 | xml_channel = channel_map.get(base_channel)
38 | if xml_channel:
39 | version_element = root.find(f".//Version[Channel='{xml_channel}']")
40 | if version_element is not None:
41 | return version_element.find('Location' if 'download' in xpath else 'Version').text
42 | return "N/A"
43 |
44 | # For Firefox, look inside package element
45 | elif 'firefox' in file_path.lower():
46 | element = root.find(f'.//package/{xpath}')
47 | else:
48 | element = root.find(xpath)
49 |
50 | return element.text if element is not None else "N/A"
51 | except Exception as e:
52 | print(f"Error processing {file_path}: {str(e)}")
53 | return f"Error: {str(e)}"
54 |
55 | def read_xml_version(file_path):
56 | if not os.path.exists(file_path):
57 | return "N/A"
58 | try:
59 | tree = parse_xml_file(file_path)
60 | root = tree.getroot()
61 |
62 | # For Edge, get the first version
63 | if 'edge' in file_path.lower():
64 | version_element = root.find('.//Version[Channel="current"]/Version')
65 | return version_element.text if version_element is not None else "N/A"
66 |
67 | # For Firefox, get the first version
68 | elif 'firefox' in file_path.lower():
69 | version_element = root.find('.//latest_version')
70 | return version_element.text if version_element is not None else "N/A"
71 |
72 | # For Chrome, get the first version
73 | elif 'chrome' in file_path.lower():
74 | version_element = root.find('.//version')
75 | return version_element.text if version_element is not None else "N/A"
76 |
77 | # For Safari, get the version from Sonoma (latest macOS)
78 | elif 'safari' in file_path.lower():
79 | try:
80 | sonoma_version = root.find('./Sonoma/version')
81 | if sonoma_version is not None and sonoma_version.text:
82 | return sonoma_version.text
83 | print(f"Warning: Could not find Safari version in {file_path}")
84 | return "N/A"
85 | except Exception as e:
86 | print(f"Error reading Safari version: {str(e)}")
87 | return "N/A"
88 |
89 | return "N/A"
90 | except Exception as e:
91 | print(f"Error reading version from {file_path}: {str(e)}")
92 | return f"Error: {str(e)}"
93 |
94 | def get_browser_lines(file_path):
95 | """Get all version lines for a browser"""
96 | if not os.path.exists(file_path):
97 | return []
98 | try:
99 | tree = parse_xml_file(file_path)
100 | root = tree.getroot()
101 | lines = []
102 |
103 | # Different number of lines for each browser
104 | max_lines = {
105 | 'safari': 4,
106 | 'firefox': 6,
107 | 'edge': 6,
108 | 'chrome': 4
109 | }
110 |
111 | browser_type = next((b for b in max_lines.keys() if b in file_path), None)
112 | if (browser_type):
113 | for i in range(1, max_lines[browser_type] + 1):
114 | line = root.find(f'.//line{i}/version')
115 | lines.append(line.text if line is not None else "N/A")
116 |
117 | return lines
118 | except Exception as e:
119 | return [f"Error: {str(e)}"]
120 |
121 | def get_safari_detail(xml_path, os_version, detail_type):
122 | """Get Safari version/URL details by OS version"""
123 | try:
124 | tree = parse_xml_file(xml_path)
125 | root = tree.getroot()
126 | element = root.find(f'.//{os_version}/{detail_type}')
127 | return element.text if element is not None else "N/A"
128 | except Exception as e:
129 | return f"Error: {str(e)}"
130 |
131 | def fetch_chrome_details(xml_path, version_path, download_path):
132 | # Adjust to use 'download_link' instead of 'latest_download'
133 | version = read_xml_value(xml_path, version_path)
134 | # Map download_path to the new tag
135 | # download_path is like 'stable/latest_download' or 'beta/beta_download'
136 | # We want to use the same parent as version_path, but always 'download_link'
137 | parent = version_path.split('/')[0]
138 | download = read_xml_value(xml_path, f"{parent}/download_link")
139 | return version, download
140 |
141 | def fetch_firefox_details(xml_path, version_path, download_path):
142 | # version_path and download_path are now the channel names: 'stable', 'beta', 'dev', 'esr', 'nightly'
143 | if not os.path.exists(xml_path):
144 | return "N/A", "N/A"
145 | try:
146 | tree = parse_xml_file(xml_path)
147 | root = tree.getroot()
148 | channel_elem = root.find(f'.//{version_path}')
149 | if channel_elem is not None:
150 | version_elem = channel_elem.find('version')
151 | download_elem = channel_elem.find('download')
152 | version = version_elem.text if version_elem is not None else "N/A"
153 | download = download_elem.text if download_elem is not None else "N/A"
154 | return version, download
155 | return "N/A", "N/A"
156 | except Exception as e:
157 | return f"Error: {str(e)}", f"Error: {str(e)}"
158 |
159 | def fetch_edge_details(xml_path, version_path, download_path):
160 | try:
161 | tree = parse_xml_file(xml_path)
162 | root = tree.getroot()
163 | # Use new channel mapping
164 | channel_map = {
165 | 'stable': 'current',
166 | 'dev': 'dev',
167 | 'beta': 'beta',
168 | 'canary': 'canary'
169 | }
170 | channel = channel_map.get(version_path, version_path)
171 | version_element = root.find(f".//Version[Channel='{channel}']")
172 | if version_element is not None:
173 | version = version_element.find('Version').text
174 | download = version_element.find('Location').text
175 | return version, download
176 | return "N/A", "N/A"
177 | except Exception as e:
178 | return f"Error: {str(e)}", f"Error: {str(e)}"
179 |
180 | def fetch_safari_details(xml_path, os_version, detail_type):
181 | try:
182 | tree = parse_xml_file(xml_path)
183 | root = tree.getroot()
184 | version = root.find(f'.//{os_version}/version').text
185 | download = root.find(f'.//{os_version}/URL').text
186 | return version, download
187 | except Exception as e:
188 | return f"Error: {str(e)}", f"Error: {str(e)}"
189 |
190 | # --- NEW functions for Safari (releases + tech previews) ---
191 | def fetch_safari_release(xml_path, *args, **kwargs):
192 | """Return the latest
entry's full_version and a link (release_notes or fallback)."""
193 | if not os.path.exists(xml_path):
194 | return "N/A", "#"
195 | try:
196 | tree = parse_xml_file(xml_path)
197 | root = tree.getroot()
198 | release = root.find('release') # first release is the latest in the file format
199 | if release is None:
200 | return "N/A", "#"
201 | # prefer explicit checks to avoid DeprecationWarning for element truth testing
202 | full_elem = release.find('full_version')
203 | major_elem = release.find('major_version')
204 | version = (
205 | full_elem.text if (full_elem is not None and full_elem.text)
206 | else (major_elem.text if (major_elem is not None and major_elem.text) else "N/A")
207 | )
208 | notes_elem = release.find('release_notes')
209 | notes = notes_elem.text if (notes_elem is not None and notes_elem.text) else "#"
210 | # If notes is a doc:// link, keep it; otherwise use it as-is.
211 | return version, notes
212 | except Exception as e:
213 | return f"Error: {str(e)}", "#"
214 |
215 | def fetch_safari_tech_previews(xml_path):
216 | """Return a list of tech preview dicts: [{'macos','version','PostDate','URL','ReleaseNotes'}, ...]"""
217 | previews = []
218 | if not os.path.exists(xml_path):
219 | return previews
220 | try:
221 | tree = parse_xml_file(xml_path)
222 | root = tree.getroot()
223 | for tp in root.findall('Safari_Technology_Preview'):
224 | previews.append({
225 | 'macos': tp.findtext('macos', default='N/A'),
226 | 'version': tp.findtext('version', default='N/A'),
227 | 'post_date': tp.findtext('PostDate', default='N/A'),
228 | 'url': tp.findtext('URL', default='#'),
229 | 'release_notes': tp.findtext('ReleaseNotes', default='#')
230 | })
231 | except Exception:
232 | pass
233 | return previews
234 |
235 | def generate_safari_tech_table(base_path, xml_path):
236 | """Generate a markdown table for Safari Technology Previews that matches other browser rows."""
237 | previews = fetch_safari_tech_previews(xml_path)
238 | if not previews:
239 | return ""
240 |
241 | table = "| **Browser** | **Version** | **CFBundle Identifier** | **Download** |\n"
242 | table += "|------------|-------------------|---------------------|------------|\n"
243 | for p in previews:
244 | display = f"Safari Technology Preview ({p['macos']})"
245 | version = p['version']
246 | bundle_id = "com.apple.SafariTechnologyPreview"
247 | # Use a technology-specific image if available
248 | image = "safari_technology.png"
249 | download = p['url'] if p['url'] and p['url'] != 'N/A' else p['release_notes']
250 | last_updated_html = f"
Post Date:
{p['post_date']}"
251 | # center the image/link inside the table cell
252 | image_html = f'
'
253 | table += (
254 | f"| **{display}** {last_updated_html} | "
255 | f"`{version}` | "
256 | f"`{bundle_id}` | "
257 | f"{image_html} |\n"
258 | )
259 | table += "\n"
260 | return table
261 |
262 | # --- NEW: fetch_all_safari_releases + generator (table matches other browsers) ---
263 | def fetch_all_safari_releases(xml_path):
264 | """Return list of all dicts from the Safari XML (preserves file order)."""
265 | releases = []
266 | if not os.path.exists(xml_path):
267 | return releases
268 | try:
269 | tree = parse_xml_file(xml_path)
270 | root = tree.getroot()
271 | for rel in root.findall('release'):
272 | releases.append({
273 | 'major_version': rel.findtext('major_version', default='N/A'),
274 | 'full_version': rel.findtext('full_version', default='N/A'),
275 | 'released': rel.findtext('released', default='N/A'),
276 | # Prefer an explicit release_notes_url child when present; otherwise use release_notes text
277 | 'release_notes': rel.findtext('release_notes', default='#'),
278 | 'release_notes_url': rel.findtext('release_notes_url', default=None)
279 | })
280 | except Exception:
281 | pass
282 | return releases
283 |
284 | def generate_safari_releases_table(base_path, xml_path):
285 | """Render a dedicated markdown table listing all Safari entries using the same layout as other browsers."""
286 | releases = fetch_all_safari_releases(xml_path)
287 | if not releases:
288 | return ""
289 | # header + separator so Markdown renders this as a proper table
290 | table = "| **Browser** | **Version** | **CFBundle Identifier** | **Release Notes** |\n"
291 | table += "|------------|-------------------|---------------------|------------|\n"
292 | for r in releases:
293 | full_version = r['full_version']
294 | # If version contains 'beta', mark as beta (but do NOT make the beta label bold)
295 | is_beta = 'beta' in (full_version or "").lower()
296 | display_base = "Safari"
297 | # non-bold for beta: show as plain text with a superscript; stable stays bold
298 | if is_beta:
299 | display_cell = f"{display_base} Beta"
300 | else:
301 | display_cell = f"**{display_base}**"
302 | version = full_version
303 | bundle_id = "com.apple.Safari" if is_beta else "com.apple.Safari"
304 | # Use the same Safari logo for both stable and beta
305 | image = "safari.png"
306 |
307 | # Prefer explicit URL node, otherwise fall back to release_notes text
308 | notes_url = r.get('release_notes_url') or r.get('release_notes') or '#'
309 | # Normalize relative developer links to a full URL
310 | if isinstance(notes_url, str) and notes_url and not notes_url.startswith('http'):
311 | if notes_url.startswith('/'):
312 | notes_url = 'https://developer.apple.com' + notes_url
313 | else:
314 | if notes_url.startswith('doc://') or notes_url == '#':
315 | notes_url = '#'
316 |
317 | # Render a Safari-logo icon linking to the release notes (fallback to text if no URL)
318 | if notes_url and notes_url != '#':
319 | # center the image/link inside the table cell
320 | note_link_html = f'
'
321 | else:
322 | note_link_html = '
N/A'
323 |
324 | last_updated_html = f"
Released:{r['released']}"
325 | table += (
326 | f"| {display_cell} {last_updated_html} | "
327 | f"`{version}` | "
328 | f"`{bundle_id}` | "
329 | f"{note_link_html} |\n"
330 | )
331 | table += "\n"
332 | return table
333 | # --- END NEW functions ---
334 |
335 | BROWSER_CONFIGS = {
336 | 'Chrome': {
337 | 'fetch_details': fetch_chrome_details,
338 | 'channels': [
339 | {'name': '', 'display': 'Chrome', 'version_path': 'stable/version', 'download_path': 'stable/download_link', 'bundle_id': 'com.google.Chrome', 'image': 'chrome.png', 'release_notes': 'https://chromereleases.googleblog.com/'},
340 | {'name': 'Extended Stable', 'display': 'Chrome', 'version_path': 'extended/version', 'download_path': 'extended/download_link', 'bundle_id': 'com.google.Chrome', 'image': 'chrome.png', 'release_notes_comment': '
_
Requires `TargetChannel` policy; link is for Stable._'},
341 | {'name': 'Beta', 'display': 'Chrome', 'version_path': 'beta/version', 'download_path': 'beta/download_link', 'bundle_id': 'com.google.Chrome.beta', 'image': 'chrome_beta.png', 'release_notes': 'https://chromereleases.googleblog.com/search/label/Beta%20updates'},
342 | {'name': 'Dev', 'display': 'Chrome', 'version_path': 'dev/version', 'download_path': 'dev/download_link', 'bundle_id': 'com.google.Chrome.dev', 'image': 'chrome_dev.png', 'release_notes': 'https://chromereleases.googleblog.com/search/label/Dev%20updates'},
343 | {'name': 'Canary', 'display': 'Chrome', 'version_path': 'canary/version', 'download_path': 'canary/download_link', 'bundle_id': 'com.google.Chrome.canary', 'image': 'chrome_canary.png'},
344 | {'name': 'Canary ASAN', 'display': 'Chrome', 'version_path': 'canary_asan/version', 'download_path': 'canary_asan/download_link', 'bundle_id': 'com.google.Chrome.canary', 'image': 'chrome_canary.png'}
345 | ]
346 | },
347 | 'Firefox': {
348 | 'fetch_details': fetch_firefox_details,
349 | 'channels': [
350 | {'name': '', 'display': 'Firefox', 'version_path': 'stable', 'download_path': 'stable', 'bundle_id': 'org.mozilla.firefox', 'image': 'firefox.png', 'release_notes': 'https://www.mozilla.org/en-US/firefox/notes/'},
351 | {'name': 'Beta', 'display': 'Firefox', 'version_path': 'beta', 'download_path': 'beta', 'bundle_id': 'org.mozilla.firefoxbeta', 'image': 'firefox.png', 'release_notes': 'https://www.mozilla.org/en-US/firefox/beta/notes/'},
352 | {'name': 'Developer', 'display': 'Firefox', 'version_path': 'dev', 'download_path': 'dev', 'bundle_id': 'org.mozilla.firefoxdev', 'image': 'firefox_developer.png', 'release_notes': 'https://www.mozilla.org/en-US/firefox/developer/notes/'},
353 | {'name': 'ESR', 'display': 'Firefox', 'version_path': 'esr', 'download_path': 'esr', 'bundle_id': 'org.mozilla.firefoxesr', 'image': 'firefox.png','release_notes': 'https://www.mozilla.org/en-US/firefox/organizations/notes/'},
354 | {'name': 'Nightly', 'display': 'Firefox', 'version_path': 'nightly', 'download_path': 'nightly', 'bundle_id': 'org.mozilla.nightly', 'image': 'firefox_nightly.png', 'release_notes': 'https://www.mozilla.org/en-US/firefox/nightly/notes/'}
355 | ]
356 | },
357 | 'Edge': {
358 | 'fetch_details': fetch_edge_details,
359 | 'channels': [
360 | {'name': '', 'display': 'Edge', 'version_path': 'stable', 'download_path': 'stable', 'bundle_id': 'com.microsoft.edgemac', 'image': 'edge.png', 'release_notes': 'https://learn.microsoft.com/en-us/deployedge/microsoft-edge-relnote-stable-channel'},
361 | {'name': 'Beta', 'display': 'Edge', 'version_path': 'beta', 'download_path': 'beta', 'bundle_id': 'com.microsoft.edgemac.beta', 'image': 'edge_beta.png', 'release_notes': 'https://learn.microsoft.com/en-us/deployedge/microsoft-edge-relnote-beta-channel'},
362 | {'name': 'Developer', 'display': 'Edge', 'version_path': 'dev', 'download_path': 'dev', 'bundle_id': 'com.microsoft.edgemac.dev', 'image': 'edge_dev.png'},
363 | {'name': 'Canary', 'display': 'Edge', 'version_path': 'canary', 'download_path': 'canary', 'bundle_id': 'com.microsoft.edgemac.canary', 'image': 'edge_canary.png'}
364 | ]
365 | },
366 | 'Safari': {
367 | # Use the new release-level fetcher and a single "release" channel
368 | 'fetch_details': fetch_safari_release,
369 | 'channels': [
370 | {'name': '', 'display': 'Safari', 'version_path': 'release', 'download_path': 'release', 'bundle_id': 'com.apple.Safari', 'image': 'safari.png', 'release_notes': 'https://developer.apple.com/documentation/safari-release-notes'}
371 | ]
372 | }
373 | }
374 |
375 | def get_last_updated_from_xml(xml_path, browser, channel=None):
376 | """Extract last updated date for the main stable channel/version for each browser, formatted as 'Month day, Year'."""
377 | if not os.path.exists(xml_path):
378 | return "N/A"
379 | def format_date(date_str):
380 | # Try to parse common formats and return 'Month day, Year'
381 | for fmt in [
382 | "%B %d, %Y %I:%M %p %Z", # e.g., May 27, 2025 10:02 AM EDT
383 | "%B %d, %Y %I:%M %p", # e.g., May 27, 2025 10:02 AM
384 | "%B %d, %Y", # e.g., May 27, 2025
385 | "%Y-%m-%d", # e.g., 2025-05-27
386 | "%Y-%m-%d %H:%M", # e.g., 2025-05-27 10:02
387 | ]:
388 | try:
389 | dt = datetime.strptime(date_str.strip(), fmt)
390 | return dt.strftime("%B %d, %Y")
391 | except Exception:
392 | continue
393 | return date_str # fallback: return as-is
394 |
395 | try:
396 | tree = parse_xml_file(xml_path)
397 | root = tree.getroot()
398 | if browser == 'Chrome':
399 | # Use channel-specific release_time if available
400 | if channel:
401 | elem = root.find(f'.//{channel}/release_time')
402 | if elem is not None and elem.text:
403 | return format_date(elem.text)
404 | elif browser == 'Firefox':
405 | # New Firefox XML: channel is one of 'stable', 'beta', 'dev', 'esr', 'nightly'
406 | if channel:
407 | channel_elem = root.find(f'.//{channel}')
408 | if channel_elem is not None:
409 | release_elem = channel_elem.find('release_time')
410 | if release_elem is not None and release_elem.text:
411 | return format_date(release_elem.text)
412 | # fallback to
413 | elem = root.find('.//last_updated')
414 | if elem is not None and elem.text:
415 | return format_date(elem.text)
416 | elif browser == 'Edge':
417 | # Match Date for the requested channel (stable->current)
418 | channel_map = {'stable': 'current', 'beta': 'beta', 'dev': 'dev', 'canary': 'canary'}
419 | wanted = channel_map.get(channel, channel) if channel else 'current'
420 | for version in root.findall('.//Version'):
421 | ch = version.find('Channel')
422 | if ch is not None and ch.text == wanted:
423 | date_elem = version.find('Date')
424 | if date_elem is not None and date_elem.text:
425 | return format_date(date_elem.text)
426 | elif browser == 'Safari':
427 | # New Safari XML uses elements with ;
428 | # Technology previews use with .
429 | if channel:
430 | # try both PostDate (tech previews) and released (release entries)
431 | elem = root.find(f'.//{channel}/PostDate') or root.find(f'.//{channel}/released')
432 | if elem is not None and elem.text:
433 | return format_date(elem.text)
434 | # fallback: if there are entries use the first /
435 | release_released = root.find('.//release/released')
436 | if release_released is not None and release_released.text:
437 | return format_date(release_released.text)
438 | # Global fallbacks
439 | elem = root.find('.//last_updated')
440 | if elem is not None and elem.text:
441 | return format_date(elem.text)
442 | mtime = os.path.getmtime(xml_path)
443 | return datetime.fromtimestamp(mtime).strftime("%B %d, %Y")
444 | except Exception as e:
445 | return "N/A"
446 |
447 | def generate_browser_table(base_path):
448 | table_content = """| **Browser** | **CFBundle Version** | **CFBundle Identifier** | **Download** |
449 | |------------|-------------------|---------------------|------------|
450 | """
451 | for browser, config in BROWSER_CONFIGS.items():
452 | # Skip Safari in the main browser table (we render a dedicated Safari releases section)
453 | if browser == 'Safari':
454 | continue
455 |
456 | xml_path = os.path.join(base_path, f'latest_{browser.lower()}_files/{browser.lower()}_latest_versions.xml')
457 | for channel in config['channels']:
458 | # Fetch version and download
459 | if browser == 'Safari':
460 | version, download = config['fetch_details'](xml_path, channel['version_path'], 'URL')
461 | last_updated = get_last_updated_from_xml(xml_path, browser, channel['version_path'])
462 | elif browser == 'Chrome':
463 | version, download = config['fetch_details'](xml_path, channel['version_path'], channel['download_path'])
464 | # For Extended Stable, use the same download as Stable
465 | if channel.get('name') == 'Extended Stable':
466 | _, stable_download = config['fetch_details'](xml_path, 'stable/version', 'stable/download_link')
467 | download = stable_download
468 | last_updated = get_last_updated_from_xml(xml_path, browser, channel['version_path'].split('/')[0])
469 | elif browser == 'Edge':
470 | version, download = config['fetch_details'](xml_path, channel['version_path'], channel['download_path'])
471 | last_updated = get_last_updated_from_xml(xml_path, browser, channel['version_path'])
472 | elif browser == 'Firefox':
473 | version, download = config['fetch_details'](xml_path, channel['version_path'], channel['download_path'])
474 | last_updated = get_last_updated_from_xml(xml_path, browser, channel['version_path'])
475 | else:
476 | version, download = "N/A", "N/A"
477 | last_updated = None
478 |
479 | channel_name = f"{channel['name']}" if channel['name'] else ""
480 | # For Extended Stable, show comment instead of release notes
481 | if 'release_notes_comment' in channel:
482 | release_notes = f"
{channel['release_notes_comment']}"
483 | else:
484 | release_notes = f"
_Release Notes_" if 'release_notes' in channel or 'release_notes' in config else ""
485 | last_updated_html = f"
Last Updated:
{last_updated}" if last_updated else ""
486 | table_content += (
487 | f"| **{channel['display']}** {channel_name} {release_notes}{last_updated_html} | "
488 | f"`{version}` | "
489 | f"`{channel['bundle_id']}` | "
490 | f"
|\n"
492 | )
493 | # Ensure the table is followed by blank lines so subsequent sections/tables render separately
494 | return table_content + "\n\n"
495 |
496 | def generate_settings_section():
497 | return """
498 | ## Browser Settings Management
499 |
500 | View your current browser policies and explore available policy options:
501 |
502 | ###
Chrome
503 | 1. **View Current Policies**: Enter `chrome://policy` in your address bar to see active policies
504 | 2. **Available Options**: [Chrome Enterprise Policy Documentation](https://chromeenterprise.google/policies/)
505 |
506 | ###
Firefox
507 | 1. **View Current Policies**: Enter `about:policies` in your address bar to see active policies
508 | 2. **Available Options**: [Firefox Policy Documentation](https://mozilla.github.io/policy-templates/)
509 |
510 | ###
Edge
511 | 1. **View Current Policies**: Enter `edge://policy` in your address bar to see active policies
512 | 2. **Available Options**: [Edge Policy Documentation](https://learn.microsoft.com/en-us/deployedge/microsoft-edge-policies)
513 |
514 | ###
Safari
515 | 1. **View Current Policies**: Open System Settings > Profiles & Device Management
516 | 2. **Available Options**: [Safari Configuration Profile Reference](https://support.apple.com/guide/deployment/welcome/web)
517 |
518 | """
519 |
520 | def generate_readme():
521 | base_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
522 | xml_files = {
523 | 'Chrome': os.path.join(base_path, 'latest_chrome_files/chrome_latest_versions.xml'),
524 | 'Firefox': os.path.join(base_path, 'latest_firefox_files/firefox_latest_versions.xml'),
525 | 'Edge': os.path.join(base_path, 'latest_edge_files/edge_latest_versions.xml'),
526 | 'Safari': os.path.join(base_path, 'latest_safari_files/safari_latest_versions.xml')
527 | }
528 | eastern = timezone('US/Eastern')
529 | current_time = datetime.now(eastern).strftime("%B %d, %Y %I:%M %p %Z")
530 | global_last_updated = current_time
531 |
532 | # Fetch versions and download URLs with new Edge mapping
533 | chrome_version, chrome_download = fetch_chrome_details(xml_files['Chrome'], 'stable/version', 'stable/download_link')
534 | firefox_version, firefox_download = fetch_firefox_details(xml_files['Firefox'], 'stable', 'stable')
535 | edge_version, edge_download = fetch_edge_details(xml_files['Edge'], 'stable', 'stable')
536 | # Use the new release-based Safari fetcher (main browser tile)
537 | safari_version, safari_download = fetch_safari_release(xml_files['Safari'])
538 |
539 | # Fetch last updated dates from XMLs (browser-specific, channel-specific)
540 | chrome_last_updated = get_last_updated_from_xml(xml_files['Chrome'], 'Chrome', 'stable')
541 | firefox_last_updated = get_last_updated_from_xml(xml_files['Firefox'], 'Firefox', 'stable')
542 | edge_last_updated = get_last_updated_from_xml(xml_files['Edge'], 'Edge')
543 | safari_last_updated = get_last_updated_from_xml(xml_files['Safari'], 'Safari')
544 |
545 | readme_content = f"""# **BOFA**
546 | **B**rowser **O**verview **F**eed for **A**pple
547 |
548 |
549 |
550 | Welcome to the **BOFA** repository! This resource tracks the latest versions of major web browsers for macOS. Feeds are automatically updated every hour from XML and JSON links directly from vendors.
551 |
552 | We welcome community contributions—fork the repository, ask questions, or share insights to help keep this resource accurate and useful for everyone. Check out the user-friendly website version below for an easier browsing experience!
553 |
554 |
555 |
556 |
557 |
558 | | 🌟 Explore the BOFA Website 🌟 |
559 | ⭐ Support the Project – Give it a Star! ⭐ |
560 |
561 |
562 | | 🌐 Visit: bofa.cocolabs.dev 🌐 |
563 |
564 |
565 |
566 |
567 | |
568 |
569 |
570 |
571 |
572 | ## Latest Stable Browser Versions
573 |
574 |
575 |
576 |  Chrome
{chrome_version}
Last Update:
{chrome_last_updated} Release Notes |
577 |  Firefox
{firefox_version}
Last Update:
{firefox_last_updated} Release Notes |
578 |  Edge
{edge_version}
Last Update:
{edge_last_updated} Release Notes |
579 |  Safari
{safari_version}
Last Update:
{safari_last_updated} Release Notes |
580 |
581 |
582 |
583 | """
584 |
585 | readme_content += f"""
586 | ## Browser Packages
587 |
588 |
All links below direct to the official browser vendor. The links provided will always download the latest available version as of the last scan update.
589 |
590 |
**Chrome**: [**_Raw XML_**](latest_chrome_files/chrome_latest_versions.xml) [**_Raw YAML_**](latest_chrome_files/chrome_latest_versions.yaml) [**_Raw JSON_**](latest_chrome_files/chrome_latest_versions.json) | **Firefox**: [**_Raw XML_**](latest_firefox_files/firefox_latest_versions.xml) [**_Raw YAML_**](latest_firefox_files/firefox_latest_versions.yaml) [**_Raw JSON_**](latest_firefox_files/firefox_latest_versions.json)
591 |
592 |
**Edge**: [**_Raw XML_**](latest_edge_files/edge_latest_versions.xml) [**_Raw YAML_**](latest_edge_files/edge_latest_versions.yaml) [**_Raw JSON_**](latest_edge_files/edge_latest_versions.json) | **Safari**: [**_Raw XML_**](latest_safari_files/safari_latest_versions.xml) [**_Raw YAML_**](latest_safari_files/safari_latest_versions.yaml) [**_Raw JSON_**](latest_safari_files/safari_latest_versions.json)
593 |
594 |
_Last Updated: {global_last_updated} (Automatically Updated every hour)_
595 |
596 |
597 |
598 | """
599 |
600 | readme_content += generate_browser_table(base_path)
601 | # Add a dedicated Safari releases table (all entries)
602 | readme_content += generate_safari_releases_table(base_path, xml_files['Safari'])
603 | # Append the new Safari Technology Preview table (if any)
604 | readme_content += generate_safari_tech_table(base_path, xml_files['Safari'])
605 | readme_content += generate_settings_section()
606 |
607 | readme_path = os.path.join(base_path, 'README.md')
608 | with open(readme_path, 'w', encoding='utf-8') as f:
609 | f.write(readme_content)
610 |
611 | if __name__ == "__main__":
612 | generate_readme()
613 |
--------------------------------------------------------------------------------